From a7855962b30d11d4ae1faa59dd725bf6f69d4a07 Mon Sep 17 00:00:00 2001 From: Jini Date: Tue, 27 Jan 2026 21:32:29 +0900 Subject: [PATCH 1/3] =?UTF-8?q?style:=20=ED=82=A4=EB=A7=81=20=EB=A9=94?= =?UTF-8?q?=EB=89=B4=EC=97=90=20=EC=9C=84=EC=A0=AF=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CollectionKeyringDetailView+Menu.swift | 13 ++++++++ .../Collection/Views/Detail/KeyringMenu.swift | 33 +++++++++++++++++-- .../Main/CollectionView+NormalMode.swift | 18 +++++----- 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/Keychy/Keychy/Presentation/Collection/Views/Detail/CollectionKeyringDetailView+Menu.swift b/Keychy/Keychy/Presentation/Collection/Views/Detail/CollectionKeyringDetailView+Menu.swift index b915e4402..bb8abfb8c 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Detail/CollectionKeyringDetailView+Menu.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Detail/CollectionKeyringDetailView+Menu.swift @@ -39,6 +39,9 @@ extension CollectionKeyringDetailView { }, onDelete: { handleMenuDelete() + }, + onWidget: { + goToWidgetOnboarding() } ) .zIndex(50) @@ -83,5 +86,15 @@ extension CollectionKeyringDetailView { showDeleteAlert = true } } + + // MARK: - 위젯 설정 화면으로 이동 + private func goToWidgetOnboarding() { + isSheetPresented = false + showMenu = false + + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + router.push(.widgetSettingView) + } + } } diff --git a/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringMenu.swift b/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringMenu.swift index 5c7111fc4..f824710ca 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringMenu.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringMenu.swift @@ -13,11 +13,13 @@ struct KeyringMenu: View { let onEdit: () -> Void let onCopy: () -> Void let onDelete: () -> Void + let onWidget: () -> Void private let menuWidth: CGFloat = 165 - private var menuHeight: CGFloat { - isMyKeyring ? 170 : 115 // 복사 버튼 있으면 170, 없으면 115 - } +// private var menuHeight: CGFloat { +// isMyKeyring ? 170 : 115 // 복사 버튼 있으면 170, 없으면 115 +// } + private let menuHeight: CGFloat = 218 @State private var isAppearing = false @@ -84,6 +86,31 @@ struct KeyringMenu: View { .contentShape(Rectangle()) } .frame(maxWidth: .infinity, alignment: .leading) + + Rectangle() + .fill(Color.gray100) + .padding(.horizontal, 10) + .frame(height: 1) + + // 위젯 버튼 + Button(action: onWidget) { + HStack(spacing: 8) { + Image(.widget) + .resizable() + .frame(width: 25, height: 25) + .foregroundStyle(.gray600) + + Text("위젯 설정") + .typography(.suit16M) + .foregroundColor(.gray600) + + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 10) + .contentShape(Rectangle()) + } + .frame(maxWidth: .infinity, alignment: .leading) } .padding(.horizontal, 10) .padding(.vertical, 20) diff --git a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift index 25b93289f..124c7abff 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift @@ -95,15 +95,15 @@ extension CollectionView { .padding(.trailing, 10) #endif - CircleGlassButton(imageName: "Widget", - action: { - isSearchFieldFocused = false - showSearchBar = false - - router.push(.widgetSettingView) - } - ) - .padding(.trailing, 10) +// CircleGlassButton(imageName: "Widget", +// action: { +// isSearchFieldFocused = false +// showSearchBar = false +// +// router.push(.widgetSettingView) +// } +// ) +// .padding(.trailing, 10) CircleGlassButton(imageName: "BundleIcon", action: { From 6504658f798670ec660156b661209b0b68849448 Mon Sep 17 00:00:00 2001 From: Jini Date: Wed, 28 Jan 2026 01:06:38 +0900 Subject: [PATCH 2/3] =?UTF-8?q?style:=20=EB=B3=B5=EC=82=AC=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=88=B4=ED=8C=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Keychy/Keychy.xcodeproj/project.pbxproj | 4 + .../DesignSystem/Typography/Typography.swift | 1 + .../Views/Detail/CopyTooltipView.swift | 59 ++++ .../Collection/Views/Detail/KeyringMenu.swift | 261 +++++++++++------- 4 files changed, 232 insertions(+), 93 deletions(-) create mode 100644 Keychy/Keychy/Presentation/Collection/Views/Detail/CopyTooltipView.swift diff --git a/Keychy/Keychy.xcodeproj/project.pbxproj b/Keychy/Keychy.xcodeproj/project.pbxproj index e64fa0fd8..a5e7a7760 100644 --- a/Keychy/Keychy.xcodeproj/project.pbxproj +++ b/Keychy/Keychy.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 3828F5472EC4CCDC00F1B040 /* CollectionView+NormalMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3828F5462EC4CCDC00F1B040 /* CollectionView+NormalMode.swift */; }; 3828F5492EC4CCE400F1B040 /* CollectionView+SearchMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3828F5482EC4CCE400F1B040 /* CollectionView+SearchMode.swift */; }; 3828F54B2EC4D0C500F1B040 /* CollectionView+Handlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3828F54A2EC4D0C500F1B040 /* CollectionView+Handlers.swift */; }; + 38489EBD2F290BD000E41FAE /* CopyTooltipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38489EBC2F290BD000E41FAE /* CopyTooltipView.swift */; }; 385425BE2EB2989400A06C02 /* CollectionCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385425BD2EB2989400A06C02 /* CollectionCellView.swift */; }; 385425C02EB2AE7800A06C02 /* CollectionViewModel+Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385425BF2EB2AE7800A06C02 /* CollectionViewModel+Sort.swift */; }; 385425C32EB2C35E00A06C02 /* WidgetSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385425C22EB2C35E00A06C02 /* WidgetSettingView.swift */; }; @@ -475,6 +476,7 @@ 3828F5462EC4CCDC00F1B040 /* CollectionView+NormalMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionView+NormalMode.swift"; sourceTree = ""; }; 3828F5482EC4CCE400F1B040 /* CollectionView+SearchMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionView+SearchMode.swift"; sourceTree = ""; }; 3828F54A2EC4D0C500F1B040 /* CollectionView+Handlers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionView+Handlers.swift"; sourceTree = ""; }; + 38489EBC2F290BD000E41FAE /* CopyTooltipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyTooltipView.swift; sourceTree = ""; }; 385425BD2EB2989400A06C02 /* CollectionCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionCellView.swift; sourceTree = ""; }; 385425BF2EB2AE7800A06C02 /* CollectionViewModel+Sort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionViewModel+Sort.swift"; sourceTree = ""; }; 385425C22EB2C35E00A06C02 /* WidgetSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetSettingView.swift; sourceTree = ""; }; @@ -960,6 +962,7 @@ 38A22A9C2EC27AC400B4C7C5 /* PackagedKeyringView+SaveImage.swift */, 3822DDC12EBBC712003125BE /* KeyringEditView.swift */, 3822DDBF2EBB2353003125BE /* KeyringMenu.swift */, + 38489EBC2F290BD000E41FAE /* CopyTooltipView.swift */, ); path = Detail; sourceTree = ""; @@ -2544,6 +2547,7 @@ 4C4733A62F1FA388005D2376 /* KeyringCompleteView+VideoGen.swift in Sources */, 4C4733A72F1FA388005D2376 /* KeyringCompleteView.swift in Sources */, 4C4733A82F1FA388005D2376 /* AcrylicPhotoVM+ImageLoad.swift in Sources */, + 38489EBD2F290BD000E41FAE /* CopyTooltipView.swift in Sources */, 4C4733A92F1FA388005D2376 /* NeonSignPreview.swift in Sources */, 4C4733AA2F1FA388005D2376 /* SpeechBubbleFrameSelectorView.swift in Sources */, 4C4733AB2F1FA388005D2376 /* KeyringViewModelProtocol+Reset.swift in Sources */, diff --git a/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift b/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift index cc18e3f1b..c15cd1222 100644 --- a/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift +++ b/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift @@ -74,6 +74,7 @@ struct Typography { static let nanum18EB = Typography(font: .custom(.nanumExtraBold, size: 18), lineSpacing: 0) static let nanum17EB = Typography(font: .custom(.nanumExtraBold, size: 17), lineSpacing: 0) static let nanum16EB = Typography(font: .custom(.nanumExtraBold, size: 16), lineSpacing: 0) + static let nanum12EB = Typography(font: .custom(.nanumExtraBold, size: 12), lineSpacing: 0) static let nanum15EB25 = Typography(font: .custom(.nanumExtraBold, size: 15), lineSpacing: 10) static let nanum15B25 = Typography(font: .custom(.nanumBold, size: 15), lineSpacing: 10) diff --git a/Keychy/Keychy/Presentation/Collection/Views/Detail/CopyTooltipView.swift b/Keychy/Keychy/Presentation/Collection/Views/Detail/CopyTooltipView.swift new file mode 100644 index 000000000..81c741998 --- /dev/null +++ b/Keychy/Keychy/Presentation/Collection/Views/Detail/CopyTooltipView.swift @@ -0,0 +1,59 @@ +// +// CopyTooltipView.swift +// Keychy +// +// Created by Jini on 1/28/26. +// + +import SwiftUI + +// 툴팁 말풍선 View +struct CopyTooltipView: View { + + @State private var isAppearing = false + + var body: some View { + VStack(spacing: 0) { + // 삼각형 (위쪽 꼬리) + Triangle() + .fill(Color.white100) + .frame(width: 36, height: 15) + .offset(x: 82, y: 2) + + // 말풍선 내용 + Text("내가 만든 키링만 복사할 수 있어요.") + .typography(.suit15SB25) + .foregroundColor(.black100) + .padding(.horizontal, 15) + .padding(.vertical, 13) + .background(.white100) + .cornerRadius(13) + } + .frame(width: 235, height: 45) + .compositingGroup() + .shadow(color: .black.opacity(0.1), radius: 12, x: 0, y: 3) + .scaleEffect(isAppearing ? 1.0 : 0.8, anchor: .top) + .opacity(isAppearing ? 1.0 : 0.0) + .onAppear { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + isAppearing = true + } + } + } +} + +// 삼각형 Shape +struct Triangle: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + path.move(to: CGPoint(x: rect.midX, y: rect.minY)) + path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) + path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) + path.closeSubpath() + return path + } +} + +#Preview { + CopyTooltipView() +} diff --git a/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringMenu.swift b/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringMenu.swift index f824710ca..c61c6f7cd 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringMenu.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringMenu.swift @@ -15,119 +15,194 @@ struct KeyringMenu: View { let onDelete: () -> Void let onWidget: () -> Void - private let menuWidth: CGFloat = 165 -// private var menuHeight: CGFloat { -// isMyKeyring ? 170 : 115 // 복사 버튼 있으면 170, 없으면 115 -// } + private let menuWidth: CGFloat = 200 private let menuHeight: CGFloat = 218 @State private var isAppearing = false + @State private var showCopyTooltip = false + @State private var questionButtonFrame: CGRect = .zero var body: some View { GeometryReader { geometry in ZStack { // 메뉴 - VStack(alignment: .leading, spacing: 5) { - // 편집 버튼 - Button(action: onEdit) { - HStack(spacing: 8) { - Image(.pencil) - .resizable() - .frame(width: 25, height: 25) - - Text("정보 수정") - .typography(.suit16M) - .foregroundColor(.gray600) - - Spacer() - } - .padding(.vertical, 10) - .padding(.horizontal, 10) - .contentShape(Rectangle()) - } - .frame(maxWidth: .infinity, alignment: .leading) + menuContent + .position( + x: geometry.size.width - menuWidth / 2 - 16, + y: position.maxY + 8 + menuHeight / 2 + ) + + // 툴팁 말풍선 + if showCopyTooltip { + CopyTooltipView() + .position( + x: geometry.size.width - 125, + y: questionButtonFrame.maxY + 32 + ) + .zIndex(1000) + } + } + } + .onAppear { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + isAppearing = true + } + } + // 툴팁 외부 클릭 시 닫기 + .onTapGesture { + if showCopyTooltip { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + showCopyTooltip = false + } + } + } + } + + private var menuContent: some View { + VStack(alignment: .leading, spacing: 5) { + // 편집 버튼 + Button(action: onEdit) { + HStack(spacing: 8) { + Image(.pencil) + .renderingMode(.template) + .resizable() + .frame(width: 25, height: 25) + .foregroundColor(.gray600) - if isMyKeyring { - // 복사 버튼 - Button(action: onCopy) { - HStack(spacing: 8) { - Image(.copy) - .resizable() - .frame(width: 25, height: 25) - - Text("복사") - .typography(.suit16M) - .foregroundColor(.gray600) - - Spacer() - } - .padding(.vertical, 10) - .padding(.horizontal, 10) - .contentShape(Rectangle()) - } - .frame(maxWidth: .infinity, alignment: .leading) - } + Text("정보 수정") + .typography(.suit16M) + .foregroundColor(.gray600) - // 삭제 버튼 - Button(action: onDelete) { - HStack(spacing: 8) { - Image(.trash) - .resizable() - .frame(width: 25, height: 25) - - Text("삭제") - .typography(.suit16M) - .foregroundColor(.pink) + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 10) + .contentShape(Rectangle()) + } + .frame(maxWidth: .infinity, alignment: .leading) + + // 복사 버튼 + HStack(spacing: 8) { + Image(.copy) + .renderingMode(.template) + .resizable() + .frame(width: 25, height: 25) + .foregroundColor(isMyKeyring ? .gray600 : .gray300) + + Text("복사") + .typography(.suit16M) + .foregroundColor(isMyKeyring ? .gray600 : .gray300) + + Spacer() + + if !isMyKeyring { + Button { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + showCopyTooltip.toggle() + } + } label: { + ZStack { + Circle() + .fill(Color.gray500) + .frame(width: 15, height: 15) - Spacer() + Text("?") + .typography(.nanum12EB) + .foregroundColor(.white) } - .padding(.vertical, 10) - .padding(.horizontal, 10) - .contentShape(Rectangle()) } - .frame(maxWidth: .infinity, alignment: .leading) + // 물음표 버튼의 위치 추적 + .background( + GeometryReader { geo in + Color.clear + .preference( + key: QuestionButtonPreferenceKey.self, + value: geo.frame(in: .global) + ) + } + ) + } + } + .padding(.vertical, 10) + .padding(.horizontal, 10) + .contentShape(Rectangle()) + .frame(maxWidth: .infinity, alignment: .leading) + .onTapGesture { + if isMyKeyring { + onCopy() + } + } + + // 삭제 버튼 + Button(action: onDelete) { + HStack(spacing: 8) { + Image(.trash) + .resizable() + .frame(width: 25, height: 25) - Rectangle() - .fill(Color.gray100) - .padding(.horizontal, 10) - .frame(height: 1) + Text("삭제") + .typography(.suit16M) + .foregroundColor(.pink) - // 위젯 버튼 - Button(action: onWidget) { - HStack(spacing: 8) { - Image(.widget) - .resizable() - .frame(width: 25, height: 25) - .foregroundStyle(.gray600) - - Text("위젯 설정") - .typography(.suit16M) - .foregroundColor(.gray600) - - Spacer() - } - .padding(.vertical, 10) - .padding(.horizontal, 10) - .contentShape(Rectangle()) - } - .frame(maxWidth: .infinity, alignment: .leading) + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 10) + .contentShape(Rectangle()) + } + .frame(maxWidth: .infinity, alignment: .leading) + + // 구분선 + Rectangle() + .fill(Color.gray100) + .padding(.horizontal, 10) + .frame(height: 1) + + // 위젯 버튼 + Button(action: onWidget) { + HStack(spacing: 8) { + Image(.widget) + .renderingMode(.template) + .resizable() + .frame(width: 24, height: 24) + .foregroundColor(.gray600) + + Text("위젯 설정") + .typography(.suit16M) + .foregroundColor(.gray600) + + Spacer() } + .padding(.vertical, 10) .padding(.horizontal, 10) - .padding(.vertical, 20) - .frame(width: menuWidth, height: menuHeight) - .glassEffect(.regular.interactive(), in: .rect(cornerRadius: 34)) - .scaleEffect(isAppearing ? 1.0 : 0.8, anchor: .topTrailing) - .opacity(isAppearing ? 1.0 : 0.0) - .position( - x: geometry.size.width - menuWidth / 2 - 16, - y: position.maxY + 8 + menuHeight / 2 - ) + .contentShape(Rectangle()) } + .frame(maxWidth: .infinity, alignment: .leading) } - .onAppear { - withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { - isAppearing = true + .padding(.horizontal, 10) + .padding(.vertical, 20) + .frame(width: menuWidth, height: menuHeight) + .glassEffect(.regular.interactive(), in: .rect(cornerRadius: 34)) + .scaleEffect(isAppearing ? 1.0 : 0.8, anchor: .topTrailing) + .opacity(isAppearing ? 1.0 : 0.0) + .onPreferenceChange(QuestionButtonPreferenceKey.self) { frame in + questionButtonFrame = frame + } + .onTapGesture { + if showCopyTooltip { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + showCopyTooltip = false + } } } } } + +// 물음표 버튼 위치 추적용 PreferenceKey +struct QuestionButtonPreferenceKey: PreferenceKey { + static var defaultValue: CGRect = .zero + + static func reduce(value: inout CGRect, nextValue: () -> CGRect) { + value = nextValue() + } +} From 9268381ff7a05c271076fbfbb4d59820f4192e63 Mon Sep 17 00:00:00 2001 From: Jini Date: Wed, 28 Jan 2026 01:25:05 +0900 Subject: [PATCH 3/3] =?UTF-8?q?style:=20=ED=88=B4=ED=8C=81=20=EB=8B=AB?= =?UTF-8?q?=EA=B8=B0=20=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=A1=B0=EC=A0=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Collection/Views/Detail/KeyringMenu.swift | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringMenu.swift b/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringMenu.swift index c61c6f7cd..54beae51d 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringMenu.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringMenu.swift @@ -188,13 +188,6 @@ struct KeyringMenu: View { .onPreferenceChange(QuestionButtonPreferenceKey.self) { frame in questionButtonFrame = frame } - .onTapGesture { - if showCopyTooltip { - withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { - showCopyTooltip = false - } - } - } } }