Skip to content

feat: BodyScan Sections(WIP)#10

Draft
Yeeun050211 wants to merge 6 commits into
mainfrom
yetwin02/cc-206-ai-bodyscan-home
Draft

feat: BodyScan Sections(WIP)#10
Yeeun050211 wants to merge 6 commits into
mainfrom
yetwin02/cc-206-ai-bodyscan-home

Conversation

@Yeeun050211
Copy link
Copy Markdown
Contributor

@Yeeun050211 Yeeun050211 commented Sep 23, 2025

Summary by CodeRabbit

  • 신기능

    • 마이페이지에서 AI 체형 분석 홈 바로가기 추가
    • 바디스캔 홈: 기존 분석 목록·카드형 미리보기 제공
    • 3단계(정면/측면/후면) 바디스캔 촬영 플로우 및 진행 표시 추가
    • 실시간 카메라 촬영 화면(촬영·닫기) 및 사진 자동 방향 보정
    • 사진 보관함 선택 기능(포토 라이브러 피커) 추가
  • UI/UX

    • 시트·툴바 기반 네비게이션 흐름 개선 및 프리뷰 환경 보강
    • 캡슐형 네비게이션 링크 도입으로 MyPage 항목 UI 통일
  • 기타

    • 카메라 접근 권한 설명(Info.plist) 추가
    • .DS_Store gitignore 추가

@Yeeun050211 Yeeun050211 requested a review from a team as a code owner September 23, 2025 12:02
@linear
Copy link
Copy Markdown

linear Bot commented Sep 23, 2025

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Sep 23, 2025

Walkthrough

AVFoundation 기반 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)
Check name Status Explanation Resolution
Title Check ❓ Inconclusive 제목 "feat: BodyScan Sections(WIP)"은 변경된 BodyScan 관련 여러 기능(홈, 플로우, 카메라, 포토 라이브러리 등)을 포괄하지만 "Sections"와 "WIP"라는 표현이 모호하여 주요 변경 내용을 구체적으로 전달하지 못합니다. 제목을 "feat: BodyScan 플로우 및 카메라 기능 추가"처럼 변경사항의 핵심 기능을 간결하고 명확하게 반영하도록 수정해 주세요.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch yetwin02/cc-206-ai-bodyscan-home

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb6781f and 2fdab40.

📒 Files selected for processing (1)
  • .gitignore (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .gitignore

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

📥 Commits

Reviewing files that changed from the base of the PR and between d634cb4 and 367f68a.

📒 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

Comment on lines 70 to 73
Text("CodiCue \(version!)")
.font(.footnote)
.foregroundStyle(Color.gray600)
}
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

옵셔널 강제 해제로 인한 크래시 가능성 제거

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.

Suggested change
Text("CodiCue \(version!)")
.font(.footnote)
.foregroundStyle(Color.gray600)
}
if let version {
Text("CodiCue \(version)")
} else {
Text("CodiCue")
}
.font(.footnote)
.foregroundStyle(Color.gray600)
}

@hoony6134 hoony6134 marked this pull request as draft September 24, 2025 11:30
@Yeeun050211 Yeeun050211 marked this pull request as ready for review September 25, 2025 15:12
@hoony6134 hoony6134 marked this pull request as draft September 25, 2025 15:37
Copy link
Copy Markdown

@rhseung rhseung left a comment

Choose a reason for hiding this comment

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

WIP

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.

3 participants