Conversation
WalkthroughEmotion 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 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)) PYProjects/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
⛔ Files ignored due to path filters (51)
Projects/Presentation/Resources/Images.xcassets/Graphic/Emotion/emotion_background_graphic.imageset/emotion_background_graphic@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/emotion_background_graphic.imageset/emotion_background_graphic@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/emotion_background_graphic.imageset/emotion_background_graphic@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/emotion_ground_graphic.imageset/ground_graphic@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/emotion_ground_graphic.imageset/ground_graphic@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/emotion_ground_graphic.imageset/ground_graphic@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/fomo_hand_graphic.imageset/fomo_hand_graphic@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/fomo_hand_graphic.imageset/fomo_hand_graphic@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/fomo_hand_graphic.imageset/fomo_hand_graphic@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/fomo_thumb_graphic.imageset/fomo_thumb_graphic@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/fomo_thumb_graphic.imageset/fomo_thumb_graphic@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/fomo_thumb_graphic.imageset/fomo_thumb_graphic@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marblePomo_graphic.imageset/marblePomo_graphic@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marblePomo_graphic.imageset/marblePomo_graphic@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marblePomo_graphic.imageset/marblePomo_graphic@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_gray_graphic.imageset/marble_gray_graphic@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_gray_graphic.imageset/marble_gray_graphic@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_gray_graphic.imageset/marble_gray_graphic@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_green_graphic.imageset/marble_green_graphic@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_green_graphic.imageset/marble_green_graphic@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_green_graphic.imageset/marble_green_graphic@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_mint_graphic.imageset/marble_mint_graphic@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_mint_graphic.imageset/marble_mint_graphic@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_mint_graphic.imageset/marble_mint_graphic@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_none_graphic.imageset/marble_none_graphic@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_none_graphic.imageset/marble_none_graphic@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_none_graphic.imageset/marble_none_graphic@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_orange_graphic.imageset/marble_orange_graphic@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_orange_graphic.imageset/marble_orange_graphic@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_orange_graphic.imageset/marble_orange_graphic@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_pomo_left_hand_graphic.imageset/marble_pomo_left_hand_graphic@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_pomo_left_hand_graphic.imageset/marble_pomo_left_hand_graphic@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_pomo_left_hand_graphic.imageset/marble_pomo_left_hand_graphic@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_pomo_right_hand_graphic.imageset/marble_pomo_right_hand_graphic@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_pomo_right_hand_graphic.imageset/marble_pomo_right_hand_graphic@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_pomo_right_hand_graphic.imageset/marble_pomo_right_hand_graphic@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_purple_graphic.imageset/marble_purple_graphic@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_purple_graphic.imageset/marble_purple_graphic@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_purple_graphic.imageset/marble_purple_graphic@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_red_graphic.imageset/marble_red_graphic@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_red_graphic.imageset/marble_red_graphic@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_red_graphic.imageset/marble_red_graphic@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_speech_graphic.imageset/marble_speech_graphic@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_speech_graphic.imageset/marble_speech_graphic@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/Emotion/marble_speech_graphic.imageset/marble_speech_graphic@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/background_graphic.imageset/background_graphic@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/background_graphic.imageset/background_graphic@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/background_graphic.imageset/background_graphic@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/double_chevron_icon.imageset/double_chevron_icon@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/double_chevron_icon.imageset/double_chevron_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/double_chevron_icon.imageset/double_chevron_icon@3x.pngis 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 fiProjects/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: 마블 퍼플 그래픽 에셋 추가 LGTM1x/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 조정, 문구 길이 축약을 검토하세요.
| { | ||
| "colors" : [ | ||
| { | ||
| "color" : { | ||
| "color-space" : "srgb", | ||
| "components" : { | ||
| "alpha" : "1.000", | ||
| "blue" : "0x01", | ||
| "green" : "0x9F", | ||
| "red" : "0x5F" | ||
| } | ||
| }, | ||
| "idiom" : "universal" | ||
| } | ||
| ], | ||
| "info" : { | ||
| "author" : "xcode", | ||
| "version" : 1 | ||
| } | ||
| } |
There was a problem hiding this comment.
💡 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))
PYLength 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.
| { | ||
| "colors" : [ | ||
| { | ||
| "color" : { | ||
| "color-space" : "srgb", | ||
| "components" : { | ||
| "alpha" : "1.000", | ||
| "blue" : "0x91", | ||
| "green" : "0xA7", | ||
| "red" : "0x25" | ||
| } | ||
| }, | ||
| "idiom" : "universal" | ||
| } | ||
| ], | ||
| "info" : { | ||
| "author" : "xcode", | ||
| "version" : 1 | ||
| } | ||
| } |
There was a problem hiding this comment.
💡 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))
PYLength 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.
| // 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) | ||
| } |
There was a problem hiding this comment.
💡 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.txtLength 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.
| 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? |
There was a problem hiding this comment.
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.
| 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.
| // 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)} |
There was a problem hiding this comment.
🛠️ 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.
| // 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.
| override func prepareForReuse() { | ||
| super.prepareForReuse() | ||
| contentView.transform = .identity | ||
| } |
There was a problem hiding this comment.
재사용 시 잘못된 뷰에 트랜스폼 리셋
애니메이션을 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.
| 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) | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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) | |
| } |
| 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.
| 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)) | ||
| } |
There was a problem hiding this comment.
무한 스크롤 재센터링 임계치가 잘못되어 과도한 리셋 발생
현재 조건은 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.
| 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.
| 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") | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
매직 스트링 제거 및 타입 안전성 강화
emotion.emotionType != "NONE"는 취약합니다. Domain의 타입(예: enum)으로 비교하거나 최소한 상수로 캡슐화하세요. 또한 confirmEmotionEnabledPublisher는 selectedEmotionSubject에서 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.
| 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.
choijungp
left a comment
There was a problem hiding this comment.
딩동 .... 우선 정말정말 감사하고 죄송합니다 ㅜㅜ !!!!!!!!!!!!
우선 완존 완존 친절하게 작성해주신 PR description 덕분에 코드 읽기가 수월했습니다 !!!
콜렉션 뷰 알못인데 .... 딩 덕분에 공부의 이유가 더 생겨서 진심 감사해유 ㅠ.ㅠ. ......
머라고 얹을말이 없어유 ........... 저두 후딱 분발해올게요 ㅠㅠ
띵푸루부 😇😇😇😇😇😇😇😇😇😇😇😇😇
| 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 | ||
| } |
There was a problem hiding this comment.
긍까 .. 이것이 구슬의 진행도에 따라 cell의 크기와 Y 위치를 정해주는 함수라는 것이고 .........
| // 화면 중앙에 가장 가까운 셀 찾기 | ||
| 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) |
There was a problem hiding this comment.
이것은 화면 중앙에서 가장 가까운 셀을 찾아서 중앙에 위치하도록 하는 것이라는 것이죠 ...................
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
이거슨 정확히 무엇일까요 ....
컬렉션 뷰 내의 셀의 progress를 계산해주는 함수 .. .? ㅠㅠ
There was a problem hiding this comment.
맞습니다!!
우선 설명에 앞서 이번 PR에서는 UICollectionViewLayoutAttributes 를 상속해서 collectionView에서 사용할 EmotionCollectionViewLayoutAttributes 를 구현했어요. 이 EmotionCollectionViewLayoutAttributes 안에 진행도를 나타내는 progress 라는 프로퍼티가 있습니다.
제가 이해한 바로는 layoutAttributesForElements 메서드를 통해 해당 rect (현재는 collectionView의 bound 인 것으로 알고 있습니다.) 의 레이아웃 속성 들을 알 수 있어요! 현재 override한 메서드에서는 EmotionCollectionViewLayoutAttributes의 progress를 가져오고, 이를 현재 상태에 맞는 progress로 계산해서 return 해준다고 보시면 될거 같습니다!
| 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 | ||
| } |
There was a problem hiding this comment.
먼가 에어플레인때가 생각이 드는데요 ..!!!
카피를 해서 주는 이유가 무엇인지 궁구맘니다 ㅠ !!!
There was a problem hiding this comment.
따로 override하지 않으면, 기본 구현되어 있는 레이아웃 속성들만 반환한다고 합니다! 그래서 커스텀으로 추가한 progress 값도 같이 넘겨주기 위해 copy를 override 했습니다.
isEqual를 override 한 이유도 같은 맥락이라 봐주시면 좋을 것 같습니다!
호옥시~ 잘못된 정보였다면 나중에 꼭 다시 말씀드리겠습니다!
🌁 Background
📱 Screenshot
👩💻 Contents
📝 Review Note
carousel collectionView 구현하기 + 중앙 snap 구현
UICollectionViewFlowLayout를 상속한EmotionCollectionViewLayout을 구현했습니다.1. cell이 항상 중앙에 위치하도록 하기 (스냅 구현)
targetContentOffset라는 메서드가 있습니다. 저도 이번에 처음알았는데,UICollectionViewFlowLayout에서 스크롤이 멈출 때 최종적으로 어느 위치에서 멈출지를 결정하는 메서드입니다.proposedContentOffset기준으로 현재 보이는 셀들의layoutAttributes를 가져옵니다. (proposedContentOffset은 scroll이 멈출 예정인 위치 입니다!)2. 구슬 위치 + 크기 조절하기
cell의 위치에 따라 구슬의 위치, 크기를 조절하기 위해 진행도 를 사용하기로 했습니다.
화면의 중앙으로부터 멀어질수록 진행도가 1에서 0까지 감소합니다. (cell이 중앙에 위치하면 1, 멀어질 수록 0)
EmotionCollectionViewLayout
layoutAttributesForElements()에서 각 셀의 중심(attributes.center.x)과 화면 중앙 X 좌표의 차이를 구합니다. 이를 이용해 진행도를 계산합니다.EmotionCollectionViewLayoutAttributes.progress에 담아서 셀까지 전달합니다.EmotionCollectionViewCell
감정 collectionView 무한 스크롤
default.mp4
a, b, c, d와 같은 감정 구슬 배열이 있다 가정합니다.c, d, a, b, c, d, a, b와 같이 설정합니다.맨 앞 c, d로 이동하면뒤쪽의 c, d로 이동시킵니다.맨 뒤 a, b로 이동하면앞쪽의 a, b로 이동시킵니다.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로 스크롤 하는 코드입니다.감정 구슬 바로가기 tap시 (collectionVew 상단의 구슬 stackView)
default.mp4
scrollToIndex(index: Int)을 통해 원하는 위치로 collectionView를 스크롤 합니다.scrollToIndex()은 아래와 같습니다.전체 감정 구슬 set이 3 세트가 datasource로 등록되어있습니다. 따라서 감정 구슬 set의 원본 item 갯수로 modulo 연산을 진행하여 선택할 구슬의 index를 계산합니다.a, b, c, d, a, b, c, d, a, b, c, d와 같이 item이 존재할 때, 원본 item의 갯수는 4개 입니다.a, b, c, d에서의 a의 index 값을 추출하는 코드입니다.index + itemCount위치로 collectionView를 스크롤 합니다. (무한 스크롤을 위해)감정 구슬 선택하기
default.mp4
그리고 이 imageView에는 panGesture가 등록되어 있습니다. 아래로 스크롤하다가 손위의 일정 영역까지 scroll 하면 구슬을 선택하는 원리입니다. 해당 영역까지 아래로 스크롤하지 못하면 구슬은 원래 위치로 돌아가게 됩니다. (동영상 참고)
다만 pangesture 때문에, 해당 imageView와 겹치는 collectionView 영역에서는 좌우 스크롤이 안먹히는 문제가 있습니다. iphone16pro 와 같이 큰 화면의 폰에서는 괜찮지만,, 가로 길이가 짧은 기기에서는 상당히 불편하다고 느껴집니다. (반드시 개선하겠습니다 ㅜ)
handlePan중 요 부분을 통해 감정구슬을 위로는 드래그하지 못하고, 아래로만 드래그할 수 있도록 강제했습니다.구슬 손 밑에 위치시키기 (감정 구슬 등록/ 감정 구슬 등록 완료화면)
view에addSubview하는 순서를 조절해서 자연스럽게 구슬이 위 사진들과 같이 위치하도록 구현했습니다.Summary by CodeRabbit