Skip to content

[Refactor-T3-174] 감정구슬 선택 화면 리팩터링#62

Merged
taipaise merged 7 commits intodevelopfrom
refactor/emotion-registration
Sep 17, 2025
Merged

[Refactor-T3-174] 감정구슬 선택 화면 리팩터링#62
taipaise merged 7 commits intodevelopfrom
refactor/emotion-registration

Conversation

@taipaise
Copy link
Copy Markdown
Collaborator

@taipaise taipaise commented Sep 13, 2025

🌁 Background

  • 감정 구슬 등록화면 V2로 리팩터링을 진행하였습니다~
  • 서버 연동은 아직,, 이지만 복잡한건 다 끝났다고 생각합니다!
    • 논의 후 API 리스폰스 수정이 필요할 것 같습니다! 추가적으로 필요한 정보는 아래와 같습니다.
      • 감정 구슬 관련 view의 tintColor
      • 감정 구슬 관련 view의 background Color
      • 감정 구슬의 description (편안한, 만족스러운 등)

📱 Screenshot

iPhone SE3 iPhone 13 mini iPhone 16 Pro
감정구슬_등록_se3 감정구슬_등록_13mini 감정구슬_등록_16pro
iPhone SE3 iPhone 13 mini iPhone 16 Pro
감정구슬_선택_se3 감정구슬_선택_13mini 감정구슬_선택_16pro
iPhone SE3 iPhone 13 mini iPhone 16 Pro
감정구슬_선택완료_se3 감정구슬_선택완료13mini 감정구슬_선택완료16pro

👩‍💻 Contents

  • 감정 구슬 선택 화면 구현
    • 감정 구슬 선택을 위한 carousel collectionView 구현
    • 무한스크롤 구현
    • collectionView 스크롤 없이 구슬을 한번에 선택할 수 있는 감정구슬 stackView 구현
  • 감정구슬 선택 완료 화면 구현

📝 Review Note

  • 데모데이 이전 코드들이 합쳐지면서 PR의 규모가 굉장히 커졌습니다. (굉장히 죄송합니다..)
  • CollectionView의 Layout에 대해 학습해보신 적이 없거나 구현이 처음이라면, layout 관련 코드가 굉장히 생소하실 것 같습니다. 저도 이번에 구현하면서 커스텀 Layout 관련 코드들은 모두 처음 본 것들이 대부분 이었거든요 ㅠ... 이해가 많이 어려우시면 PR 설명회를 하는 게 좋을 것 같습니다!! (아니면 조금 뒤로 미루는 것도 좋을 것 같습니다!)
  • 생각보다, collectionView의 스크롤과, collectionView와 겹치는 pangesture의 스크롤 동작을 동시 처리하는게 어렵습니다. 이번 pr에서는 해결하지 못했지만, 이후에는 꼭 개선해보도록 하겠습니다.
  • 디자이너 분들의 요청으로, 화면의 세로가 짧은 se3와 같은 기기에서는 감정 구슬 선택 시 포모의 손이 보이지 않습니다.

carousel collectionView 구현하기 + 중앙 snap 구현

  • 감정 구슬 선택 화면에서 캐러셀 뷰의 요구사항은 크게 다음과 같습니다
    1. cell이 중앙에 위치할 것
    2. 가운데 있는 cell이 양 옆에 있는 cell보다 클 것. (다시 말해 중앙으로 오면서 구슬이 커져야 합니다.)
    3. 한 번에 3개의 구슬이 화면에 표시 될 것
    4. 무한 스크롤이 가능할 것
  • 복잡한 레이아웃 조건을 만족시키기 위해 UICollectionViewFlowLayout를 상속한 EmotionCollectionViewLayout을 구현했습니다.
  • 아래에서는 크게 1, 2 번 요구사항 구현 방법에 대해 간단히 말씀드리겠습니다.

1. cell이 항상 중앙에 위치하도록 하기 (스냅 구현)

  • targetContentOffset 라는 메서드가 있습니다. 저도 이번에 처음알았는데, UICollectionViewFlowLayout에서 스크롤이 멈출 때 최종적으로 어느 위치에서 멈출지를 결정하는 메서드입니다.
    • 해당 메서드에서 proposedContentOffset 기준으로 현재 보이는 셀들의 layoutAttributes를 가져옵니다. (proposedContentOffset은 scroll이 멈출 예정인 위치 입니다!)
    • 이 중에서, 화면의 중앙 (centerX)와 가장 가까운 cell을 찾습니다.
    • 해당 cell이 화면의 중앙 (centerX)와 일치하도록 조절합니다. (contentOffset을 조절)
override func targetContentOffset(forProposedContentOffset proposed: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        guard let collectionView else { return proposed }

        let visibleRect = CGRect(
            origin: CGPoint(x: proposed.x, y: collectionView.contentOffset.y),
            size: collectionView.bounds.size)

        guard
            let layoutAttributesList = super.layoutAttributesForElements(in: visibleRect),
            !layoutAttributesList.isEmpty
        else { return proposed }

        // 화면 중앙에 가장 가까운 셀 찾기
        let centerX = proposed.x + collectionView.bounds.width / 2
        let closestTarget = layoutAttributesList.min { abs($0.center.x - centerX) < abs($1.center.x - centerX) }

        guard let closestTarget else { return proposed }

        let newX = closestTarget.center.x - collectionView.bounds.width / 2
        return CGPoint(x: newX, y: proposed.y)
    }

2. 구슬 위치 + 크기 조절하기

  • cell의 위치에 따라 구슬의 위치, 크기를 조절하기 위해 진행도 를 사용하기로 했습니다.

  • 화면의 중앙으로부터 멀어질수록 진행도가 1에서 0까지 감소합니다. (cell이 중앙에 위치하면 1, 멀어질 수록 0)

  • EmotionCollectionViewLayout

    • layoutAttributesForElements()에서 각 셀의 중심(attributes.center.x)과 화면 중앙 X 좌표의 차이를 구합니다. 이를 이용해 진행도를 계산합니다.
    • 계산한 progress는 커스텀 레이아웃 속성 EmotionCollectionViewLayoutAttributes.progress 에 담아서 셀까지 전달합니다.
  • EmotionCollectionViewCell

    • Cell 내의 apply를 사용하면, 커스텀 레이아웃 속성을 적용할 수 있다고 합니다.
image
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
    super.apply(layoutAttributes)
    guard let attributes = layoutAttributes as? EmotionCollectionViewLayoutAttributes else { return }

    let progress = attributes.progress                      // 0 ~ 1
    let targetScale = Layout.maxImageViewSize / Layout.imageViewSize
    let scale = 1.0 + (targetScale - 1.0) * progress        // 크기 보간
    let translateY = Layout.maxTranslateY * progress        // 세로 이동 보간

    let transform = CGAffineTransform.identity
        .scaledBy(x: scale, y: scale)
        .translatedBy(x: 0, y: translateY)

    emotionImageView.transform = transform
}
  • 레이아웃이 계산해준 progress를 읽어서, 크기와 세로 이동을 조절해줍니다.
  • AffineTransform을 이용하여 cell이 화면의 중앙에 위치할 수록 (progress가 1에 가까울 수록) 구슬이 커지고, 아래쪽으로 위치하도록 합니다.

감정 collectionView 무한 스크롤

default.mp4
  • 무한 스크롤을 구현할 때 주의한 점은 아래와 같습니다.
    1. 일부 사용자들은 끝을 보기 위해 손가락을 화면에서 떼지 않고 스크롤을 할 수도 있다.
    2. item 선택 시 일관된 방법으로 선택한 item의 index를 계산할 수 있어야 한다.
  • 가장 처음 구상했던 아이디어는 아래와 같습니다.
    • a, b, c, d와 같은 감정 구슬 배열이 있다 가정합니다.
    • 맨 앞, 맨 뒤에 선택할 수 없는 dummy를 배치하고, 해당 dummy로 스크롤하면 반대쪽 끝으로 스크롤 시킵니다. (애니메이션 없이)
    • 저희는 화면에 한 번에 하나의 cell이 아니라, 총 3개의 cell(감정 구슬)이 보여야 하기 때문에, datasource 배열을 c, d, a, b, c, d, a, b 와 같이 설정합니다.
    • 맨 앞 c, d로 이동하면 뒤쪽의 c, d 로 이동시킵니다. 맨 뒤 a, b로 이동하면 앞쪽의 a, b로 이동시킵니다.
  • 위 와 같이 기본 아이디어를 가져가되, 일관된 방법으로 index를 계산하고, 비정상적인 조작(손을 떼지않고 계속 스크롤) 에 대응하기 위해 원본 item 배열을 3배로 복사하여 datasource로 사용하기로 했습니다.
    • 예를 들어 a, b, c, d 와 같은 원본 배열이 있으면, a, b, c, d, a, b, c, d, a, b, c, d 와 같이 설정하는 방법입니다.
  • scrollViewDidEndDecelerating 에서 collectionView의 scroll이 끝났을때 index 를 판단하고, 중간위치의 item 배열에서 벗어나면 animation 없이 중간쪽에 있는 item으로 스크롤 하도록 했습니다. (아래 코드 참고해주시면 감사하겠습니다!)
    • 예를 들어 a, b, c, a, b, c, a, b, c 가 있을 때, 첫번째, 세번째 a에서 스크롤이 멈추면 두번째 a로 스크롤 하는 코드입니다.
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        let centerPoint = CGPoint(
            x: emotionCollectionView.contentOffset.x + emotionCollectionView.frame.midX,
            y: emotionCollectionView.contentOffset.y)

        guard
            let collectionView = scrollView as? UICollectionView,
            let indexPath = collectionView.indexPathForItem(at: centerPoint),
            itemCount > 0
        else { return }

        let index = indexPath.row % itemCount
        if indexPath.row < itemCount / 3 || indexPath.row >= itemCount * 2 / 3 {
            scrollToIndex(index: index)
        }
        viewModel.action(input: .selectEmotion(index: index))
    }

감정 구슬 바로가기 tap시 (collectionVew 상단의 구슬 stackView)

