diff --git a/HappyAnding/HappyAnding.xcodeproj/project.pbxproj b/HappyAnding/HappyAnding.xcodeproj/project.pbxproj index d0d284bb..dd6e74e5 100644 --- a/HappyAnding/HappyAnding.xcodeproj/project.pbxproj +++ b/HappyAnding/HappyAnding.xcodeproj/project.pbxproj @@ -160,6 +160,7 @@ F96D45BB29804057000C2441 /* EnvironmentValues+Alerter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F96D45BA29804057000C2441 /* EnvironmentValues+Alerter.swift */; }; F96D45BD29816578000C2441 /* StickyHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F96D45BC29816578000C2441 /* StickyHeader.swift */; }; F9724BBF292755E400860F8A /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9724BBE292755E400860F8A /* Comment.swift */; }; + F975C2172BD96994006CC401 /* Int+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F975C2162BD96994006CC401 /* Int+Extension.swift */; }; F976E82C29368E0D0088BBA1 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = F976E82B29368E0D0088BBA1 /* Version.swift */; }; F976E85129395B350088BBA1 /* ShareExtensionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F976E85029395B350088BBA1 /* ShareExtensionViewModel.swift */; }; F98017182BBC29A7004F2EA7 /* SCZ+Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = F98017172BBC29A7004F2EA7 /* SCZ+Color.swift */; }; @@ -342,6 +343,11 @@ F96D45BA29804057000C2441 /* EnvironmentValues+Alerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+Alerter.swift"; sourceTree = ""; }; F96D45BC29816578000C2441 /* StickyHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickyHeader.swift; sourceTree = ""; }; F9724BBE292755E400860F8A /* Comment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; + F975C2122BD91321006CC401 /* SF-Compact-Rounded-Semibold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Compact-Rounded-Semibold.otf"; sourceTree = ""; }; + F975C2132BD91321006CC401 /* SF-Compact-Rounded-Medium.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Compact-Rounded-Medium.otf"; sourceTree = ""; }; + F975C2142BD91322006CC401 /* SF-Compact-Rounded-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Compact-Rounded-Bold.otf"; sourceTree = ""; }; + F975C2152BD91322006CC401 /* SF-Compact-Rounded-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Compact-Rounded-Regular.otf"; sourceTree = ""; }; + F975C2162BD96994006CC401 /* Int+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Extension.swift"; sourceTree = ""; }; F976E82B29368E0D0088BBA1 /* Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; F976E85029395B350088BBA1 /* ShareExtensionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionViewModel.swift; sourceTree = ""; }; F98017172BBC29A7004F2EA7 /* SCZ+Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SCZ+Color.swift"; sourceTree = ""; }; @@ -642,6 +648,7 @@ F9AC2BB92935D34C00165820 /* Image+View.swift */, A3439AFA2939B0E80043E273 /* UserDefaults+Extension.swift */, F96D45BA29804057000C2441 /* EnvironmentValues+Alerter.swift */, + F975C2162BD96994006CC401 /* Int+Extension.swift */, ); path = Extensions; sourceTree = ""; @@ -722,6 +729,7 @@ A329F70F2BCA4F8F00ED20DA /* Resources */ = { isa = PBXGroup; children = ( + F975C2112BD91309006CC401 /* SFCompactRounded */, A329F7102BCA4FB200ED20DA /* Pretendard */, ); path = Resources; @@ -801,6 +809,17 @@ path = SettingViews; sourceTree = ""; }; + F975C2112BD91309006CC401 /* SFCompactRounded */ = { + isa = PBXGroup; + children = ( + F975C2142BD91322006CC401 /* SF-Compact-Rounded-Bold.otf */, + F975C2132BD91321006CC401 /* SF-Compact-Rounded-Medium.otf */, + F975C2152BD91322006CC401 /* SF-Compact-Rounded-Regular.otf */, + F975C2122BD91321006CC401 /* SF-Compact-Rounded-Semibold.otf */, + ); + path = SFCompactRounded; + sourceTree = ""; + }; F9DB8ECB293B30EC00516CE1 /* Recovered References */ = { isa = PBXGroup; children = ( @@ -937,6 +956,7 @@ packageReferences = ( F94B435B2907B19A00987819 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, 4D6A9EFD29A36E9C00D02522 /* XCRemoteSwiftPackageReference "WrappingHStack" */, + F975C2182BDA3A70006CC401 /* XCRemoteSwiftPackageReference "swiftui-introspect" */, ); productRefGroup = 87E99C6B28F94EA6009B691F /* Products */; projectDirPath = ""; @@ -1070,6 +1090,7 @@ 87E99C7028F94EA6009B691F /* ShortcutTabView.swift in Sources */, F99569182901DC4D0060AAEF /* UIFont+Extension.swift in Sources */, F91A72C32999160E00CA135A /* Alerter.swift in Sources */, + F975C2172BD96994006CC401 /* Int+Extension.swift in Sources */, A3D348552BD1233000DE814C /* View+Font.swift in Sources */, 87E99CAD28FFF261009B691F /* ReadShortcutView.swift in Sources */, F930E0002BBD51EC003C2686 /* Seal.swift in Sources */, @@ -1610,6 +1631,14 @@ minimumVersion = 10.0.0; }; }; + F975C2182BDA3A70006CC401 /* XCRemoteSwiftPackageReference "swiftui-introspect" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/siteline/swiftui-introspect"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.1.3; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ diff --git a/HappyAnding/HappyAnding/Assets.xcassets/app.imageset/Contents.json b/HappyAnding/HappyAnding/Assets.xcassets/app.imageset/Contents.json new file mode 100644 index 00000000..2be286fa --- /dev/null +++ b/HappyAnding/HappyAnding/Assets.xcassets/app.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "filename" : "app.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/HappyAnding/HappyAnding/Assets.xcassets/app.imageset/app.svg b/HappyAnding/HappyAnding/Assets.xcassets/app.imageset/app.svg new file mode 100644 index 00000000..c70e67bf --- /dev/null +++ b/HappyAnding/HappyAnding/Assets.xcassets/app.imageset/app.svg @@ -0,0 +1,3 @@ + + + diff --git a/HappyAnding/HappyAnding/Assets.xcassets/appLarge.imageset/Contents.json b/HappyAnding/HappyAnding/Assets.xcassets/appLarge.imageset/Contents.json new file mode 100644 index 00000000..2d4082a0 --- /dev/null +++ b/HappyAnding/HappyAnding/Assets.xcassets/appLarge.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "filename" : "appLarge.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/HappyAnding/HappyAnding/Assets.xcassets/appLarge.imageset/appLarge.svg b/HappyAnding/HappyAnding/Assets.xcassets/appLarge.imageset/appLarge.svg new file mode 100644 index 00000000..c4fb9a0c --- /dev/null +++ b/HappyAnding/HappyAnding/Assets.xcassets/appLarge.imageset/appLarge.svg @@ -0,0 +1,3 @@ + + + diff --git a/HappyAnding/HappyAnding/Assets.xcassets/profileIcon.imageset/Contents.json b/HappyAnding/HappyAnding/Assets.xcassets/profileIcon.imageset/Contents.json new file mode 100644 index 00000000..29ca914c --- /dev/null +++ b/HappyAnding/HappyAnding/Assets.xcassets/profileIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "profileIcon.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HappyAnding/HappyAnding/Assets.xcassets/profileIcon.imageset/profileIcon.svg b/HappyAnding/HappyAnding/Assets.xcassets/profileIcon.imageset/profileIcon.svg new file mode 100644 index 00000000..a85dd99d --- /dev/null +++ b/HappyAnding/HappyAnding/Assets.xcassets/profileIcon.imageset/profileIcon.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/HappyAnding/HappyAnding/Extensions/Int+Extension.swift b/HappyAnding/HappyAnding/Extensions/Int+Extension.swift new file mode 100644 index 00000000..1c943226 --- /dev/null +++ b/HappyAnding/HappyAnding/Extensions/Int+Extension.swift @@ -0,0 +1,17 @@ +// +// Int+Extension.swift +// HappyAnding +// +// Created by JeonJimin on 4/25/24. +// + +import Foundation + +extension Int { + /// Int 타입의 숫자를 받아서 천 단위로 콤마를 추가한 문자열을 반환 + func formatNumber() -> String { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + return formatter.string(from: NSNumber(value: self)) ?? "" + } +} diff --git a/HappyAnding/HappyAnding/Extensions/SCZ+Color.swift b/HappyAnding/HappyAnding/Extensions/SCZ+Color.swift index 7ba67af5..f4abdd16 100644 --- a/HappyAnding/HappyAnding/Extensions/SCZ+Color.swift +++ b/HappyAnding/HappyAnding/Extensions/SCZ+Color.swift @@ -582,4 +582,12 @@ extension SCZColor { static let opacity16 = Color(hexString: "DEE7FF") static let opacity08 = Color(hexString: "EFF3FF") } + + struct SCZRed { + static let dangerouslyRed = Color(hexString: "DD0008") + static let red = Color(hexString: "FF453A") + } + + static let systemWhite = Color(hexString: "FFFFFF") + static let systemBlack = Color(hexString: "000000") } diff --git a/HappyAnding/HappyAnding/Extensions/String/String+Date.swift b/HappyAnding/HappyAnding/Extensions/String/String+Date.swift index 598bdc6c..b7f78ebd 100644 --- a/HappyAnding/HappyAnding/Extensions/String/String+Date.swift +++ b/HappyAnding/HappyAnding/Extensions/String/String+Date.swift @@ -37,17 +37,67 @@ extension String { func getPostDateFormat() -> String? { let inputFormatter = DateFormatter() - inputFormatter.dateFormat = "yyyyMMddHHmmss" - - if let date = inputFormatter.date(from: self) { - let outputFormatter = DateFormatter() - outputFormatter.locale = Locale(identifier: "ko_KR") - outputFormatter.dateFormat = "M월 d일 a h시 m분" - - let output = outputFormatter.string(from: date) - return output - } else { - return nil - } + inputFormatter.dateFormat = "yyyyMMddHHmmss" + + if let date = inputFormatter.date(from: self) { + let outputFormatter = DateFormatter() + outputFormatter.locale = Locale(identifier: "ko_KR") + outputFormatter.dateFormat = "M월 d일 a h시 m분" + + let output = outputFormatter.string(from: date) + return output + } else { + return nil + } + } + func getVersionDateFormat() -> String? { + let inputFormatter = DateFormatter() + inputFormatter.dateFormat = "yyyyMMddHHmmss" + + if let date = inputFormatter.date(from: self) { + let outputFormatter = DateFormatter() + outputFormatter.locale = Locale(identifier: "ko_KR") + outputFormatter.dateFormat = "yyyy년 M월 d일" + + let output = outputFormatter.string(from: date) + return output + } else { + return nil + } + } + + func getCommentDateFormat() -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyyMMddHHmmss" + + let commentDateFormatter = DateFormatter() + commentDateFormatter.dateFormat = "yyyy.MM.dd" + + guard let date = dateFormatter.date(from: self) else { + return "" + } + + let calendar = Calendar.current + let currentDate = Date() + + let difference = calendar.dateComponents([.year, .month, .weekOfYear, .day, .hour, .minute], from: date, to: currentDate) + + if let years = difference.year, years > 0 { + return commentDateFormatter.string(from: date) + } else if let months = difference.month, months >= 11 { + return commentDateFormatter.string(from: date) + } else if let months = difference.month, months > 0 { + return "\(months)개월 전" + } else if let weeks = difference.weekOfYear, weeks > 0 { + return "\(weeks)주 전" + } else if let days = difference.day, days > 0 { + return "\(days)일 전" + } else if let hours = difference.hour, hours > 0 { + return "\(hours)시간 전" + } else if let minutes = difference.minute, minutes > 0 { + return "\(minutes)분 전" + } else { + return "방금 전" + } } } diff --git a/HappyAnding/HappyAnding/Extensions/View/View+Font.swift b/HappyAnding/HappyAnding/Extensions/View/View+Font.swift index 8c6da923..cb6aa041 100644 --- a/HappyAnding/HappyAnding/Extensions/View/View+Font.swift +++ b/HappyAnding/HappyAnding/Extensions/View/View+Font.swift @@ -18,6 +18,13 @@ enum Pretendard: String { case bold = "Pretendard-Bold" } +enum SFCompactRounded: String { + case regular = "SF-Compact-Rounded-Regular" + case medium = "SF-Compact-Rounded-Medium" + case semiBold = "SF-Compact-Rounded-Semibold" + case bold = "SF-Compact-Rounded-Bold" +} + ///Pretendard 폰트를 기존 modifier와 같이 사용할 수 있게 해 주는 Extension ///사용 예시 ///Text("단축어").largeTitle() @@ -55,6 +62,10 @@ extension View { func customPretendard(fontName: Pretendard, size: CGFloat) -> some View { self.modifier(CustomFontStyle(fontName: fontName.rawValue, size: size)) } + + func customSFCompactRounded(fontName: SFCompactRounded, size: CGFloat) -> some View { + self.modifier(CustomFontStyle(fontName: fontName.rawValue, size: size)) + } } struct CustomFontStyle: ViewModifier { diff --git a/HappyAnding/HappyAnding/Extensions/View/View+Shape.swift b/HappyAnding/HappyAnding/Extensions/View/View+Shape.swift index 2c5a964b..f6121a9c 100644 --- a/HappyAnding/HappyAnding/Extensions/View/View+Shape.swift +++ b/HappyAnding/HappyAnding/Extensions/View/View+Shape.swift @@ -30,6 +30,14 @@ extension View { ) .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) } + + func roundedBackground(background: Color) -> some View { + self + .padding(.horizontal, 10) + .padding(.vertical, 8) + .background(background) + .roundedBorder(cornerRadius: 16, color: Color.white.opacity(0.12), isNormalBlend: true) + } } @@ -46,3 +54,16 @@ struct RoundedCorner: Shape { return Path(path.cgPath) } } + +//MARK: - 삼각형 +/// 삼각형 모양을 그리는 경우에 사용합니다. +struct Triangle: Shape { + func path(in rect: CGRect) -> Path { + Path { path in + path.move(to: CGPoint(x: rect.midX, y: rect.minY)) + path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) + path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) + path.closeSubpath() + } + } +} diff --git a/HappyAnding/HappyAnding/Extensions/View/View+UIFont.swift b/HappyAnding/HappyAnding/Extensions/View/View+UIFont.swift index 546ecb8d..476014e0 100644 --- a/HappyAnding/HappyAnding/Extensions/View/View+UIFont.swift +++ b/HappyAnding/HappyAnding/Extensions/View/View+UIFont.swift @@ -64,9 +64,27 @@ extension View { func smallShortcutIcon() -> some View { ModifiedContent(content: self, modifier: FontWithLineHeight(font: .smallShortcutIcon, lineHeight: 20)) } + func regular16() -> some View { + ModifiedContent(content: self, modifier: FontWithLineHeight(font: UIFont(name: Pretendard.regular.rawValue, size: 16) ?? UIFont.systemFont(ofSize: 16), lineHeight: 24)) + } + func medium16() -> some View { + ModifiedContent(content: self, modifier: FontWithLineHeight(font: UIFont(name: Pretendard.medium.rawValue, size: 16) ?? UIFont.systemFont(ofSize: 16), lineHeight: 24)) + } + func medium17() -> some View { + ModifiedContent(content: self, modifier: FontWithLineHeight(font: UIFont(name: Pretendard.medium.rawValue, size: 17) ?? UIFont.systemFont(ofSize: 17), lineHeight: 24)) + } + func semiBold17() -> some View { + ModifiedContent(content: self, modifier: FontWithLineHeight(font: UIFont(name: Pretendard.semiBold.rawValue, size: 17) ?? UIFont.systemFont(ofSize: 17), lineHeight: 24)) + } + func numRegular16() -> some View { + ModifiedContent(content: self, modifier: FontWithLineHeight(font: UIFont(name: SFCompactRounded.regular.rawValue, size: 16) ?? UIFont.systemFont(ofSize: 16), lineHeight: 24)) + } + + func descriptionReadable() -> some View { + ModifiedContent(content: self, modifier: FontWithLineHeight(font: UIFont(name: Pretendard.regular.rawValue, size: 16) ?? UIFont.systemFont(ofSize: 16), lineHeight: 24)) + } } - // MARK: - View Modifier /** 자간을 위한 View Modifier입니다. diff --git a/HappyAnding/HappyAnding/Info.plist b/HappyAnding/HappyAnding/Info.plist index e9717ee2..6fa80349 100644 --- a/HappyAnding/HappyAnding/Info.plist +++ b/HappyAnding/HappyAnding/Info.plist @@ -36,6 +36,10 @@ Pretendard-SemiBold.otf Pretendard-Medium.otf Pretendard-Regular.otf + SF-Compact-Rounded-Bold.otf + SF-Compact-Rounded-Semibold.otf + SF-Compact-Rounded-Medium.otf + SF-Compact-Rounded-Regular.otf diff --git a/HappyAnding/HappyAnding/Resources/SFCompactRounded/SF-Compact-Rounded-Bold.otf b/HappyAnding/HappyAnding/Resources/SFCompactRounded/SF-Compact-Rounded-Bold.otf new file mode 100755 index 00000000..d24e427a Binary files /dev/null and b/HappyAnding/HappyAnding/Resources/SFCompactRounded/SF-Compact-Rounded-Bold.otf differ diff --git a/HappyAnding/HappyAnding/Resources/SFCompactRounded/SF-Compact-Rounded-Medium.otf b/HappyAnding/HappyAnding/Resources/SFCompactRounded/SF-Compact-Rounded-Medium.otf new file mode 100755 index 00000000..604aac22 Binary files /dev/null and b/HappyAnding/HappyAnding/Resources/SFCompactRounded/SF-Compact-Rounded-Medium.otf differ diff --git a/HappyAnding/HappyAnding/Resources/SFCompactRounded/SF-Compact-Rounded-Regular.otf b/HappyAnding/HappyAnding/Resources/SFCompactRounded/SF-Compact-Rounded-Regular.otf new file mode 100755 index 00000000..a9abd5e6 Binary files /dev/null and b/HappyAnding/HappyAnding/Resources/SFCompactRounded/SF-Compact-Rounded-Regular.otf differ diff --git a/HappyAnding/HappyAnding/Resources/SFCompactRounded/SF-Compact-Rounded-Semibold.otf b/HappyAnding/HappyAnding/Resources/SFCompactRounded/SF-Compact-Rounded-Semibold.otf new file mode 100755 index 00000000..5484aa18 Binary files /dev/null and b/HappyAnding/HappyAnding/Resources/SFCompactRounded/SF-Compact-Rounded-Semibold.otf differ diff --git a/HappyAnding/HappyAnding/TextLiteral.swift b/HappyAnding/HappyAnding/TextLiteral.swift index fc358608..0a9d8f7e 100644 --- a/HappyAnding/HappyAnding/TextLiteral.swift +++ b/HappyAnding/HappyAnding/TextLiteral.swift @@ -20,11 +20,12 @@ enum TextLiteral { static let confirm: String = "확인" static let close: String = "닫기" static let done: String = "완료" - static let edit: String = "편집" + static let edit: String = "수정하기" static let update: String = "업데이트" static let later: String = "나중에" - static let share: String = "공유" - static let delete: String = "삭제" + static let share: String = "공유하기" + static let delete: String = "삭제하기" + static let report: String = "신고하기" static let withdrawnUser: String = "탈퇴한 사용자" static let defaultUser: String = "user" @@ -53,7 +54,6 @@ enum TextLiteral { // MARK: - RecentRegisteredView static let recentRegisteredViewTitle: String = "최신 단축어" - static let newShortcutsTitle: String = "새로 올라온" // MARK: - LovedShortcutView static let lovedShortcutViewTitle: String = "사랑받는" @@ -117,35 +117,27 @@ enum TextLiteral { static let categoryModalViewTitle: String = "카테고리" // MARK: - ReadShortcutView - static let readShortcutViewBasicTabTitle: String = "기본 정보" - static let readShortcutViewVersionTabTitle: String = "버전 정보" - static let readShortcutViewCommentTabTitle: String = "댓글" + static let readShortcutViewVersionTitle: String = "버전 업데이트 정보" + static let readShortcutViewCommentTitle: String = "댓글" static let readShortcutViewDeletionTitle: String = "단축어 삭제" static let readShortcutViewDeletionMessage: String = "단축어를 삭제하시겠어요?" static let readShortcutViewDeletionMessageDowngrade: String = "단축어를 삭제하시겠어요? \n이 글을 삭제하면 등급이 내려가요." - static let readShortcutViewDeleteFixesTitle: String = "수정사항 삭제" - static let readShortcutViewDeleteFixes: String = "수정사항을 삭제하시겠어요?" - static let readShortcutViewKeepFixes: String = "계속 작성" + static let readShortcutViewFilterNew: String = "최신" + static let readShortcutViewFilterAll: String = "전체" static let readShortcutViewCommentDescriptionBeforeLogin: String = "로그인 후 댓글을 작성할 수 있어요" - static let readShortcutViewCommentDescription: String = "댓글을 입력하세요" - - // MARK: - ReadShortcutContentView - static let readShortcutContentViewDescription: String = "단축어 설명" - static let readShortcutContentViewPostedDate: String = "작성 날짜" + static let readShortcutViewCommentDescription: String = "댓글 남기기" + static let readShortcutViewShortcutHeart: String = "하트를 날려 감사를 표했어요" static let readShortcutContentViewCategory: String = "카테고리" - static let readShortcutContentViewRequiredApps: String = "단축어 사용에 필요한 앱" - static let readShortcutContentViewRequirements: String = "단축어 사용을 위한 요구사항" - - // MARK: - ReadShortcutVersionView - static let readShortcutVersionViewNoUpdates: String = "최신 버전의 단축어에요" - static let readShortcutVersionViewUpdateContent: String = "업데이트 내용" - static let readShortcutVersionViewDownloadPreviousVersion: String = "이전 버전 다운로드" + static let readShortcutContentViewRequiredApps: String = "필요한 앱" + //추후 사용안하는 경우 삭제 + static let readShortcutViewDeleteFixesTitle: String = "수정사항 삭제" + static let readShortcutViewDeleteFixes: String = "수정사항을 삭제하시겠어요?" + static let readShortcutViewKeepFixes: String = "계속 작성" + // MARK: - ReadShortcutCommentView - static let readShortcutCommentViewNoComments: String = "등록된 댓글이 없어요" static let readShortcutCommentViewDeletionTitle: String = "댓글 삭제" static let readShortcutCommentViewDeletionMessage: String = "답글도 함께 삭제돼요. 댓글을 삭제하시겠어요?" - static let readShortcutCommentViewReply: String = "답글" static let readShortcutCommentViewEdit: String = "수정" // MARK: - UpdateShortcutView @@ -243,9 +235,6 @@ enum TextLiteral { // MARK: - SearchView static let searchViewPrompt: String = "제목 또는 관련앱으로 검색하세요" - static let searchViewRecommendedKeyword: String = "추천 검색어" - static let searchViewProposal: String = "단축어 제안하기" - static let searchViewProposalURL: String = "https://docs.google.com/forms/d/e/1FAIpQLScQc3KeYjDGCE-C2YRU-Hwy2XNy5bt89KVX1OMUzRiySaMX1Q/viewform" static let searchViewMoreResult: String = "더 많은 검색 결과 보기" static let searchViewRelatedShortcut: String = "관련된 단축어" static let searchVIewRelatedPost: String = "관련된 글" diff --git a/HappyAnding/HappyAnding/ViewModel/ReadShortcutViewModels/ReadShortcutViewModel.swift b/HappyAnding/HappyAnding/ViewModel/ReadShortcutViewModels/ReadShortcutViewModel.swift index eb945867..5a38c809 100644 --- a/HappyAnding/HappyAnding/ViewModel/ReadShortcutViewModels/ReadShortcutViewModel.swift +++ b/HappyAnding/HappyAnding/ViewModel/ReadShortcutViewModels/ReadShortcutViewModel.swift @@ -47,6 +47,8 @@ final class ReadShortcutViewModel: ObservableObject { @Published var isLinkValid = false @Published var isDescriptionValid = false + @Published var isVersionFolded = true + var isUpdateValid: Bool { isLinkValid && isDescriptionValid } @@ -71,16 +73,6 @@ final class ReadShortcutViewModel: ObservableObject { } } - func moveTab(to tab: Int) { - self.currentTab = tab - } - - func setReply(to comment: Comment) { - self.nestedCommentTarget = comment.user_nickname - self.comment.bundle_id = comment.bundle_id - self.comment.depth = 1 - } - func checkIfDownloaded() { if (shortcutsZipViewModel.userInfo?.downloadedShortcuts.firstIndex(where: { $0.id == shortcut.id })) == nil { shortcut.numberOfDownload += 1 @@ -93,10 +85,41 @@ final class ReadShortcutViewModel: ObservableObject { func onViewDisappear() { if isMyLike != isMyFirstLike { - shortcutsZipViewModel.updateNumberOfLike(isMyLike: isMyLike, shortcut: shortcut) + } } + func toggleIsMyLike() { + isMyLike.toggle() + shortcutsZipViewModel.updateNumberOfLike(isMyLike: isMyLike, shortcut: shortcut) + } + func checkAuthor() -> Bool { + return self.shortcut.author == shortcutsZipViewModel.currentUser() + } + + func getUrl() -> URL? { + if let url = URL(string: shortcut.downloadLink[0]) { + checkIfDownloaded() + isDownloadingShortcut = true + return url + } + return nil + } + + + //MARK: - Shortcut + + func refreshShortcut() { + self.shortcut = shortcutsZipViewModel.fetchShortcutDetail(id: shortcut.id) ?? shortcut + } + + func updateShortcut() { + shortcutsZipViewModel.updateShortcutVersion(shortcut: shortcut, + updateDescription: updateDescription, + updateLink: updatedLink) + self.shortcut = shortcutsZipViewModel.fetchShortcutDetail(id: shortcut.id) ?? shortcut + isUpdatingShortcut.toggle() + } func deleteShortcut() { shortcutsZipViewModel.deleteShortcutIDInUser(shortcutID: shortcut.id) shortcutsZipViewModel.deleteShortcutInCuration(curationsIDs: shortcut.curationIDs, shortcutID: shortcut.id) @@ -104,13 +127,27 @@ final class ReadShortcutViewModel: ObservableObject { shortcutsZipViewModel.shortcutsMadeByUser = shortcutsZipViewModel.shortcutsMadeByUser.filter { $0.id != shortcut.id } shortcutsZipViewModel.updateShortcutGrade() } + func shareShortcut() { + guard let deepLink = URL(string: "ShortcutsZip://myPage/detailView?shortcutID=\(shortcut.id)") else { return } + let activityVC = UIActivityViewController(activityItems: [deepLink], applicationActivities: nil) + let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene + guard let window = windowScene?.windows.first else { return } + window.rootViewController?.present(activityVC, animated: true, completion: nil) + } - func cancelEditingComment() { - self.isEditingComment.toggle() - self.comment = self.comment.resetComment() - self.commentText = "" + //MARK: - UserGrading + func checkDownGrading() { + isDeletingShortcut.toggle() + isDowngradingUserLevel = shortcutsZipViewModel.isShortcutDowngrade() } + func fetchUserGrade(id: String) -> Image { + shortcutsZipViewModel.fetchShortcutGradeImage(isBig: false, shortcutGrade: shortcutsZipViewModel.checkShortcutGrade(userID: id)) + } +} + +//MARK: - Comment +extension ReadShortcutViewModel { func postComment() { if !isEditingComment { comment.contents = commentText @@ -130,34 +167,10 @@ final class ReadShortcutViewModel: ObservableObject { self.comments.comments = comments.fetchSortedComment() } - func cancelNestedComment() { - comment.bundle_id = "\(Date().getDate())_\(UUID().uuidString)" - comment.depth = 0 - } - - func checkAuthor() -> Bool { - return self.shortcut.author == shortcutsZipViewModel.currentUser() - } - - func shareShortcut() { - guard let deepLink = URL(string: "ShortcutsZip://myPage/detailView?shortcutID=\(shortcut.id)") else { return } - let activityVC = UIActivityViewController(activityItems: [deepLink], applicationActivities: nil) - let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene - guard let window = windowScene?.windows.first else { return } - window.rootViewController?.present(activityVC, animated: true, completion: nil) - } - - func checkDowngrading() { - isDeletingShortcut.toggle() - isDowngradingUserLevel = shortcutsZipViewModel.isShortcutDowngrade() - } - - func updateShortcut() { - shortcutsZipViewModel.updateShortcutVersion(shortcut: shortcut, - updateDescription: updateDescription, - updateLink: updatedLink) - self.shortcut = shortcutsZipViewModel.fetchShortcutDetail(id: shortcut.id) ?? shortcut - isUpdatingShortcut.toggle() + func cancelEditingComment() { + self.isEditingComment.toggle() + self.comment = self.comment.resetComment() + self.commentText = "" } func deleteComment() { @@ -170,11 +183,20 @@ final class ReadShortcutViewModel: ObservableObject { shortcutsZipViewModel.setData(model: comments) } - func fetchUserGrade(id: String) -> Image { - shortcutsZipViewModel.fetchShortcutGradeImage(isBig: false, shortcutGrade: shortcutsZipViewModel.checkShortcutGrade(userID: id)) + + //MARK: - 대댓글 + func getReplyNumber(bundleId: String) -> Int { + self.comments.comments.filter{ $0.bundle_id == bundleId }.count-1 } - func refreshShortcut() { - self.shortcut = shortcutsZipViewModel.fetchShortcutDetail(id: shortcut.id) ?? shortcut + func setReply(to comment: Comment) { + self.nestedCommentTarget = comment.user_nickname + " " + comment.contents + self.comment.bundle_id = comment.bundle_id + self.comment.depth = 1 + } + + func cancelReply() { + comment.bundle_id = "\(Date().getDate())_\(UUID().uuidString)" + comment.depth = 0 } } diff --git a/HappyAnding/HappyAnding/ViewModel/ShortcutsZipViewModel.swift b/HappyAnding/HappyAnding/ViewModel/ShortcutsZipViewModel.swift index 4065cb6c..627f0fde 100644 --- a/HappyAnding/HappyAnding/ViewModel/ShortcutsZipViewModel.swift +++ b/HappyAnding/HappyAnding/ViewModel/ShortcutsZipViewModel.swift @@ -998,7 +998,7 @@ extension ShortcutsZipViewModel { func fetchShortcutGradeImage(isBig: Bool, shortcutGrade: ShortcutGrade) -> Image { switch shortcutGrade { case .level0: - return Image(systemName: "person.crop.circle.fill") + return Image("profileIcon") case .level1: return Image(isBig ? "level1Big" : "level1Small") case .level2: diff --git a/HappyAnding/HappyAnding/Views/Components/ShortcutIcon.swift b/HappyAnding/HappyAnding/Views/Components/ShortcutIcon.swift index 800d259f..e00b6905 100644 --- a/HappyAnding/HappyAnding/Views/Components/ShortcutIcon.swift +++ b/HappyAnding/HappyAnding/Views/Components/ShortcutIcon.swift @@ -7,6 +7,7 @@ import SwiftUI +//TODO: 추후 사이즈별 아이콘 크기 조절 필요 struct ShortcutIcon: View { @Environment(\.colorScheme) var colorScheme @@ -16,17 +17,23 @@ struct ShortcutIcon: View { var body: some View { ZStack { - RoundedRectangle(cornerRadius: 13) - .foregroundStyle(SCZColor.colors[color]?.color(for: colorScheme).fillGradient() ?? Color.clear.toGradient()) - .roundedBorder(cornerRadius: 13, color: .white, isNormalBlend: true, opacity: 0.24) + Image(systemName: "app.fill") + .resizable() + .scaledToFit() .frame(width: size, height: size) + .foregroundStyle(SCZColor.colors[color]?.color(for: colorScheme).fillGradient() ?? Color.clear.toGradient()) + Image(size>50 ? "appLarge" : "app") + .resizable() + .scaledToFit() + .frame(width: size, height: size) + .foregroundStyle(SCZColor.colors[color]?.color(for: colorScheme).strokeGradient() ?? Color.clear.toGradient()) Image(systemName: sfSymbol) - .font(.system(size: 28)) + .font(.system(size: size/2)) .foregroundStyle(Color.white) } } } - + #Preview { - ShortcutIcon(sfSymbol: "play.rectangle.fill", color: "Red", size: 56) + ShortcutIcon(sfSymbol: "alarm.fill", color: "Yellow", size: 96) } diff --git a/HappyAnding/HappyAnding/Views/ReadShortcutViews/ReadShortcutView.swift b/HappyAnding/HappyAnding/Views/ReadShortcutViews/ReadShortcutView.swift index e5e2a269..67e944b4 100644 --- a/HappyAnding/HappyAnding/Views/ReadShortcutViews/ReadShortcutView.swift +++ b/HappyAnding/HappyAnding/Views/ReadShortcutViews/ReadShortcutView.swift @@ -7,703 +7,478 @@ import SwiftUI -import WrappingHStack - struct ReadShortcutView: View { @Environment(\.presentationMode) var presentation: Binding @Environment(\.openURL) private var openURL @Environment(\.loginAlertKey) var loginAlerter - @StateObject var viewModel: ReadShortcutViewModel - @StateObject var writeNavigation = WriteShortcutNavigation() @AppStorage("useWithoutSignIn") var useWithoutSignIn: Bool = false + + @StateObject var viewModel: ReadShortcutViewModel @FocusState private var isFocused: Bool - @Namespace var namespace - @Namespace var bottomID - private let tabItems = [TextLiteral.readShortcutViewBasicTabTitle, TextLiteral.readShortcutViewVersionTabTitle, TextLiteral.readShortcutViewCommentTabTitle] - private let hapticManager = HapticManager.instance + @State var isCommentSectionActivated = false + var body: some View { - ZStack { - ScrollViewReader { proxy in - ScrollView { - VStack(spacing: 0) { - StickyHeader(height: 40) - - /// 단축어 타이틀 - ReadShortcutViewHeader(viewModel: self.viewModel) + ScrollViewReader { proxy in + ScrollView { + VStack(alignment: .leading, spacing: 12) { + //Header - 단축어 기본정보 (title, subtitle, author) + ReadShortcutHeader(viewModel: self.viewModel) + .padding(.top, 16) + + //TODO: 이미지 없는경우 예외처리 + + // Divider() + // .background(SCZColor.CharcoalGray.opacity08) + // .padding(.vertical, 8) + // + //이미지 + // ScrollView(.horizontal) { + // } + // .padding(.horizontal, 8) + + Divider() + .background(SCZColor.CharcoalGray.opacity08) + .padding(.vertical, 8) + + //description + Text(viewModel.shortcut.description.replacingOccurrences(of: "\\n", with: "\n")) + .descriptionReadable() + .foregroundStyle(SCZColor.CharcoalGray.color) + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + + if !viewModel.shortcut.requiredApp.isEmpty { + Divider() + .background(SCZColor.CharcoalGray.opacity08) + .padding(.vertical, 8) - /// 탭뷰 (기본 정보, 버전 정보, 댓글) - LazyVStack(pinnedViews: [.sectionHeaders]) { - Section(header: tabBarView) { - ZStack { - TabView(selection: $viewModel.currentTab) { - Color.clear - .tag(0) - Color.clear - .tag(1) - Color.clear - .tag(2) + //필요한 앱 + VStack(alignment: .leading, spacing: 16) { + HStack(spacing: 6) { + SectionTitle(text: TextLiteral.readShortcutContentViewRequiredApps) + Image(systemName: "info.circle.fill") + .customSF(size: 20) + .foregroundStyle(SCZColor.CharcoalGray.opacity24) + } + VStack(alignment: .leading) { + ForEach(viewModel.shortcut.requiredApp, id: \.self) { requirement in + Text(requirement) + .medium16() + .foregroundStyle(SCZColor.CharcoalGray.opacity88) + .roundedBackground(background: SCZColor.CharcoalGray.opacity08) + } + } + } + .padding(.horizontal, 8) + } + + Divider() + .background(SCZColor.CharcoalGray.opacity08) + .padding(.vertical, 8) + + //버전 업데이트 정보 + VStack(alignment: .leading, spacing: 6) { + HStack { + SectionTitle(text: TextLiteral.readShortcutViewVersionTitle) + Spacer() + Menu { + Button(TextLiteral.readShortcutViewFilterNew) { + withAnimation { + viewModel.isVersionFolded = true } - .tabViewStyle(.page(indexDisplayMode: .never)) - .frame(minHeight: UIScreen.screenHeight / 2) - - switch viewModel.currentTab { - case 0: - ReadShortcutContentView(viewModel: self.viewModel) - case 1: - ReadShortcutVersionView(viewModel: self.viewModel) - case 2: - ReadShortcutCommentView(viewModel: self.viewModel) - .id(bottomID) - default: - EmptyView() + } + Button(TextLiteral.readShortcutViewFilterAll) { + withAnimation { + viewModel.isVersionFolded = false } } - .animation(.easeInOut, value: viewModel.currentTab) - .padding(.top, 4) - .padding(.horizontal, 16) + } label: { + HStack(spacing: 4) { + Text(viewModel.isVersionFolded ? TextLiteral.readShortcutViewFilterNew : TextLiteral.readShortcutViewFilterAll) + .body1() + Image(systemName: "chevron.down") + .customSF(size: 12) + } + .foregroundStyle(SCZColor.CharcoalGray.opacity64) + .roundedBackground(background: SCZColor.CharcoalGray.opacity04) + } + } + VStack(alignment: .leading, spacing: 8) { + UpdateListItem( + isLatest: true, + version: viewModel.shortcut.updateDescription.count - 0, + description: viewModel.shortcut.updateDescription[0], + date: viewModel.shortcut.date[0], + openDownloadURL: { + if !useWithoutSignIn { + if let url = URL(string: viewModel.shortcut.downloadLink[0]) { + viewModel.checkIfDownloaded() + viewModel.isDownloadingShortcut = true + openURL(url) + } + viewModel.updateNumberOfDownload(index: 0) + } else { + loginAlerter.isPresented = true + } + } + ) + + if !viewModel.isVersionFolded { + ForEach(1.. some View { - WriteShortcutView(viewModel: WriteShortcutViewModel(isEdit: true, shortcut: viewModel.shortcut)) - } -} - -extension ReadShortcutView { - - // MARK: - 댓글창 - - private var commentTextField: some View { - - VStack(spacing: 0) { - if viewModel.comment.depth == 1 && !viewModel.isEditingComment { - nestedCommentTargetView - } - HStack { - if viewModel.comment.depth == 1 && !viewModel.isEditingComment { - Image(systemName: "arrow.turn.down.right") - .smallIcon() - .foregroundStyle(Color.gray4) - } - TextField(useWithoutSignIn ? TextLiteral.readShortcutViewCommentDescriptionBeforeLogin : TextLiteral.readShortcutViewCommentDescription, text: $viewModel.commentText, axis: .vertical) - .keyboardType(.default) - .disabled(useWithoutSignIn) - .disableAutocorrection(true) - .textInputAutocapitalization(.never) - .shortcutsZipBody2() - .lineLimit(viewModel.comment.depth == 1 ? 2 : 4) - .focused($isFocused) - .onAppear { - UIApplication.shared.hideKeyboard() + + Menu { + Button(TextLiteral.share) { + viewModel.shareShortcut() + } + if viewModel.checkAuthor() { + Button(TextLiteral.edit) { + //TODO: 수정 화면 연결 + viewModel.isEditingShortcut.toggle() + } + Button(TextLiteral.delete) { + viewModel.checkDownGrading() + } + } else { + Button(TextLiteral.report) { + } + } + } label: { + Image(systemName: "ellipsis") + .customSF(size: 18) + .foregroundStyle(SCZColor.CharcoalGray.opacity48) } - - Button { - viewModel.postComment() - isFocused.toggle() - } label: { - Image(systemName: "paperplane.fill") - .mediumIcon() - .foregroundStyle(viewModel.commentText == "" ? Color.gray2 : Color.gray5) } - .disabled(viewModel.commentText == "" ? true : false) } - .padding(.vertical, 12) - .padding(.horizontal, 16) - .background( - Rectangle() - .fill(Color.gray1) - .cornerRadius(12 ,corners: (viewModel.comment.depth == 1) && (!viewModel.isEditingComment) ? [.bottomLeft, .bottomRight] : .allCorners) - ) - .padding(.horizontal, 16) - .padding(.bottom, 20) } - } - - private var nestedCommentTargetView: some View { - - HStack { - Text("@ \(viewModel.nestedCommentTarget)") - .shortcutsZipFootnote() - .foregroundStyle(Color.gray5) - - Spacer() - - Button { - viewModel.cancelNestedComment() + .alert(TextLiteral.readShortcutViewDeletionTitle, isPresented: $viewModel.isDeletingShortcut) { + Button(role: .cancel) { } label: { - Image(systemName: "xmark") - .smallIcon() - .foregroundStyle(Color.gray5) + Text(TextLiteral.cancel) } - } - .padding(.horizontal, 16) - .padding(.vertical, 11) - .background( - Rectangle() - .fill(Color.gray2) - .cornerRadius(12 ,corners: [.topLeft, .topRight]) - ) - .padding(.horizontal, 16) - } - - // MARK: - 내비게이션바 아이템 - - @ViewBuilder - private func readShortcutViewNavigationBarItems() -> some View { - if viewModel.checkAuthor() { - Menu { - Section { - editButton - updateButton - shareButton - deleteButton - } + + Button(role: .destructive) { + viewModel.deleteShortcut() + self.presentation.wrappedValue.dismiss() } label: { - Image(systemName: "ellipsis") - .mediumIcon() - .foregroundStyle(Color.gray4) + Text(TextLiteral.delete) } - } else { - shareButton - } - } - - private var editButton: some View { - Button { - viewModel.isEditingShortcut.toggle() - } label: { - Label(TextLiteral.edit, systemImage: "square.and.pencil") - } - } - - private var updateButton: some View { - Button { - viewModel.isUpdatingShortcut.toggle() - } label: { - Label(TextLiteral.update, systemImage: "clock.arrow.circlepath") - } - } - - private var shareButton: some View { - Button { - viewModel.shareShortcut() - } label: { - Label(TextLiteral.share, systemImage: "square.and.arrow.up") - .foregroundStyle(Color.gray4) - .fontWeight(.medium) - } - } - - private var deleteButton: some View { - Button(role: .destructive) { - viewModel.checkDowngrading() - } label: { - Label(TextLiteral.delete, systemImage: "trash.fill") + } message: { + Text(viewModel.isDowngradingUserLevel ? TextLiteral.readShortcutViewDeletionMessageDowngrade : TextLiteral.readShortcutViewDeletionMessage) } } +} + +struct UpdateListItem: View { + var isLatest = false + let version: Int + let description: String + let date: String - // MARK: - 탭바 + var openDownloadURL: () -> () - private var tabBarView: some View { - HStack(spacing: 20) { - ForEach(Array(zip(self.tabItems.indices, self.tabItems)), id: \.0) { index, name in - tabBarItem(title: name, tabID: index) - } - } - .padding(.horizontal, 16) - .frame(height: 36) - .background(Color.shortcutsZipWhite) - } - private func tabBarItem(title: String, tabID: Int) -> some View { - Button { - viewModel.moveTab(to: tabID) - } label: { - VStack { - if viewModel.currentTab == tabID { - Text(title) - .shortcutsZipHeadline() - .foregroundStyle(Color.gray5) - Color.gray5 - .frame(height: 2) - .matchedGeometryEffect(id: "underline", in: namespace, properties: .frame) - - } else { - Text(title) - .shortcutsZipBody1() - .foregroundStyle(Color.gray3) - Color.clear.frame(height: 2) + var body: some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + Button { + openDownloadURL() + } label: { + Text("Ver \(version).0") + .underline() + .medium16() + .foregroundStyle(SCZColor.CharcoalGray.opacity64) + } + if isLatest { + Image(systemName: "sparkles") + .customSF(size: 24) + .foregroundStyle(SCZColor.CharcoalGray.opacity88) } } - .animation(.spring(), value: viewModel.currentTab) + if !description.isEmpty { + Text(description) + .regular16() + .foregroundStyle(SCZColor.CharcoalGray.color) + } + Text(date.getVersionDateFormat() ?? "") + .regular16() + .foregroundStyle(SCZColor.CharcoalGray.opacity48) } - .buttonStyle(.plain) } } -extension ReadShortcutView { - - //MARK: - 단축어 타이틀 - - struct ReadShortcutViewHeader: View { - @Environment(\.loginAlertKey) var loginAlerter - @EnvironmentObject var shortcutsZipViewModel: ShortcutsZipViewModel - - @StateObject var viewModel: ReadShortcutViewModel - - @AppStorage("useWithoutSignIn") var useWithoutSignIn: Bool = false - - private let hapticManager = HapticManager.instance - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - HStack { - - /// 단축어 아이콘 - VStack { - Image(systemName: viewModel.shortcut.sfSymbol) - .mediumShortcutIcon() - .foregroundStyle(Color.textIcon) - } - .frame(width: 52, height: 52) - .background(Color.fetchGradient(color: viewModel.shortcut.color)) - .cornerRadius(8) - - Spacer() - - /// 좋아요 버튼 - Text("\(viewModel.isMyLike ? Image(systemName: "heart.fill") : Image(systemName: "heart")) \(viewModel.numberOfLike)") - .shortcutsZipBody2() - .padding(10) - .foregroundStyle(viewModel.isMyLike ? Color.textIcon : Color.gray4) - .background(viewModel.isMyLike ? Color.shortcutsZipPrimary : Color.gray1) - .cornerRadius(12) - .onTapGesture { - if !useWithoutSignIn { - viewModel.isMyLike.toggle() - viewModel.numberOfLike += viewModel.isMyLike ? 1 : -1 - hapticManager.impact(style: .rigid) - } else { - loginAlerter.isPresented = true - } - } - } +private struct SectionTitle: View { + let text: String + var body: some View { + Text(text) + .medium16() + .foregroundStyle(SCZColor.CharcoalGray.opacity48) + } +} + +struct ReadShortcutHeader: View { + @StateObject var viewModel: ReadShortcutViewModel + var body: some View { + HStack(spacing: 8) { + ShortcutIcon(sfSymbol: viewModel.shortcut.sfSymbol, color: viewModel.shortcut.color, size: 96) + .padding(.horizontal, 8) + + VStack(alignment: .leading, spacing: 3) { + Text(viewModel.shortcut.subtitle) + .body1() + .foregroundStyle(SCZColor.CharcoalGray.opacity48) + Text(viewModel.shortcut.title) + .title1() + .foregroundStyle(SCZColor.Medium) - /// 단축어 이름, 한 줄 설명 - VStack(alignment: .leading, spacing: 4) { - Text("\(viewModel.shortcut.title)") - .shortcutsZipTitle1() - .foregroundStyle(Color.gray5) - .fixedSize(horizontal: false, vertical: true) - - Text("\(viewModel.shortcut.subtitle)") - .shortcutsZipBody1() - .foregroundStyle(Color.gray3) - .fixedSize(horizontal: false, vertical: true) + Spacer() + HStack(spacing: 4) { + //TODO: 프로필 이미지 적용 필요 + viewModel.fetchUserGrade(id: viewModel.author.id) + .customSF(size: 32) + .foregroundStyle(SCZColor.CharcoalGray.opacity04) + Text(viewModel.author.nickname.isEmpty ? TextLiteral.withdrawnUser : viewModel.author.nickname) + .subTitle1() + .foregroundStyle(SCZColor.CharcoalGray.opacity88) } - - /// 단축어 작성자 닉네임 - UserNameCell(userInformation: viewModel.author, gradeImage: viewModel.userGrade) } - .frame(maxWidth: .infinity, minHeight: 160, alignment: .leading) - .padding(.bottom, 20) - .padding(.horizontal, 16) - .background(Color.shortcutsZipWhite) + .padding(.vertical, 5.5) } } +} + +struct ReadShortcutCommentView: View { + @EnvironmentObject var shortcutsZipViewModel: ShortcutsZipViewModel + @StateObject var viewModel: ReadShortcutViewModel + @AppStorage("useWithoutSignIn") var useWithoutSignIn: Bool = false - //MARK: - 기본 정보 탭 + @FocusState var isFocused: Bool - struct ReadShortcutContentView: View { - - @StateObject var viewModel: ReadShortcutViewModel - - var body: some View { - VStack(alignment: .leading, spacing: 32) { - - VStack(alignment: .leading) { - Text(TextLiteral.readShortcutContentViewDescription) - .shortcutsZipBody2() - .fontWeight(.bold) - .foregroundStyle(Color.gray6) - Text(.init(viewModel.shortcut.description)) - .shortcutsZipBody2() - .foregroundStyle(Color.gray5) - .tint(.shortcutsZipPrimary) - .lineLimit(nil) - } - - splitList(title: TextLiteral.readShortcutContentViewCategory, content: viewModel.shortcut.category) - - if !viewModel.shortcut.requiredApp.isEmpty { - splitList(title: TextLiteral.readShortcutContentViewRequiredApps, content: viewModel.shortcut.requiredApp) + var body: some View { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + HStack(alignment: .center, spacing: 8) { + Image(systemName: "text.bubble.fill") + .foregroundStyle(SCZColor.CharcoalGray.opacity24) + Text(TextLiteral.readShortcutViewCommentTitle) + .foregroundStyle(SCZColor.CharcoalGray.opacity48) + .semiBold17() } - Spacer() + Divider() + .background(SCZColor.CharcoalGray.opacity08) + .padding(.vertical, 8) } - .padding(.top, 16) - .frame(maxWidth: .infinity, alignment: .leading) - } - - private func splitList(title: String, content: [String]) -> some View { - VStack(alignment: .leading) { - - Text(title) - .shortcutsZipBody2() - .fontWeight(.bold) - .foregroundStyle(Color.gray6) - - WrappingHStack(content, id: \.self, alignment: .leading, spacing: .constant(8), lineSpacing: 8) { item in - if Category.allCases.contains(where: { $0.rawValue == item }) { - Text(Category(rawValue: item)?.translateName() ?? "") - .shortcutsZipBody2() - .padding(.trailing, 8) - .foregroundStyle(Color.gray5) - } else { - Text(item) - .shortcutsZipBody2() - .padding(.trailing, 8) - .foregroundStyle(Color.gray5) - } - if item != content.last { - Divider() - } - } - } + commentView + + commentTextField } - } - - //MARK: - 버전 정보 탭 - - struct ReadShortcutVersionView: View { - - @Environment(\.openURL) var openURL - @Environment(\.loginAlertKey) var loginAlerter - @EnvironmentObject var shortcutsZipViewModel: ShortcutsZipViewModel - - @StateObject var viewModel: ReadShortcutViewModel - - @AppStorage("useWithoutSignIn") var useWithoutSignIn: Bool = false - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - - if viewModel.shortcut.updateDescription.count == 1 { - HStack { - Text("Ver 1.0") - .shortcutsZipBody2() - .foregroundStyle(Color.gray5) - - Spacer() - - Text(viewModel.shortcut.date.first?.getVersionUpdateDateFormat() ?? "") - .shortcutsZipBody2() - .foregroundStyle(Color.gray3) - } - Text(TextLiteral.readShortcutVersionViewNoUpdates) - .shortcutsZipBody2() - .foregroundStyle(Color.gray4) - } else { - Text(TextLiteral.readShortcutVersionViewUpdateContent) - .shortcutsZipBody2() - .foregroundStyle(Color.gray4) - - versionView - } + .alert(TextLiteral.readShortcutCommentViewDeletionTitle, isPresented: $viewModel.isDeletingComment) { + Button(role: .cancel) { - Spacer() - .frame(maxHeight: .infinity) + } label: { + Text(TextLiteral.cancel) } - .padding(.top, 16) - } - - private var versionView: some View { - ForEach(Array(zip(viewModel.shortcut.updateDescription, viewModel.shortcut.updateDescription.indices)), id: \.0) { data, index in - VStack(alignment: .leading, spacing: 12) { - HStack { - Text("Ver \(viewModel.shortcut.updateDescription.count - index).0") - .shortcutsZipBody2() - .foregroundStyle(Color.gray5) - - Spacer() - - Text(viewModel.shortcut.date[index].getVersionUpdateDateFormat()) - .shortcutsZipBody2() - .foregroundStyle(Color.gray3) - } - - if data.trimmingCharacters(in: .whitespacesAndNewlines) != "" { - Text(data) - .shortcutsZipBody2() - .foregroundStyle(Color.gray5) - } - - if index != 0 { - Button { - if !useWithoutSignIn { - if let url = URL(string: viewModel.shortcut.downloadLink[index]) { - viewModel.checkIfDownloaded() - viewModel.updateNumberOfDownload(index: index) - openURL(url) - } - } else { - loginAlerter.isPresented = true - } - } label: { - Text(TextLiteral.readShortcutVersionViewDownloadPreviousVersion) - .shortcutsZipBody2() - .foregroundStyle(Color.shortcutsZipPrimary) - } - } - - Divider() - .foregroundStyle(Color.gray1) - } + Button(role: .destructive) { + viewModel.deleteComment() + } label: { + Text(TextLiteral.delete) } + } message: { + Text(TextLiteral.readShortcutCommentViewDeletionMessage) } } - //MARK: - 댓글 탭 - - struct ReadShortcutCommentView: View { + private var commentView: some View { - @EnvironmentObject var shortcutsZipViewModel: ShortcutsZipViewModel - - @StateObject var viewModel: ReadShortcutViewModel - - @AppStorage("useWithoutSignIn") var useWithoutSignIn: Bool = false - - @FocusState var isFocused: Bool - - var body: some View { - VStack(alignment: .leading) { - - if viewModel.comments.comments.isEmpty { - Text(TextLiteral.readShortcutCommentViewNoComments) - .shortcutsZipBody2() - .foregroundStyle(Color.gray4) - .padding(.top, 16) - } else { - commentView - } - - Spacer() - .frame(maxHeight: .infinity) + ForEach(Array(viewModel.comments.comments.enumerated()), id: \.element) { index, comment in + if comment.depth == 0 && index != 0 { + Divider() + .background(SCZColor.CharcoalGray.opacity04) } - .padding(.top, 16) - .alert(TextLiteral.readShortcutCommentViewDeletionTitle, isPresented: $viewModel.isDeletingComment) { - Button(role: .cancel) { - - } label: { - Text(TextLiteral.cancel) + HStack(alignment: .top, spacing: 8) { + if comment.depth == 1 { + Image(systemName: "arrow.turn.down.right") + .customSF(size: 19) + .foregroundStyle(SCZColor.CharcoalGray.opacity24) } + viewModel.fetchUserGrade(id: comment.user_id) + .customSF(size: 27) - Button(role: .destructive) { - viewModel.deleteComment() - } label: { - Text(TextLiteral.delete) - } - } message: { - Text(TextLiteral.readShortcutCommentViewDeletionMessage) - } - } - - private var commentView: some View { - - ForEach(viewModel.comments.comments, id: \.self) { comment in - - HStack(alignment: .top, spacing: 8) { - if comment.depth == 1 { - Image(systemName: "arrow.turn.down.right") - .smallIcon() - .foregroundStyle(Color.gray4) - } - - VStack(alignment: .leading, spacing: 8) { - - /// 유저 정보 - HStack(spacing: 8) { - - viewModel.fetchUserGrade(id: comment.user_id) - .font(.system(size: 24, weight: .medium)) - .frame(width: 24, height: 24) - .foregroundStyle(Color.gray3) - - Text(comment.user_nickname) - .shortcutsZipBody2() - .foregroundStyle(Color.gray4) - - Spacer() - - Text(comment.date.getVersionUpdateDateFormat()) - .shortcutsZipFootnote() - .foregroundStyle(Color.gray4) - } - .padding(.bottom, 4) - - /// 댓글 내용 - Text(.init(comment.contents)) - .textSelection(.enabled) - .shortcutsZipBody2() - .foregroundStyle(Color.gray5) - .tint(.shortcutsZipPrimary) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.leading, 4) - - /// 답글, 수정, 삭제 버튼 - HStack(spacing: 0) { - if !useWithoutSignIn { - Button { - viewModel.setReply(to: comment) - isFocused = true - } label: { - Text(TextLiteral.readShortcutCommentViewReply) - .shortcutsZipFootnote() - .foregroundStyle(Color.gray4) - .frame(width: 32, height: 24) - } - } - - if let user = shortcutsZipViewModel.userInfo { - if user.id == comment.user_id { + VStack(alignment: .leading, spacing: 12) { + /// 유저 정보 + HStack(spacing: 8) { + Text(comment.user_nickname) + .medium16() + .foregroundStyle(SCZColor.CharcoalGray.color) + Text(comment.date.getCommentDateFormat()) + .medium16() + .foregroundStyle(SCZColor.CharcoalGray.opacity48) + Spacer() + if let user = shortcutsZipViewModel.userInfo { + if user.id == comment.user_id { + Menu { + //수정 Button { withAnimation(.easeInOut) { viewModel.isEditingComment.toggle() viewModel.comment = comment + viewModel.commentText = comment.contents + isFocused = true } } label: { Text(TextLiteral.readShortcutCommentViewEdit) @@ -711,7 +486,7 @@ extension ReadShortcutView { .foregroundStyle(Color.gray4) .frame(width: 32, height: 24) } - + //삭제 Button { viewModel.isDeletingComment.toggle() viewModel.deletedComment = comment @@ -721,16 +496,148 @@ extension ReadShortcutView { .foregroundStyle(Color.gray4) .frame(width: 32, height: 24) } + } label: { + Image(systemName: "ellipsis") + .customSF(size: 18) + .foregroundStyle(SCZColor.CharcoalGray.opacity48) } } } - - Divider() - .background(Color.gray1) + } + .medium16() + + /// 댓글 내용 + Text(.init(comment.contents)) + .textSelection(.enabled) + .regular16() + .foregroundStyle(SCZColor.CharcoalGray.opacity88) + .tint(.shortcutsZipPrimary) + .frame(maxWidth: .infinity, alignment: .leading) + + if comment.depth == 0 { + HStack(alignment: .center, spacing: 18) { + if !useWithoutSignIn { + HStack(spacing: 4) { + //하트 + Button { + + } label: { + Image(systemName: "heart.fill") + .customSF(size: 17) + .foregroundStyle(SCZColor.CharcoalGray.opacity16) + } + }.frame(width: 64, alignment: .leading) + HStack(spacing: 4) { + //대댓글 + Button { + viewModel.setReply(to: comment) + isFocused = true + //스크롤? + } label: { + Image(systemName: "bubble.left.and.text.bubble.right.fill") + .customSF(size: 22) + .foregroundStyle(SCZColor.CharcoalGray.opacity24) + } + let num = viewModel.getReplyNumber(bundleId: comment.bundle_id) + if num != 0 { + Text(num.formatNumber()) + .numRegular16() + .foregroundStyle(SCZColor.CharcoalGray.opacity64) + } + } + } + } + } + } + } + .padding(.top, comment.depth == 1 ? 8 : 0) + } + } + + private var commentTextField: some View { + + VStack(spacing: 0) { + if viewModel.comment.depth == 1 && !viewModel.isEditingComment { + nestedCommentTargetView + } + HStack(spacing: 8) { + TextField(useWithoutSignIn ? TextLiteral.readShortcutViewCommentDescriptionBeforeLogin : TextLiteral.readShortcutViewCommentDescription, text: $viewModel.commentText, axis: .vertical) + .focused($isFocused) + .padding(.leading, 16) + .padding(.vertical, viewModel.commentText.isEmpty ? 10 : 12) + .textFieldStyle(CommentTextFieldStyle()) + .disabled(useWithoutSignIn) + + VStack(alignment: .center, spacing: 6) { + Button { + viewModel.postComment() + isFocused.toggle() + } label: { + Image(systemName: viewModel.commentText.count > 512 ? "xmark.circle.fill" : "arrow.up.circle.fill") + .customSF(size: 32) + .foregroundStyle( + viewModel.commentText.isEmpty ? SCZColor.CharcoalGray.opacity24 : + viewModel.commentText.count > 512 ? SCZColor.systemWhite : SCZColor.systemWhite, + viewModel.commentText.isEmpty ? SCZColor.CharcoalGray.opacity08 : + viewModel.commentText.count > 512 ? SCZColor.SCZRed.red : SCZColor.SCZBlue.strong + ) + } + .disabled(viewModel.commentText == "" || viewModel.commentText.count > 512 ? true : false) + + if viewModel.commentText.count > 512 { + Text("-\(viewModel.commentText.count - 512)") + .body2() + .foregroundStyle(SCZColor.SCZRed.dangerouslyRed) } } - .padding(.bottom, 16) + .padding(.trailing, 8) } + .background( + Rectangle() + .fill(SCZColor.CharcoalGray.opacity08) + .cornerRadius(16 ,corners: (viewModel.comment.depth == 1) && (!viewModel.isEditingComment) ? [.bottomLeft, .bottomRight] : .allCorners) + ) } + .onAppear { + UIApplication.shared.hideKeyboard() + } + .padding(.bottom, isFocused ? 7.5 : 70) + .id("CommentTextField") + } + private var nestedCommentTargetView: some View { + + HStack { + Text("@\(viewModel.nestedCommentTarget)") + .lineLimit(1) + .descriptionReadable() + .foregroundStyle(SCZColor.CharcoalGray.opacity48) + .frame(maxWidth: .infinity, alignment: .leading) + + Button { + viewModel.cancelReply() + } label: { + Image(systemName: "xmark") + .customSF(size: 15) + .foregroundStyle(SCZColor.CharcoalGray.opacity48) + } + } + .padding(.horizontal, 16) + .padding(.vertical, 16) + .background( + Rectangle() + .fill(SCZColor.CharcoalGray.opacity16) + .cornerRadius(16 ,corners: [.topLeft, .topRight]) + ) + } +} + +private struct CommentTextFieldStyle: TextFieldStyle { + func _body(configuration: TextField) -> some View { + configuration + .keyboardType(.default) + .disableAutocorrection(true) + .textInputAutocapitalization(.never) + .descriptionReadable() + .frame(maxWidth: .infinity, maxHeight: 271, alignment: .leading) } }