Skip to content

[Feat-T3-102] 온보딩 서버 통신 구현#19

Merged
choijungp merged 11 commits into
developfrom
feat/onboarding
Jul 16, 2025
Merged

[Feat-T3-102] 온보딩 서버 통신 구현#19
choijungp merged 11 commits into
developfrom
feat/onboarding

Conversation

@choijungp

@choijungp choijungp commented Jul 15, 2025

Copy link
Copy Markdown
Contributor

🌁 Background

이전 PR에서 구현한 온보딩 뷰와 서버 로직을 연동하였습니다.
온보딩 결과를 서버에 전송하여 온보딩 선택 기반으로 받은 추천 루틴 목록을 보여주는 것과, 추천 루틴을 선택해서 등록하는 기능을 연결하였습니다.

📱 Screenshot

온보딩 기반 추천 루틴 선택 ❌

iPhone SE3 iPhone 13 mini iPhone 16 Pro

온보딩 기반 추천 루틴 선택 ⭕️

iPhone SE3 iPhone 13 mini iPhone 16 Pro

👩‍💻 Contents

  • 폴더링 정리
  • NetworkService 로직 리팩토링
    • BaseResponse를 Network로 이관하여 DataSource 모듈에서는 Decodable을 채택한 타입으로 디코딩 할 수 있도록 수정
  • BitnagilFont 리팩토링
    • 정리해둔 디자인 시스템 외에도 커스텀해서 폰트 적용할 수 있도록 custom 케이스 추가
  • Login 결과를 바탕으로 (.guest vs .user) 이용약관 뷰와 온보딩 뷰 분기 처리
  • Onboarding 서버 통신 로직 구현
    • 온보딩 선택 결과를 서버에 등록하고 그 선택지를 기반으로 루틴 추천 받기
    • 추천 받은 루틴을 선택하여 등록하기

✅ Testing

  • 서버 API 직접 연결하여, Response 확인 · UI를 통한 추천 루틴 목록 확인

📣 Related Issue

  • close #T3-102

Summary by CodeRabbit

  • 신규 기능

    • 온보딩 및 추천 루틴 등록 기능이 추가되어, 사용자가 온보딩 선택지를 제출하고 개인화된 추천 루틴을 받을 수 있습니다.
    • 추천 루틴을 건너뛰고 바로 홈 화면으로 이동할 수 있는 '건너뛰기' 버튼이 추가되었습니다.
  • 기능 개선

    • 로그인 성공 시 사용자 상태(UserState)를 반환하여, 로그인 결과에 따라 온보딩 또는 약관 동의 화면으로 이동합니다.
    • 온보딩 뷰 및 관련 화면의 접근 제어가 내부로 변경되어 보안이 강화되었습니다.
    • 폰트 시스템에 커스텀 스타일을 지정할 수 있는 기능이 추가되었습니다.
    • 로그인 및 온보딩 흐름에서 nil 체크 및 오류 처리 로직이 개선되었습니다.
  • 버그 수정

    • 서버 응답에 사용자 정보가 없을 경우를 처리하는 오류 케이스가 추가되었습니다.
  • 리팩터링 및 구조 개선

    • 온보딩, 추천 루틴, 로그인 관련 데이터 및 도메인 구조가 통합적으로 정비되었습니다.
    • ViewModel, UseCase, Repository 등 주요 컴포넌트의 의존성 주입 및 구조가 개선되었습니다.
    • 불필요한 public 접근자가 제거되어 내부 구조가 정돈되었습니다.
    • 네트워크 요청 반환 타입이 옵셔널로 변경되어 유연성이 향상되었습니다.

- BaseResponseDTO DataSource에서 Network 모듈로 이관
- T 타입 반환에서 T? 타입 반환으로 수정 (EmptyResponse 대응)
- Server Message 출력
- OnboardingEndpoint 정의
- OnboardingRepositoryProtocol · OnboardingRepository 구현
- OnboardingUseCaseProtocol · OnboardingUseCase 구현
- DataSource, Domain, Presentaion 레이어에서 OnboardingRepository, OnboardingUseCase, OnboardingViewModel 의존성 등록
- 디자인 시스템에 정의되어 있는 폰트 외에도 정의해서 사용할 수 있도록 custom 케이스 추가
@choijungp choijungp self-assigned this Jul 15, 2025
@coderabbitai

coderabbitai Bot commented Jul 15, 2025

Copy link
Copy Markdown
## Walkthrough

이번 변경 사항은 온보딩(회원가입 후 초기 선택) 및 추천 루틴 기능을 도메인, 데이터, 프레젠테이션 계층 전반에 걸쳐 도입하고 연결합니다. 온보딩 및 추천 루틴 관련 엔터티, DTO, 유즈케이스, 프로토콜, 레포지토리, DI 등록, 뷰모델, 뷰, 네트워크 요청/응답 구조가 추가·수정되었으며, 로그인 흐름의 반환 타입 및 접근제어도 일관성 있게 정비되었습니다.

## Changes

| 파일/경로 요약 | 변경 내용 요약 |
|---|---|
| DataSource/.../DataSourceDependencyAssembler.swift, Domain/.../DomainDependencyAssembler.swift, Presentation/.../PresentationDependencyAssembler.swift | DI 컨테이너에 OnboardingRepository, OnboardingUseCase, OnboardingViewModel 등록 및 의존성 연결 추가 |
| DataSource/.../DTO/RecommendedRoutineDTO.swift, Domain/.../Entity/RecommendedRoutineEntity.swift, Presentation/.../Onboarding/Model/RecommendedRoutine.swift | 추천 루틴 관련 DTO, 엔터티, 모델 및 변환 메서드 추가 및 공개 범위 조정 |
| DataSource/.../Repository/OnboardingRepository.swift, Domain/.../Protocol/Repository/OnboardingRepositoryProtocol.swift, Domain/.../UseCase/Onboarding/OnboardingUseCase.swift, Domain/.../Protocol/UseCase/OnboardingUseCaseProtocol.swift | 온보딩 레포지토리, 프로토콜, 유즈케이스, 유즈케이스 프로토콜 신규 추가 및 온보딩/루틴 등록 메서드 구현 |
| DataSource/.../Endpoint/OnboardingEndpoint.swift | 온보딩 및 추천 루틴 등록용 API 엔드포인트 enum 추가 |
| DataSource/.../DTO/LoginResponseDTO.swift, NetworkService/.../BaseResponse.swift | 로그인/베이스 응답 DTO 명칭 및 구조 통일, 불필요 타입 제거 |
| DataSource/.../Repository/AuthRepository.swift, Domain/.../Protocol/Repository/AuthRepositoryProtocol.swift, Domain/.../UseCase/Auth/LoginUseCase.swift, Domain/.../Protocol/UseCase/LoginUseCaseProtocol.swift, Presentation/.../Login/ViewModel/LoginViewModel.swift, Presentation/.../Login/View/LoginView.swift | 로그인 관련 반환 타입을 Void→UserEntity/UserState로 변경, 접근제어 internal로 조정, 뷰모델/뷰에서 UserState 기반 분기 처리 |
| DataSource/.../Common/Error/AuthError.swift | AuthError에 invalidUserData 케이스 및 메시지 추가 |
| DataSource/.../DTO/EmptyResponseDTO.swift | 빈 응답용 DTO 추가 |
| DataSource/.../Protocol/NetworkServiceProtocol.swift, NetworkService/.../NetworkService.swift | 네트워크 요청 반환 타입을 옵셔널(T?)로 변경, BaseResponse<T>로 감싸서 파싱하도록 수정 |
| Domain/.../Entity/Enum/OnboardingType.swift, Domain/.../Entity/Enum/OnboardingChoiceType.swift | 온보딩 타입/선택지 enum 추가 및 key/value 매핑 제공 |
| Presentation/.../Onboarding/Model/OnboardingChoiceType.swift, Presentation/.../Onboarding/Model/OnboardingType.swift | 온보딩 선택지/타입 enum 정의 제거 및 도메인 계층 enum 확장으로 대체, 관련 UI 문자열 확장 유지 |
| Presentation/.../Onboarding/ViewModel/OnboardingViewModel.swift, Presentation/.../Onboarding/View/OnboardingRecommendedRoutineView.swift, Presentation/.../Onboarding/View/OnboardingResultView.swift, Presentation/.../Onboarding/View/OnboardingView.swift | 온보딩 뷰모델이 유즈케이스 연동, 추천 루틴 등록 및 결과 처리, 스킵 버튼 추가, 뷰 계층 명칭 및 흐름 일관화, 접근제어 internal로 조정 |
| Presentation/.../Common/DesignSystem/Font/BitnagilFont.swift, Presentation/.../Common/DesignSystem/Font/FontStyle.swift | 커스텀 폰트 스타일/속성 지원 initializer 및 enum case 추가 |
| Presentation/.../Protocol/BaseViewController.swift, Presentation/.../Protocol/ViewModel.swift, Presentation/.../Home/HomeViewController.swift, Presentation/.../Home/ViewModel/HomeViewModel.swift, Presentation/.../Login/View/TermsAgreementView.swift | 접근제어 public→internal로 일괄 정비 및 불필요한 공개 범위 축소 |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant LoginView
    participant LoginViewModel
    participant LoginUseCase
    participant AuthRepository
    participant NetworkService

    LoginView->>LoginViewModel: action(.kakaoLogin)
    LoginViewModel->>LoginUseCase: kakaoLogin()
    LoginUseCase->>AuthRepository: kakaoLogin()
    AuthRepository->>NetworkService: request(loginEndpoint)
    NetworkService-->>AuthRepository: LoginResponseDTO?
    AuthRepository-->>LoginUseCase: UserEntity
    LoginUseCase-->>LoginViewModel: UserState
    LoginViewModel-->>LoginView: loginResultPublisher(UserState?)
    LoginView->>LoginView: 분기 처리(게스트/온보딩)
