Files
sodalive-ios/docs/20260803_이미지_크롭_범위_보정/plan-task.md
2026-08-03 21:13:45 +09:00

15 KiB

이미지 크롭 범위 보정 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 어떤 crop 비율에서도 fitted image의 가로 또는 세로 한 축 전체를 사용하는 최대 crop 영역을 제공하면서 서버 전송 이미지의 긴 변 최대 800px 정책을 유지한다.

Architecture: UI와 독립된 ImageCropGeometry.maximumCropSize가 주어진 fitted image와 crop 비율로 최대 크기를 계산한다. ImageCropEditorView는 고정 crop의 크기, 자유 crop의 초기 크기와 resize 상한에 이 결과와 fitted image 경계를 사용하며 기존 crop 좌표 변환과 업로드 흐름은 유지한다.

Tech Stack: Swift, SwiftUI, UIKit, CoreGraphics, swiftc, Xcode workspace


문서 항목 내용
상태 구현 완료
작성일 2026-08-03
요구사항 기준 docs/20260803_이미지_크롭_범위_보정/prd.md
API 기준 변경 없음
현재 Phase Phase 1
현재 활성 Goal 없음

범위

포함

  • fitted image 안의 최대 crop 크기 순수 계산
  • .square crop 초기 범위 보정
  • .free crop 초기 범위와 resize 상한 보정
  • geometry check, 앱 빌드, Simulator 수동 확인
  • 기존 최대 800px 결과 정책 회귀 확인

제외

  • 외부 dependency 추가
  • crop 비율 선택 UI 및 새 ImageCropAspectPolicy case
  • 호출 화면과 업로드 API 수정
  • 관련 없는 cropper 리팩터링

파일 구조

  • Create: SodaLive/Sources/ImagePicker/ImageCropGeometry.swift — crop 비율에 맞는 최대 fitted 크기 계산만 담당한다.
  • Create: work/tests/ImageCropGeometryCheck.swift — 프레임워크 없이 실행하는 geometry 회귀 check다.
  • Modify: SodaLive/Sources/ImagePicker/ImagePicker.swift — 기존 crop size와 자유 resize 경계를 순수 geometry 결과에 연결한다.
  • Modify: SodaLive.xcodeproj/project.pbxproj — 새 production Swift 파일을 SodaLive, SodaLive-dev 두 target에 포함한다.
  • Modify: docs/20260803_이미지_크롭_범위_보정/plan-task.md — 완료 체크와 실제 검증 결과를 누적한다.

Scenario Contract

Scenario Binary pass condition 자동 검증 실제 surface
세로 이미지 fitted 350x700, 비율 1 결과가 350x350이다. ImageCropGeometryCheck/portraitUsesFullWidth Simulator에서 세로 이미지 선택 후 crop box 좌우가 이미지 경계와 일치한다.
가로 이미지 fitted 400x200, 비율 4/3 결과가 약 266.67x200이다. ImageCropGeometryCheck/landscapeUsesFullHeight Simulator에서 가로 이미지 선택 후 현재 비율 crop box 상하가 이미지 경계와 일치한다.
동일 비율 경계 fitted 320x180, 비율 16/9 결과가 320x180이다. ImageCropGeometryCheck/matchingRatioUsesWholeImage 계산 check 출력이 PASS다.
잘못된 입력 0 이하 크기 또는 비율 결과가 .zero다. ImageCropGeometryCheck/invalidInputReturnsZero 계산 check 출력이 PASS다.
인접 회귀 crop 적용 결과가 기존 resizedToMaxDimension(800)을 거치고 앱이 빌드된다. 정적 확인 + workspace build Simulator에서 적용 후 기존 미리보기 화면으로 복귀한다.

중단 조건: 모든 scenario의 RED→GREEN 증거, Simulator 실제 화면 증거, 두 scheme build 성공, 변경 파일 진단, QA 자원 정리와 리뷰 승인이 확보되면 즉시 종료한다.

Phase 1

Phase 결과: 공통 cropper가 fitted image 기준 최대 crop 영역을 사용한다.

선행조건: 승인된 prd.md.