default.mp4
  • 상단에서 구슬 바로 가기 stackView에는, 감정구슬 imageView들이 subview로 등록되어있습니다.
  • 그리고 각 구슬 imageView에는 tapGesture가 등록되어있으며, 해당 제스쳐는 아래와 같습니다.
    @objc private func handleTapSmallMarbleImage(gesture: UITapGestureRecognizer) {
        guard
            let tappedSmallMarbleView = gesture.view,
            let index = smallMarbleStackView.arrangedSubviews.firstIndex(of: tappedSmallMarbleView)
        else { return }

        scrollToIndex(index: index + 1, animated: true) { [weak self] in
            self?.viewModel.action(input: .selectEmotion(index: index + 1))
        }
    }
  • 선택한 imageView의 stackView에서의 index를 추출하고, scrollToIndex(index: Int) 을 통해 원하는 위치로 collectionView를 스크롤 합니다.
  • scrollToIndex()은 아래와 같습니다.
    private func scrollToIndex(
        index: Int,
        animated: Bool = false,
        completion: (() -> Void)? = nil
    ) {
        guard itemCount > 0 else { return }

        let index = (index + itemCount) % itemCount
        let targetIndex = index + itemCount
        let indexPath = IndexPath(item: targetIndex, section: 0)

        emotionMarbleImageView.isHidden = true
        emotionCollectionView.scrollToItem(
            at: indexPath,
            at: .centeredHorizontally,
            animated: animated)

        if !animated {
            completion?()
        } else {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                completion?()
            }
        }
    }
  • 위에서 말씀드린 것처럼, 현재는 전체 감정 구슬 set 이 3 세트가 datasource로 등록되어있습니다. 따라서 감정 구슬 set의 원본 item 갯수로 modulo 연산을 진행하여 선택할 구슬의 index를 계산합니다.
    • 예를 들어, a, b, c, d, a, b, c, d, a, b, c, d 와 같이 item이 존재할 때, 원본 item의 갯수는 4개 입니다.
      • 첫번째 a를 고를 때 -> 0 % 4 = 0
      • 두번째 a를 고를 때 -> 4 % 4 = 0
      • 세번째 a를 고를 때 -> 8 % 4 = 0
    • 이렇게 어떤 위치의 a를 고르더라도, 원본 item 배열인 a, b, c, d 에서의 a의 index 값을 추출하는 코드입니다.
    • index를 추출하고 나면, 중간에 위치한 a로 스크롤 하기 위해 index + itemCount 위치로 collectionView를 스크롤 합니다. (무한 스크롤을 위해)

감정 구슬 선택하기

default.mp4
  • 사용자가 인지할 수는 없지만, collectionView의 중앙에 위치한 감정구슬과 정확히 일치하는 감정구슬 ImageView가 하나 더 있습니다.
image
  • 그리고 이 imageView에는 panGesture가 등록되어 있습니다. 아래로 스크롤하다가 손위의 일정 영역까지 scroll 하면 구슬을 선택하는 원리입니다. 해당 영역까지 아래로 스크롤하지 못하면 구슬은 원래 위치로 돌아가게 됩니다. (동영상 참고)

  • 다만 pangesture 때문에, 해당 imageView와 겹치는 collectionView 영역에서는 좌우 스크롤이 안먹히는 문제가 있습니다. iphone16pro 와 같이 큰 화면의 폰에서는 괜찮지만,, 가로 길이가 짧은 기기에서는 상당히 불편하다고 느껴집니다. (반드시 개선하겠습니다 ㅜ)

    let translationY = gesture.translation(in: view).y
    let offSet = max(0, translationY)
    
    guard offSet <= handMarbleView.frame.midY - marbleImageViewMidY else { return }
    • 팬제스쳐를 핸들링 하는 함수 handlePan 중 요 부분을 통해 감정구슬을 위로는 드래그하지 못하고, 아래로만 드래그할 수 있도록 강제했습니다.

구슬 손 밑에 위치시키기 (감정 구슬 등록/ 감정 구슬 등록 완료화면)

스크린샷 2025-09-13 오후 9 04 33 스크린샷 2025-09-13 오후 9 03 26
  • 진짜 별거 아니지만, 감정 구슬 관련 화면들에는 포모가 구슬을 들고 있는 이미지가 많습니다.
  • 엄지 손가락 밑에 구슬이 위치하거나, 손 이미지 밑에 구슬이 위치해야합니다.
  • viewaddSubview 하는 순서를 조절해서 자연스럽게 구슬이 위 사진들과 같이 위치하도록 구현했습니다.

Summary by CodeRabbit

  • 신기능
    • 새로운 감정 등록 화면 도입: 가로 스냅 컬렉션/마블 드래그 인터랙션, 선택 상태 안내, 확인 가능 여부 표시.
    • 감정 등록 완료 화면 추가: 배경/손/말풍선 그래픽과 메시지 표시.
    • 디자인 시스템 확장: 신규 색상 팔레트(그린/민트/퍼플/레드 단계), 감정 관련 그래픽과 아이콘, 폰트 스타일 추가.
  • 리팩터
    • 기존 감정 등록 화면을 대체하고, 홈에서 새로운 화면으로 이동하도록 네비게이션 업데이트.
    • 추천 루틴 화면에서 감정 등록 버튼 흐름 정리.

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

coderabbitai bot commented Sep 13, 2025

Walkthrough

Emotion registration feature overhauled: new view controller with custom collection layout, cell, and completion screen; Emotion model expanded with UI data and samples; ViewModel gains selection/confirmation publishers. Added numerous color and image assets, updated design system accessors (colors, graphics, icons), a new font style, navigational wiring in Home, and removed a legacy view and delegate.

Changes

Cohort / File(s) Summary of changes
Color assets (Green, Mint, Purple, Red)
Projects/Presentation/Resources/Colors.xcassets/Green/*, .../Mint/*, .../Purple/*, .../Red/*
Added asset catalogs and colorsets: Green5/10/500, Mint10/500, Purple5/10/500, Red10/500, plus Contents.json metadata.
Emotion graphics assets
Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/Contents.json, .../Graphic/Emotion/*/*.imageset/Contents.json
Added image sets and metadata for emotion-related graphics (background, ground, speech, marble variants, pomo hands, FOMO assets) with 1x/2x/3x entries.
General image assets
Projects/Presentation/Resources/Images.xcassets/background_graphic.imageset/Contents.json, .../double_chevron_icon.imageset/Contents.json
Added background graphic and double chevron icon image sets with 1x/2x/3x variants.
Design system - Colors
Projects/Presentation/Sources/Common/DesignSystem/BitnagilColor.swift
Added color tokens for Red, Green, Purple, Mint; reorganized Green10/Purple10 into grouped sections.
Design system - Graphics
Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift
Added 15 UIImage accessors for new emotion graphics and completion assets.
Design system - Icons
Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift
Added doubleChevronIcon and helper method returning rotated, template-rendered icon by Direction.
Design system - Font
Projects/Presentation/Sources/Common/DesignSystem/Font/FontStyle.swift
Added case cafe24Title2 and its FontAttributes mapping.
Emotion model & samples
Projects/Presentation/Sources/EmotionRegister/Model/Emotion.swift
Emotion now Hashable with id and UI fields; added copy(); introduced Marble enum with UI mappings; added emotinonDummies.
Emotion register UI - layout & cell
Projects/Presentation/Sources/EmotionRegister/View/EmotionCollectionViewLayout*.swift, .../EmotionCollectionViewCell.swift
Added custom horizontal layout with progress attribute and animated cell scaling/translation based on progress.
Emotion registration screens
Projects/Presentation/Sources/EmotionRegister/View/EmotionRegistrationViewController.swift, .../EmotionRegisterCompletionViewController.swift
Added new registration flow controller with drag interaction and completion screen UI; configured assets, bindings, and layout.
ViewModel updates
Projects/Presentation/Sources/EmotionRegister/ViewModel/EmotionRegisterViewModel.swift
Added Input.selectEmotion(index:), selectedEmotion and confirmEnabled publishers; fetch uses emotinonDummies; selection logic validates and publishes state.
Legacy view removal
Projects/Presentation/Sources/EmotionRegister/View/EmotionRegisterView.swift
Deleted old EmotionRegisterView and its collection view logic and delegate/data source extensions.
Navigation update (Home)
Projects/Presentation/Sources/Home/View/HomeViewController.swift
Switched navigation target to EmotionRegistrationViewController.
Delegate removal (RecommendedRoutine)
Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift
Removed RegisterEmotionButtonViewDelegate conformance and navigation to old EmotionRegisterView.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant HomeVC
  participant EmotionRegistrationVC as Registration VC
  participant EmotionVM as ViewModel
  participant CompletionVC

  User->>HomeVC: Tap "Emotion Register"
  HomeVC->>Registration VC: push(viewModel)
  Registration VC->>EmotionVM: action(.fetchEmotions)
  EmotionVM-->>Registration VC: emotions (dummy list)
  Registration VC->>EmotionVM: action(.selectEmotion(index))
  EmotionVM-->>Registration VC: selectedEmotionPublisher (Emotion)
  EmotionVM-->>Registration VC: confirmEmotionEnabledPublisher (Bool)
  note over Registration VC: Update marble image, colors, texts

  User->>Registration VC: Drag marble downward
  alt Passed threshold
    Registration VC->>CompletionVC: init(emotion), push
  else Not enough
    Registration VC->>Registration VC: Snap back & show info
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

새 민트빛, 보랏빛, 초록빛 번져와,
토끼는 귀를 씰룩, 감정 구슬을 돌려봐. 🐇
휙- 이중 화살, 방향 딱 맞춰주고,
스르륵 드래그하면 축하 무대가 열려요! 🎉
말풍선 속에 톡- 오늘 마음을 기록하자.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed PR 제목 "[Refactor-T3-174] 감정구슬 선택 화면 리팩터링"은 변경의 핵심인 감정구슬 선택/등록 화면의 리팩터링(새로운 EmotionRegistrationViewController 도입, 커스텀 컬렉션뷰 레이아웃 및 레이아웃 어트리뷰트, 셀 애니메이션과 뷰모델 변경 등)을 명확히 요약하고 있으며 불필요한 정보나 모호한 표현이 없습니다.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/emotion-registration

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

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

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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

❤️ Share

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

Copy link
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: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
Projects/Presentation/Sources/Home/View/HomeViewController.swift (2)

622-631: 신규 EmotionRegistrationViewController 전환 변경 반영 (LGTM) + DI 실패 시 크래시 방지 제안

전환 대상 교체는 적절합니다. 다만 DI 실패 시 fatalError로 앱이 종료됩니다. 릴리즈 빌드에서 치명적이므로 로깅 후 반환하도록 바꾸는 것을 권장합니다.

적용 예시:

-    private func goToEmotionRegisterView() {
-        guard let emotionRegisterViewModel = DIContainer.shared.resolve(type: EmotionRegisterViewModel.self) else {
-            fatalError("emotionRegisterViewModel 의존성이 등록되지 않았습니다.")
-        }
+    private func goToEmotionRegisterView() {
+        guard let emotionRegisterViewModel = DIContainer.shared.resolve(type: EmotionRegisterViewModel.self) else {
+            BitnagilLogger.log(logType: .error, message: "EmotionRegisterViewModel 의존성이 누락되었습니다.")
+            return
+        }
         let emotionRegistrationViewController = EmotionRegistrationViewController(viewModel: emotionRegisterViewModel)
         emotionRegistrationViewController.hidesBottomBarWhenPushed = true
         navigationController?.pushViewController(emotionRegistrationViewController, animated: true)
     }

474-501: 업데이트 알럿에서 exit(0) 호출 제거 권장

iOS HIG에 반하며 사용자 경험/심사 리스크가 있습니다. 스토어로만 이동하고 앱 종료는 하지 않는 것으로 조정하세요.

-        let cancel = UIAlertAction(
-            title: "취소",
-            style: .default,
-            handler: { _ in exit(0) })
+        let cancel = UIAlertAction(title: "취소", style: .cancel)
@@
-            handler: { _ in
-                UIApplication.shared.open(updateURL, options: [:], completionHandler: { _ in exit(0) })
-            })
+            handler: { _ in
+                UIApplication.shared.open(updateURL, options: [:], completionHandler: nil)
+            })
Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)

107-114: 컴파일 오류: Direction.rotation에서 return 누락