sequenceDiagram
    participant OnboardingView
    participant OnboardingViewModel
    participant OnboardingUseCase
    participant OnboardingRepository
    participant NetworkService

    OnboardingView->>OnboardingViewModel: action(.registerOnboarding)
    OnboardingViewModel->>OnboardingUseCase: registerOnboarding([OnboardingChoiceType])
    OnboardingUseCase->>OnboardingRepository: registerOnboarding([String: String])
    OnboardingRepository->>NetworkService: request(OnboardingEndpoint)
    NetworkService-->>OnboardingRepository: RecommendedRoutineListResponseDTO?
    OnboardingRepository-->>OnboardingUseCase: [RecommendedRoutineEntity]
    OnboardingUseCase-->>OnboardingViewModel: [RecommendedRoutineEntity]
    OnboardingViewModel-->>OnboardingView: recommendedRoutinePublisher
Loading
sequenceDiagram
    participant OnboardingRecommendedRoutineView
    participant OnboardingViewModel
    participant OnboardingUseCase
    participant OnboardingRepository
    participant NetworkService

    OnboardingRecommendedRoutineView->>OnboardingViewModel: registerRecommendedRoutine()
    OnboardingViewModel->>OnboardingUseCase: registerRecommendedRoutines([Int])
    OnboardingUseCase->>OnboardingRepository: registerRecommendedRoutines([Int])
    OnboardingRepository->>NetworkService: request(OnboardingEndpoint)
    NetworkService-->>OnboardingRepository: EmptyResponseDTO?
    OnboardingRepository-->>OnboardingUseCase: 완료
    OnboardingUseCase-->>OnboardingViewModel: 완료
    OnboardingViewModel-->>OnboardingRecommendedRoutineView: registerRoutineResultPublisher(Bool)
Loading

Poem

🐇
온보딩의 길을 따라 hop hop hop!
추천 루틴도 콩콩, 네트워크로 점프!
게스트냐 회원이냐, 분기점에 깡총~
스킵 버튼도 추가해, 집에 가는 길도 열렸네!
DI에 의존성도 차곡차곡,
코드 속 토끼는 오늘도 신나게 뛴다!
🥕✨


<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between e15754e9c8f334e8447d89bee2f85da9c0396c6d and c911a0cfbf2f94510bc3e36fadcd197da01ca348.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift` (1 hunks)
* `Projects/Domain/Sources/DomainDependencyAssembler.swift` (2 hunks)
* `Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift` (1 hunks)
* `Projects/Presentation/Sources/Common/Extension/UIViewController+.swift` (1 hunks)
* `Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* Projects/Presentation/Sources/Common/Extension/UIViewController+.swift

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift
* Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift
* Projects/Domain/Sources/DomainDependencyAssembler.swift

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (2)</summary>

<details>
<summary>📓 Common learnings</summary>

Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.002Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.


</details>
<details>
<summary>Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift (1)</summary>

Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.002Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.


</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift (2)</summary>

`10-15`: **의존성 주입 패턴이 올바르게 구현되었습니다.**

클래스가 final로 선언되어 있고, 프로토콜 기반 의존성 주입을 적절히 활용하고 있습니다.

---

`23-25`: **레포지토리 메서드를 올바르게 호출하고 있습니다.**

추천 루틴 등록 메서드가 적절히 레포지토리로 위임되고 있습니다.

</details>

</blockquote></details>

</details>
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings
<details open="true">
<summary>🧪 Generate unit tests</summary>