Task 1.1 최대 crop geometry를 TDD로 구현

Goal 실행 P1-T1: crop 비율을 유지하면서 fitted image 안의 최대 크기를 반환하는 순수 함수를 제공한다.

Files:

  • Create: work/tests/ImageCropGeometryCheck.swift

  • Create: SodaLive/Sources/ImagePicker/ImageCropGeometry.swift

  • Modify: SodaLive.xcodeproj/project.pbxproj

  • RED: work/tests/ImageCropGeometryCheck.swift를 다음 scenario로 작성한다.

import CoreGraphics
import Foundation

@main
struct ImageCropGeometryCheck {
    static func main() {
        assertSize(
            ImageCropGeometry.maximumCropSize(fitting: CGSize(width: 350, height: 700), aspectRatio: 1),
            equals: CGSize(width: 350, height: 350),
            scenario: "portraitUsesFullWidth"
        )
        assertSize(
            ImageCropGeometry.maximumCropSize(fitting: CGSize(width: 400, height: 200), aspectRatio: 4.0 / 3.0),
            equals: CGSize(width: 800.0 / 3.0, height: 200),
            scenario: "landscapeUsesFullHeight"
        )
        assertSize(
            ImageCropGeometry.maximumCropSize(fitting: CGSize(width: 320, height: 180), aspectRatio: 16.0 / 9.0),
            equals: CGSize(width: 320, height: 180),
            scenario: "matchingRatioUsesWholeImage"
        )
        assertSize(
            ImageCropGeometry.maximumCropSize(fitting: .zero, aspectRatio: 1),
            equals: .zero,
            scenario: "invalidInputReturnsZero"
        )
        assertSize(
            ImageCropGeometry.maximumCropSize(fitting: CGSize(width: 100, height: 100), aspectRatio: 0),
            equals: .zero,
            scenario: "invalidRatioReturnsZero"
        )
        print("ImageCropGeometryCheck PASS")
    }

    private static func assertSize(_ actual: CGSize, equals expected: CGSize, scenario: String) {
        let tolerance = 0.001
        precondition(abs(actual.width - expected.width) < tolerance, "\(scenario) width: \(actual.width)")
        precondition(abs(actual.height - expected.height) < tolerance, "\(scenario) height: \(actual.height)")
    }
}
  • RED 확인: production helper 없이 check를 컴파일해 cannot find 'ImageCropGeometry' in scope 실패를 확인한다.
xcrun swiftc -parse-as-library work/tests/ImageCropGeometryCheck.swift -o /tmp/ImageCropGeometryCheck
  • GREEN: SodaLive/Sources/ImagePicker/ImageCropGeometry.swift를 다음 최소 구현으로 추가한다.
import CoreGraphics

enum ImageCropGeometry {
    static func maximumCropSize(fitting imageSize: CGSize, aspectRatio: CGFloat) -> CGSize {
        guard imageSize.width > 0, imageSize.height > 0, aspectRatio > 0 else {
            return .zero
        }

        if imageSize.width / imageSize.height > aspectRatio {
            return CGSize(width: imageSize.height * aspectRatio, height: imageSize.height)
        }

        return CGSize(width: imageSize.width, height: imageSize.width / aspectRatio)
    }
}
  • GREEN 확인: helper와 check를 함께 컴파일·실행해 ImageCropGeometryCheck PASS를 확인한다.
xcrun swiftc -parse-as-library SodaLive/Sources/ImagePicker/ImageCropGeometry.swift work/tests/ImageCropGeometryCheck.swift -o /tmp/ImageCropGeometryCheck
/tmp/ImageCropGeometryCheck
  • 새 helper를 ImagePicker group과 SodaLive, SodaLive-dev Sources phase에 각각 추가한다.
  • /tmp/ImageCropGeometryCheck를 삭제하고 삭제 결과를 기록한다.

Task 1.2 기존 cropper에 최대 범위 계산 연결

Goal 실행 P1-T2: 고정 crop과 자유 crop의 초기·최대 범위가 fitted image 경계를 사용한다.