각 case에서 반환 키워드가 없어 빌드 실패합니다.

     var rotation: Float {
         switch self {
-        case .up: 90
-        case .down: -90
-        case .left: 0
-        case .right: 180
+        case .up: return 90
+        case .down: return -90
+        case .left: return 0
+        case .right: return 180
         }
     }
♻️ Duplicate comments (1)
Projects/Presentation/Sources/EmotionRegister/View/EmotionRegisterCompletionViewController.swift (1)

134-137: 라벨 인셋 상수 오사용 수정

speechLabel 제약에서 잘못된 상수를 사용 중입니다. 위의 패치 코멘트 참고해 교정하세요.

🧹 Nitpick comments (24)
Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_gray_graphic.imageset/Contents.json (1)

18-22: 틴트 무시를 명시해 컬러 그래픽 보존

템플릿 렌더링으로 오동작할 여지가 있어 original 의도를 명시하는 것을 권장합니다.

   ],
-  "info" : {
+  "properties" : {
+    "template-rendering-intent" : "original"
+  },
+  "info" : {
     "author" : "xcode",
     "version" : 1
   }
Projects/Presentation/Resources/Colors.xcassets/Purple/Purple5.colorset/Contents.json (1)

1-20: 광색역(Display P3) 필요 여부 검토

브랜드 컬러 정확도가 중요하면 display-p3 변형 추가를 고려해 주세요(선택).

Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_none_graphic.imageset/Contents.json (1)

18-22: 틴트 무시를 명시해 컬러 그래픽 보존

템플릿 렌더링 방지를 위해 original 의도 명시를 권장합니다.

   ],
-  "info" : {
+  "properties" : {
+    "template-rendering-intent" : "original"
+  },
+  "info" : {
     "author" : "xcode",
     "version" : 1
   }
Projects/Presentation/Resources/Colors.xcassets/Green/Green10.colorset/Contents.json (1)

1-20: 색상 명도 단계 사용 목적 확인 및 대비 점검

Green10은 매우 연한 톤(배경용)에 가깝습니다. 텍스트/아이콘 전경색으로 사용 시 대비 부족 가능성이 큽니다. 사용 목적별(배경/보조면) 가이드에 부합하는지 검토해 주세요.

빠른 대비 확인(배경=Green10 기준, 전경=검정/흰색):

#!/bin/bash
python - <<'PY'
# sRGB 대비 계산 (WCAG)
def srgb_to_lin(c): c/=255; return c/12.92 if c<=0.04045*255 else ((c/255+0.055)/1.055)**2.4
def L(r,g,b): 
    rL,gL,bL=map(srgb_to_lin,[r,g,b]); return 0.2126*rL+0.7152*gL+0.0722*bL
def contrast(fg,bg):
    L1,L2=sorted([L(*fg),L(*bg)],reverse=True); return (L1+0.05)/(L2+0.05)
green10=(0xE6,0xF5,0xC6)
white=(255,255,255); black=(0,0,0)
print("Green10 vs white:", round(contrast(green10, white),2))
print("Green10 vs black:", round(contrast(green10, black),2))
PY
Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/fomo_hand_graphic.imageset/Contents.json (1)

1-23: Contents.json에 template-rendering-intent: "original" 추가 — fomo_hand_graphic 색상 보존

파일: Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/fomo_hand_graphic.imageset/Contents.json
현재 properties가 없어 의도치 않은 템플릿(틴트) 렌더링 가능성이 있습니다. 색상 보존이 필요하면 아래 변경 적용.

   ],
   "info" : { "author" : "xcode", "version" : 1 }
+  ,
+  "properties": {
+    "template-rendering-intent": "original"
+  }
 }
Projects/Presentation/Resources/Colors.xcassets/Red/Red10.colorset/Contents.json (1)

1-20: Red10 — 다크 모드 appearance 변형 추가 권장

Red10은 라이트 배경용 옅은 톤으로, 다크 모드에서 대비 저하/눈부심 우려가 있어 다크 전용 변형 추가 권장.
사용처 확인: Projects/Presentation/Sources/Common/DesignSystem/BitnagilColor.swift:56 (static let red10 = UIColor(named: "Red10", in: bundle, compatibleWith: nil))
Assets 수정 대상: Projects/Presentation/Resources/Colors.xcassets/Red/Red10.colorset/Contents.json — dark appearance 변형 또는 다크용 색상값 추가.

Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marblePomo_graphic.imageset/Contents.json (1)

1-23: 자산 네이밍을 snake_case로 통일하세요 (marble_pomo_graphic)

  • 이미지 세트 및 파일명 변경: Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marblePomo_graphic.imageset → marble_pomo_graphic.imageset. Contents.json의 filename을 marble_pomo_graphic@1x.png / @2x / @3x로, 실파일명도 동일하게 수정하세요.
  • 코드 참조 수정: Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift (현재 58행)에서 UIImage(named: "marblePomo_graphic", in: bundle, with: nil) → UIImage(named: "marble_pomo_graphic", in: bundle, with: nil). EmotionRegisterCompletionViewController는 BitnagilGraphic.marblePomoGraphic을 사용하므로 별도 수정 불필요합니다.
  • 이미지 틴트 방지: Contents.json에 template-rendering-intent 추가 적용 (예시):
   ],
   "info" : { "author" : "xcode", "version" : 1 }
+  ,
+  "properties": {
+    "template-rendering-intent": "original"
+  }
 }
Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/Contents.json (1)

1-6: 폴더 네임스페이스 활성화 제안

이 그룹(Emotion)에 네임스페이스를 부여해 자산명 충돌을 예방하세요.

 {
   "info" : {
     "author" : "xcode",
     "version" : 1
-  }
+  },
+  "properties": {
+    "provides-namespace": true
+  }
 }
Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/emotion_background_graphic.imageset/Contents.json (1)

1-23: 이미지셋 구성 적정 (1x/2x/3x 모두 제공)

구성은 적절합니다. iOS 지원 단말이 @2x/@3x만인 점을 고려하면 @1x 제거로 용량 최적화 여지도 있습니다(선택).

Projects/Presentation/Resources/Colors.xcassets/Red/Red500.colorset/Contents.json (1)

1-20: 토큰/다크모드 변형 검토

값(#FF5150)과 네이밍 일치합니다. 다크모드 별도 톤이 필요하면 appearances 변형을 추가하세요.

   "colors" : [
     {
       "color" : {
         "color-space" : "srgb",
         "components" : {
           "alpha" : "1.000",
           "blue" : "0x50",
           "green" : "0x51",
           "red" : "0xFF"
         }
       },
-      "idiom" : "universal"
+      "idiom" : "universal"
     }
-  ],
+  ],
+  "appearances": [
+    { "appearance": "luminosity", "value": "dark", "colors": [ /* 다크 변형 정의 */ ] }
+  ],
Projects/Presentation/Resources/Images.xcassets/background_graphic.imageset/Contents.json (1)

1-23: 폴더 구조·자산명 일관성 확인

Projects/Presentation/Resources/Images.xcassets/background_graphic.imageset에 있는 'background_graphic'이 코드에서는 'emotion_background_graphic'으로 참조됩니다 (Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift:54).

  • 범용 백그라운드라면 루트에 유지.
  • 감정 플로우 전용이라면 이미지셋을 Graphic/Emotion으로 이동하거나 파일명을 'emotion_background_graphic'으로 변경하고 코드와 동기화.
Projects/Presentation/Sources/Common/DesignSystem/Font/FontStyle.swift (1)

56-58: cafe24Title2의 lineHeight(30) 재확인 제안

16pt에서 30pt는 subtitle1(28), body1(24)와 리듬이 크게 달라집니다. 디자인 의도가 아니라면 28 권장합니다.

-        case .cafe24Title2: FontAttributes(fontSize: 16, lineHeight: 30, letterSpacing: -0.5)
+        case .cafe24Title2: FontAttributes(fontSize: 16, lineHeight: 28, letterSpacing: -0.5)
Projects/Presentation/Resources/Images.xcassets/double_chevron_icon.imageset/Contents.json (1)

1-23: 아이콘 tint 처리 및 벡터화(PDF) 고려

tintColor 사용 예정이면 템플릿 렌더링 플래그를 추가해 두는 것이 안전합니다. 가능하면 단일 PDF 벡터로 전환도 권장합니다.

   ],
+  "properties" : {
+    "template-rendering-intent" : "template"
+  },
   "info" : {
Projects/Presentation/Sources/Home/View/HomeViewController.swift (1)

209-221: FloatingMenu 표시/숨김 시 페이드 인이 적용되지 않는 문제

초기 isHidden = true만 설정되어 있어 최초 표시 시 알파가 1이면 페이드 인이 보이지 않습니다. 초기 알파를 0으로 두고, 표시 직전 0으로 보장한 뒤 애니메이션하는 것을 권장합니다.

     floatingMenu.isHidden = true
+    floatingMenu.alpha = 0
     floatingMenu.delegate = self
@@
     private func toggleFloatingButton() {
         floatingButton.toggle()
         isShowingFloatingMenu.toggle()
 
         floatingMenu.isHidden = !isShowingFloatingMenu
         dimmedView.isHidden = !isShowingFloatingMenu
 
-        UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseOut]) {
+        if isShowingFloatingMenu { self.floatingMenu.alpha = 0 } // ensure fade-in
+        UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseOut]) {
             self.dimmedView.alpha = self.isShowingFloatingMenu ? 1 : 0
             self.floatingMenu.alpha = self.isShowingFloatingMenu ? 1 : 0
         }
     }

Also applies to: 593-604

Projects/Presentation/Sources/EmotionRegister/View/EmotionCollectionViewCell.swift (1)

55-58: 이미지 뷰 콘텐츠 모드 지정 제안

원형 마스크에 맞춰 보이도록 scaleAspectFill 설정을 권장합니다.

     private func configureAttribute() {
         emotionImageView.layer.cornerRadius = Layout.imageViewSize / 2
         emotionImageView.layer.masksToBounds = true
+        emotionImageView.contentMode = .scaleAspectFill
     }
Projects/Presentation/Sources/EmotionRegister/View/EmotionCollectionViewLayoutAttributes.swift (1)

22-26: isEqual의 타입 미스매치 처리 개선

다른 타입 입력 시 false를 반환하면 불필요한 레이아웃 무효화가 늘어납니다. 상위 구현으로 위임하세요.

-    override func isEqual(_ object: Any?) -> Bool {
-        guard let object = object as? EmotionCollectionViewLayoutAttributes else { return false }
-
-        return super.isEqual(object) && object.progress == progress
-    }
+    override func isEqual(_ object: Any?) -> Bool {
+        guard let object = object as? EmotionCollectionViewLayoutAttributes else {
+            return super.isEqual(object)
+        }
+        return super.isEqual(object) && object.progress == progress
+    }
Projects/Presentation/Sources/EmotionRegister/ViewModel/EmotionRegisterViewModel.swift (1)

47-55: 심볼 오타: emotinonDummies 확인 및 수정 권장

