Skip to content

[Feat-T3-184] 설명서 기능 구현#60

Merged
choijungp merged 3 commits intodevelopfrom
feat/tutorial
Sep 4, 2025
Merged

[Feat-T3-184] 설명서 기능 구현#60
choijungp merged 3 commits intodevelopfrom
feat/tutorial

Conversation

@choijungp
Copy link
Copy Markdown
Contributor

@choijungp choijungp commented Sep 3, 2025

🌁 Background

설명서 기능을 구현했어요 ~
쏘이지 ^.^

📱 Screenshot

1. 설명서

iPhone SE3 iPhone 13 mini iPhone 16 Pro
Simulator Screenshot - iPhone SE (3rd generation) - 2025-09-03 at 15 31 26 Simulator Screenshot - iPhone 13 mini - 2025-09-03 at 15 27 14 Simulator Screenshot - iPhone 16 Pro - 2025-09-03 at 15 29 45

2. 오늘 감정 등록하기 설명서

iPhone SE3 iPhone 13 mini iPhone 16 Pro
Simulator Screenshot - iPhone SE (3rd generation) - 2025-09-03 at 15 31 29 Simulator Screenshot - iPhone 13 mini - 2025-09-03 at 15 27 16 Simulator Screenshot - iPhone 16 Pro - 2025-09-03 at 15 29 47

3. 홈에서 더보기 설명서

iPhone SE3 iPhone 13 mini iPhone 16 Pro
Simulator Screenshot - iPhone SE (3rd generation) - 2025-09-03 at 15 31 33 Simulator Screenshot - iPhone 13 mini - 2025-09-03 at 15 27 19 Simulator Screenshot - iPhone 16 Pro - 2025-09-03 at 15 29 49

4. 맞춤 추천 루틴 설명서

iPhone SE3 iPhone 13 mini iPhone 16 Pro
Simulator Screenshot - iPhone SE (3rd generation) - 2025-09-03 at 15 31 37 Simulator Screenshot - iPhone 13 mini - 2025-09-03 at 15 27 22 Simulator Screenshot - iPhone 16 Pro - 2025-09-03 at 15 29 52

5. 루틴 수정하기 설명서

iPhone SE3 iPhone 13 mini iPhone 16 Pro
Simulator Screenshot - iPhone SE (3rd generation) - 2025-09-03 at 15 31 40 Simulator Screenshot - iPhone 13 mini - 2025-09-03 at 15 27 24 Simulator Screenshot - iPhone 16 Pro - 2025-09-03 at 15 29 56

👩‍💻 Contents

  • 설명서 UI 구현

📣 Related Issue

  • close #T3-184

Summary by CodeRabbit

  • 신기능
    • 홈 화면 도움말 버튼으로 “설명서” 화면 진입 가능.
    • 설명서 목록(4개 항목)에서 항목 선택 시 제목·설명·이미지 포함 상세가 하단 시트로 표시되며 디밍 애니메이션과 iOS 버전별 시트 높이 처리를 지원.
    • 튜토리얼 주제 4종 추가(감정 등록, 홈 더보기, 추천 루틴, 루틴 편집).
    • 고해상도 튜토리얼 그래픽 리소스(1x/2x/3x) 등록 및 관련 UI 컴포넌트 추가.

@choijungp choijungp requested a review from taipaise September 3, 2025 06:40
@choijungp choijungp self-assigned this Sep 3, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Sep 3, 2025

Walkthrough

튜토리얼 기능을 추가했다. 튜토리얼 이미지 자산을 등록하고 BitnagilGraphic에 이미지 참조를 추가했으며, Tutorial 모델과 목록/셀/상세 뷰컨트롤러를 도입하고 홈의 도움말 버튼에 튜토리얼 화면으로 이동하는 액션을 연결했다.

Changes