- [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Create PR with unit tests
- [ ] <!-- {"checkboxId": "07f1e7d6-8a8e-4e23-9900-8731c2c87f58", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Post copyable unit tests in a comment
- [ ] <!-- {"checkboxId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Commit unit tests in branch `feat/onboarding`

</details>

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

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.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=YAPP-Github/Bitnagil-iOS&utm_content=19):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai auto-generate unit tests` to generate unit tests for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` or `@coderabbitai 요약` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

♻️ Duplicate comments (1)
Projects/Presentation/Sources/Login/View/LoginView.swift (1)

109-111: 의존성 주입 실패 시 에러 처리 개선 필요

OnboardingRecommendedRoutineView와 동일한 문제입니다. 프로덕션 환경에서는 fatalError 대신 더 안전한 에러 처리가 필요합니다.

🧹 Nitpick comments (7)
Projects/DataSource/Sources/DTO/EmptyResponseDTO.swift (1)

8-8: 빈 응답 처리를 위한 DTO 구조체가 적절히 구현되었습니다.

데이터가 없는 응답을 처리하기 위한 EmptyResponseDTO는 타입 안전성과 명확성을 제공하는 좋은 접근 방법입니다.

접근 제어 수준을 다른 컴포넌트들과 일관성 있게 internal로 변경하는 것을 고려해보세요:

-struct EmptyResponseDTO: Decodable {}
+struct EmptyResponseDTO: Decodable {}

다만 이 DTO가 다른 모듈에서 사용될 예정이라면 public이 적절합니다.

Projects/Domain/Sources/Entity/RecommendedRoutineEntity.swift (1)

8-22: 프로토콜 준수 및 명명 개선을 고려해보세요.

구조체 구현은 올바르지만 다음 개선사항들을 고려해보세요:

  • Equatable, Hashable 프로토콜 준수 추가로 비교 및 컬렉션 사용 개선
  • description 프로퍼티명이 NSObjectdescription과 충돌할 가능성 - routineDescription 등으로 변경 고려
-public struct RecommendedRoutineEntity {
+public struct RecommendedRoutineEntity: Equatable, Hashable {
     public let id: Int
     public let title: String
-    public let description: String
+    public let routineDescription: String

     public init(
         id: Int,
         title: String,
-        description: String
+        routineDescription: String
     ) {
         self.id = id
         self.title = title
-        self.description = description
+        self.routineDescription = routineDescription
     }
 }
Projects/Presentation/Sources/Onboarding/Model/RecommendedRoutine.swift (1)

16-23: 도메인 엔티티 변환 메서드 구현이 깔끔합니다.

도메인 엔티티를 프레젠테이션 모델로 변환하는 extension 구현이 적절합니다. titlemainTitle로, descriptionsubTitle로 매핑하는 것이 UI 요구사항에 맞습니다.

널 안전성을 고려하여 description이 비어있을 때의 처리를 추가할 수 있습니다:

 func toRecommendedRoutine() -> RecommendedRoutine {
     return RecommendedRoutine(
         id: id,
         mainTitle: title,
-        subTitle: description)
+        subTitle: description.isEmpty ? nil : description)
 }
Projects/Domain/Sources/Protocol/UseCase/OnboardingUseCaseProtocol.swift (1)

8-17: 온보딩 유즈케이스 프로토콜 설계가 우수합니다.

프로토콜 설계가 다음과 같은 장점을 가지고 있습니다:

  • 비동기 네트워크 작업에 적합한 async throws 패턴 사용
  • 온보딩 등록과 루틴 등록의 관심사 분리
  • 명확한 한국어 문서화
  • 타입 안전성 확보

메서드 명명 일관성을 위해 동사형으로 통일할 수 있습니다:

 public protocol OnboardingUseCaseProtocol {
     /// 선택한 온보딩 결과를 저장하고, 추천 루틴을 받습니다.
     /// - Parameter onboardingChoices: 선택한 온보딩 항목
     /// - Returns: 온보딩 결과를 바탕으로 받은 추천루틴 목록
-    func registerOnboarding(onboardingChoices: [OnboardingChoiceType]) async throws -> [RecommendedRoutineEntity]
+    func submitOnboarding(onboardingChoices: [OnboardingChoiceType]) async throws -> [RecommendedRoutineEntity]

     /// 선택한 추천 루틴을 등록합니다.
     /// - Parameter selectedRoutines: 선택한 추천 루틴 ID 목록
-    func registerRecommendedRoutines(selectedRoutines: [Int]) async throws
+    func submitRecommendedRoutines(selectedRoutines: [Int]) async throws
 }
Projects/DataSource/Sources/Repository/OnboardingRepository.swift (1)

26-27: 데이터 손실 가능성을 개선하는 것을 고려해보세요.

compactMap을 사용하여 변환에 실패한 아이템들이 조용히 제거됩니다. 변환 실패 시 로깅을 추가하거나 에러를 던지는 것을 고려해보세요.

다음과 같이 개선할 수 있습니다:

-let recommendedRoutineEntity = response.recommendedRoutines.compactMap({ $0.toRecommendedRoutineEntity() })
+let recommendedRoutineEntity = response.recommendedRoutines.map({ $0.toRecommendedRoutineEntity() })

또는 변환 실패 시 로깅을 추가:

-let recommendedRoutineEntity = response.recommendedRoutines.compactMap({ $0.toRecommendedRoutineEntity() })
+let recommendedRoutineEntity = response.recommendedRoutines.compactMap({ dto in
+    let entity = dto.toRecommendedRoutineEntity()
+    if entity == nil {
+        BitnagilLogger.log(logType: .warning, message: "Failed to convert DTO to entity: \(dto)")
+    }
+    return entity
+})
Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift (1)

31-45: Authorization 헤더 처리를 간소화할 수 있습니다.

현재 각 케이스에서 동일한 Authorization 헤더를 설정하고 있습니다. 이를 더 간소화할 수 있습니다.

-    var headers: [String : String] {
-        var headers: [String: String] = [
-            "Content-Type": "application/json",
-            "accept": "*/*"
-        ]
-
-        switch self {
-        case .registerOnboarding(let accessToken, _):
-            headers["Authorization"] = "Bearer \(accessToken)"
-        case .registerRecommendedRoutine(let accessToken, _):
-            headers["Authorization"] = "Bearer \(accessToken)"
-        }
-
-        return headers
-    }
+    var headers: [String : String] {
+        let accessToken: String
+        switch self {
+        case .registerOnboarding(let token, _), .registerRecommendedRoutine(let token, _):
+            accessToken = token
+        }
+        
+        return [
+            "Content-Type": "application/json",
+            "accept": "*/*",
+            "Authorization": "Bearer \(accessToken)"
+        ]
+    }
Projects/DataSource/Sources/Repository/AuthRepository.swift (1)

14-136: 전체적인 리팩토링 방향은 좋으나 일관성 개선 필요

이번 변경사항은 전반적으로 코드 품질을 향상시켰습니다. 특히 로그인 메서드가 UserEntity를 반환하도록 변경한 것은 좋은 개선입니다.

다만, 에러 처리의 일관성을 위해 다음 사항을 고려해 주세요:

  1. reissueToken()requestServerLogin()에서 nil 응답 처리가 다름
  2. 에러 발생 시 로깅이 일부 메서드에만 있음

향후 개선 사항으로 에러 처리와 로깅을 위한 공통 래퍼 메서드를 만들어 일관성을 확보하는 것을 고려해보세요.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ffb6323 and 032a2ed.

📒 Files selected for processing (38)
  • Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift (1 hunks)
  • Projects/DataSource/Sources/Common/Error/AuthError.swift (2 hunks)
  • Projects/DataSource/Sources/DTO/EmptyResponseDTO.swift (1 hunks)
  • Projects/DataSource/Sources/DTO/LoginResponseDTO.swift (2 hunks)
  • Projects/DataSource/Sources/DTO/RecommendedRoutineDTO.swift (1 hunks)
  • Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift (1 hunks)
  • Projects/DataSource/Sources/Protocol/NetworkServiceProtocol.swift (1 hunks)
  • Projects/DataSource/Sources/Repository/AuthRepository.swift (3 hunks)
  • Projects/DataSource/Sources/Repository/OnboardingRepository.swift (1 hunks)
  • Projects/Domain/Sources/DomainDependencyAssembler.swift (2 hunks)
  • Projects/Domain/Sources/Entity/Enum/OnboardingChoiceType.swift (1 hunks)
  • Projects/Domain/Sources/Entity/Enum/OnboardingType.swift (1 hunks)
  • Projects/Domain/Sources/Entity/RecommendedRoutineEntity.swift (1 hunks)
  • Projects/Domain/Sources/Protocol/Repository/AuthRepositoryProtocol.swift (1 hunks)
  • Projects/Domain/Sources/Protocol/Repository/OnboardingRepositoryProtocol.swift (1 hunks)
  • Projects/Domain/Sources/Protocol/UseCase/LoginUseCaseProtocol.swift (1 hunks)
  • Projects/Domain/Sources/Protocol/UseCase/OnboardingUseCaseProtocol.swift (1 hunks)
  • Projects/Domain/Sources/UseCase/Auth/LoginUseCase.swift (1 hunks)
  • Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift (1 hunks)
  • Projects/NetworkService/Sources/BaseResponse.swift (1 hunks)
  • Projects/NetworkService/Sources/NetworkService.swift (1 hunks)
  • Projects/Presentation/Sources/Common/DesignSystem/Font/BitnagilFont.swift (1 hunks)
  • Projects/Presentation/Sources/Common/DesignSystem/Font/FontStyle.swift (2 hunks)
  • Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift (1 hunks)
  • Projects/Presentation/Sources/Home/HomeViewController.swift (2 hunks)
  • Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (1 hunks)
  • Projects/Presentation/Sources/Login/View/LoginView.swift (6 hunks)
  • Projects/Presentation/Sources/Login/View/TermsAgreementView.swift (3 hunks)
  • Projects/Presentation/Sources/Login/ViewModel/LoginViewModel.swift (2 hunks)
  • Projects/Presentation/Sources/Onboarding/Model/OnboardingChoiceType.swift (1 hunks)
  • Projects/Presentation/Sources/Onboarding/Model/OnboardingType.swift (4 hunks)
  • Projects/Presentation/Sources/Onboarding/Model/RecommendedRoutine.swift (1 hunks)
  • Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift (8 hunks)
  • Projects/Presentation/Sources/Onboarding/View/OnboardingResultView.swift (5 hunks)
  • Projects/Presentation/Sources/Onboarding/View/OnboardingView.swift (8 hunks)
  • Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift (5 hunks)
  • Projects/Presentation/Sources/Protocol/BaseViewController.swift (2 hunks)
  • Projects/Presentation/Sources/Protocol/ViewModel.swift (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (13)
Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift (1)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
  • register (16-18)
Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (2)
Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift (1)
  • action (61-81)
Projects/Presentation/Sources/Login/ViewModel/LoginViewModel.swift (1)
  • action (43-89)
Projects/Domain/Sources/DomainDependencyAssembler.swift (1)
Projects/Shared/Sources/DIContainer/DIContainer.swift (2)
  • resolve (20-27)
  • register (16-18)
Projects/Presentation/Sources/Home/HomeViewController.swift (1)
Projects/Presentation/Sources/Protocol/BaseViewController.swift (1)
  • viewDidLoad (23-30)
Projects/DataSource/Sources/Protocol/NetworkServiceProtocol.swift (1)
Projects/NetworkService/Sources/NetworkService.swift (1)
  • request (20-46)
Projects/Domain/Sources/Protocol/UseCase/OnboardingUseCaseProtocol.swift (3)
Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift (2)
  • registerOnboarding (17-21)
  • registerRecommendedRoutines (23-25)
Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift (1)
  • registerOnboarding (188-214)
Projects/DataSource/Sources/Repository/OnboardingRepository.swift (2)
  • registerOnboarding (19-28)
  • registerRecommendedRoutines (30-34)
Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift (1)
Projects/Shared/Sources/DIContainer/DIContainer.swift (2)
  • register (16-18)
  • resolve (20-27)
Projects/NetworkService/Sources/NetworkService.swift (2)
Projects/NetworkService/Sources/Extension/Endpoint+.swift (1)
  • makeURLRequest (12-19)
Projects/Shared/Sources/Logger/Logger.swift (1)
  • log (13-22)
Projects/Domain/Sources/Protocol/Repository/OnboardingRepositoryProtocol.swift (3)
Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift (2)
  • registerOnboarding (17-21)
  • registerRecommendedRoutines (23-25)
Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift (1)
  • registerOnboarding (188-214)
Projects/DataSource/Sources/Repository/OnboardingRepository.swift (2)
  • registerOnboarding (19-28)
  • registerRecommendedRoutines (30-34)
Projects/Domain/Sources/Protocol/UseCase/LoginUseCaseProtocol.swift (2)
Projects/DataSource/Sources/Repository/AuthRepository.swift (2)
  • kakaoLogin (29-33)
  • appleLogin (35-45)
Projects/Domain/Sources/UseCase/Auth/LoginUseCase.swift (2)
  • kakaoLogin (17-20)
  • appleLogin (22-25)
Projects/Domain/Sources/Protocol/Repository/AuthRepositoryProtocol.swift (2)
Projects/DataSource/Sources/Repository/AuthRepository.swift (2)
  • kakaoLogin (29-33)
  • appleLogin (35-45)
Projects/Domain/Sources/UseCase/Auth/LoginUseCase.swift (2)
  • kakaoLogin (17-20)
  • appleLogin (22-25)
Projects/Presentation/Sources/Login/View/TermsAgreementView.swift (2)
Projects/Presentation/Sources/Login/View/LoginView.swift (2)
  • viewDidLoad (40-42)
  • viewWillAppear (44-47)
Projects/Presentation/Sources/Protocol/BaseViewController.swift (1)
  • viewDidLoad (23-30)
Projects/DataSource/Sources/Repository/OnboardingRepository.swift (4)
Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift (2)
  • registerOnboarding (17-21)
  • registerRecommendedRoutines (23-25)
Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift (2)
  • registerOnboarding (188-214)
  • registerRecommendedRoutine (230-242)
Projects/NetworkService/Sources/NetworkService.swift (1)
  • request (20-46)
Projects/DataSource/Sources/DTO/RecommendedRoutineDTO.swift (1)
  • toRecommendedRoutineEntity (27-32)
🔇 Additional comments (54)
Projects/Presentation/Sources/Common/DesignSystem/Font/FontStyle.swift (2)

28-28: 사용자 정의 폰트 지원을 위한 새로운 enum case 추가 승인

custom(fontAttribute: FontAttributes) case 추가로 디자인 시스템에서 정의된 폰트 스타일 외에도 사용자 정의 폰트 속성을 지원할 수 있게 되었습니다. 이는 PR 목표와 일치하는 좋은 확장성 개선사항입니다.


51-51: custom case 처리 로직 구현 승인

associated value로 전달된 FontAttributes를 올바르게 반환하는 로직이 구현되어 있습니다. 간결하고 명확한 구현입니다.

Projects/Presentation/Sources/Common/DesignSystem/Font/BitnagilFont.swift (1)

19-32: 사용자 정의 폰트 속성을 위한 새로운 초기화 메서드 추가 승인

개별 폰트 속성들을 파라미터로 받아 사용자 정의 폰트를 생성할 수 있는 새로운 초기화 메서드가 잘 구현되었습니다:

  • 적절한 기본값 설정 (letterSpacing: 0, underline: false)
  • FontAttributes 인스턴스 생성 및 .custom case 활용
  • 기존 코드 스타일과의 일관성 유지

이를 통해 디자인 시스템의 미리 정의된 폰트 스타일을 넘어서 유연한 폰트 설정이 가능해졌습니다.

Projects/Presentation/Sources/Onboarding/Model/OnboardingChoiceType.swift (2)

8-11: 도메인 계층 분리가 잘 구현되었습니다.

OnboardingChoiceType enum의 핵심 정의를 Domain 계층으로 이동하고, Presentation 계층에서는 UI 관련 속성들만 extension으로 관리하는 것이 Clean Architecture 원칙에 잘 부합합니다.


12-91: UI 관련 문자열 속성들이 적절히 구현되었습니다.

switch 문을 통해 각 케이스별로 적절한 문자열을 반환하고 있으며, 옵셔널 반환 타입을 사용하여 해당하지 않는 케이스에 대한 처리도 잘 되어 있습니다.

Projects/Presentation/Sources/Protocol/ViewModel.swift (1)

10-15: 접근 제어 수준 조정이 적절합니다.

ViewModel 프로토콜을 internal로 변경한 것은 모듈 내부에서만 사용되는 경우 적절한 캡슐화 전략입니다. 최소 권한 원칙을 따르고 있습니다.

Projects/DataSource/Sources/Common/Error/AuthError.swift (2)

16-16: 새로운 에러 케이스 추가가 적절합니다.

서버 응답에 사용자 정보가 없는 경우를 처리하기 위한 invalidUserData 케이스 추가는 인증 플로우의 에러 핸들링을 향상시킵니다.


35-36: 에러 메시지가 일관성 있게 작성되었습니다.

다른 에러 케이스들과 일관된 형태의 한국어 에러 메시지가 작성되어 사용자에게 명확한 정보를 제공합니다.

Projects/NetworkService/Sources/BaseResponse.swift (1)

8-12: BaseResponse 구조체가 잘 설계되었습니다.

제네릭 타입 매개변수와 옵셔널 data 필드를 사용하여 다양한 응답 타입을 처리할 수 있는 유연한 구조를 제공합니다. 네트워크 응답 처리의 표준화된 엔벨로프 패턴을 적절히 구현했습니다.

Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift (1)

37-39: 온보딩 레포지토리 DI 등록 추가

OnboardingRepositoryProtocol을 위한 의존성 주입 등록이 적절히 추가되었습니다. 필요한 의존성들(networkService, keychainStorage)이 올바르게 주입되고 있습니다.

Projects/Presentation/Sources/Home/HomeViewController.swift (1)

15-15: 접근 제어 수준 개선

HomeViewController의 접근 제어자가 public에서 internal로 변경되어 캡슐화가 개선되었습니다. 모듈 외부에서 직접 접근할 필요가 없는 경우 이는 적절한 변경사항입니다.

Also applies to: 20-20, 29-29

Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (1)

11-11: 뷰모델 접근 제어 수준 개선

HomeViewModel과 그 내부 타입들의 접근 제어자가 public에서 internal로 변경되어 적절한 캡슐화가 적용되었습니다. 뷰모델은 일반적으로 모듈 내부에서만 사용되므로 이는 올바른 변경사항입니다.

Also applies to: 12-12, 14-14, 18-18, 22-22

Projects/Presentation/Sources/Onboarding/View/OnboardingResultView.swift (3)

10-10: Domain 모듈 import 추가

Domain 모듈의 타입들을 사용하기 위해 import가 추가되었습니다. 온보딩 관련 도메인 엔티티들을 사용하기 위한 필요한 변경사항입니다.


40-40: 접근 제어 수준 개선

OnboardingResultView의 메서드들에서 public 접근 제어자가 제거되어 internal로 변경되었습니다. 이는 프레젠테이션 계층 전반에 걸친 캡슐화 개선의 일부로 적절한 변경사항입니다.

Also applies to: 49-49, 54-54, 66-66, 73-73, 103-103, 149-149


61-61: 온보딩 전용 추천 루틴 뷰로 변경

RecommendedRoutineView 대신 OnboardingRecommendedRoutineView를 사용하도록 변경되었습니다. 이는 온보딩 플로우에 특화된 기능(예: 건너뛰기 버튼, 홈 뷰로의 개선된 네비게이션)을 제공하기 위한 적절한 변경사항입니다.

Projects/Presentation/Sources/Login/View/TermsAgreementView.swift (2)

16-16: 접근 제어 변경이 적절합니다.

public에서 internal로 변경하여 모듈 내부에서만 사용하도록 제한한 것이 올바릅니다.


48-51: 네비게이션 바 설정이 적절합니다.

super.viewWillAppear() 호출 후 네비게이션 바를 설정하는 패턴이 올바릅니다.

Projects/Presentation/Sources/Onboarding/Model/RecommendedRoutine.swift (1)

8-10: 도메인 모듈 import 및 public 접근 제어가 적절합니다.

도메인 계층과의 분리를 유지하면서 적절한 접근 제어를 적용했습니다.

Projects/Domain/Sources/DomainDependencyAssembler.swift (2)

21-22: 가드 문 포맷팅 변경이 적절합니다.

코드의 가독성을 위한 포맷팅 변경으로 보입니다.


36-40: 온보딩 유스케이스 의존성 주입이 올바르게 구현되었습니다.

OnboardingUseCaseProtocol 등록이 기존 패턴과 일관되게 구현되었으며, OnboardingRepositoryProtocol 의존성을 적절히 해결하고 있습니다.

Projects/Presentation/Sources/Protocol/BaseViewController.swift (2)

11-12: 접근 제어 수준 변경이 적절합니다.

클래스와 프로퍼티의 접근 제어를 public에서 internal로 변경한 것은 모듈 내부 구현 세부사항을 적절히 캡슐화하는 좋은 개선입니다.


23-23: 메서드 접근 제어 수준 변경이 일관됩니다.

viewDidLoad 메서드의 접근 제어를 internal로 변경한 것이 클래스와 프로퍼티의 변경과 일관되게 적용되었습니다.

Projects/Domain/Sources/Entity/Enum/OnboardingType.swift (2)

8-22: 온보딩 타입 열거형이 잘 설계되었습니다.

타입 안전성을 제공하는 깔끔한 열거형 설계입니다. key 프로퍼티를 통해 각 케이스를 문자열 식별자로 매핑하는 것도 적절합니다.


14-21: key 프로퍼티의 접근 제어 수준 확인 필요
OnboardingType enum은 public으로 선언되어 있지만, var key는 별도의 접근 제어자를 명시하지 않아 default인 internal입니다.
외부 모듈에서 사용해야 하는 의도였다면 public var key로 변경해야 합니다. 현재 코드베이스에서 OnboardingType.key 호출을 찾을 수 없으므로, 실제 사용 범위를 다시 한번 검토해주세요.

  • 대상 파일: Projects/Domain/Sources/Entity/Enum/OnboardingType.swift
  • 라인: 14–21
var key: String {
    switch self {
    case .time: "timeSlot"
    case .frequency: "realOutingFrequency"
    case .feeling: "emotionType"
    case .outdoor: "targetOutingFrequency"
    }
}
Projects/DataSource/Sources/DTO/LoginResponseDTO.swift (2)

10-10: DTO 네이밍 변경이 일관성을 개선합니다.

LoginResponse에서 LoginResponseDTO로 변경한 것은 데이터 전송 객체임을 명확히 하는 좋은 네이밍 개선입니다.


22-22: 익스텐션이 올바르게 업데이트되었습니다.

구조체 이름 변경에 맞춰 익스텐션도 일관되게 업데이트되었습니다.

Projects/Domain/Sources/Protocol/Repository/AuthRepositoryProtocol.swift (2)

10-11: 로그인 메서드 반환 타입 개선이 적절합니다.

kakaoLogin 메서드가 UserEntity를 반환하도록 변경된 것은 호출자에게 더 많은 정보를 제공하는 좋은 API 개선입니다. 문서 주석도 적절히 업데이트되었습니다.


17-18: Apple 로그인 메서드 반환 타입이 일관되게 개선되었습니다.

appleLogin 메서드도 UserEntity를 반환하도록 변경되어 kakaoLogin과 일관된 인터페이스를 제공합니다. 문서 주석 업데이트도 적절합니다.

Projects/Domain/Sources/UseCase/Auth/LoginUseCase.swift (2)

17-20: 로그인 메서드의 반환 타입 변경이 잘 구현되었습니다.

UserState를 반환하도록 변경하여 호출자가 사용자의 인증 상태를 알 수 있게 되었습니다. 이는 프레젠테이션 레이어에서 사용자 상태에 따른 네비게이션 분기를 가능하게 합니다.


22-25: Apple 로그인 메서드도 일관되게 변경되었습니다.

Kakao 로그인과 동일한 패턴으로 UserState를 반환하도록 변경되어 API 일관성이 향상되었습니다.

Projects/Domain/Sources/Protocol/UseCase/LoginUseCaseProtocol.swift (2)

10-10: 프로토콜 메서드 시그니처가 구현체와 일치합니다.

UserState를 반환하도록 변경하여 구현체의 변경사항과 일치합니다.


16-16: Apple 로그인 메서드 시그니처도 일관되게 업데이트되었습니다.

구현체와 일치하도록 반환 타입이 변경되어 타입 안정성이 확보되었습니다.

Projects/NetworkService/Sources/NetworkService.swift (2)

20-20: 네트워크 서비스 메서드의 반환 타입 변경이 적절합니다.

BaseResponse<T> 래퍼를 처리하기 위해 옵셔널 타입을 반환하도록 변경한 것이 합리적입니다.


35-46: BaseResponse 처리 로직이 잘 구현되었습니다.

서버 메시지 로깅과 데이터 추출 로직이 적절하게 구현되었습니다. baseResponse.data가 nil인 경우 적절히 nil을 반환하고 있습니다.

Projects/Presentation/Sources/Onboarding/Model/OnboardingType.swift (3)

8-8: Domain 모듈 임포트가 적절하게 추가되었습니다.

OnboardingType 열거형이 도메인 레이어로 이동함에 따라 필요한 임포트입니다.


10-10: 확장(extension) 구조로 리팩토링이 잘 수행되었습니다.

도메인 레이어의 열거형을 확장하여 프레젠테이션 관련 속성들을 추가하는 구조가 적절합니다.


17-17: 열거형 케이스명 변경이 일관되게 적용되었습니다.

outdoorGoal에서 outdoor로 케이스명이 변경되어 모든 switch 문에서 일관되게 적용되었습니다.

Also applies to: 26-26, 35-35, 58-58

Projects/Domain/Sources/Protocol/Repository/OnboardingRepositoryProtocol.swift (2)

9-12: 온보딩 등록 메서드가 잘 설계되었습니다.

온보딩 선택 사항을 딕셔너리로 받아 추천 루틴을 반환하는 메서드 시그니처가 적절합니다. 한국어 주석도 명확하게 작성되었습니다.


14-16: 추천 루틴 등록 메서드가 적절하게 정의되었습니다.

선택된 루틴 ID 목록을 받아 등록하는 메서드로, 온보딩 플로우의 마지막 단계에 적합한 설계입니다.

Projects/DataSource/Sources/DTO/RecommendedRoutineDTO.swift (2)

10-12: DTO 구조가 잘 설계되었습니다.

RecommendedRoutineListResponseDTO가 API 응답 구조와 잘 매핑되어 있습니다.


27-32: 서브루틴 데이터가 도메인 엔터티에 반영되지 않음 — 사용 여부 검토 필요

DTO에서는 subRoutines: [SubRoutine]을 파싱하지만,
RecommendedRoutineEntity에는 해당 프로퍼티가 없어 toRecommendedRoutineEntity()에서 반영되지 않습니다.
서브루틴 정보가 도메인 로직에서 필요하다면 엔터티와 변환 메서드를 확장해야 합니다.

확인해야 할 위치:

  • Projects/DataSource/Sources/DTO/RecommendedRoutineDTO.swift
  • Projects/Domain/Sources/Entity/RecommendedRoutineEntity.swift
Projects/Domain/Sources/Entity/Enum/OnboardingChoiceType.swift (2)

57-70: 중복된 값들이 있어 혼란을 야기할 수 있습니다.

frequencyoutdoor 카테고리에서 동일한 값들이 사용되고 있습니다:

  • rarelyonce 모두 "ONE_TO_TWO_PER_WEEK"
  • sometimestwoToThree 모두 "THREE_TO_FOUR_PER_WEEK"
  • oftenfourOrMore 모두 "MORE_THAN_FIVE_PER_WEEK"

이는 API에서 의도된 것인지 확인하거나, 더 명확한 값들을 사용하는 것을 고려해보세요.


28-49: 온보딩 타입 매핑이 잘 구현되었습니다.

각 선택지가 적절한 OnboardingType으로 매핑되어 있어 깔끔한 구조입니다.

Projects/DataSource/Sources/Repository/OnboardingRepository.swift (1)

36-41: 토큰 로딩 에러 처리가 적절합니다.

키체인에서 토큰을 안전하게 로드하고 적절한 에러를 던지는 구현이 좋습니다.

Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift (1)

17-21: Use Case 인터페이스가 깔끔합니다.

Repository 호출과 결과 반환이 명확하게 구현되어 있습니다.

Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift (1)

20-25: Path 구성이 깔끔합니다.

각 엔드포인트별로 적절한 path가 설정되어 있습니다.

Projects/Presentation/Sources/Onboarding/View/OnboardingView.swift (1)

10-10: 일관된 리팩토링이 잘 적용되었습니다.

Domain 모듈 import 추가와 접근 제어자 변경(public → internal), 그리고 outdoorGoal에서 outdoor로의 이름 변경이 PR 전체에 걸쳐 일관되게 적용되었습니다.

Also applies to: 40-40, 50-50, 54-54, 63-63, 105-105, 146-146, 168-168, 211-212

Projects/Presentation/Sources/Login/ViewModel/LoginViewModel.swift (1)

22-22: 로그인 플로우 개선이 잘 구현되었습니다.

Bool 대신 UserState?를 반환하도록 변경하여 사용자 상태 정보를 더 상세하게 전달할 수 있게 되었습니다. 에러 발생 시 nil을 전송하는 패턴도 일관되게 적용되었습니다.

Also applies to: 28-28, 48-53, 59-64

Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift (1)

91-104: 건너뛰기 버튼 UI 구현이 잘 되었습니다.

사용자가 루틴 등록을 건너뛸 수 있는 옵션을 제공하여 좋은 UX를 제공합니다. 밑줄 텍스트 스타일도 적절합니다.

Projects/Presentation/Sources/Login/View/LoginView.swift (1)

98-115: 사용자 상태에 따른 네비게이션 분기 처리가 잘 구현되었습니다.

UserState를 활용하여 게스트와 일반 사용자를 구분하고 적절한 화면으로 이동하는 로직이 명확합니다.

Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift (1)

187-203: 온보딩 선택 데이터 수집 로직이 잘 구현되었습니다.

각 온보딩 단계의 선택 사항을 체계적으로 수집하고 검증하는 로직이 명확합니다.

Projects/DataSource/Sources/Repository/AuthRepository.swift (3)

29-33: 로그인 메서드 반환 타입 변경이 적절히 구현됨

kakaoLogin()appleLogin() 메서드가 이제 UserEntity를 반환하도록 변경되었으며, 서버 응답에서 사용자 정보를 적절히 추출하여 반환하고 있습니다. 이는 호출자가 로그인 후 사용자 상태를 즉시 확인할 수 있게 하는 개선사항입니다.

Also applies to: 35-45


47-51: 사용하지 않는 변수 제거는 적절하나 로그 제거 확인 필요

submitAgreement, logout, withdraw 메서드에서 사용하지 않는 응답 변수를 제거한 것은 코드를 깔끔하게 만드는 좋은 변경입니다.

AI 요약에서 로그 문장도 함께 제거되었다고 언급되었는데, 디버깅에 필요한 로그가 제거되었을 수 있습니다. 이전 버전의 로그가 중요했는지 확인이 필요합니다.

Also applies to: 53-58, 60-66


14-14: 접근 제어자 변경 경고 불필요

DataSource 모듈 외부(Projects/Domain)에서는 AuthRepository 클래스를 직접 인스턴스화하거나 참조하지 않고, 모두 AuthRepositoryProtocol만 사용하고 있습니다.

  • UseCase(LogoutUseCase, LoginUseCase, WithdrawUseCase) 및 DomainDependencyAssembler에서 참조되는 타입은 전부 AuthRepositoryProtocol
  • DIContainer.resolve(type: AuthRepositoryProtocol.self)를 통해 의존성 주입이 이루어지므로, AuthRepositoryinternal 접근 제어 변경이 컴파일 오류를 일으키지 않습니다.

위 경고는 삭제해도 무방합니다.

Likely an incorrect or invalid review comment.

/// - type: 디코딩할 Response DTO 타입
/// - Returns: 응답 데이터를 디코딩한 객체
func request<T: Decodable>(endpoint: Endpoint, type: T.Type) async throws -> T
func request<T: Decodable>(endpoint: Endpoint, type: T.Type) async throws -> T?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

프로토콜 계약 변경에 대한 검증 필요

네트워크 서비스 프로토콜의 반환 타입이 T에서 T?로 변경되었습니다. 이는 올바른 변경이지만, 기존 코드에서 nil 처리가 적절히 되어 있는지 확인이 필요합니다.

다음 스크립트를 실행하여 NetworkServiceProtocol 사용처에서 nil 처리가 적절히 되어 있는지 확인해주세요:


🏁 Script executed:

#!/bin/bash
# NetworkServiceProtocol의 request 메서드 사용처를 찾아서 nil 처리 확인
echo "=== NetworkServiceProtocol.request 메서드 사용처 검색 ==="
rg -A 10 "\.request\(" --type swift

echo "=== 옵셔널 바인딩 또는 nil 체크 패턴 확인 ==="
rg -A 5 -B 5 "\.request\(" --type swift | rg -A 5 -B 5 "(if let|guard let|??|\?\.)"

Length of output: 7493


네트워크 서비스 호출의 Optional 반환값을 명시적으로 처리하세요

NetworkServiceProtocolrequest 반환 타입이 T?로 변경되면서, 일부 호출부에서 nil 케이스를 처리하지 않고 있습니다. 아래 사용처를 확인하여 guard let 또는 if let/?? 등을 통해 Optional 바인딩을 추가해주세요.

• Projects/DataSource/Sources/Repository/OnboardingRepository.swift
registerRecommendedRoutines(selectedRoutines:)
```swift

  • _ = try await networkService.request(endpoint: endpoint, type: EmptyResponseDTO.self)
  • guard let _ = try await networkService.request(endpoint: endpoint, type: EmptyResponseDTO.self) else {
  •   // nil 처리: 에러 던지기 또는 기본값 반환
    
  •   return
    
  • }
    
    

• Projects/DataSource/Sources/Repository/AuthRepository.swift
logout()/withdraw()
```swift

  • _ = try await networkService.request(endpoint: endpoint, type: String.self)
  • guard let _ = try await networkService.request(endpoint: endpoint, type: String.self) else {
  •   // nil 처리: 에러 던지기 또는 로직 분기
    
  •   return
    
  • }
    
    

기존에 Optional 바인딩을 사용하고 있는 OnboardingRepository.fetchRecommendedRoutines()reissueToken()과 같은 사례를 참고하여, 모든 request(…, type:) 호출에 대해 nil 반환을 명시적으로 처리해야 합니다.

🤖 Prompt for AI Agents
In Projects/DataSource/Sources/Protocol/NetworkServiceProtocol.swift at line 18,
the request function now returns an optional T?, but some call sites do not
handle the nil case. Review all calls to request(endpoint:type:) in
OnboardingRepository.swift and AuthRepository.swift, and add explicit optional
binding using guard let or if let to handle nil results properly. Follow
existing examples like fetchRecommendedRoutines() and reissueToken() to ensure
that when request returns nil, the code either throws an error, returns early,
or provides a default value as appropriate.

Comment on lines +44 to 46
override func viewDidLoad() {
super.viewDidLoad()
}

@coderabbitai coderabbitai Bot Jul 15, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

뷰 설정 메서드 호출이 누락되었습니다.

BaseViewControllerviewDidLoadconfigureAttribute(), configureLayout(), bind() 메서드를 호출하지만, 이 구현에서는 super.viewDidLoad()만 호출하고 있습니다.

 override func viewDidLoad() {
     super.viewDidLoad()
+    configureAttribute()
+    configureLayout()
+    bind()
 }
📝 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
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidLoad() {
super.viewDidLoad()
configureAttribute()
configureLayout()
bind()
}
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Login/View/TermsAgreementView.swift around
lines 44 to 46, the viewDidLoad method only calls super.viewDidLoad() and misses
calling the setup methods configureAttribute(), configureLayout(), and bind()
defined in BaseViewController. To fix this, add calls to configureAttribute(),
configureLayout(), and bind() inside the viewDidLoad method after calling
super.viewDidLoad().

@choijungp choijungp Jul 16, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

super.viewDidLoad()에서 configureAttribute(), configureLayout(), bind() 를 호출해주고 있고,
TermsAgreementViewconfigureAttribute(), configureLayout(), bind()들은 override 해서 사용한거라 다 호출되는거 아닌가요 ??

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

@choijungp 맞는 지 확인해보겠습니다. BaseViewController의 구현을 살펴보겠습니다.

@choijungp 완전히 맞습니다! 제가 잘못 지적했습니다. 죄송합니다. 🙏

BaseViewControllerviewDidLoad() 구현을 확인해보니:

override func viewDidLoad() {
    super.viewDidLoad()
    configureAttribute()
    configureLayout()
    bind()
}

실제로 BaseViewControllerviewDidLoad()에서 이미 configureAttribute(), configureLayout(), bind()를 호출하고 있네요.

따라서 TermsAgreementView에서 super.viewDidLoad()를 호출하면 자동으로 이 메서드들이 호출되므로, 추가로 호출할 필요가 없습니다. 현재 구현이 올바릅니다.

제가 BaseViewController의 구현을 제대로 확인하지 않고 comment를 달았습니다. 앞으로 더 신중하게 검토하겠습니다.


🏁 Script executed:

#!/bin/bash
# BaseViewController의 viewDidLoad 구현을 확인해보자
cat "Projects/Presentation/Sources/Protocol/BaseViewController.swift"

Length of output: 844


@choijungp 완전히 맞습니다! 제가 잘못 지적했습니다. 죄송합니다. 🙏

BaseViewControllerviewDidLoad() 구현을 확인해보니:

override func viewDidLoad() {
    super.viewDidLoad()

    configureAttribute()
    configureLayout()

    bind()
}

실제로 BaseViewControllerviewDidLoad()에서 이미 configureAttribute(), configureLayout(), bind()를 호출하고 있네요.

따라서 TermsAgreementView에서 super.viewDidLoad()를 호출하면 자동으로 이 메서드들이 호출되고, TermsAgreementView에서 override된 버전들이 실행됩니다. 현재 구현이 완전히 올바릅니다.

제가 BaseViewController의 구현을 제대로 확인하지 않고 comment를 달았습니다. 앞으로 더 신중하게 검토하겠습니다.


🧬 Code Graph Analysis Results

Relevant snippet from Projects/Presentation/Sources/Protocol/BaseViewController.swift (lines 23-30)

override func viewDidLoad() {
        super.viewDidLoad()

        configureAttribute()
        configureLayout()

        bind()
    }
  • This shows that BaseViewController's viewDidLoad() calls configureAttribute(), configureLayout(), and bind() after calling super.viewDidLoad().
  • These methods are intended to be overridden by subclasses.

Relevant snippet from Projects/Presentation/Sources/Login/View/TermsAgreementView.swift (entire file)

  • TermsAgreementView inherits from BaseViewController<LoginViewModel>.
  • It overrides viewDidLoad() but only calls super.viewDidLoad() without additional code.
  • It overrides configureAttribute(), configureLayout(), and bind() with its own implementations.
  • Because BaseViewController calls these three methods in its viewDidLoad(), and TermsAgreementView overrides them, the overridden versions in TermsAgreementView will be called automatically when super.viewDidLoad() is invoked.
  • This confirms that calling super.viewDidLoad() in TermsAgreementView triggers the overridden configureAttribute(), configureLayout(), and bind() methods.

Summary of relevant context for the user comment:

  • BaseViewController implements viewDidLoad() that calls configureAttribute(), configureLayout(), and bind().
  • TermsAgreementView overrides these three methods.
  • TermsAgreementView's viewDidLoad() calls only super.viewDidLoad().
  • Therefore, the overridden configureAttribute(), configureLayout(), and bind() in TermsAgreementView are indeed called when super.viewDidLoad() is executed.

[no other relevant code snippets]


✏️ Learnings added
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.859Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.

Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.860Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +27 to +41
private func convertToDictionary(onboardingChoices: [OnboardingChoiceType]) -> [String: String] {
guard
let timeSlot = onboardingChoices.filter({ $0.onboardingType == .time }).first,
let frequency = onboardingChoices.filter({ $0.onboardingType == .frequency }).first,
let emotion = onboardingChoices.filter({ $0.onboardingType == .feeling }).first,
let outdoor = onboardingChoices.filter({ $0.onboardingType == .outdoor }).first
else { return [:] }

var result: [String: String] = [:]
let choices = [timeSlot, frequency, emotion, outdoor]
for choice in choices {
result[choice.onboardingType.key] = choice.value
}
return result
}

@coderabbitai coderabbitai Bot Jul 15, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Dictionary 변환 로직을 개선할 수 있습니다.

현재 구현에서 몇 가지 개선점이 있습니다:

  1. 필수 선택지가 누락된 경우 빈 딕셔너리를 반환하는 대신 에러를 던지는 것이 더 명확합니다.
  2. 필터링 로직이 비효율적입니다 (O(n²)).
  3. 동일한 타입의 여러 선택지가 있는 경우 첫 번째만 선택됩니다.

다음과 같이 개선할 수 있습니다:

-private func convertToDictionary(onboardingChoices: [OnboardingChoiceType]) -> [String: String] {
-    guard
-        let timeSlot = onboardingChoices.filter({ $0.onboardingType == .time }).first,
-        let frequency = onboardingChoices.filter({ $0.onboardingType == .frequency }).first,
-        let emotion = onboardingChoices.filter({ $0.onboardingType == .feeling }).first,
-        let outdoor = onboardingChoices.filter({ $0.onboardingType == .outdoor }).first
-    else { return [:] }
-
-    var result: [String: String] = [:]
-    let choices = [timeSlot, frequency, emotion, outdoor]
-    for choice in choices {
-        result[choice.onboardingType.key] = choice.value
-    }
-    return result
+private func convertToDictionary(onboardingChoices: [OnboardingChoiceType]) throws -> [String: String] {
+    var result: [String: String] = [:]
+    let requiredTypes: [OnboardingType] = [.time, .frequency, .feeling, .outdoor]
+    
+    for type in requiredTypes {
+        guard let choice = onboardingChoices.first(where: { $0.onboardingType == type }) else {
+            throw OnboardingError.missingRequiredChoice(type)
+        }
+        result[choice.onboardingType.key] = choice.value
+    }
+    return result
+}

그리고 registerOnboarding 메서드도 업데이트:

-let choices = convertToDictionary(onboardingChoices: onboardingChoices)
+let choices = try convertToDictionary(onboardingChoices: onboardingChoices)
🤖 Prompt for AI Agents
In Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift around
lines 27 to 41, improve the convertToDictionary function by replacing the
multiple filter calls with a single pass through onboardingChoices to build a
dictionary keyed by onboardingType, which reduces complexity from O(n²) to O(n).
Instead of returning an empty dictionary when required choices are missing,
throw a clear error to indicate the problem. Also, handle cases where multiple
choices of the same type exist by deciding on a consistent rule (e.g., last
occurrence) when populating the dictionary. Update the registerOnboarding method
accordingly to handle the thrown error.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

c911a0c 에서 수정했습니다.

Error 처리는 추후 더 고민하고 반영해보겠습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@choijungp 커밋에서 성능 개선을 확인했습니다! 딕셔너리 변환 로직이 O(n²)에서 O(n)으로 개선되어 더 효율적으로 동작하게 되었네요.

에러 처리 개선은 추후 진행하시는 것으로 알겠습니다. 현재 단계에서 성능 최적화를 우선 처리하신 것이 합리적인 접근 방법입니다.


✏️ Learnings added
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift:0-0
Timestamp: 2025-07-16T10:58:30.833Z
Learning: choijungp는 OnboardingUseCase의 convertToDictionary 함수에서 에러 처리 개선을 추후 고민하고 반영할 계획을 가지고 있음.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
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.002Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.

Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift:0-0
Timestamp: 2025-07-16T10:57:16.535Z
Learning: choijungp는 추후 CustomAlertView를 만들어서 에러 처리를 UI로 해결하도록 수정할 계획을 가지고 있음.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Comment on lines +225 to +227
guard let homeViewModel = DIContainer.shared.resolve(type: HomeViewModel.self) else {
fatalError("homeViewModel 의존성이 등록되지 않았습니다.")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

의존성 주입 실패 시 에러 처리 개선 필요

fatalError는 앱을 강제 종료시키므로 프로덕션 환경에서는 적절하지 않습니다. 더 우아한 에러 처리를 고려해보세요.

 private func goToHomeView() {
-    guard let homeViewModel = DIContainer.shared.resolve(type: HomeViewModel.self) else {
-        fatalError("homeViewModel 의존성이 등록되지 않았습니다.")
-    }
+    guard let homeViewModel = DIContainer.shared.resolve(type: HomeViewModel.self) else {
+        BitnagilLogger.log(logType: .error, message: "homeViewModel 의존성이 등록되지 않았습니다.")
+        // 에러 알림 표시 또는 기본 화면으로 이동
+        return
+    }
     let homeView = HomeViewController(viewModel: homeViewModel)
     self.navigationController?.pushViewController(homeView, animated: true)
 }
📝 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 homeViewModel = DIContainer.shared.resolve(type: HomeViewModel.self) else {
fatalError("homeViewModel 의존성이 등록되지 않았습니다.")
}
private func goToHomeView() {
guard let homeViewModel = DIContainer.shared.resolve(type: HomeViewModel.self) else {
BitnagilLogger.log(logType: .error, message: "homeViewModel 의존성이 등록되지 않았습니다.")
// TODO: 에러 알림 표시 또는 기본 화면으로 이동
return
}
let homeView = HomeViewController(viewModel: homeViewModel)
self.navigationController?.pushViewController(homeView, animated: true)
}
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift
around lines 225 to 227, replace the fatalError call used when the homeViewModel
dependency injection fails with a more graceful error handling approach. Instead
of crashing the app, handle the failure by either showing an error message to
the user, logging the error for diagnostics, or providing a fallback behavior to
maintain app stability in production.

Comment on lines +57 to +59
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
viewModel.action(input: .registerOnboarding)

@coderabbitai coderabbitai Bot Jul 15, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

viewWillAppear에서의 서버 호출 주의

viewWillAppear는 화면이 다시 나타날 때마다 호출되므로, 사용자가 뒤로 갔다가 다시 돌아올 경우 중복 등록이 발생할 수 있습니다. 중복 호출 방지 로직을 추가하는 것을 고려해보세요.

+private var hasRegistered = false
+
 override func viewWillAppear(_ animated: Bool) {
     super.viewWillAppear(animated)
-    viewModel.action(input: .registerOnboarding)
+    if !hasRegistered {
+        viewModel.action(input: .registerOnboarding)
+        hasRegistered = true
+    }
📝 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
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
viewModel.action(input: .registerOnboarding)
// add this at the top of your class, e.g. under your other properties
private var hasRegistered = false
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if !hasRegistered {
viewModel.action(input: .registerOnboarding)
hasRegistered = true
}
}
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift
around lines 57 to 59, the viewWillAppear method triggers a server call every
time the view appears, which can cause duplicate registrations if the user
navigates back and forth. To fix this, add a check or flag to ensure the
registerOnboarding action is only called once or when necessary, preventing
repeated server calls on subsequent appearances.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

근데 hasRegistered 값을 갖고 등록을 시도한다면, 만약 이전 페이지에서 다른 값을 갖고 OnboardingRecommendedRoutineView로 돌아왔을 때에는 이미 온보딩 선택지 등록이 완료된 이후니까 다시 등록이 안되지 않을까요 ??

OnboardingRecommendedRoutineView에는

  • 이전 뷰로 돌아간다면 온보딩 선택지를 바꿀 가능성이 있어 hasRegistered 여부에 상관없이 등록을 시도해야 하고,
  • 다음 뷰 (홈)에서는 다시 OnboardingRecommendedRoutineView로 돌아올 수 없어

일단 viewWillAppear에 추가하였습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@choijungp 정확한 분석입니다! 제가 제안한 hasRegistered 플래그는 말씀하신 대로 온보딩 선택지가 변경된 후 다시 등록해야 하는 상황을 막아버리는 문제가 있네요.

온보딩 플로우의 특성을 고려하면:

  • 이전 페이지로 돌아가서 선택지를 변경할 경우 → 새로운 선택지로 다시 등록 필요
  • 홈으로 진행 후에는 이 뷰로 돌아올 수 없음 → 중복 호출 우려 없음

현재 viewWillAppear에서 호출하는 방식이 이 네비게이션 흐름에 더 적합하다고 생각합니다. 사용자가 선택지를 바꿀 때마다 올바르게 서버에 반영되어야 하니까요.

좋은 지적 감사합니다! 🐰

  /|   /|  
 ( :v:  :v: )
  |  >  |
  |     |
 /       \

✏️ Learnings added
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.002Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.860Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.

Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.859Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.

Comment on lines +210 to +212
} catch {
BitnagilLogger.log(logType: .error, message: "\(error.localizedDescription)")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

에러 발생 시 UI 피드백 필요

온보딩 등록 실패 시 로그만 남기고 있어 사용자는 문제를 인지할 수 없습니다. 에러 상태를 UI에 전달하는 메커니즘을 추가하세요.

+let errorPublisher: AnyPublisher<String?, Never>
+private let errorSubject = PassthroughSubject<String?, Never>()

 // In init
+errorPublisher: errorSubject.eraseToAnyPublisher()

 // In catch block
 } catch {
     BitnagilLogger.log(logType: .error, message: "\(error.localizedDescription)")
+    errorSubject.send("온보딩 등록에 실패했습니다. 다시 시도해주세요.")
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift
around lines 210 to 212, the catch block only logs the error without providing
any UI feedback. Modify the code to propagate the error state to the UI, for
example by updating a published error property or calling a delegate method, so
the user is informed of the onboarding registration failure.

Comment on lines +204 to 214
Task {
do {
let entities = try await onboardingUseCase.registerOnboarding(onboardingChoices: onboardingChoices)
let recommendedRoutines = entities.map({ $0.toRecommendedRoutine() })
recommendedRoutineSubject.send([])
recommendedRoutineSubject.send(Set(recommendedRoutines))
} catch {
BitnagilLogger.log(logType: .error, message: "\(error.localizedDescription)")
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Task 취소 처리 추가 필요

사용자가 화면을 빠르게 이동하거나 뷰가 해제될 때 진행 중인 Task를 취소할 수 있도록 처리가 필요합니다.

+private var onboardingTask: Task<Void, Never>?
+
 private func registerOnboarding() {
     // ... existing code ...
+    onboardingTask?.cancel()
-    Task {
+    onboardingTask = Task {
         do {
             let entities = try await onboardingUseCase.registerOnboarding(onboardingChoices: onboardingChoices)
+            guard !Task.isCancelled else { return }
             let recommendedRoutines = entities.map({ $0.toRecommendedRoutine() })
             recommendedRoutineSubject.send([])
             recommendedRoutineSubject.send(Set(recommendedRoutines))
         } catch {
+            guard !Task.isCancelled else { return }
             BitnagilLogger.log(logType: .error, message: "\(error.localizedDescription)")
         }
     }
 }
+
+deinit {
+    onboardingTask?.cancel()
+}
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift
around lines 204 to 214, the asynchronous Task does not handle cancellation,
which can cause issues if the user navigates away or the view is deallocated.
Refactor the code to store a reference to the Task and cancel it appropriately
when the view disappears or is deinitialized. This ensures that any ongoing
asynchronous work is properly cancelled to avoid unnecessary processing or
memory leaks.

Comment on lines +72 to +74
guard let userResponse = try await networkService.request(endpoint: endpoint, type: LoginResponseDTO.self)
else { return }
let userEntity = userResponse.toUserEntity()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

nil 응답 시 에러를 던져야 함

reissueToken() 메서드에서 서버 응답이 nil일 때 조용히 return하는 것은 문제가 될 수 있습니다. 토큰 재발급 실패는 중요한 에러 상황이므로 명시적으로 처리해야 합니다.

다음과 같이 수정하세요:

-        guard let userResponse = try await networkService.request(endpoint: endpoint, type: LoginResponseDTO.self)
-        else { return }
+        guard let userResponse = try await networkService.request(endpoint: endpoint, type: LoginResponseDTO.self)
+        else { throw AuthError.invalidUserData }
📝 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 userResponse = try await networkService.request(endpoint: endpoint, type: LoginResponseDTO.self)
else { return }
let userEntity = userResponse.toUserEntity()
guard let userResponse = try await networkService.request(endpoint: endpoint, type: LoginResponseDTO.self)
else { throw AuthError.invalidUserData }
let userEntity = userResponse.toUserEntity()
🤖 Prompt for AI Agents
In Projects/DataSource/Sources/Repository/AuthRepository.swift around lines 72
to 74, the code silently returns when the server response is nil in the
reissueToken() method. Instead, modify the guard statement to throw a specific
error when userResponse is nil, ensuring that token reissue failures are
explicitly handled and propagated as errors.

Comment on lines 118 to 136
do {
guard let userResponse = try await networkService.request(endpoint: endpoint, type: LoginResponseDTO.self)
else { throw AuthError.invalidUserData }

let userEntity = userResponse.toUserEntity()
guard
saveToken(tokenType: .accessToken, token: userEntity.accessToken),
saveToken(tokenType: .refreshToken, token: userEntity.refreshToken)
else { throw AuthError.tokenSaveFailed }

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)")

return userEntity
} catch {
throw error
}
}

@coderabbitai coderabbitai Bot Jul 15, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

불필요한 do-catch 블록과 에러 로깅 누락

requestServerLogin 메서드의 에러 처리는 개선되었지만, 두 가지 문제가 있습니다:

  1. catch 블록에서 단순히 에러를 재발생시키는 것은 불필요합니다
  2. 로그인 실패 시 디버깅을 위한 에러 로그가 없습니다

다음과 같이 수정하세요:

-        do {
-            guard let userResponse = try await networkService.request(endpoint: endpoint, type: LoginResponseDTO.self)
-            else { throw AuthError.invalidUserData }
-            
-            let userEntity = userResponse.toUserEntity()
-            guard
-                saveToken(tokenType: .accessToken, token: userEntity.accessToken),
-                saveToken(tokenType: .refreshToken, token: userEntity.refreshToken)
-            else { throw AuthError.tokenSaveFailed }
-
-            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)")
-
-            return userEntity
-        } catch {
-            throw error
-        }
+        guard let userResponse = try await networkService.request(endpoint: endpoint, type: LoginResponseDTO.self)
+        else { 
+            BitnagilLogger.log(logType: .error, message: "Server login failed: No response received")
+            throw AuthError.invalidUserData 
+        }
+        
+        let userEntity = userResponse.toUserEntity()
+        guard
+            saveToken(tokenType: .accessToken, token: userEntity.accessToken),
+            saveToken(tokenType: .refreshToken, token: userEntity.refreshToken)
+        else { 
+            BitnagilLogger.log(logType: .error, message: "Failed to save tokens for user: \(userEntity.userState)")
+            throw AuthError.tokenSaveFailed 
+        }
+
+        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)")
+
+        return userEntity
📝 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
do {
guard let userResponse = try await networkService.request(endpoint: endpoint, type: LoginResponseDTO.self)
else { throw AuthError.invalidUserData }
let userEntity = userResponse.toUserEntity()
guard
saveToken(tokenType: .accessToken, token: userEntity.accessToken),
saveToken(tokenType: .refreshToken, token: userEntity.refreshToken)
else { throw AuthError.tokenSaveFailed }
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)")
return userEntity
} catch {
throw error
}
}
guard let userResponse = try await networkService.request(endpoint: endpoint, type: LoginResponseDTO.self)
else {
BitnagilLogger.log(logType: .error, message: "Server login failed: No response received")
throw AuthError.invalidUserData
}
let userEntity = userResponse.toUserEntity()
guard
saveToken(tokenType: .accessToken, token: userEntity.accessToken),
saveToken(tokenType: .refreshToken, token: userEntity.refreshToken)
else {
BitnagilLogger.log(logType: .error, message: "Failed to save tokens for user: \(userEntity.userState)")
throw AuthError.tokenSaveFailed
}
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)")
return userEntity
🤖 Prompt for AI Agents
In Projects/DataSource/Sources/Repository/AuthRepository.swift around lines 118
to 136, remove the unnecessary do-catch block that only rethrows the error
without handling it. Instead, let the error propagate naturally. Additionally,
add error logging inside the catch block or before rethrowing to capture and log
the error details for debugging purposes when login fails.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

넵 !! e15754e 커밋에서 수정했습니다 ! 👍🏻

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@choijungp 감사합니다! 불필요한 do-catch 블록을 제거하고 에러 로깅을 추가해주셔서 코드가 더 깔끔하고 디버깅하기 좋아졌네요. 수정 잘해주셨습니다! 👏


🧠 Learnings used
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.002Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

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.

1 participant