emotinonDummies가 Projects/Presentation/Sources/EmotionRegister/Model/Emotion.swift:50에 정의되어 있고(// TODO: - 서버 연동 후 삭제), EmotionEntity.toEmotion()는 같은 파일 Projects/Presentation/Sources/EmotionRegister/Model/Emotion.swift:40에 존재합니다. 변수명 오타로 판단되므로 emotinonDummies → emotionDummies로 변경하거나, 삭제 예정이라면 TODO에 그 이유를 명시하세요.

Projects/Presentation/Sources/EmotionRegister/View/EmotionCollectionViewLayout.swift (1)

15-15: final 클래스에서는 class 대신 static 사용

SwiftLint 경고 해소 및 컨벤션 통일을 권장합니다.

-    override class var layoutAttributesClass: AnyClass { EmotionCollectionViewLayoutAttributes.self }
+    override static var layoutAttributesClass: AnyClass { EmotionCollectionViewLayoutAttributes.self }
Projects/Presentation/Sources/EmotionRegister/View/EmotionRegisterCompletionViewController.swift (3)

12-31: 상수 사용 일관성

Layout.speechLabelHorizontalSpacing를 정의했지만 실제 제약에는 speechImageHorizontalSpacing를 재사용하고 있습니다. 가독성과 향후 조정 편의성을 위해 올바른 상수를 참조하세요.

적용 패치:

-            make.horizontalEdges.equalTo(speechImageView).inset(Layout.speechImageHorizontalSpacing)
+            make.horizontalEdges.equalTo(speechImageView).inset(Layout.speechLabelHorizontalSpacing)

42-50: 멀티라인 리터럴 들여쓰기 주의

삼중 따옴표 문자열의 선행 공백은 닫는 구분자의 들여쓰기에 따라 제거되지만, 들여쓰기 불일치 시 의도치 않은 좌측 여백이 생길 수 있습니다. 한 줄 문자열 + “\n” 조합 또는 String.localizedStringWithFormat으로 명시적으로 구성하는 것도 고려하세요.


61-65: 임시 네비게이션 바 처리 정리

“임시뒤로가기” 및 스택 필터링 로직은 TODO 상태입니다. 출시 전 상용 문구로 교체하고, 스택 필터링은 viewDidAppear에서 한 번만 수행하거나, 이전 VC에서 push 시점에 조정하는 편이 안전합니다.

Projects/Presentation/Sources/EmotionRegister/View/EmotionRegistrationViewController.swift (2)

86-95: 레이아웃 분기 위치 개선 제안

safeArea와 frame의 maxY 비교는 기기군/회전/멀티태스킹에 취약합니다. viewSafeAreaInsetsDidChange에서 bottomInset 값 기반으로 한 번만 제약을 갱신하거나, traitCollection.verticalSizeClass/뷰 높이 임계치 기반 분기를 권장합니다.


269-279: 트리플링 스냅샷 로딩 순서 OK

중간 세트로 스크롤 후 첫 감정 선택 트리거 흐름이 일관적입니다. 다만 emotions.count == 0 방어(guard)만 추가해두면 더 안전합니다.

Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift (1)

10-13: 번들 참조 견고성

Bundle(for: IntroViewController.self)에 의존하면 해당 타입 이동/삭제 시 그래픽 로딩이 실패합니다. 가능하면 같은 모듈의 고정 타입(예: BitnagilGraphic.self) 또는 리소스 전용 토큰 클래스로 교체하세요.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

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

⛔ Files ignored due to path filters (51)
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/emotion_background_graphic.imageset/emotion_background_graphic@1x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/emotion_background_graphic.imageset/emotion_background_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/emotion_background_graphic.imageset/emotion_background_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/emotion_ground_graphic.imageset/ground_graphic@1x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/emotion_ground_graphic.imageset/ground_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/emotion_ground_graphic.imageset/ground_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/fomo_hand_graphic.imageset/fomo_hand_graphic@1x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/fomo_hand_graphic.imageset/fomo_hand_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/fomo_hand_graphic.imageset/fomo_hand_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/fomo_thumb_graphic.imageset/fomo_thumb_graphic@1x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/fomo_thumb_graphic.imageset/fomo_thumb_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/fomo_thumb_graphic.imageset/fomo_thumb_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marblePomo_graphic.imageset/marblePomo_graphic@1x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marblePomo_graphic.imageset/marblePomo_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marblePomo_graphic.imageset/marblePomo_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_gray_graphic.imageset/marble_gray_graphic@1x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_gray_graphic.imageset/marble_gray_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_gray_graphic.imageset/marble_gray_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_green_graphic.imageset/marble_green_graphic@1x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_green_graphic.imageset/marble_green_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_green_graphic.imageset/marble_green_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_mint_graphic.imageset/marble_mint_graphic@1x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_mint_graphic.imageset/marble_mint_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_mint_graphic.imageset/marble_mint_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_none_graphic.imageset/marble_none_graphic@1x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_none_graphic.imageset/marble_none_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_none_graphic.imageset/marble_none_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_orange_graphic.imageset/marble_orange_graphic@1x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_orange_graphic.imageset/marble_orange_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_orange_graphic.imageset/marble_orange_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_pomo_left_hand_graphic.imageset/marble_pomo_left_hand_graphic@1x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_pomo_left_hand_graphic.imageset/marble_pomo_left_hand_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_pomo_left_hand_graphic.imageset/marble_pomo_left_hand_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_pomo_right_hand_graphic.imageset/marble_pomo_right_hand_graphic@1x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_pomo_right_hand_graphic.imageset/marble_pomo_right_hand_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_pomo_right_hand_graphic.imageset/marble_pomo_right_hand_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_purple_graphic.imageset/marble_purple_graphic@1x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_purple_graphic.imageset/marble_purple_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_purple_graphic.imageset/marble_purple_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_red_graphic.imageset/marble_red_graphic@1x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_red_graphic.imageset/marble_red_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_red_graphic.imageset/marble_red_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_speech_graphic.imageset/marble_speech_graphic@1x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_speech_graphic.imageset/marble_speech_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_speech_graphic.imageset/marble_speech_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/background_graphic.imageset/background_graphic@1x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/background_graphic.imageset/background_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/background_graphic.imageset/background_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/double_chevron_icon.imageset/double_chevron_icon@1x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/double_chevron_icon.imageset/double_chevron_icon@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/double_chevron_icon.imageset/double_chevron_icon@3x.png is excluded by !**/*.png
📒 Files selected for processing (46)
  • Projects/Presentation/Resources/Colors.xcassets/Green/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Colors.xcassets/Green/Green10.colorset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Colors.xcassets/Green/Green5.colorset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Colors.xcassets/Green/Green500.colorset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Colors.xcassets/Mint/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Colors.xcassets/Mint/Mint10.colorset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Colors.xcassets/Mint/Mint500.colorset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Colors.xcassets/Purple/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Colors.xcassets/Purple/Purple10.colorset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Colors.xcassets/Purple/Purple5.colorset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Colors.xcassets/Purple/Purple500.colorset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Colors.xcassets/Red/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Colors.xcassets/Red/Red10.colorset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Colors.xcassets/Red/Red500.colorset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/emotion_background_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/emotion_ground_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/fomo_hand_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/fomo_thumb_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marblePomo_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_gray_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_green_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_mint_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_none_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_orange_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_pomo_left_hand_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_pomo_right_hand_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_purple_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_red_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_speech_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/background_graphic.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/double_chevron_icon.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Sources/Common/DesignSystem/BitnagilColor.swift (1 hunks)
  • Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift (1 hunks)
  • Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1 hunks)
  • Projects/Presentation/Sources/Common/DesignSystem/Font/FontStyle.swift (2 hunks)
  • Projects/Presentation/Sources/EmotionRegister/Model/Emotion.swift (2 hunks)
  • Projects/Presentation/Sources/EmotionRegister/View/EmotionCollectionViewCell.swift (1 hunks)
  • Projects/Presentation/Sources/EmotionRegister/View/EmotionCollectionViewLayout.swift (1 hunks)
  • Projects/Presentation/Sources/EmotionRegister/View/EmotionCollectionViewLayoutAttributes.swift (1 hunks)
  • Projects/Presentation/Sources/EmotionRegister/View/EmotionRegisterCompletionViewController.swift (1 hunks)
  • Projects/Presentation/Sources/EmotionRegister/View/EmotionRegisterView.swift (0 hunks)
  • Projects/Presentation/Sources/EmotionRegister/View/EmotionRegistrationViewController.swift (1 hunks)
  • Projects/Presentation/Sources/EmotionRegister/ViewModel/EmotionRegisterViewModel.swift (2 hunks)
  • Projects/Presentation/Sources/Home/View/HomeViewController.swift (1 hunks)
  • Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift (0 hunks)
💤 Files with no reviewable changes (2)
  • Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift
  • Projects/Presentation/Sources/EmotionRegister/View/EmotionRegisterView.swift
🧰 Additional context used
🧬 Code graph analysis (6)
Projects/Presentation/Sources/EmotionRegister/ViewModel/EmotionRegisterViewModel.swift (2)
Projects/DataSource/Sources/Repository/EmotionRepository.swift (1)
  • fetchEmotions (13-20)
Projects/Domain/Sources/UseCase/Emotion/EmotionUseCase.swift (1)
  • fetchEmotions (18-21)
Projects/Presentation/Sources/EmotionRegister/View/EmotionCollectionViewCell.swift (1)
Projects/Presentation/Sources/EmotionRegister/View/EmotionRegistrationViewController.swift (1)
  • configureAttribute (100-148)
Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)
Projects/Presentation/Sources/Common/Extension/UIImage+.swift (2)
  • rotate (56-67)
  • rotate (69-71)
Projects/Presentation/Sources/EmotionRegister/View/EmotionCollectionViewLayout.swift (1)
Projects/Presentation/Sources/EmotionRegister/View/EmotionCollectionViewLayoutAttributes.swift (1)
  • copy (13-20)
Projects/Presentation/Sources/EmotionRegister/View/EmotionRegisterCompletionViewController.swift (2)
Projects/Presentation/Sources/EmotionRegister/View/EmotionRegistrationViewController.swift (1)
  • viewDidLoad (78-81)
Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (1)
  • configureCustomNavigationBar (19-29)
Projects/Presentation/Sources/EmotionRegister/View/EmotionRegistrationViewController.swift (4)
Projects/Presentation/Sources/EmotionRegister/View/EmotionRegisterCompletionViewController.swift (3)
  • viewDidLoad (56-65)
  • configureAttribute (67-80)
  • configureLayout (82-138)
Projects/Presentation/Sources/EmotionRegister/ViewModel/EmotionRegisterViewModel.swift (3)
  • action (38-45)
  • fetchEmotions (47-61)
  • selectEmotion (63-75)
Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (1)
  • configureCustomNavigationBar (19-29)
Projects/Presentation/Sources/EmotionRegister/Model/Emotion.swift (1)
  • copy (25-36)
🪛 SwiftLint (0.57.0)
Projects/Presentation/Sources/EmotionRegister/ViewModel/EmotionRegisterViewModel.swift

[Warning] 52-52: TODOs should be resolved (- 서버 연동 후 삭제)

(todo)

Projects/Presentation/Sources/EmotionRegister/View/EmotionCollectionViewLayout.swift

[Warning] 15-15: Prefer static over class in a final class

(static_over_final_class)

Projects/Presentation/Sources/EmotionRegister/View/EmotionRegisterCompletionViewController.swift

[Warning] 61-61: TODOs should be resolved (- 임시 네비 바 설정)

(todo)

Projects/Presentation/Sources/EmotionRegister/Model/Emotion.swift

[Warning] 18-18: TODOs should be resolved (- 서버 연동 후 삭제)

(todo)


[Warning] 49-49: TODOs should be resolved (- 서버 연동 후 삭제)

(todo)

🔇 Additional comments (33)
Projects/Presentation/Resources/Colors.xcassets/Red/Contents.json (1)

1-6: 표준 Contents.json 추가는 적절합니다

상위 Red 색상 그룹의 메타데이터 구성은 정상입니다. 별도 액션 없음.

Projects/Presentation/Resources/Colors.xcassets/Mint/Contents.json (1)

1-6: 표준 Contents.json 추가는 적절합니다

상위 Mint 색상 그룹의 메타데이터 구성은 정상입니다. 별도 액션 없음.

Projects/Presentation/Resources/Colors.xcassets/Green/Contents.json (1)

1-6: 표준 Contents.json 추가는 적절합니다

상위 Green 색상 그룹의 메타데이터 구성은 정상입니다. 별도 액션 없음.

Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_gray_graphic.imageset/Contents.json (2)

1-23: 이미지 세트 정의가 올바릅니다

1x/2x/3x 스케일 정의와 메타데이터가 정상입니다.


1-23: 검증 완료 — 이미지 파일 존재 및 용량 확인
결과: marble_gray_graphic@1x.png 12K, @2x 32K, @3x 60K — 과도한 용량으로 인한 성능 리스크 아님.

Projects/Presentation/Resources/Colors.xcassets/Purple/Purple5.colorset/Contents.json (2)

1-20: SRGB 컴포넌트 정의 적절

Purple5 색 값 정의 형식(srgb, 0x** 문자열)과 idiom 설정이 정상입니다.


1-20: 토큰 매핑 확인 — Purple5가 BitnagilColor에 정확히 매핑되어 있습니다.
Projects/Presentation/Sources/Common/DesignSystem/BitnagilColor.swift:65 — static let purple5 = UIColor(named: "Purple5", in: bundle, compatibleWith: nil)

Projects/Presentation/Resources/Colors.xcassets/Green/Green5.colorset/Contents.json (2)

1-20: SRGB 컴포넌트 정의 적절

Green5 색 값 정의 형식과 idiom 설정이 정상입니다.


1-20: 확인 완료 — Green5 토큰 매핑 일치

Projects/Presentation/Sources/Common/DesignSystem/BitnagilColor.swift 60행에서 UIColor(named: "Green5") 및 static let green5가 확인되었습니다.

Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_none_graphic.imageset/Contents.json (2)

1-23: 이미지 세트 정의가 올바릅니다

1x/2x/3x 스케일 정의와 메타데이터가 정상입니다.


1-23: 확인 완료 — 참조 PNG 파일 존재 및 용량 확인됨
marble_none_graphic@1x.png = 16K, @2x = 48K, @3x = 84K.

Projects/Presentation/Resources/Colors.xcassets/Purple/Contents.json (1)

1-6: 표준 Contents.json 추가는 적절합니다

상위 Purple 색상 그룹의 메타데이터 구성은 정상입니다. 별도 액션 없음.

Projects/Presentation/Resources/Colors.xcassets/Purple/Purple10.colorset/Contents.json (1)

1-20: 다크 모드 변형 추가 권장 — 사용처 확인 필요

  • Purple10 색상셋은 현재 단일 sRGB 값만 정의되어 있음. 배경/넓은 면적에 쓰일 경우 dark appearance(예: userInterfaceStyle = dark) 변형을 추가하세요.
  • 텍스트로 쓰일 경우 WCAG 명도 대비 확인 필요.

참고: 레포 검색 결과 'Purple10' 문자열은 디자인 시스템 래퍼에서 UIColor(named: "Purple10", in: bundle, compatibleWith: nil)로만 로드되는 것으로 확인됨 — Projects/Presentation/Sources/Common/DesignSystem/BitnagilColor.swift:66. 래퍼의 사용처(배경/텍스트 여부)를 확인해 적용 범위를 결정하세요.

Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/emotion_ground_graphic.imageset/Contents.json (1)

1-23: 의도치 않은 틴트 방지를 위한 rendering-intent 명시

해당 에셋(Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/emotion_ground_graphic.imageset)은 사진형 그래픽이므로 Contents.json에 properties.template-rendering-intent을 "original"로 고정하세요.

검증 요약: 코드베이스에 .withRenderingMode(.alwaysTemplate) 호출이 다수 존재(예: Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift, .../RecommendedRoutine/...RoutineLevelButton.swift, .../EmotionRegister/...EmotionRegisterCompletionViewController.swift). 아이콘은 코드에서 명시적으로 템플릿 모드로 강제하고 있으므로, 사진형 에셋은 asset 속성으로 "original"을 지정해도 필요 시 코드에서 .withRenderingMode(.alwaysTemplate)로 템플릿 적용 가능함. 다만 암묵적으로 에셋의 기본이 템플릿이기를 기대하는 사용처가 없는지 확인하세요.

권장 적용(diff):

 {
   "images" : [
     ...
   ],
   "info" : { "author" : "xcode", "version" : 1 }
+  ,
+  "properties": {
+    "template-rendering-intent": "original"
+  }
 }

추가 확인(선택): 해당 에셋 사용처 검색 — rg -n "ground_graphic" -S 로 암묵적 템플릿 사용 여부 확인.

Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_red_graphic.imageset/Contents.json (1)

1-23: 에셋 네이밍 매칭 확인 — 접근자 존재함

Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift의 46행에 static let marbleRedGraphic = UIImage(named: "marble_red_graphic", in: bundle, with: nil)로 정의되어 있어 에셋 이름과 일치합니다.

Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_orange_graphic.imageset/Contents.json (1)

1-23: 네이밍 매칭 확인 — 문제 없음
Assets.xcassets의 Graphic/Emotion/marble_orange_graphic.imageset(Contents.json)에 정의된 파일명(marble_orange_graphic@1x/2x/3x.png)이 코드의 UIImage(named: "marble_orange_graphic") 참조(Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift:48)와 일치합니다.

Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_pomo_left_hand_graphic.imageset/Contents.json (1)

1-23: 승인 — 네이밍 일관성 확인 완료
해상도/idiom 구성이 적정합니다. 오른손 자산 네이밍은 Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift:56-57에서 marble_pomo_left_hand_graphic / marble_pomo_right_hand_graphic로 쌍이 확인됩니다.

Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_speech_graphic.imageset/Contents.json (1)

1-23: 이미지셋(@1x/@2x/@3x) 누락 검사 — 수정 스크립트

원본 스크립트가 glob 매치 실패로 Contents.json 경로를 못 찾아 에러가 발생했습니다. 리포지토리 루트에서 아래 스크립트를 실행해 누락을 재검증하세요. (jq 필요)

#!/usr/bin/env bash
set -euo pipefail
shopt -s nullglob globstar

base='Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion'
if [[ ! -d "$base" ]]; then
  echo "error: base directory not found: $base" >&2
  exit 1
fi

found=0
for dir in "$base"/**/*.imageset; do
  [[ -d "$dir" ]] || continue
  found=1
  name=$(basename "$dir" .imageset)
  contents="$dir/Contents.json"
  if [[ ! -f "$contents" ]]; then
    printf "%s: missing Contents.json\n" "$name"
    continue
  fi

  missing=0
  printf "%s: " "$name"
  for s in 1x 2x 3x; do
    f=$(jq -r '.images[] | select(.scale=="'"$s"'") | .filename // empty' "$contents" 2>/dev/null || echo "")
    if [[ -z "$f" || ! -f "$dir/$f" ]]; then
      printf "missing(%s) " "$s"
      missing=1
    fi
  done

  if [[ $missing -eq 0 ]]; then
    echo "ok"
  else
    echo
  fi
done

if [[ $found -eq 0 ]]; then
  echo "warning: no .imageset directories found under $base" >&2
  exit 2
fi
Projects/Presentation/Sources/Common/DesignSystem/Font/FontStyle.swift (1)

29-29: cafe24Title2 케이스 추가 적절합니다

enum/switch 모두 누락 없이 커버되어 일관적입니다.

Projects/Presentation/Resources/Colors.xcassets/Purple/Purple500.colorset/Contents.json (1)

1-20: purple500 → "Purple500" 매핑 확인; 다크 모드 변형 없음 — 디자인팀 확인 필요

  • BitnagilColor.swift (Projects/Presentation/Sources/Common/DesignSystem/BitnagilColor.swift:67): static let purple500 = UIColor(named: "Purple500", in: bundle, compatibleWith: nil)
  • 에셋 (Projects/Presentation/Resources/Colors.xcassets/Purple/Purple500.colorset/Contents.json): 단일 universal 색상 #692BD0, appearances/dark 변형 없음

매핑 정상. 다크 모드 적용 필요성은 브랜드/디자인팀에서 결정할 것.

Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_purple_graphic.imageset/Contents.json (1)

1-23: 마블 퍼플 그래픽 에셋 추가 LGTM

1x/2x/3x 구성 정상이며 일반 이미지 용도에 적합합니다.

Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_pomo_right_hand_graphic.imageset/Contents.json (1)

1-23: 'pomo' 네이밍 일치 — 수정 불필요

Assets(Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_pomo_right_hand_graphic.imageset)와 BitnagilGraphic.swift(Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift:57)의 접근자가 모두 "marble_pomo_right_hand_graphic"를 참조합니다. 'fomo' 혼용은 검색에서 발견되지 않았습니다.

Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/fomo_thumb_graphic.imageset/Contents.json (1)

1-23: 확인 완료 — 네이밍과 접근자 일치
Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift:45의 fomoThumbGraphic이 "fomo_thumb_graphic" 리소스를 참조하며, Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/fomo_thumb_graphic.imageset/Contents.json에 @1x/@2x/@3x 파일이 존재합니다. "pomo" 혼용은 검색되지 않았습니다.

Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_green_graphic.imageset/Contents.json (1)

1-23: 마블 그린 그래픽 에셋 추가 LGTM

해상도 세트와 폴더 구조가 일관됩니다.

Projects/Presentation/Resources/Colors.xcassets/Mint/Mint10.colorset/Contents.json (1)

1-20: Mint10(#E2F3F6) 에셋 추가 LGTM

토큰-에셋 네이밍이 명확하고 팔레트 확장과 정합적입니다.

Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_mint_graphic.imageset/Contents.json (1)

1-23: 에셋 정의 정상 반영 (LGTM)

1x/2x/3x 스케일 모두 정의되어 있고 idiom/universal 설정도 적절합니다.

Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)

95-98: 새 아이콘 API 패턴 일관 유지 (LGTM)

doubleChevronIcon(direction:)이 기존 회전 패턴과 동일하게 템플릿 렌더링을 적용합니다.

Projects/Presentation/Sources/EmotionRegister/View/EmotionCollectionViewCell.swift (1)

35-49: 레이아웃 진행도 기반 트랜스폼 적용 (LGTM)

스케일 후 번역 순서가 의도대로 고정 픽셀 이동을 보장합니다. 진행도 클램프는 레이아웃에서 보장되므로 추가 처리 불필요합니다.

Projects/Presentation/Sources/EmotionRegister/ViewModel/EmotionRegisterViewModel.swift (1)

19-22: 출력 스트림 구성 적절 (LGTM)

선택/확정 가능 여부를 분리한 퍼블리셔 설계는 명확합니다.

Also applies to: 25-36

Projects/Presentation/Sources/EmotionRegister/View/EmotionCollectionViewLayout.swift (1)

34-54: 스냅 타깃 계산 로직 명확하고 안전 (LGTM)

가장 가까운 셀 중심으로 스냅하는 구현이 간결합니다.

Projects/Presentation/Sources/Common/DesignSystem/BitnagilColor.swift (1)

55-72: 팔레트 확장 반영 (LGTM)

Red/Green/Purple/Mint 계열 추가와 섹션 분리는 명확합니다.

Projects/Presentation/Sources/EmotionRegister/Model/Emotion.swift (2)

25-36: copy에서 id 재발급은 적절함

DiffableDataSource에서 아이템 중복을 피하기 위한 고유 id 재생성 의도가 분명합니다. 그대로 유지해도 좋습니다.


164-201: 멀티라인 문자열 UI 적합성 확인

description은 개행이 포함된 멀티라인입니다. 실제 사용처(예: 2줄 제한 라벨)에서 의도대로 줄바꿈/말줄임이 되는지 확인이 필요합니다. 필요 시 lineBreakMode(.byTruncatingTail)와 lineBreakStrategy 조정, 문구 길이 축약을 검토하세요.

Comment on lines +1 to +20
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x01",
"green" : "0x9F",
"red" : "0x5F"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
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

텍스트 사용 시 접근성 대비 기준 확인

Green500는 전경색(텍스트/아이콘) 용도로 쓰일 수 있는 채도/명도입니다. 라이트/다크 배경 각각에서 WCAG 대비(AA/AAA) 충족 여부를 확인해 주세요. 필요 시 다크 모드 변형 추가를 고려하세요.

간단 대비 체크:


🏁 Script executed:

#!/bin/bash
python - <<'PY'
def srgb_to_lin(c): c/=255; return c/12.92 if c<=0.04045*255 else ((c/255+0.055)/1.055)**2.4
def L(r,g,b): 
    rL,gL,bL=map(srgb_to_lin,[r,g,b]); return 0.2126*rL+0.7152*gL+0.0722*bL
def contrast(fg,bg):
    L1,L2=sorted([L(*fg),L(*bg)],reverse=True); return (L1+0.05)/(L2+0.05)
green500=(0x5F,0x9F,0x01)
for bg in ((255,255,255),(0,0,0)):
    print("vs", "white" if bg==(255,255,255) else "black", ":", round(contrast(green500,bg),2))
PY

Length of output: 31


대응 필요 — Green500(0x5F,0x9F,0x01) 전경색 사용 불가

화이트 대비 1.41, 블랙 대비 1.81로 WCAG AA(정상 텍스트 4.5:1) 미충족.

위치: Projects/Presentation/Resources/Colors.xcassets/Green/Green500.colorset/Contents.json

조치: 라이트/다크별 전용 변형을 추가하거나 색상(밝기/채도)을 조정해 각 모드에서 대비 ≥4.5(정상 텍스트) / ≥3.0(큰 텍스트) 확보 후 재검증.

🤖 Prompt for AI Agents
Projects/Presentation/Resources/Colors.xcassets/Green/Green500.colorset/Contents.json
lines 1-20: Green500 (R:0x5F,G:0x9F,B:0x01) fails WCAG contrast (white 1.41,
black 1.81) and cannot be used as foreground; update the asset to provide either
separate light/dark variants or a modified color with sufficient contrast.
Specifically, add appearance-specific color entries (appearance: "luminosity" or
separate "light" and "dark" color dictionaries) or adjust the
RGB/brightness/saturation for each appearance so the computed contrast ratio is
≥4.5:1 for normal text (or ≥3.0:1 for large text), then replace the current
single color entry with those variants in Contents.json and re-run contrast
verification.

Comment on lines +1 to +20
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x91",
"green" : "0xA7",
"red" : "0x25"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
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

토큰-자산 이름 동기화와 대비 확인

디자인시스템 토큰(mint500)과 자산 이름(Mint500) 매칭을 확인해 주세요. 또한 전경색으로 사용 시 라이트/다크 배경 대비 기준 충족을 점검하는 것이 좋습니다.

빠른 확인:


🏁 Script executed:

#!/bin/bash
# 토큰/이름 사용처 스캔
rg -nP 'Mint500|mint500' -S -C2
# 대비 체크
python - <<'PY'
def srgb_to_lin(c): c/=255; return c/12.92 if c<=0.04045*255 else ((c/255+0.055)/1.055)**2.4
def L(r,g,b): 
    rL,gL,bL=map(srgb_to_lin,[r,g,b]); return 0.2126*rL+0.7152*gL+0.0722*bL
def contrast(fg,bg):
    L1,L2=sorted([L(*fg),L(*bg)],reverse=True); return (L1+0.05)/(L2+0.05)
mint500=(0x25,0xA7,0x91)
for bg in ((255,255,255),(0,0,0)):
    print("vs", "white" if bg==(255,255,255) else "black", ":", round(contrast(mint500,bg),2))
PY

Length of output: 1141


Mint500 토큰↔자산 이름 일치 확인 — 대비 불충분, 수정 필요

  • 매핑: Projects/Presentation/Sources/Common/DesignSystem/BitnagilColor.swift:71 에서 mint500 → UIColor(named: "Mint500") 정의, 사용처: Projects/Presentation/Sources/EmotionRegister/Model/Emotion.swift:101.
  • 대비: 흰색 대비 1.39, 검정색 대비 1.84 — 일반 텍스트용 WCAG AA(≥4.5:1) 미달. 텍스트 전경색으로 사용 불가; 색상 조정 또는 사용 용도(아이콘/배경/보조색 등) 변경 필요.
🤖 Prompt for AI Agents
Projects/Presentation/Resources/Colors.xcassets/Mint/Mint500.colorset/Contents.json
(lines 1-20): the asset currently named/defined as Mint500 has RGB components
that produce insufficient contrast (white 1.39, black 1.84) and thus cannot be
used for foreground text; either (A) change the asset color components to a
darker or lighter value that achieves a WCAG AA contrast ratio ≥4.5:1 against
the intended foreground (or against white/black if used dynamically), or (B)
restrict its usage to non-text roles (icons, backgrounds, accents) and update
any documentation/comments to reflect that; if you choose to rename the asset to
match the token used in code, ensure the asset filename and the UIColor(named:
"...") key in
Projects/Presentation/Sources/Common/DesignSystem/BitnagilColor.swift (line ~71)
are identical (case-sensitive) and update
Projects/Presentation/Sources/EmotionRegister/Model/Emotion.swift usage if
needed.

Comment on lines +53 to 59
// MARK: - Emotion Register Completion
static let emotionCompletionBackgroundGraphic = UIImage(named: "emotion_background_graphic", in: bundle, with: nil)
static let emotionCompletionGroundGraphic = UIImage(named: "emotion_ground_graphic", in: bundle, with: nil)
static let pomoLeftHandGraphic = UIImage(named: "marble_pomo_left_hand_graphic", in: bundle, with: nil)
static let pomoRightHandGraphic = UIImage(named: "marble_pomo_right_hand_graphic", in: bundle, with: nil)
static let marblePomoGraphic = UIImage(named: "marblePomo_graphic", in: bundle, with: nil)
}
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