Cohort / File(s) Summary
Asset Catalog: Tutorial 그룹 추가
Projects/Presentation/Resources/Images.xcassets/Tutorial/Contents.json, .../Tutorial/tutorial_emotion_graphic.imageset/Contents.json, .../Tutorial/tutorial_home_more_graphic.imageset/Contents.json, .../Tutorial/tutorial_recommendation_routine_graphic.imageset/Contents.json, .../Tutorial/tutorial_edit_routine_graphic.imageset/Contents.json
튜토리얼용 자산 카탈로그 및 4개 이미지셋 메타데이터(1x/2x/3x) 추가.
DesignSystem: 이미지 참조 추가
Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift
튜토리얼 그래픽 4종에 대한 정적 UIImage 프로퍼티 추가.
Home: 도움말 버튼 네비게이션
Projects/Presentation/Sources/Home/View/HomeViewController.swift
helpButton 터치업액션에 TutorialViewController 생성 및 네비게이션 푸시 추가 (hidesBottomBarWhenPushed = true, [weak self]).
Tutorial: 모델
Projects/Presentation/Sources/Tutorial/Model/Tutorial.swift
enum Tutorial: CaseIterable 추가. 케이스별 title, description, tutorialImage 프로퍼티 구현.
Tutorial: 셀 컴포넌트
Projects/Presentation/Sources/Tutorial/View/Component/TutorialTableCell.swift
TutorialTableCell 추가(제목 라벨, 우측 chevron 아이콘, 레이아웃 및 스타일 설정).
Tutorial: 상세 화면
Projects/Presentation/Sources/Tutorial/View/TutorialDetailViewController.swift
TutorialDetailViewController 추가(타이틀/설명/이미지 표시, 시트 표시 구성, onDismiss 콜백).
Tutorial: 목록 및 프레젠테이션
Projects/Presentation/Sources/Tutorial/View/TutorialViewController.swift
튜토리얼 목록(섹션=케이스) 구현. 셀 탭 시 dimmed 오버레이와 시트 형식으로 상세 화면 표시, iOS 버전별 detent 처리 및 onDismiss로 오버레이 제거.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User as 사용자
  participant Home as HomeViewController
  participant Nav as UINavigationController
  participant TutorialVC as TutorialViewController

  User->>Home: 도움말 버튼 탭
  Home->>TutorialVC: 생성
  Home->>Nav: push(TutorialViewController)
  Nav-->>User: 튜토리얼 목록 표시
Loading
sequenceDiagram
  autonumber
  actor User as 사용자
  participant TutorialVC as TutorialViewController
  participant Overlay as DimmedView
  participant Detail as TutorialDetailViewController
  participant Sheet as SheetPresentationController

  User->>TutorialVC: 셀 탭
  TutorialVC->>Overlay: 기존 제거 후 새 오버레이 추가 및 페이드인
  TutorialVC->>Detail: Tutorial 인스턴스로 초기화
  TutorialVC->>Sheet: 시트 detent/코너/스크롤 확장 설정
  TutorialVC->>Detail: present(as: sheet)
  Note over Detail,Sheet: 상세 내용 표시

  User-->>Detail: 닫기(스와이프/내리기)
  Detail->>TutorialVC: onDismiss 콜백
  TutorialVC->>Overlay: 페이드아웃 후 제거
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

깡충깡충, 새 그림이 도착했네 🐇
버튼 톡 한 번에 창이 펴지고
제목 반짝, 설명 속삭이며
시트 위에 그림이 살포시 내려앉네.
토끼가 축하해요 — 튜토리얼 준비 끝! ✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/tutorial

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • 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.
  • 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit 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.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (18)
Projects/Presentation/Resources/Images.xcassets/Tutorial/Contents.json (1)

1-6: 상위 Tutorial 그룹 생성 잘 됨 + 네임스페이스 제공 옵션 제안

에셋 이름 충돌을 피하려면 상위 폴더에 provides-namespace를 켜두는 것을 추천합니다.

다음처럼 속성 블록을 추가해 주세요:

 {
-  "info" : {
-    "author" : "xcode",
-    "version" : 1
-  }
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "provides-namespace" : true
+  }
 }
Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift (2)

37-41: 튜토리얼 이미지 상수 추가 LGTM + 문자열 하드코딩 리스크 완화 제안

문자열 하드코딩(타이포 위험) 완화를 위해 최소한 이름 상수를 한 곳으로 모으거나(예: private enum) 코드 생성 도구(SwiftGen 등) 도입을 권장합니다. 현재 상태로도 동작에는 문제 없습니다.

아래와 같이 문자열을 enum으로 치환하는 소규모 변경을 고려해 보세요:

-    static let tutorialEmotionGraphic = UIImage(named: "tutorial_emotion_graphic", in: bundle, with: nil)
-    static let tutorialHomeMoreGraphic = UIImage(named: "tutorial_home_more_graphic", in: bundle, with: nil)
-    static let tutorialRecommendationRoutineGraphic = UIImage(named: "tutorial_recommendation_routine_graphic", in: bundle, with: nil)
-    static let tutorialEditRoutineGraphic = UIImage(named: "tutorial_edit_routine_graphic", in: bundle, with: nil)
+    static let tutorialEmotionGraphic = UIImage(named: AssetName.tutorialEmotionGraphic.rawValue, in: bundle, with: nil)
+    static let tutorialHomeMoreGraphic = UIImage(named: AssetName.tutorialHomeMoreGraphic.rawValue, in: bundle, with: nil)
+    static let tutorialRecommendationRoutineGraphic = UIImage(named: AssetName.tutorialRecommendationRoutineGraphic.rawValue, in: bundle, with: nil)
+    static let tutorialEditRoutineGraphic = UIImage(named: AssetName.tutorialEditRoutineGraphic.rawValue, in: bundle, with: nil)

