feat: BodyScan Sections(WIP)#10
Conversation
WalkthroughAVFoundation 기반 CameraService(세션 구성·시작·정지·고해상도 촬영)와 AVCapturePhotoCaptureDelegate 처리, 미리보기 연결용 PreviewView/CameraPreview, 캡처된 UIImage의 방향 보정 확장이 포함된 BodyScanCameraView가 추가되었습니다. SwiftUI 화면들이 새로 추가되어 바디 스캔 흐름(BodyScanFlowView, BodyScanStep1View), 홈/리스트 UI(BodyScanHomeView), 시뮬레이터용 PhotoLibraryPicker 래퍼가 도입되었고 MyPageMainView에는 바디 스캔 네비게이션 링크가 추가되었습니다. Xcode 프로젝트에 NSCameraUsageDescription 설정과 .gitignore에 .DS_Store 추가도 포함됩니다. 공개 API 노출 변경은 없습니다. Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Pre-merge checks and finishing touches❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✨ Finishing touches🧪 Generate unit tests
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
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: 1
🧹 Nitpick comments (10)
CodiCue/BodyScan/BodyScanCameraView.swift (5)
174-176: 강제 캐스트 제거 및 SwiftLint 경고 해소final class에서는
static사용 권장,as!강제 캐스트 제거 권장.-final class PreviewView: UIView { - override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self } - var videoPreviewLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer } -} +final class PreviewView: UIView { + static override var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self } + var videoPreviewLayer: AVCaptureVideoPreviewLayer { + guard let previewLayer = layer as? AVCaptureVideoPreviewLayer else { + fatalError("PreviewView.layer must be AVCaptureVideoPreviewLayer") + } + return previewLayer + } +}
74-110: 세션 구성은 전용 큐에서 수행해 데이터 레이스 방지
configure()가 메인에서 세션에 접근하고, start/stop은sessionQueue에서 실행됩니다. 세션 접근을 단일 큐로 일원화하세요.- func configure() { - // 여러 번 호출돼도 한 번만 구성되도록 - guard session.inputs.isEmpty && session.outputs.isEmpty else { return } - - session.beginConfiguration() - session.sessionPreset = .photo - - // 카메라 접근 권한 - switch AVCaptureDevice.authorizationStatus(for: .video) { - case .authorized: - break - case .notDetermined: - AVCaptureDevice.requestAccess(for: .video) { _ in } - default: - break - } - - // 후면 카메라 - guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back), - let input = try? AVCaptureDeviceInput(device: device), - session.canAddInput(input) else { - session.commitConfiguration() - return - } - session.addInput(input) - self.videoDeviceInput = input - - // 사진 출력 - guard session.canAddOutput(photoOutput) else { - session.commitConfiguration() - return - } - session.addOutput(photoOutput) - photoOutput.isHighResolutionCaptureEnabled = true - - session.commitConfiguration() - } + func configure() { + sessionQueue.async { + // 여러 번 호출돼도 한 번만 구성되도록 + guard self.session.inputs.isEmpty && self.session.outputs.isEmpty else { return } + + self.session.beginConfiguration() + self.session.sessionPreset = .photo + + // 카메라 접근 권한 + switch AVCaptureDevice.authorizationStatus(for: .video) { + case .authorized: + break + case .notDetermined: + AVCaptureDevice.requestAccess(for: .video) { _ in } + default: + break + } + + // 후면 카메라 + guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back), + let input = try? AVCaptureDeviceInput(device: device), + self.session.canAddInput(input) else { + self.session.commitConfiguration() + return + } + self.session.addInput(input) + self.videoDeviceInput = input + + // 사진 출력 + guard self.session.canAddOutput(self.photoOutput) else { + self.session.commitConfiguration() + return + } + self.session.addOutput(self.photoOutput) + self.photoOutput.isHighResolutionCaptureEnabled = true + + self.session.commitConfiguration() + } + }
41-46: 연속 탭(중복 촬영) 방지빠른 연속 탭 시
captureCompletion이 덮어써질 수 있습니다. 촬영 중 버튼 비활성화 권장.@StateObject private var camera = CameraService() + @State private var isCapturing = false @@ - Button { - camera.capturePhoto { image in - onCapture(image) - dismiss() - } - } label: { + Button { + guard !isCapturing else { return } + isCapturing = true + camera.capturePhoto { image in + onCapture(image) + isCapturing = false + dismiss() + } + } label: { @@ - .padding(.bottom, 36) + .padding(.bottom, 36) + .disabled(isCapturing)Also applies to: 18-19, 52-53
163-169: 프리뷰 레이어 방향 고정(포트레이트)미리보기 방향 불일치 보정.
func makeUIView(context: Context) -> PreviewView { let v = PreviewView() v.videoPreviewLayer.session = session v.videoPreviewLayer.videoGravity = .resizeAspectFill + if let conn = v.videoPreviewLayer.connection, conn.isVideoOrientationSupported { + conn.videoOrientation = .portrait + } return v }
81-89: 권한 거부/미동의 시 UX 처리 필요권한이 .denied/.restricted일 때 사용자 안내(UI) 없이 진행됩니다. 시트에서 안내/설정 이동 제공을 권장합니다.
원하시면 권한 상태에 따른 얼럿/가이드 뷰 초안을 드리겠습니다.
CodiCue/BodyScan/BodyScanStep1View.swift (1)
12-12: 불필요한 옵셔널 초기화 제거
= nil은 중복입니다.- @State private var capturedImage: UIImage? = nil + @State private var capturedImage: UIImage?CodiCue/BodyScan/BodyScanFlowView.swift (2)
42-44: 강제 언래핑 제거
BodyScanPose(rawValue:)!는 범위를 벗어나면 크래시합니다. 안전한 기본값으로 처리하세요.- private var pose: BodyScanPose { BodyScanPose(rawValue: step)! } + private var pose: BodyScanPose { BodyScanPose(rawValue: step) ?? .front }
39-41: 미사용 프로퍼티 정리
isPreview는 사용되지 않습니다. 제거 권장.- private var isPreview: Bool { - ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" - }CodiCue/BodyScan/BodyScanHomeView.swift (2)
100-102: 다크 모드 대응 배경색 사용카드 배경을
Color.white로 고정하면 다크 모드에서 부자연스럽습니다. 시스템 배경 색상 사용 권장.- .fill(Color.white) + .fill(Color(.systemBackground))
121-126: DateFormatter 재생성 비용 절감매 호출마다 포매터 생성 대신 캐싱 권장.
- var dateString: String { - let f = DateFormatter() - f.locale = Locale(identifier: "ko_KR") - f.dateFormat = "yyyy. MM. dd." - return f.string(from: date) - } + var dateString: String { + Self.df.string(from: date) + }파일 내에 아래 정적 프로퍼티 추가:
private extension BodyScanReport { static let df: DateFormatter = { let f = DateFormatter() f.locale = Locale(identifier: "ko_KR") f.dateFormat = "yyyy. MM. dd." return f }() }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
CodiCue/BodyScan/BodyScanCameraView.swift(1 hunks)CodiCue/BodyScan/BodyScanFlowView.swift(1 hunks)CodiCue/BodyScan/BodyScanHomeView.swift(1 hunks)CodiCue/BodyScan/BodyScanStep1View.swift(1 hunks)CodiCue/BodyScan/PhotoLibraryPicker.swift(1 hunks)CodiCue/Mypage/MyPageMainView.swift(3 hunks)
🧰 Additional context used
🪛 SwiftLint (0.57.0)
CodiCue/BodyScan/BodyScanStep1View.swift
[Warning] 12-12: Initializing an optional variable with nil is redundant
(redundant_optional_initialization)
CodiCue/BodyScan/BodyScanCameraView.swift
[Error] 175-175: Force casts should be avoided
(force_cast)
[Warning] 174-174: Prefer static over class in a final class
(static_over_final_class)
🔇 Additional comments (4)
CodiCue/Mypage/MyPageMainView.swift (2)
76-83: 버전 문자열 계산 로직은 OK
guard let으로 안전 처리되어 있습니다. 상단 표시부의 강제 언래핑만 정리하면 됩니다.변경 후 프리뷰/런타임에서 정상 표기되는지 한번 확인 부탁드립니다.
108-128: 네비게이션 래퍼 구성 적절스타일/접근성 일관성 유지되며, 기존 버튼과 통일된 UI입니다.
CodiCue/BodyScan/PhotoLibraryPicker.swift (1)
11-41: 구성 양호단일 이미지 선택 플로우가 간결하고 안전합니다. 메인 스레드에서 콜백 전달도 적절합니다.
CodiCue/BodyScan/BodyScanCameraView.swift (1)
81-89: Info.plist: NSCameraUsageDescription 존재 여부 확인 필요실기기에서 카메라 접근 시 NSCameraUsageDescription이 반드시 필요합니다(누락 시 크래시/앱스토어 거부).
레포 루트에서 아래 스크립트를 실행한 뒤 출력 결과를 붙여 주세요.
#!/bin/bash set -euo pipefail python3 - <<'PY' import plistlib, os keys = ['NSCameraUsageDescription','NSPhotoLibraryUsageDescription','NSPhotoLibraryAddUsageDescription'] found = False for root, dirs, files in os.walk('.'): for f in files: if f == 'Info.plist': found = True path = os.path.join(root, f) try: with open(path, 'rb') as fh: data = plistlib.load(fh) except Exception as e: print(f"{path}: ERROR parsing plist: {e}") continue for k in keys: print(f"{path}: {k} -> {'PRESENT' if k in data and data[k] else 'MISSING'}") if not found: print("No Info.plist files found in repository") PY
| Text("CodiCue \(version!)") | ||
| .font(.footnote) | ||
| .foregroundStyle(Color.gray600) | ||
| } |
There was a problem hiding this comment.
옵셔널 강제 해제로 인한 크래시 가능성 제거
version!은 Info.plist 키 누락 시 크래시합니다.
- Text("CodiCue \(version!)")
+ if let version {
+ Text("CodiCue \(version)")
+ } else {
+ Text("CodiCue")
+ }📝 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.
| Text("CodiCue \(version!)") | |
| .font(.footnote) | |
| .foregroundStyle(Color.gray600) | |
| } | |
| if let version { | |
| Text("CodiCue \(version)") | |
| } else { | |
| Text("CodiCue") | |
| } | |
| .font(.footnote) | |
| .foregroundStyle(Color.gray600) | |
| } |
Summary by CodeRabbit
신기능
UI/UX
기타