에셋 명 일관성/존재 확인 요청

"marblePomo_graphic"만 카멜+스네이크가 혼재합니다(다른 키는 snake_case). 에셋 카탈로그의 실제 이름과 일치하는지 점검 바랍니다. 일관된 네이밍을 권장합니다.

아래 스크립트로 코드에서 참조하는 이미지 이름이 xcassets에 존재하는지 확인할 수 있습니다.


🏁 Script executed:

#!/bin/bash
# xcassets 내 이미지 이름 수집
fd -t f 'Contents.json' 'Projects/Presentation/Resources/Images.xcassets' \
  | xargs -I{} jq -r '..|.filename? // empty' {} \
  | sed 's/\.[^.]*$//' | sort -u > /tmp/asset_names.txt

# 코드에서 참조하는 이름 수집
rg -nP '\bUIImage\(named:\s*"([^"]+)"' -o \
  Projects/Presentation/Sources | sed -E 's/.*"([^"]+)".*/\1/' \
  | sort -u > /tmp/code_image_names.txt

echo "다음 키는 코드에 있으나 에셋에 없음:"
comm -23 /tmp/code_image_names.txt /tmp/asset_names.txt

Length of output: 722


에셋 누락·네이밍 불일치 — 코드에서 참조하는 이미지가 xcassets에 없습니다