아래 코드는 선택 사항으로, 파일 아무 곳(예: enum 끝 직전)에 추가해 주세요:

// 문자열 하드코딩 회피용
private enum AssetName: String {
    case tutorialEmotionGraphic = "tutorial_emotion_graphic"
    case tutorialHomeMoreGraphic = "tutorial_home_more_graphic"
    case tutorialRecommendationRoutineGraphic = "tutorial_recommendation_routine_graphic"
    case tutorialEditRoutineGraphic = "tutorial_edit_routine_graphic"
}

37-41: 에셋 자동 검증 스크립트 통합 권장
제공된 스크립트로 BitnagilGraphic에 선언된 튜토리얼 에셋(4개)이 모두 정상 존재함을 확인했습니다. CI 파이프라인에 해당 스크립트를 추가해 런타임 누락 에셋 검증을 자동화하세요.

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

121-128: 도움말 중복 푸시 방지 및 접근성 라벨 추가 권장

빠른 연속 탭 시 동일 VC가 스택에 중복 푸시될 수 있어 가드가 있으면 안전합니다. 또한 접근성 라벨/힌트를 추가해 주세요.

-        helpButton.addAction(
-            UIAction { [weak self] _ in
-                let tutorialView = TutorialViewController()
-                tutorialView.hidesBottomBarWhenPushed = true
-                self?.navigationController?.pushViewController(tutorialView, animated: true)
-            },
-            for: .touchUpInside)
+        helpButton.addAction(
+            UIAction { [weak self] _ in
+                guard let self = self,
+                      let nav = self.navigationController,
+                      !(nav.topViewController is TutorialViewController) else { return }
+                let tutorialView = TutorialViewController()
+                tutorialView.hidesBottomBarWhenPushed = true
+                nav.pushViewController(tutorialView, animated: true)
+            },
+            for: .touchUpInside)

추가(선택): 아래를 동일 메서드 내에 함께 설정

helpButton.accessibilityLabel = "설명서"
helpButton.accessibilityHint = "앱 사용 방법을 확인합니다"
Projects/Presentation/Sources/Tutorial/Model/Tutorial.swift (2)

29-40: 프로퍼티명 ‘description’ 충돌 여지 및 로컬라이제이션 고려

description은 Swift 전반에서 의미가 특수하므로 혼선 방지를 위해 detailText 등으로의 변경을 고려해 주세요. 문자열은 하드코딩보다 로컬라이제이션 키로 관리하면 확장성이 좋아집니다.


42-53: 이미지 옵셔널 처리 정책 확인

tutorialImageUIImage?라 뷰에서 nil이면 빈 이미지가 표시됩니다. 기본 플레이스홀더를 반환하도록 non-optional로 두는 것도 방법입니다(디자인 시스템 내 기본 이미지가 있다면).

Projects/Presentation/Sources/Tutorial/View/Component/TutorialTableCell.swift (3)

12-16: 오탈자: titleLableLeadingSpacingtitleLabelLeadingSpacing

상수명 오탈자 수정 권장(사용처 포함).

-        static let titleLableLeadingSpacing: CGFloat = 20
+        static let titleLabelLeadingSpacing: CGFloat = 20

51-55: 레이블 트렁케이션/겹침 방지 제약 추가

타이틀이 길場合 chevron과 겹칠 수 있습니다. trailing 제약을 추가해 주세요.

         titleLabel.snp.makeConstraints { make in
-            make.leading.equalToSuperview().offset(Layout.titleLableLeadingSpacing)
+            make.leading.equalToSuperview().offset(Layout.titleLabelLeadingSpacing)
             make.centerY.equalToSuperview()
+            make.trailing.lessThanOrEqualTo(chevronIcon.snp.leading).offset(-8)
         }

35-45: 셀 배경/코너 처리와 디스클로저 인디케이터

셀의 layer 대신 contentView에 코너를 주면 시스템 배경과 더 잘 어울립니다. 또한 기본 accessoryType = .disclosureIndicator 사용을 고려하면 접근성 측면에서 유리합니다(커스텀 아이콘 유지가 목적이면 현행 유지 가능).

Projects/Presentation/Sources/Tutorial/View/TutorialDetailViewController.swift (4)

47-60: 동적 폰트/접근성 대응: 고정 줄수(2) 대신 자동 줄바꿈 권장

