From a38241ee677fb5bb1854c241a62bf84fd44a780f Mon Sep 17 00:00:00 2001 From: Jini Date: Thu, 12 Feb 2026 22:56:01 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EB=B3=80=ED=99=98=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EB=8F=85=EB=A6=BD=EC=A0=81=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModels/DuZzonKuFramePreviewView.swift | 99 +++++++++++-------- .../DuZzonKuVM+ImageConversion.swift | 14 +-- .../DuZzonKu/ViewModels/DuZzonKuVM.swift | 45 +++++++-- 3 files changed, 102 insertions(+), 56 deletions(-) diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuFramePreviewView.swift b/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuFramePreviewView.swift index b5be40ce..ed7e8813 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuFramePreviewView.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuFramePreviewView.swift @@ -32,10 +32,10 @@ struct DuZzonKuFramePreviewView: View { case photoLibrary } - // 제스처 임시 값 - @State private var currentScale: CGFloat = 1.0 - @State private var currentRotation: Angle = .zero - @State private var currentOffset: CGSize = .zero + // 제스처 임시 값 (인덱스별로 저장) + @State private var currentScales: [Int: CGFloat] = [:] + @State private var currentRotations: [Int: Angle] = [:] + @State private var currentOffsets: [Int: CGSize] = [:] // 크기 설정 private let targetFrameHeight: CGFloat = 376 @@ -84,17 +84,17 @@ struct DuZzonKuFramePreviewView: View { // 현재 편집 중인 영역에 사진 저장 if let index = editingRectIndex { viewModel.setPhoto(image, at: index) + // 새 사진 선택 시 해당 인덱스의 변환 초기화 + viewModel.setPhotoScale(1.0, at: index) + viewModel.setPhotoRotation(.zero, at: index) + viewModel.setPhotoOffset(.zero, at: index) + currentScales[index] = 1.0 + currentRotations[index] = .zero + currentOffsets[index] = .zero } else { viewModel.selectedPhotoImage = image } - // 새 사진 선택 시 변환 초기화 - viewModel.photoScale = 1.0 - viewModel.photoRotation = .zero - viewModel.photoOffset = .zero - currentScale = 1.0 - currentRotation = .zero - currentOffset = .zero showEditButton = false editingRectIndex = nil } @@ -130,17 +130,17 @@ struct DuZzonKuFramePreviewView: View { // 현재 편집 중인 영역에 사진 저장 if let index = editingRectIndex { viewModel.setPhoto(uiImage, at: index) + // 새 사진 선택 시 해당 인덱스의 변환 초기화 + viewModel.setPhotoScale(1.0, at: index) + viewModel.setPhotoRotation(.zero, at: index) + viewModel.setPhotoOffset(.zero, at: index) + currentScales[index] = 1.0 + currentRotations[index] = .zero + currentOffsets[index] = .zero } else { viewModel.selectedPhotoImage = uiImage } - // 새 사진 선택 시 변환 초기화 - viewModel.photoScale = 1.0 - viewModel.photoRotation = .zero - viewModel.photoOffset = .zero - currentScale = 1.0 - currentRotation = .zero - currentOffset = .zero showEditButton = false editingRectIndex = nil } @@ -152,19 +152,20 @@ struct DuZzonKuFramePreviewView: View { } } - private var photoGestures: some Gesture { + private func photoGestures(for index: Int) -> some Gesture { // 확대/축소 let magnificationGesture = MagnificationGesture(minimumScaleDelta: 0.0) .onChanged { value in Task { @MainActor in - currentScale = value + currentScales[index] = value } } .onEnded { value in Task { @MainActor in - let newScale = viewModel.photoScale * value - viewModel.photoScale = min(max(newScale, 0.5), 3.0) - currentScale = 1.0 + let currentScale = viewModel.getPhotoScale(at: index) + let newScale = currentScale * value + viewModel.setPhotoScale(min(max(newScale, 0.5), 3.0), at: index) + currentScales[index] = 1.0 } } @@ -172,13 +173,14 @@ struct DuZzonKuFramePreviewView: View { let rotationGesture = RotationGesture(minimumAngleDelta: .zero) .onChanged { value in Task { @MainActor in - currentRotation = value + currentRotations[index] = value } } .onEnded { value in Task { @MainActor in - viewModel.photoRotation += value - currentRotation = .zero + let currentRotation = viewModel.getPhotoRotation(at: index) + viewModel.setPhotoRotation(currentRotation + value, at: index) + currentRotations[index] = .zero } } @@ -186,7 +188,7 @@ struct DuZzonKuFramePreviewView: View { let dragGesture = DragGesture(minimumDistance: 10) .onChanged { value in Task { @MainActor in - currentOffset = CGSize( + currentOffsets[index] = CGSize( width: value.translation.width, height: value.translation.height ) @@ -194,11 +196,15 @@ struct DuZzonKuFramePreviewView: View { } .onEnded { value in Task { @MainActor in - viewModel.photoOffset = CGSize( - width: viewModel.photoOffset.width + value.translation.width, - height: viewModel.photoOffset.height + value.translation.height + let currentOffset = viewModel.getPhotoOffset(at: index) + viewModel.setPhotoOffset( + CGSize( + width: currentOffset.width + value.translation.width, + height: currentOffset.height + value.translation.height + ), + at: index ) - currentOffset = .zero + currentOffsets[index] = .zero } } @@ -207,19 +213,26 @@ struct DuZzonKuFramePreviewView: View { .simultaneously(with: dragGesture) } - private var finalScale: CGFloat { - let calculatedScale = viewModel.photoScale * currentScale + // 특정 인덱스의 최종 변환 값 계산 + private func finalScale(for index: Int) -> CGFloat { + let baseScale = viewModel.getPhotoScale(at: index) + let currentScale = currentScales[index] ?? 1.0 + let calculatedScale = baseScale * currentScale return min(max(calculatedScale, 0.5), 3.0) } - private var finalRotation: Angle { - viewModel.photoRotation + currentRotation + private func finalRotation(for index: Int) -> Angle { + let baseRotation = viewModel.getPhotoRotation(at: index) + let currentRotation = currentRotations[index] ?? .zero + return baseRotation + currentRotation } - private var finalOffset: CGSize { - CGSize( - width: viewModel.photoOffset.width + currentOffset.width, - height: viewModel.photoOffset.height + currentOffset.height + private func finalOffset(for index: Int) -> CGSize { + let baseOffset = viewModel.getPhotoOffset(at: index) + let currentOffset = currentOffsets[index] ?? .zero + return CGSize( + width: baseOffset.width + currentOffset.width, + height: baseOffset.height + currentOffset.height ) } @@ -311,9 +324,9 @@ struct DuZzonKuFramePreviewView: View { .resizable() .scaledToFill() .frame(width: photoWidth, height: photoHeight) - .scaleEffect(finalScale) - .rotationEffect(finalRotation) - .offset(finalOffset) + .scaleEffect(finalScale(for: index)) + .rotationEffect(finalRotation(for: index)) + .offset(finalOffset(for: index)) .clipShape(clipShape) // 수정 버튼 표시 시 딤 처리 @@ -324,7 +337,7 @@ struct DuZzonKuFramePreviewView: View { .frame(width: photoWidth, height: photoHeight) .clipShape(clipShape) .contentShape(clipShape) - .gesture(photoGestures) + .gesture(photoGestures(for: index)) .onTapGesture { withAnimation(.easeInOut(duration: 0.2)) { if editingRectIndex == index { diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuVM+ImageConversion.swift b/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuVM+ImageConversion.swift index 84d37011..cb219760 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuVM+ImageConversion.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuVM+ImageConversion.swift @@ -107,9 +107,11 @@ extension DuZzonKuVM { ) } - // 사진 변환 적용 (확대/축소, 회전, 이동) - // 현재는 모든 사진에 동일한 변환 적용 - // 필요시 인덱스별로 다른 변환 저장 가능 + // 해당 인덱스의 변환 적용 + let photoScale = getPhotoScale(at: index) + let photoRotation = getPhotoRotation(at: index) + let photoOffset = getPhotoOffset(at: index) + let centerX = drawRect.midX let centerY = drawRect.midY @@ -127,10 +129,10 @@ extension DuZzonKuVM { photo.draw(in: centeredRect) context.cgContext.restoreGState() } - - // 2. 프레임 이미지 - originalFrameImage.draw(in: CGRect(origin: .zero, size: targetFrameSize)) } + + // 2. 프레임 이미지 + originalFrameImage.draw(in: CGRect(origin: .zero, size: targetFrameSize)) } bodyImage = composedImage diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuVM.swift b/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuVM.swift index 5e6a8b2f..90693b9b 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuVM.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuVM.swift @@ -35,10 +35,36 @@ class DuZzonKuVM: KeyringViewModelProtocol { var selectedPhotoImage: UIImage? = nil var photoImages: [Int: UIImage] = [:] // 인덱스별 사진 저장 - // MARK: - Photo Transform State - var photoScale: CGFloat = 1.0 - var photoRotation: Angle = .zero - var photoOffset: CGSize = .zero + // MARK: - Photo Transform State (인덱스별) + var photoScales: [Int: CGFloat] = [:] + var photoRotations: [Int: Angle] = [:] + var photoOffsets: [Int: CGSize] = [:] + + // 특정 인덱스의 변환 값 가져오기 (기본값 반환) + func getPhotoScale(at index: Int) -> CGFloat { + return photoScales[index] ?? 1.0 + } + + func getPhotoRotation(at index: Int) -> Angle { + return photoRotations[index] ?? .zero + } + + func getPhotoOffset(at index: Int) -> CGSize { + return photoOffsets[index] ?? .zero + } + + // 특정 인덱스의 변환 값 설정 + func setPhotoScale(_ scale: CGFloat, at index: Int) { + photoScales[index] = scale + } + + func setPhotoRotation(_ rotation: Angle, at index: Int) { + photoRotations[index] = rotation + } + + func setPhotoOffset(_ offset: CGSize, at index: Int) { + photoOffsets[index] = offset + } // MARK: - Body Image var bodyImage: UIImage? = nil @@ -91,6 +117,11 @@ class DuZzonKuVM: KeyringViewModelProtocol { /// 특정 인덱스의 사진 제거 func removePhoto(at index: Int) { photoImages.removeValue(forKey: index) + // 변환 값도 제거 + photoScales.removeValue(forKey: index) + photoRotations.removeValue(forKey: index) + photoOffsets.removeValue(forKey: index) + if index == 0 { selectedPhotoImage = nil } @@ -163,9 +194,9 @@ class DuZzonKuVM: KeyringViewModelProtocol { selectedFrame = nil selectedPhotoImage = nil photoImages.removeAll() - photoScale = 1.0 - photoRotation = .zero - photoOffset = .zero + photoScales.removeAll() + photoRotations.removeAll() + photoOffsets.removeAll() bodyImage = nil availableFrames.removeAll() isComposingPhoto = false From f1fb3f5ef39f67884d45ae9e643cb58e17431094 Mon Sep 17 00:00:00 2001 From: Jini Date: Thu, 12 Feb 2026 22:59:14 +0900 Subject: [PATCH 2/4] =?UTF-8?q?style:=20compose=EC=8B=9C=20=EC=82=AC?= =?UTF-8?q?=EC=A7=84=EC=97=90=20cornerRadius=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DuZzonKu/ViewModels/DuZzonKuVM+ImageConversion.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuVM+ImageConversion.swift b/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuVM+ImageConversion.swift index cb219760..8610b9e9 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuVM+ImageConversion.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuVM+ImageConversion.swift @@ -79,7 +79,11 @@ extension DuZzonKuVM { // 1-2. 해당 인덱스의 사진 그리기 if let photo = getPhoto(at: index) { context.cgContext.saveGState() - context.cgContext.addRect(photoRect) + + // cornerRadius 적용하여 클리핑 + let cornerRadius = rect.cornerRadius ?? 0 + let path = UIBezierPath(roundedRect: photoRect, cornerRadius: cornerRadius) + context.cgContext.addPath(path.cgPath) context.cgContext.clip() // 사진을 영역에 맞게 scaledToFill로 그리기 From 6a425a54ec213baa381adf6e9eca0e12967ba5dd Mon Sep 17 00:00:00 2001 From: Jini Date: Thu, 12 Feb 2026 23:36:33 +0900 Subject: [PATCH 3/4] =?UTF-8?q?style:=20=EA=B3=A0=EB=A6=AC=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EB=B0=94=EB=94=94=20?= =?UTF-8?q?=EC=98=A4=ED=94=84=EC=85=8B=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DuZzonKu/ViewModels/DuZzonKuFramePreviewView.swift | 1 + .../DuZzonKu/ViewModels/DuZzonKuVM+ImageConversion.swift | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuFramePreviewView.swift b/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuFramePreviewView.swift index ed7e8813..c97b7b8d 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuFramePreviewView.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuFramePreviewView.swift @@ -278,6 +278,7 @@ struct DuZzonKuFramePreviewView: View { .onDisappear { isFrameLoaded = false } + .offset(x: 2) .onAppear { print("checkerBoardRects:", frame.checkerBoardRects ?? []) } diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuVM+ImageConversion.swift b/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuVM+ImageConversion.swift index 8610b9e9..6b87f2c8 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuVM+ImageConversion.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuVM+ImageConversion.swift @@ -58,6 +58,8 @@ extension DuZzonKuVM { let renderer = UIGraphicsImageRenderer(size: targetFrameSize) let composedImage = renderer.image { context in + // 전체 context를 x축으로 4포인트 이동 + context.cgContext.translateBy(x: 2, y: 0) // 1. 각 체커보드 영역에 체커보드 + 사진 그리기 for (index, rect) in checkerBoardRects.enumerated() { @@ -120,7 +122,7 @@ extension DuZzonKuVM { let centerY = drawRect.midY context.cgContext.translateBy(x: centerX, y: centerY) - context.cgContext.translateBy(x: photoOffset.width, y: photoOffset.height) + context.cgContext.translateBy(x: photoOffset.width + 2, y: photoOffset.height) context.cgContext.rotate(by: CGFloat(photoRotation.radians)) context.cgContext.scaleBy(x: photoScale, y: photoScale) From 6a039aefe83cc7cb9a34e66a3bb20c7510d9e6ca Mon Sep 17 00:00:00 2001 From: Jini Date: Fri, 13 Feb 2026 00:08:59 +0900 Subject: [PATCH 4/4] =?UTF-8?q?style:=20=EC=98=A4=ED=94=84=EC=85=8B=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DuZzonKu/ViewModels/DuZzonKuVM+ImageConversion.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuVM+ImageConversion.swift b/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuVM+ImageConversion.swift index 6b87f2c8..259d47b1 100644 --- a/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuVM+ImageConversion.swift +++ b/Keychy/Keychy/Presentation/KeyringMaker/Templates/DuZzonKu/ViewModels/DuZzonKuVM+ImageConversion.swift @@ -58,7 +58,7 @@ extension DuZzonKuVM { let renderer = UIGraphicsImageRenderer(size: targetFrameSize) let composedImage = renderer.image { context in - // 전체 context를 x축으로 4포인트 이동 + // 전체 context를 x축으로 2포인트 이동 context.cgContext.translateBy(x: 2, y: 0) // 1. 각 체커보드 영역에 체커보드 + 사진 그리기 @@ -122,7 +122,7 @@ extension DuZzonKuVM { let centerY = drawRect.midY context.cgContext.translateBy(x: centerX, y: centerY) - context.cgContext.translateBy(x: photoOffset.width + 2, y: photoOffset.height) + context.cgContext.translateBy(x: photoOffset.width, y: photoOffset.height) context.cgContext.rotate(by: CGFloat(photoRotation.radians)) context.cgContext.scaleBy(x: photoScale, y: photoScale)