다음 이미지들이 코드에서 참조되지만 Projects/Presentation/Resources/Images.xcassets에 존재하지 않습니다. 에셋을 추가하거나 코드 이름을 xcassets에 맞게 수정하세요.

  • 누락된 이미지 키: asterisk_icon, bitnagil_chevron_icon, checked_icon, clear_icon, delete_icon, divide_line_icon, double_chevron_icon, emotion_background_graphic, emotion_ground_graphic, exclamation_filled_icon, fomo_hand_graphic, fomo_thumb_graphic, marblePomo_graphic, marble_gray_graphic, marble_green_graphic, marble_mint_graphic, marble_none_graphic, marble_orange_graphic, marble_pomo_left_hand_graphic, marble_pomo_right_hand_graphic, marble_purple_graphic, marble_red_graphic, marble_speech_graphic, routine_creation_date, routine_creation_icon, routine_creation_list_icon, routine_creation_one_icon, routine_creation_repeat_icon, routine_creation_three_icon, routine_creation_time_icon, routine_creation_two_icon, setting_icon, unchecked_icon

  • 네이밍: marblePomo_graphic는 camel+snake 혼용(다른 키는 snake_case). 실제 xcassets 이름과 일치시키고 네이밍을 일관화하세요.

🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift
around lines 53–59, multiple image names referenced by UIImage(named:in:with:)
do not exist in Projects/Presentation/Resources/Images.xcassets and one name
(marblePomo_graphic) mixes camelCase and snake_case; fix by either adding the
missing asset files to Images.xcassets with the exact keys listed in the review
(asterisk_icon, bitnagil_chevron_icon, checked_icon, clear_icon, delete_icon,
divide_line_icon, double_chevron_icon, emotion_background_graphic,
emotion_ground_graphic, exclamation_filled_icon, fomo_hand_graphic,
fomo_thumb_graphic, marblePomo_graphic OR the corrected snake_case variant,
marble_gray_graphic, marble_green_graphic, marble_mint_graphic,
marble_none_graphic, marble_orange_graphic, marble_pomo_left_hand_graphic,
marble_pomo_right_hand_graphic, marble_purple_graphic, marble_red_graphic,
marble_speech_graphic, routine_creation_date, routine_creation_icon,
routine_creation_list_icon, routine_creation_one_icon,
routine_creation_repeat_icon, routine_creation_three_icon,
routine_creation_time_icon, routine_creation_two_icon, setting_icon,
unchecked_icon) or rename the code references to match the existing xcassets
keys (prefer snake_case like marble_pomo_graphic for consistency); ensure names
are an exact match (including underscores and case) and update any references
across the codebase accordingly.