설명 라벨을 numberOfLines = 0으로 두고 높이 제약을 제거하면 다양한 폰트 크기에 안전합니다.

-        descriptionLabel.attributedText = BitnagilFont(style: .body2, weight: .medium).attributedString(text: descriptionText)
-        descriptionLabel.textColor = BitnagilColor.gray40
-        descriptionLabel.numberOfLines = 2
+        descriptionLabel.attributedText = BitnagilFont(style: .body2, weight: .medium).attributedString(text: descriptionText)
+        descriptionLabel.textColor = BitnagilColor.gray40
+        descriptionLabel.numberOfLines = 0

69-80: 레이블 높이 고정 해제(오토레이아웃 모호성/잘림 방지)

고정 높이 제약은 다국어/동적 폰트에서 잘림을 유발할 수 있습니다. 제거 권장.

         titleLabel.snp.makeConstraints { make in
             make.top.equalTo(safeArea).offset(Layout.titleLabelTopSpacing)
             make.leading.equalTo(safeArea).offset(Layout.horizontalMargin)
-            make.height.equalTo(Layout.titleLabelHeight)
         }

         descriptionLabel.snp.makeConstraints { make in
             make.top.equalTo(titleLabel.snp.bottom).offset(Layout.descriptionLabelTopSpacing)
             make.leading.equalTo(safeArea).offset(Layout.horizontalMargin)
             make.trailing.equalTo(safeArea).inset(Layout.horizontalMargin)
-            make.height.equalTo(Layout.descriptionLabelHeight)
         }

82-86: 이미지 뷰 하단 제약 추가(작은 화면에서 오버플로 방지)

최소한 safeArea 하단 이하로 넘치지 않도록 제약을 추가해 주세요.

         tutorialImage.snp.makeConstraints { make in
             make.top.equalTo(descriptionLabel.snp.bottom).offset(Layout.tutorialImageTopSpacing)
             make.leading.equalTo(safeArea).offset(Layout.horizontalMargin)
             make.trailing.equalTo(safeArea).inset(Layout.horizontalMargin)
+            make.bottom.lessThanOrEqualTo(safeArea).inset(Layout.horizontalMargin)
         }

42-45: onDismiss 호출 타이밍 가드

viewDidDisappear는 다른 화면으로 잠시 가려져도 호출됩니다. 실제 pop/dismiss 시에만 콜백되도록 가드해 주세요.

-    override func viewDidDisappear(_ animated: Bool) {
-        super.viewDidDisappear(animated)
-        onDismiss?()
-    }
+    override func viewDidDisappear(_ animated: Bool) {
+        super.viewDidDisappear(animated)
+        if isBeingDismissed || isMovingFromParent {
+            onDismiss?()
+        }
+    }
Projects/Presentation/Sources/Tutorial/View/TutorialViewController.swift (5)

35-36: 소형 기기에서 콘텐츠 잘림 가능성 — 스크롤 허용 또는 레이아웃 보정 제안

테이블을 비스크롤로 고정하고 Top 오프셋(86pt)을 크게 잡아 작은 디바이스(iPhone SE 등)에서 섹션 수가 늘면 하단이 잘릴 수 있습니다. 스크린샷/실기기에서 오버플로우가 없는지 확인 부탁드립니다. 필요 시 스크롤 허용과 적당한 inset으로 UX를 유지하는 방안을 추천합니다.

적용 예시:

-        tutorialTableView.isScrollEnabled = false
+        tutorialTableView.isScrollEnabled = true
+        tutorialTableView.contentInset = UIEdgeInsets(top: 24, left: 0, bottom: 24, right: 0)

Also applies to: 46-50


41-43: 네비게이션 바 숨김 타이밍 조정 권장

viewDidLoad에서 시스템 내비게이션 바를 숨기면 다음 화면까지 숨김 상태가 전파될 수 있습니다. viewWillAppear/WillDisappear로 이동해 화면 전환 간 일관성을 보장하는 편이 안전합니다. (앱 전역에서 커스텀 바만 쓰지 않는다면 특히)

외부에 추가할 코드 예시:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    navigationController?.setNavigationBarHidden(true, animated: false)
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    navigationController?.setNavigationBarHidden(false, animated: false)
}

91-99: dimmedView 레이아웃/애니메이션 개선

frame 고정보다 제약 기반으로 전체 화면을 채우고, backgroundColor 대신 alpha를 애니메이션하는 편이 회전/치수 변경 대응과 퍼포먼스에 유리합니다.

-        let newDimmedView = UIView()
-        newDimmedView.backgroundColor = .black.withAlphaComponent(0.0)
-        newDimmedView.frame = view.bounds
+        let newDimmedView = UIView()
+        newDimmedView.backgroundColor = .black
+        newDimmedView.alpha = 0
         view.addSubview(newDimmedView)