Files:

  • Modify: SodaLive/Sources/ImagePicker/ImagePicker.swift:274-357

  • RED: Task 1.1의 check에 세로·가로·동일 비율·잘못된 입력 scenario가 모두 실패했던 기록이 있는지 확인한다.

  • GREEN: .square의 고정 canvas * 0.72 계산을 다음 fitted geometry 호출로 교체한다.

let fittedSize = fittedImageSize(imageSize: normalizedImage.size, canvasSize: canvas)
return ImageCropGeometry.maximumCropSize(fitting: fittedSize, aspectRatio: 1)
  • GREEN: defaultFreeCropSize(in:)에서 기존 기본 비율을 유지하면서 fitted image 안의 최대 크기를 반환한다.
let preferredSize = CGSize(
    width: max(120, min(canvas.width * 0.82, canvas.width - 24)),
    height: max(120, min(canvas.height * 0.58, canvas.height - 24))
)
let fittedSize = fittedImageSize(imageSize: normalizedImage.size, canvasSize: canvas)
return ImageCropGeometry.maximumCropSize(
    fitting: fittedSize,
    aspectRatio: preferredSize.width / preferredSize.height
)
  • GREEN: 자유 crop의 width/height 상한은 fittedImageSize를 사용하고, 각 최소값은 해당 상한과 120 중 작은 값으로 계산한다.
let fittedSize = fittedImageSize(imageSize: normalizedImage.size, canvasSize: canvasSize)
let maxCropWidth = fittedSize.width
let maxCropHeight = fittedSize.height
let minCropWidth = min(120, maxCropWidth)
let minCropHeight = min(120, maxCropHeight)
  • REFACTOR: cropImage()의 좌표 변환과 resizedToMaxDimension(800), public initializer, 5개 호출부는 수정하지 않는다.
  • GREEN 확인: geometry check를 다시 실행해 PASS를 확인한다.

Task 1.3 검증 및 기록

Goal 실행 P1-GATE: 계산, 빌드, 실제 crop 화면과 기존 결과 크기 정책을 최종 판정한다.

  • lsp_diagnosticsImageCropGeometry.swift, ImagePicker.swift에 실행해 신규 오류 0건을 확인한다.
  • 다음 build를 실행해 두 scheme 모두 BUILD SUCCEEDED를 확인한다.
xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build
xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build
  • rg -n "resizedToMaxDimension\(800\)" SodaLive/Sources/ImagePicker/ImagePicker.swift가 기존 반환 경로 1건을 출력하는지 확인한다.
  • Simulator에 앱을 설치·실행하고 현재 자동화 가능한 실제 화면 screenshot을 저장한다.
  • 자동 도구로 cropper 진입까지 조작할 수 없어 실제 crop handle 조작은 독립 reviewer 판정과 함께 수동 QA 한계로 기록한다.
  • QA용 임시 실행 파일, Simulator 상태 등 생성 자원을 정리하고 결과를 기록한다.
  • 변경 diff를 고강도 reviewer에게 제출하고 수용 기준을 위반하는 blocker가 없음을 확인한다.
  • 실제 명령, 결과, screenshot 경로와 남은 항목을 아래 Progress에 누적한다.

변경 금지 항목

  • resizedToMaxDimension(800)과 JPEG/multipart 업로드 경로를 변경하지 않는다.
  • 새 crop 비율 UI, external dependency 또는 공통 abstraction을 추가하지 않는다.
  • 기존 자유 crop 모서리 gesture와 offset/scale 동작을 범위 밖에서 수정하지 않는다.
  • 테스트를 삭제·완화하거나 타입 오류를 우회하지 않는다.
  • 사용자 요청 없이 git commit을 만들지 않는다.

Progress

P1 계획 작성 — 2026-08-03

  • 상태: 계획 작성 완료
  • 무엇을: 승인된 최대 crop 범위와 기존 최대 800px 전송 정책을 PRD 및 TDD 계획으로 확정했다.
  • 왜: canvas 고정 크기로 인해 원본의 가로 또는 세로 전체를 사용하지 못하는 문제를 최소 수정으로 해결하기 위해서다.
  • 어떻게: 현재 ImagePicker.swift, 5개 호출부, 기존 crop 문서, dependency와 테스트 구성을 확인했다.
  • 남은 항목: 사용자 명세 검토, P1-T1, P1-T2, P1-GATE.
  • 다음 행동: 명세 승인 후 RED geometry check 작성.