Comment on lines +12 to +23
struct Emotion: Hashable {
let id = UUID()
let emotionType: String
let emotionTitle: String
let emotionImageUrl: URL?
let emotionMessage: String?
// TODO: - 서버 연동 후 삭제
var image: UIImage?
var fomoImage: UIImage?
var textColor: UIColor?
var backgroundColor: UIColor?
var titleDescription: String?
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

Hashable 합성 불가 – 수동 구현 필요

UIImage?/UIColor?는 Hashable이 아니므로 현재 Hashable 합성이 실패합니다. DiffableDataSource의 아이템 키로 쓰려면 id 기반의 동치성과 해시를 수동으로 구현하세요.

다음 패치를 권장합니다(동일 파일 내, struct 본문에 추가).

 struct Emotion: Hashable {
     let id = UUID()
@@
     var titleDescription: String?
 
+    static func == (lhs: Emotion, rhs: Emotion) -> Bool {
+        lhs.id == rhs.id
+    }
+
+    func hash(into hasher: inout Hasher) {
+        hasher.combine(id)
+    }
 }
📝 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
struct Emotion: Hashable {
let id = UUID()
let emotionType: String
let emotionTitle: String
let emotionImageUrl: URL?
let emotionMessage: String?
// TODO: - 서버 연동 후 삭제
var image: UIImage?
var fomoImage: UIImage?
var textColor: UIColor?
var backgroundColor: UIColor?
var titleDescription: String?
struct Emotion: Hashable {
let id = UUID()
let emotionType: String
let emotionTitle: String
let emotionImageUrl: URL?
let emotionMessage: String?
// TODO: - 서버 연동 후 삭제
var image: UIImage?
var fomoImage: UIImage?
var textColor: UIColor?
var backgroundColor: UIColor?
var titleDescription: String?
static func == (lhs: Emotion, rhs: Emotion) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 18-18: TODOs should be resolved (- 서버 연동 후 삭제)

(todo)

🤖 Prompt for AI Agents
In Projects/Presentation/Sources/EmotionRegister/Model/Emotion.swift around
lines 12 to 23, Hashable synthesis fails because UIImage/UIColor are not
Hashable; implement manual Hashable/Equatable using only the id property: add an
explicit static func ==(lhs: Emotion, rhs: Emotion) -> Bool that returns lhs.id
== rhs.id and implement func hash(into hasher: inout Hasher) {
hasher.combine(id) }; keep all other properties unchanged so DiffableDataSource
item identity is based solely on id.

Comment on lines +49 to +58
// TODO: - 서버 연동 후 삭제
let emotinonDummies: [Emotion] = Marble.allCases.map { Emotion(
emotionType: $0.rawValue,
emotionTitle: $0.name,
emotionImageUrl: nil,
emotionMessage: $0.description,
image: $0.marbleImage,
textColor: $0.textColor,
backgroundColor: $0.backgroundColor,
titleDescription: $0.koreanDescription)}
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

더미 데이터 노출 최소화 및 오탈자 수정 제안

전역 let emotinonDummies는 출시 빌드에 노출될 위험이 있습니다. DEBUG 가드로 감싸고 오탈자(emotinon → emotion)도 정리해 주세요.

다음 패치를 권장합니다(호출처가 많다면 typealias로 점진 이행 가능).

-// TODO: - 서버 연동 후 삭제
-let emotinonDummies: [Emotion] = Marble.allCases.map { Emotion(
+#if DEBUG
+// TODO: - 서버 연동 후 삭제
+let emotionDummies: [Emotion] = Marble.allCases.map { Emotion(
     emotionType: $0.rawValue,
@@
     titleDescription: $0.koreanDescription)}
+#endif

호출처에서는 일단 기존 이름을 유지해야 한다면 아래 보완을 함께 적용하세요(같은 파일 하단에 추가).

+#if DEBUG
+@available(*, deprecated, message: "Use `emotionDummies` instead.")
+let emotinonDummies = emotionDummies
+#endif
📝 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
// TODO: - 서버 연동 후 삭제
let emotinonDummies: [Emotion] = Marble.allCases.map { Emotion(
emotionType: $0.rawValue,
emotionTitle: $0.name,
emotionImageUrl: nil,
emotionMessage: $0.description,
image: $0.marbleImage,
textColor: $0.textColor,
backgroundColor: $0.backgroundColor,
titleDescription: $0.koreanDescription)}
#if DEBUG
// TODO: - 서버 연동 후 삭제
let emotionDummies: [Emotion] = Marble.allCases.map { Emotion(
emotionType: $0.rawValue,
emotionTitle: $0.name,
emotionImageUrl: nil,
emotionMessage: $0.description,
image: $0.marbleImage,
textColor: $0.textColor,
backgroundColor: $0.backgroundColor,
titleDescription: $0.koreanDescription)}
#endif
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 49-49: TODOs should be resolved (- 서버 연동 후 삭제)

(todo)

🤖 Prompt for AI Agents
In Projects/Presentation/Sources/EmotionRegister/Model/Emotion.swift around
lines 49–58, the global constant named emotinonDummies exposes dummy data in
release builds and has a typo; wrap the dummy data declaration in a compile-time
DEBUG guard (#if DEBUG ... #endif) so it is only included in debug builds,
rename the constant to the correct emotionDummies, and add a deprecated
compatibility alias named emotinonDummies (pointing to emotionDummies) to avoid
breaking callers while signaling migration; ensure the original TODO stays or is
removed per policy and update any references incrementally.

Comment on lines +30 to +33
override func prepareForReuse() {
super.prepareForReuse()
contentView.transform = .identity
}
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

재사용 시 잘못된 뷰에 트랜스폼 리셋

애니메이션을 emotionImageView에 적용하고 있는데 contentView를 리셋하고 있습니다. 셀 재사용 시 잔상 발생 가능.

-    override func prepareForReuse() {
-        super.prepareForReuse()
-        contentView.transform = .identity
-    }
+    override func prepareForReuse() {
+        super.prepareForReuse()
+        emotionImageView.transform = .identity
+    }
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/EmotionRegister/View/EmotionCollectionViewCell.swift
around lines 30-33, the cell currently resets contentView.transform in
prepareForReuse even though animations are applied to emotionImageView; change
the reset to target the animated view: keep super.prepareForReuse(), set
emotionImageView.transform = .identity, and also remove any running animations
by calling emotionImageView.layer.removeAllAnimations() (optionally reset alpha
or other modified view properties) so reused cells don't show animation
remnants.

Comment on lines +352 to +367
private func configureMarbleImageView() {
marbleImageViewPanGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(gesture:)))
marbleImageViewPanGesture?.cancelsTouchesInView = false

handMarbleView.layer.cornerRadius = Layout.emotionMarbleImageViewSize / 2
handMarbleView.layer.masksToBounds = true
handMarbleView.backgroundColor = .clear

emotionMarbleImageView.isUserInteractionEnabled = true
emotionMarbleImageView.layer.cornerRadius = Layout.emotionMarbleImageViewSize / 2
emotionMarbleImageView.layer.masksToBounds = true

guard let marbleImageViewPanGesture else { return }

emotionMarbleImageView.addGestureRecognizer(marbleImageViewPanGesture)
}
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

세로 드래그와 컬렉션 스크롤 제스처 충돌 가능성

cancelsTouchesInView = false로 두면 컬렉션의 가로 스와이프와 동시에 인식되어 미묘한 끊김이 발생할 수 있습니다. 팬 제스처를 델리게이트로 분리해 “세로 우선”만 시작되도록 제한하고, cancelsTouchesInView는 true로 변경하세요.

다음 패치를 권장합니다.

-        marbleImageViewPanGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(gesture:)))
-        marbleImageViewPanGesture?.cancelsTouchesInView = false
+        marbleImageViewPanGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(gesture:)))
+        marbleImageViewPanGesture?.cancelsTouchesInView = true
+        marbleImageViewPanGesture?.delegate = self

그리고 파일 하단에 델리게이트 구현을 추가하세요.

 extension EmotionRegistrationViewController: UIScrollViewDelegate {
@@
 }
 
+extension EmotionRegistrationViewController: UIGestureRecognizerDelegate {
+    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
+        guard gestureRecognizer === marbleImageViewPanGesture else { return true }
+        let v = marbleImageViewPanGesture?.velocity(in: view) ?? .zero
+        // 세로 성분이 더 클 때만 시작
+        return abs(v.y) > abs(v.x)
+    }
+
+    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
+                           shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
+        // 드래그 중 컬렉션 스크롤과 동시 인식 방지
+        gestureRecognizer === marbleImageViewPanGesture ? false : 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
private func configureMarbleImageView() {
marbleImageViewPanGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(gesture:)))
marbleImageViewPanGesture?.cancelsTouchesInView = false
handMarbleView.layer.cornerRadius = Layout.emotionMarbleImageViewSize / 2
handMarbleView.layer.masksToBounds = true
handMarbleView.backgroundColor = .clear
emotionMarbleImageView.isUserInteractionEnabled = true
emotionMarbleImageView.layer.cornerRadius = Layout.emotionMarbleImageViewSize / 2
emotionMarbleImageView.layer.masksToBounds = true
guard let marbleImageViewPanGesture else { return }
emotionMarbleImageView.addGestureRecognizer(marbleImageViewPanGesture)
}
private func configureMarbleImageView() {
marbleImageViewPanGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(gesture:)))
marbleImageViewPanGesture?.cancelsTouchesInView = true
marbleImageViewPanGesture?.delegate = self
handMarbleView.layer.cornerRadius = Layout.emotionMarbleImageViewSize / 2
handMarbleView.layer.masksToBounds = true
handMarbleView.backgroundColor = .clear
emotionMarbleImageView.isUserInteractionEnabled = true
emotionMarbleImageView.layer.cornerRadius = Layout.emotionMarbleImageViewSize / 2
emotionMarbleImageView.layer.masksToBounds = true
guard let marbleImageViewPanGesture else { return }
emotionMarbleImageView.addGestureRecognizer(marbleImageViewPanGesture)
}
Suggested change
private func configureMarbleImageView() {
marbleImageViewPanGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(gesture:)))
marbleImageViewPanGesture?.cancelsTouchesInView = false
handMarbleView.layer.cornerRadius = Layout.emotionMarbleImageViewSize / 2
handMarbleView.layer.masksToBounds = true
handMarbleView.backgroundColor = .clear
emotionMarbleImageView.isUserInteractionEnabled = true
emotionMarbleImageView.layer.cornerRadius = Layout.emotionMarbleImageViewSize / 2
emotionMarbleImageView.layer.masksToBounds = true
guard let marbleImageViewPanGesture else { return }
emotionMarbleImageView.addGestureRecognizer(marbleImageViewPanGesture)
}
extension EmotionRegistrationViewController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer === marbleImageViewPanGesture else { return true }
let v = marbleImageViewPanGesture?.velocity(in: view) ?? .zero
// 시작은 세로 우선일 때만 허용
return abs(v.y) > abs(v.x)
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
// 마블 드래그 중 컬렉션 스크롤과 동시 인식 방지
return gestureRecognizer === marbleImageViewPanGesture ? false : true
}
}
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/EmotionRegister/View/EmotionRegistrationViewController.swift
around lines 352 to 367, change marbleImageViewPanGesture?.cancelsTouchesInView
= false to true, set marbleImageViewPanGesture?.delegate = self, and add a
UIGestureRecognizerDelegate implementation at the bottom of the file that (1)
implements gestureRecognizerShouldBegin(_:) to inspect the pan gesture's
translation/velocity and only begin when vertical movement is dominant (abs(dy)
> abs(dx)), and (2) implements
gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) to return false for
horizontal collection-swipes; also make the view controller conform to
UIGestureRecognizerDelegate. Ensure the delegate uses the pan gesture instance
safely (guard/optional cast) and uses the gesture's translation(in:) or
velocity(in:) to decide vertical-priority start.