+        newDimmedView.translatesAutoresizingMaskIntoConstraints = false
+        newDimmedView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
         dimmedView = newDimmedView

-        UIView.animate(withDuration: 0.25) {
-            newDimmedView.backgroundColor = UIColor.black.withAlphaComponent(0.7)
-        }
+        UIView.animate(withDuration: 0.25) {
+            newDimmedView.alpha = 0.7
+        }

105-108: 고정 detent 높이(348pt) 검증 요청

다국어/Dynamic Type 확대나 소형 기기에서 콘텐츠가 넘칠 수 있습니다. 실제 디바이스별로 잘리지 않는지 확인 부탁드립니다. 필요 시 고정값 대신 적절한 detent 조합(.medium/.large)이나 동적 높이 계산으로 보완을 고려하세요.


86-88: 중복 프레젠트 가드 추가 권장

빠른 연속 탭 시 이미 프레젠트 중인데 다시 present를 호출할 수 있습니다. 간단한 가드로 방지하세요.

     func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        guard presentedViewController == nil else { return }
         let tutorial = Tutorial.allCases[indexPath.section]
         …
         present(tutorialDetailView, animated: true)
     }

Also applies to: 124-124

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 673dc3c and 11c37b5.

⛔ Files ignored due to path filters (12)
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_edit_routine_graphic.imageset/tutorial_edit_routine_graphic.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_edit_routine_graphic.imageset/tutorial_edit_routine_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_edit_routine_graphic.imageset/tutorial_edit_routine_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_emotion_graphic.imageset/tutorial_emotion_graphic.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_emotion_graphic.imageset/tutorial_emotion_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_emotion_graphic.imageset/tutorial_emotion_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_home_more_graphic.imageset/tutorial_home_more_graphic.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_home_more_graphic.imageset/tutorial_home_more_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_home_more_graphic.imageset/tutorial_home_more_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_recommendation_routine_graphic.imageset/tutorial_recommendation_routine_graphic.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_recommendation_routine_graphic.imageset/tutorial_recommendation_routine_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_recommendation_routine_graphic.imageset/tutorial_recommendation_routine_graphic@3x.png is excluded by !**/*.png
📒 Files selected for processing (11)
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_edit_routine_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_emotion_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_home_more_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_recommendation_routine_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift (1 hunks)
  • Projects/Presentation/Sources/Home/View/HomeViewController.swift (1 hunks)
  • Projects/Presentation/Sources/Tutorial/Model/Tutorial.swift (1 hunks)
  • Projects/Presentation/Sources/Tutorial/View/Component/TutorialTableCell.swift (1 hunks)
  • Projects/Presentation/Sources/Tutorial/View/TutorialDetailViewController.swift (1 hunks)
  • Projects/Presentation/Sources/Tutorial/View/TutorialViewController.swift (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
Projects/Presentation/Sources/Tutorial/View/Component/TutorialTableCell.swift (2)
Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)
  • chevronIcon (48-50)
Projects/Presentation/Sources/Tutorial/View/TutorialViewController.swift (2)
  • configureAttribute (29-36)
  • configureLayout (38-51)
Projects/Presentation/Sources/Tutorial/View/TutorialDetailViewController.swift (2)
Projects/Presentation/Sources/Tutorial/View/TutorialViewController.swift (3)
  • viewDidLoad (23-27)
  • configureAttribute (29-36)
  • configureLayout (38-51)
Projects/Presentation/Sources/Common/DesignSystem/Font/BitnagilFont.swift (1)
  • attributedString (50-66)
Projects/Presentation/Sources/Tutorial/View/TutorialViewController.swift (3)
Projects/Presentation/Sources/Tutorial/View/TutorialDetailViewController.swift (3)
  • viewDidLoad (36-40)
  • configureAttribute (47-59)
  • configureLayout (61-87)
Projects/Presentation/Sources/Tutorial/View/Component/TutorialTableCell.swift (3)
  • configureAttribute (35-45)
  • configureLayout (47-61)
  • configure (31-33)
Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (1)
  • configureCustomNavigationBar (19-29)
🔇 Additional comments (4)
Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_emotion_graphic.imageset/Contents.json (1)

1-23: 스케일/idiom 구성 적절합니다

1x/2x/3x, universal 설정 정상입니다. 별도 이슈 없습니다.

Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_recommendation_routine_graphic.imageset/Contents.json (1)

1-23: 구성 일관성 OK

emotion과 동일한 포맷으로 잘 추가되었습니다. 문제 없습니다.

Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_home_more_graphic.imageset/Contents.json (1)

1-23: 구성 정상

파일명/스케일/idiom 정의가 일관적입니다. OK.

Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_edit_routine_graphic.imageset/Contents.json (1)

1-23: 튜토리얼 편집 그래픽 에셋 등록 LGTM

1x/2x/3x 모두 universal로 잘 등록되었습니다. 코드 참조 이름(tutorial_edit_routine_graphic)과 일치하는지만 최종 빌드에서 한 번 확인해 주세요.

Comment on lines +101 to +112
let tutorialDetailView = TutorialDetailViewController(tutorial: tutorial)
if let sheet = tutorialDetailView.sheetPresentationController {
sheet.prefersGrabberVisible = false
if #available(iOS 16.0, *) {
sheet.detents = [.custom { _ in Layout.tutorialDetailViewHeight }]
} else {
sheet.detents = [.medium()]
}
sheet.prefersScrollingExpandsWhenScrolledToEdge = false
sheet.preferredCornerRadius = 20
}

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

시트 제스처로 닫힐 때 dimmedView 잔존 가능 — 해제 처리 누락

onDismiss 콜백에만 의존하면 사용자가 스와이프로 시트를 닫는 경로에서 dimmedView가 남아 UI를 가릴 수 있습니다. presentationController 델리게이트를 연결해 모든 디스미스 경로에서 오버레이를 제거하세요. 또, 시트 스타일을 명시하는 것이 안전합니다.

         let tutorialDetailView = TutorialDetailViewController(tutorial: tutorial)
+        tutorialDetailView.modalPresentationStyle = .pageSheet
+        tutorialDetailView.presentationController?.delegate = self
         if let sheet = tutorialDetailView.sheetPresentationController {
             sheet.prefersGrabberVisible = false
             if #available(iOS 16.0, *) {
                 sheet.detents = [.custom { _ in Layout.tutorialDetailViewHeight }]
             } else {
                 sheet.detents = [.medium()]
             }
             sheet.prefersScrollingExpandsWhenScrolledToEdge = false
             sheet.preferredCornerRadius = 20
         }

-        tutorialDetailView.onDismiss = { [weak self] in
-            guard let self else { return }
-            guard let dimmedView = self.dimmedView else { return }
-            UIView.animate(withDuration: 0.1, animations: {
-                dimmedView.alpha = 0
-            }, completion: { _ in
-                dimmedView.removeFromSuperview()
-                self.dimmedView = nil
-            })
-        }
+        tutorialDetailView.onDismiss = { [weak self] in
+            self?.removeDimmedView()
+        }

파일 하단(선택한 범위 외)에 추가:

private func removeDimmedView() {
    guard let dimmedView = dimmedView else { return }
    UIView.animate(withDuration: 0.1, animations: {
        dimmedView.alpha = 0
    }, completion: { [weak self] _ in
        dimmedView.removeFromSuperview()
        self?.dimmedView = nil
    })
}

extension TutorialViewController: UIAdaptivePresentationControllerDelegate {
    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        removeDimmedView()
    }
}

Also applies to: 113-124

🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Tutorial/View/TutorialViewController.swift
around lines 101-112 (and also apply same fix for 113-124), the sheet dismissal
path can leave dimmedView orphaned because onDismiss alone doesn't catch
swipe-to-dismiss; add a private removeDimmedView() helper that animates alpha to
0, removes the view and nils the property, implement
UIAdaptivePresentationControllerDelegate and in
presentationControllerDidDismiss(_:) call removeDimmedView(), and ensure you set
tutorialDetailView.presentationController?.delegate = self (and optionally set
tutorialDetailView.modalPresentationStyle = .pageSheet or .formSheet to be
explicit) so all dismissal paths remove the overlay.

Copy link
Copy Markdown
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.

오늘도 고생하셨습니다~!!! 설명서가 이미지라서 매우 다행입니다,,

Comment on lines +77 to +78
make.leading.equalTo(safeArea).offset(Layout.horizontalMargin)
make.trailing.equalTo(safeArea).inset(Layout.horizontalMargin)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

사소한 부분이기도하고, 아마 잘 알고계신 부분일 수도 있지만,

Suggested change
make.leading.equalTo(safeArea).offset(Layout.horizontalMargin)
make.trailing.equalTo(safeArea).inset(Layout.horizontalMargin)
make.horizontalEgdes.equalTo(safeArea).inset(Layout.horizontalMargin)

으로 조금 더 간결하게 사용할 수도 있습니다~!

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.

헐 따봉 !!! 몰랐스요 !!!!!!!!!!!! 감사함니두아 !!!

Comment on lines +41 to +42
navigationController?.setNavigationBarHidden(true, animated: false)
configureCustomNavigationBar(navigationBarStyle: .withBackButton(title: "설명서"))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

configureAttribute에 위치시키는 건 어떨까요!
혹시 configureAttribute 함수 대신 configureLayout에 위치시키신 이유가 있을까요??

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.

흠냐 ..... 그냥 뭔가 습관적으루 대부분 Layout에 위치시켜놨는데 ...
Attribute가 더 맞는 것 같으네요 !!!!! 수정하겠슴니두아 ~~

if #available(iOS 16.0, *) {
sheet.detents = [.custom { _ in Layout.tutorialDetailViewHeight }]
} else {
sheet.detents = [.medium()]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

medium으로 했을 때 이미지 비율이 괜찮을까요??? iOS 버전을 생각하지 못했는데, 아차 싶습니다 ㅜㅜ

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.

이미지 비율은 괜찮을 것 같은데 ... 다만 Sheet 높이가 지금보다 조금 높다진다는 단점이 있어요 ...... ㅠㅠ
그냥 CustomBottomSheet를 계속해서 하는게 좋을까유 ??

지금 설명서는 그렇다 쳐도 삭제, 수정하기 Sheet도 비슷하게 구현되어 있어서유 ...

생각나는 방법으로는 ....

  1. CustomBottomSheet 사용하기
  2. 최소 지원 OS 16.0으로 높여버리기 ㅎ.ㅎ

medium으로 했을 땐 이르케 됩니다 !! (13 mini 기준)
Simulator Screenshot - iPhone 13 mini - 2025-09-04 at 16 18 42

Copy link
Copy Markdown

@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: 0

♻️ Duplicate comments (1)
Projects/Presentation/Sources/Tutorial/View/TutorialViewController.swift (1)

101-124: 치명적: 스와이프 디스미스 시 dimmedView 고아화 — delegate 연결 및 공통 제거 함수 필요

onDismiss만으로는 스와이프 닫힘을 못 잡아 오버레이가 남습니다. presentationController.delegate를 설정하고 모든 경로에서 공통 제거 함수를 호출하세요.

         let tutorialDetailView = TutorialDetailViewController(tutorial: tutorial)
+        tutorialDetailView.modalPresentationStyle = .pageSheet
+        tutorialDetailView.presentationController?.delegate = self
         if let sheet = tutorialDetailView.sheetPresentationController {
             sheet.prefersGrabberVisible = false
             if #available(iOS 16.0, *) {
                 sheet.detents = [.custom { _ in Layout.tutorialDetailViewHeight }]
             } else {
                 sheet.detents = [.medium()]
             }
             sheet.prefersScrollingExpandsWhenScrolledToEdge = false
             sheet.preferredCornerRadius = 20
         }

-        tutorialDetailView.onDismiss = { [weak self] in
-            guard let self else { return }
-            guard let dimmedView = self.dimmedView else { return }
-            UIView.animate(withDuration: 0.1, animations: {
-                dimmedView.alpha = 0
-            }, completion: { _ in
-                dimmedView.removeFromSuperview()
-                self.dimmedView = nil
-            })
-        }
+        tutorialDetailView.onDismiss = { [weak self] in
+            self?.removeDimmedView()
+        }

아래 코드를 파일 하단에 추가하세요(선택 영역 외):

private func removeDimmedView() {
    guard let dimmedView = dimmedView else { return }
    UIView.animate(withDuration: 0.1, animations: {
        dimmedView.alpha = 0
    }, completion: { [weak self] _ in
        dimmedView.removeFromSuperview()
        self?.dimmedView = nil
    })
}

extension TutorialViewController: UIAdaptivePresentationControllerDelegate {
    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        removeDimmedView()
    }
}
🧹 Nitpick comments (5)
Projects/Presentation/Sources/Tutorial/View/TutorialViewController.swift (5)

91-99: 오버레이 레이아웃/애니메이션 개선: frame → 제약, 배경색 대신 alpha 페이드

회전·사이즈 변경 대응과 단순한 페이드를 위해 제약과 alpha 사용을 권장합니다.

-        let newDimmedView = UIView()
-        newDimmedView.backgroundColor = .black.withAlphaComponent(0.0)
-        newDimmedView.frame = view.bounds
+        let newDimmedView = UIView()
+        newDimmedView.backgroundColor = .black
+        newDimmedView.alpha = 0
         view.addSubview(newDimmedView)
+        newDimmedView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
         dimmedView = newDimmedView

-        UIView.animate(withDuration: 0.25) {
-            newDimmedView.backgroundColor = UIColor.black.withAlphaComponent(0.7)
-        }
+        UIView.animate(withDuration: 0.25) {
+            newDimmedView.alpha = 0.7
+        }

37-40: 미래 확장성: isScrollEnabled=false로 콘텐츠 잘림 위험

튜토리얼 항목이 늘어나면 소형 기기에서 하단이 잘릴 수 있습니다. 스크롤 활성화 또는 동적 높이 조정 검토 바랍니다.

-        tutorialTableView.isScrollEnabled = false
+        tutorialTableView.isScrollEnabled = true

86-89: 선택 UX 정리: 선택 직후 deselect

하이라이트 정책이 바뀌어도 안전하게 선택 상태가 남지 않도록 처리해 두는 편이 깔끔합니다.

     func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        tableView.deselectRow(at: indexPath, animated: true)
         let tutorial = Tutorial.allCases[indexPath.section]

104-106: 고정 높이 detent는 기기/회전에 취약 — context 기반 상한 적용 제안

안전 마진을 두고 최대 detent 값을 고려하면 레이아웃 깨짐을 줄일 수 있습니다.

-                sheet.detents = [.custom { _ in Layout.tutorialDetailViewHeight }]
+                sheet.detents = [.custom { context in
+                    min(Layout.tutorialDetailViewHeight, context.maximumDetentValue * 0.9)
+                }]

29-33: 디자인 토큰 일관성

다른 화면(예: TutorialDetail)과 맞추려면 시스템 색 대신 디자인 시스템 색상 사용을 고려해 주세요.

-        view.backgroundColor = .systemBackground
+        view.backgroundColor = BitnagilColor.gray99
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 11c37b5 and 65a0128.

⛔ Files ignored due to path filters (12)
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_edit_routine_graphic.imageset/tutorial_edit_routine_graphic.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_edit_routine_graphic.imageset/tutorial_edit_routine_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_edit_routine_graphic.imageset/tutorial_edit_routine_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_emotion_graphic.imageset/tutorial_emotion_graphic.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_emotion_graphic.imageset/tutorial_emotion_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_emotion_graphic.imageset/tutorial_emotion_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_home_more_graphic.imageset/tutorial_home_more_graphic.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_home_more_graphic.imageset/tutorial_home_more_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_home_more_graphic.imageset/tutorial_home_more_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_recommendation_routine_graphic.imageset/tutorial_recommendation_routine_graphic.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_recommendation_routine_graphic.imageset/tutorial_recommendation_routine_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_recommendation_routine_graphic.imageset/tutorial_recommendation_routine_graphic@3x.png is excluded by !**/*.png
📒 Files selected for processing (11)
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_edit_routine_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_emotion_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_home_more_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_recommendation_routine_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift (1 hunks)
  • Projects/Presentation/Sources/Home/View/HomeViewController.swift (1 hunks)
  • Projects/Presentation/Sources/Tutorial/Model/Tutorial.swift (1 hunks)
  • Projects/Presentation/Sources/Tutorial/View/Component/TutorialTableCell.swift (1 hunks)
  • Projects/Presentation/Sources/Tutorial/View/TutorialDetailViewController.swift (1 hunks)
  • Projects/Presentation/Sources/Tutorial/View/TutorialViewController.swift (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_recommendation_routine_graphic.imageset/Contents.json
🚧 Files skipped from review as they are similar to previous changes (9)
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_emotion_graphic.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_home_more_graphic.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/Contents.json
  • Projects/Presentation/Sources/Tutorial/View/Component/TutorialTableCell.swift
  • Projects/Presentation/Sources/Home/View/HomeViewController.swift
  • Projects/Presentation/Sources/Tutorial/View/TutorialDetailViewController.swift
  • Projects/Presentation/Resources/Images.xcassets/Tutorial/tutorial_edit_routine_graphic.imageset/Contents.json
  • Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift
  • Projects/Presentation/Sources/Tutorial/Model/Tutorial.swift
🧰 Additional context used
🧬 Code graph analysis (1)
Projects/Presentation/Sources/Tutorial/View/TutorialViewController.swift (3)
Projects/Presentation/Sources/Tutorial/View/TutorialDetailViewController.swift (3)
  • viewDidLoad (36-40)
  • configureAttribute (47-61)
  • configureLayout (63-90)
Projects/Presentation/Sources/Tutorial/View/Component/TutorialTableCell.swift (3)
  • configureAttribute (35-45)
  • configureLayout (47-61)
  • configure (31-33)
Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (1)
  • configureCustomNavigationBar (19-29)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

@choijungp choijungp merged commit 296387e into develop Sep 4, 2025
2 checks passed
@choijungp choijungp deleted the feat/tutorial branch September 4, 2025 08:51
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