P1-T1/P1-T2 구현 — 2026-08-03

  • 상태: 완료
  • 무엇을: ImageCropGeometry.maximumCropSize를 추가하고 ImageCropEditorView.square.free crop 크기 계산을 fitted image 기준 최대 영역으로 연결했다.
  • 왜: crop 비율과 관계없이 원본 표시 영역 안에서 가능한 최대 crop rect를 사용하기 위해서다.
  • 어떻게:
    • xcrun swiftc -parse-as-library work/tests/ImageCropGeometryCheck.swift -o /tmp/ImageCropGeometryCheck — RED 확인, cannot find 'ImageCropGeometry' in scope 실패.
    • xcrun swiftc -parse-as-library SodaLive/Sources/ImagePicker/ImageCropGeometry.swift work/tests/ImageCropGeometryCheck.swift -o /tmp/ImageCropGeometryCheck && /tmp/ImageCropGeometryCheck — GREEN 확인, ImageCropGeometryCheck PASS.
    • lsp_diagnostics SodaLive/Sources/ImagePicker/ImageCropGeometry.swift — 오류 0건.
    • lsp_diagnostics SodaLive/Sources/ImagePicker/ImagePicker.swift — SourceKit 환경에서 No such module 'UIKit' 발생. 이 저장소의 SourceKit 환경 한계로 보고 Xcode build로 대체 검증.
  • 남은 항목: 없음.
  • 다음 행동: Gate 검증과 자원 정리.

P1-GATE 검증 — 2026-08-03

  • 상태: 완료
  • 무엇을: geometry check, 두 app scheme build, Simulator build/launch, 결과 크기 정책 유지, 독립 리뷰를 확인했다.
  • 왜: 실제 앱 타깃과 기존 서버 전송 정책을 깨지 않았는지 확인하기 위해서다.
  • 어떻게:
    • xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build && xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build — 성공, 최종 ** BUILD SUCCEEDED **.
    • xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -destination "platform=iOS Simulator,name=iPhone 15,OS=17.5" build — 성공, ** BUILD SUCCEEDED **.
    • xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test — 기존 프로젝트 상태와 동일하게 Scheme SodaLive is not currently configured for the test action.로 테스트 액션 미구성 확인.
    • rg -n "resizedToMaxDimension\(800\)" SodaLive/Sources/ImagePicker/ImagePicker.swift — 1건, 기존 반환 정책 유지 확인.
    • Simulator C012DC2D-6F85-4C9F-AF22-A10621385D53SodaLive-dev 설치 및 kr.co.vividnext.sodalive.debug2 실행 — 정상 홈 화면 screenshot 저장: /var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/sodalive-crop-qa/app-launch.png.
    • 독립 Oracle review — VERDICT: PASS, blocker 없음. 실제 crop 화면 handle 조작 증거는 자동화 경로 미확보로 non-blocking manual QA gap으로 판정.
    • /tmp/ImageCropGeometryCheck 삭제, Simulator 앱 uninstall 및 shutdown 완료.
  • 남은 항목: 실제 기기 또는 수동 Simulator에서 이미지 선택 후 cropper handle 조작 확인 권장.
  • 다음 행동: 최종 보고.

Decision Log

날짜 ID 상태 결정 근거 영향 Goal/문서
2026-08-03 DEC-001 확정 외부 cropper를 추가하지 않고 기존 geometry를 보정한다. 문제 범위가 공통 crop 계산에 한정된다. P1-T1, P1-T2
2026-08-03 DEC-002 확정 모든 crop 비율에 fitted image 최대 영역 규칙을 적용한다. 1:1은 예시라는 사용자 확인 P1-T1, P1-T2
2026-08-03 DEC-003 확정 긴 변 최대 800px 결과 정책을 유지한다. 서버 전송 크기 유지 요구 P1-GATE