Comment on lines +461 to +477
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let centerPoint = CGPoint(
x: emotionCollectionView.contentOffset.x + emotionCollectionView.frame.midX,
y: emotionCollectionView.contentOffset.y)

guard
let collectionView = scrollView as? UICollectionView,
let indexPath = collectionView.indexPathForItem(at: centerPoint),
itemCount > 0
else { return }

let index = indexPath.row % itemCount
if indexPath.row < itemCount / 3 || indexPath.row >= itemCount * 2 / 3 {
scrollToIndex(index: index)
}
viewModel.action(input: .selectEmotion(index: index))
}
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

무한 스크롤 재센터링 임계치가 잘못되어 과도한 리셋 발생

현재 조건은 middle 세트 밖 여부가 아니라 “itemCount의 1/3, 2/3”을 기준으로 하고 있어 대부분의 위치에서 불필요한 리센터링이 발생합니다. middle 세트(n..2n-1) 밖일 때만 리센터링해야 합니다.

수정 패치:

-        let index = indexPath.row % itemCount
-        if indexPath.row < itemCount / 3 || indexPath.row >= itemCount * 2 / 3 {
+        let index = indexPath.row % itemCount
+        if indexPath.row < itemCount || indexPath.row >= 2 * itemCount {
             scrollToIndex(index: index)
         }
📝 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
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let centerPoint = CGPoint(
x: emotionCollectionView.contentOffset.x + emotionCollectionView.frame.midX,
y: emotionCollectionView.contentOffset.y)
guard
let collectionView = scrollView as? UICollectionView,
let indexPath = collectionView.indexPathForItem(at: centerPoint),
itemCount > 0
else { return }
let index = indexPath.row % itemCount
if indexPath.row < itemCount / 3 || indexPath.row >= itemCount * 2 / 3 {
scrollToIndex(index: index)
}
viewModel.action(input: .selectEmotion(index: index))
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let centerPoint = CGPoint(
x: emotionCollectionView.contentOffset.x + emotionCollectionView.frame.midX,
y: emotionCollectionView.contentOffset.y)
guard
let collectionView = scrollView as? UICollectionView,
let indexPath = collectionView.indexPathForItem(at: centerPoint),
itemCount > 0
else { return }
let index = indexPath.row % itemCount
if indexPath.row < itemCount || indexPath.row >= 2 * itemCount {
scrollToIndex(index: index)
}
viewModel.action(input: .selectEmotion(index: index))
}
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/EmotionRegister/View/EmotionRegistrationViewController.swift
around lines 461–477, the condition that triggers recentring uses itemCount/3
and itemCount*2/3 which is incorrect and causes excessive recentring; change the
check to detect positions outside the middle set (middle set spans indices
itemCount..(itemCount*2 - 1)), i.e. replace the current if that uses itemCount/3
and itemCount*2/3 with a check that recentres only when indexPath.row <
itemCount || indexPath.row >= itemCount * 2 so the view recentres only when the
selected item is outside the middle repeated set.

Comment on lines +63 to +75
private func selectEmotion(index: Int) {
let emotions = emotionListSubject.value

guard
index >= 0,
index < emotions.count
else { return }

let emotion = emotions[index]

selectedEmotionSubject.send(emotion)
confirmEmotionEnabledSubject.send(emotion.emotionType != "NONE")
}
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

매직 스트링 제거 및 타입 안전성 강화

emotion.emotionType != "NONE"는 취약합니다. Domain의 타입(예: enum)으로 비교하거나 최소한 상수로 캡슐화하세요. 또한 confirmEmotionEnabledPublisherselectedEmotionSubject에서 map으로 유도하면 단일 소스로 일관성을 유지할 수 있습니다.

예시:

-        selectedEmotionSubject.send(emotion)
-        confirmEmotionEnabledSubject.send(emotion.emotionType != "NONE")
+        selectedEmotionSubject.send(emotion)
+        confirmEmotionEnabledSubject.send(EmotionType(rawValue: emotion.emotionType) != .none)

또는 Output 구성 시:

-    private let confirmEmotionEnabledSubject = PassthroughSubject<Bool, Never>()
+    private let confirmEmotionEnabledSubject = PassthroughSubject<Bool, Never>() // (임시) 서버 연동 전 유지

// init 내부 대안(선택):
// confirmEmotionEnabledPublisher를 selectedEmotionPublisher.map으로 유도
// .map { $0.map { EmotionType(rawValue: $0.emotionType) != .none } ?? false }
📝 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
private func selectEmotion(index: Int) {
let emotions = emotionListSubject.value
guard
index >= 0,
index < emotions.count
else { return }
let emotion = emotions[index]
selectedEmotionSubject.send(emotion)
confirmEmotionEnabledSubject.send(emotion.emotionType != "NONE")
}
private func selectEmotion(index: Int) {
let emotions = emotionListSubject.value
guard
index >= 0,
index < emotions.count
else { return }
let emotion = emotions[index]
selectedEmotionSubject.send(emotion)
confirmEmotionEnabledSubject.send(EmotionType(rawValue: emotion.emotionType) != .none)
}
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/EmotionRegister/ViewModel/EmotionRegisterViewModel.swift
around lines 63 to 75, avoid the magic string comparison emotion.emotionType !=
"NONE" and the duplicated state updates: change the model to use a typed
EmotionType (enum) or at minimum a private constant for "NONE", compare against
that typed value, and stop directly sending confirmEmotionEnabledSubject here;
instead derive confirmEmotionEnabledPublisher/subject by mapping
selectedEmotionSubject (e.g., map { $0.emotionType != .none } ) so confirm state
is a single source of truth driven from selectedEmotionSubject; remove the
confirmEmotionEnabledSubject.send call and update types/signatures accordingly.

Copy link
Copy Markdown
Contributor

@choijungp choijungp left a comment

Choose a reason for hiding this comment

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

딩동 .... 우선 정말정말 감사하고 죄송합니다 ㅜㅜ !!!!!!!!!!!!

우선 완존 완존 친절하게 작성해주신 PR description 덕분에 코드 읽기가 수월했습니다 !!!
콜렉션 뷰 알못인데 .... 딩 덕분에 공부의 이유가 더 생겨서 진심 감사해유 ㅠ.ㅠ. ......

머라고 얹을말이 없어유 ........... 저두 후딱 분발해올게요 ㅠㅠ
띵푸루부 😇😇😇😇😇😇😇😇😇😇😇😇😇

Comment on lines +35 to +49
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)

guard let attributes = layoutAttributes as? EmotionCollectionViewLayoutAttributes else { return }

let progress = attributes.progress
let targetScale = Layout.maxImageViewSize / Layout.imageViewSize
let scale = 1.0 + (targetScale - 1.0) * progress
let translateY = Layout.maxTranslateY * progress
let transform = CGAffineTransform.identity
.scaledBy(x: scale, y: scale)
.translatedBy(x: 0, y: translateY)

emotionImageView.transform = transform
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

긍까 .. 이것이 구슬의 진행도에 따라 cell의 크기와 Y 위치를 정해주는 함수라는 것이고 .........

Comment on lines +46 to +53
// 화면 중앙에 가장 가까운 셀 찾기
let centerX = proposed.x + collectionView.bounds.width / 2
let closestTarget = layoutAttributesList.min { abs($0.center.x - centerX) < abs($1.center.x - centerX) }

guard let closestTarget else { return proposed }

let newX = closestTarget.center.x - collectionView.bounds.width / 2
return CGPoint(x: newX, y: proposed.y)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

이것은 화면 중앙에서 가장 가까운 셀을 찾아서 중앙에 위치하도록 하는 것이라는 것이죠 ...................

Comment on lines +56 to +73
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard
let collectionView,
let baseAttributesList = super.layoutAttributesForElements(in: rect)
else { return super.layoutAttributesForElements(in: rect) }

let centerX = collectionView.contentOffset.x + collectionView.bounds.width / 2

return baseAttributesList.map { base in
guard let copied = base.copy() as? EmotionCollectionViewLayoutAttributes else { return base }

let distanceFromCenter = abs(copied.center.x - centerX)
let t = min(distanceFromCenter / baseDistance, 1)

copied.progress = 1 - t
return copied
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

이거슨 정확히 무엇일까요 ....
컬렉션 뷰 내의 셀의 progress를 계산해주는 함수 .. .? ㅠㅠ

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

맞습니다!!

우선 설명에 앞서 이번 PR에서는 UICollectionViewLayoutAttributes 를 상속해서 collectionView에서 사용할 EmotionCollectionViewLayoutAttributes 를 구현했어요. 이 EmotionCollectionViewLayoutAttributes 안에 진행도를 나타내는 progress 라는 프로퍼티가 있습니다.

제가 이해한 바로는 layoutAttributesForElements 메서드를 통해 해당 rect (현재는 collectionView의 bound 인 것으로 알고 있습니다.) 의 레이아웃 속성 들을 알 수 있어요! 현재 override한 메서드에서는 EmotionCollectionViewLayoutAttributes의 progress를 가져오고, 이를 현재 상태에 맞는 progress로 계산해서 return 해준다고 보시면 될거 같습니다!

Comment on lines +13 to +20
override func copy(with zone: NSZone? = nil) -> Any {
let superCopy = super.copy(with: zone)

guard let copied = superCopy as? EmotionCollectionViewLayoutAttributes else { return superCopy }

copied.progress = progress
return copied
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

먼가 에어플레인때가 생각이 드는데요 ..!!!
카피를 해서 주는 이유가 무엇인지 궁구맘니다 ㅠ !!!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

따로 override하지 않으면, 기본 구현되어 있는 레이아웃 속성들만 반환한다고 합니다! 그래서 커스텀으로 추가한 progress 값도 같이 넘겨주기 위해 copy를 override 했습니다.

isEqual를 override 한 이유도 같은 맥락이라 봐주시면 좋을 것 같습니다!

호옥시~ 잘못된 정보였다면 나중에 꼭 다시 말씀드리겠습니다!

@taipaise taipaise merged commit 37d650e into develop Sep 17, 2025
2 checks passed
@choijungp choijungp deleted the refactor/emotion-registration branch November 23, 2025 05:47
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