diff --git a/Projects/Domain/Sources/Entity/Enum/RepeatType.swift b/Projects/Domain/Sources/Entity/Enum/RepeatType.swift new file mode 100644 index 00000000..ee69ff9e --- /dev/null +++ b/Projects/Domain/Sources/Entity/Enum/RepeatType.swift @@ -0,0 +1,11 @@ +// +// RepeatType.swift +// Domain +// +// Created by 이동현 on 8/15/25. +// + +public enum RepeatType { + case daily + case weekly +} diff --git a/Projects/Domain/Sources/Entity/Enum/WeekType.swift b/Projects/Domain/Sources/Entity/Enum/WeekType.swift index 822bfa68..531f1c91 100644 --- a/Projects/Domain/Sources/Entity/Enum/WeekType.swift +++ b/Projects/Domain/Sources/Entity/Enum/WeekType.swift @@ -5,7 +5,7 @@ // Created by 이동현 on 8/3/25. // -public enum WeekType: String, CaseIterable { +public enum Week: String, CaseIterable { case monday = "MONDAY" case tuesday = "TUESDAY" case wednesday = "WEDNESDAY" diff --git a/Projects/Domain/Sources/Entity/RoutineEntity.swift b/Projects/Domain/Sources/Entity/RoutineEntity.swift index ceb7d8e5..2b8b4915 100644 --- a/Projects/Domain/Sources/Entity/RoutineEntity.swift +++ b/Projects/Domain/Sources/Entity/RoutineEntity.swift @@ -9,7 +9,7 @@ public struct RoutineEntity { public let routineId: String? public let historySeq: Int public let routineName: String - public let repeatDay: [WeekType] + public let repeatDay: [Week] public let executionTime: String public let subRoutineSearchResultDto: [SubRoutineEntity] public let modifiedYn: Bool @@ -29,7 +29,7 @@ public struct RoutineEntity { completeYn: Bool, routineType: String ) { - let weekType: [WeekType] = repeatDay?.compactMap(WeekType.init(rawValue:)) ?? [] + let weekType: [Week] = repeatDay?.compactMap(Week.init(rawValue:)) ?? [] self.routineId = routineId self.historySeq = historySeq diff --git a/Projects/Domain/Sources/Entity/RoutineSummaryEntity.swift b/Projects/Domain/Sources/Entity/RoutineSummaryEntity.swift index 616f408b..0fb2cce5 100644 --- a/Projects/Domain/Sources/Entity/RoutineSummaryEntity.swift +++ b/Projects/Domain/Sources/Entity/RoutineSummaryEntity.swift @@ -8,7 +8,7 @@ public struct RoutineSummaryEntity { public let routineId: String? public let routineName: String - public let repeatDay: [WeekType] + public let repeatDay: [Week] public let executionTime: String public init( @@ -17,7 +17,7 @@ public struct RoutineSummaryEntity { repeatDay: [String]?, executionTime: String ) { - let weekType: [WeekType] = repeatDay?.compactMap(WeekType.init(rawValue:)) ?? [] + let weekType: [Week] = repeatDay?.compactMap(Week.init(rawValue:)) ?? [] self.routineId = routineId self.routineName = routineName diff --git a/Projects/Presentation/Project.swift b/Projects/Presentation/Project.swift index f9ecfd0a..c3b7f012 100644 --- a/Projects/Presentation/Project.swift +++ b/Projects/Presentation/Project.swift @@ -2,6 +2,9 @@ import ProjectDescription let project = Project( name: "Presentation", + settings: .settings( + base: ["OTHER_LDFLAGS": "$(inherited) -ObjC"] + ), targets: [ .target( name: "Presentation", @@ -15,6 +18,7 @@ let project = Project( .external(name: "SnapKit"), .external(name: "Kingfisher"), .external(name: "Lottie"), + .external(name: "FSCalendar"), .project(target: "Domain", path: "../Domain"), .project(target: "Shared", path: "../Shared") ] diff --git a/Projects/Presentation/Resources/Colors.xcassets/Skyblue.colorset/Contents.json b/Projects/Presentation/Resources/Colors.xcassets/Skyblue.colorset/Contents.json index dc8f7e62..dc347a17 100644 --- a/Projects/Presentation/Resources/Colors.xcassets/Skyblue.colorset/Contents.json +++ b/Projects/Presentation/Resources/Colors.xcassets/Skyblue.colorset/Contents.json @@ -16,5 +16,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "localizable" : true } } diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/Contents.json b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_date.imageset/Contents.json b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_date.imageset/Contents.json new file mode 100644 index 00000000..918fbc7e --- /dev/null +++ b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_date.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "routine_creation_date@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "routine_creation_date@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "routine_creation_date@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_date.imageset/routine_creation_date@1x.png b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_date.imageset/routine_creation_date@1x.png new file mode 100644 index 00000000..97b7e05f Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_date.imageset/routine_creation_date@1x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_date.imageset/routine_creation_date@2x.png b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_date.imageset/routine_creation_date@2x.png new file mode 100644 index 00000000..de295b06 Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_date.imageset/routine_creation_date@2x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_date.imageset/routine_creation_date@3x.png b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_date.imageset/routine_creation_date@3x.png new file mode 100644 index 00000000..38af2459 Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_date.imageset/routine_creation_date@3x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_list_icon.imageset/Contents.json b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_list_icon.imageset/Contents.json new file mode 100644 index 00000000..f493995c --- /dev/null +++ b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_list_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "routine_creation_list_icon@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "routine_creation_list_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "routine_creation_list_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_list_icon.imageset/routine_creation_list_icon@1x.png b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_list_icon.imageset/routine_creation_list_icon@1x.png new file mode 100644 index 00000000..1a524e5a Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_list_icon.imageset/routine_creation_list_icon@1x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_list_icon.imageset/routine_creation_list_icon@2x.png b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_list_icon.imageset/routine_creation_list_icon@2x.png new file mode 100644 index 00000000..c4460d2d Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_list_icon.imageset/routine_creation_list_icon@2x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_list_icon.imageset/routine_creation_list_icon@3x.png b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_list_icon.imageset/routine_creation_list_icon@3x.png new file mode 100644 index 00000000..30358a29 Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_list_icon.imageset/routine_creation_list_icon@3x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_one_icon.imageset/Contents.json b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_one_icon.imageset/Contents.json new file mode 100644 index 00000000..2c8e9431 --- /dev/null +++ b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_one_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "routine_creation_one_icon@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "routine_creation_one_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "routine_creation_one_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_one_icon.imageset/routine_creation_one_icon@1x.png b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_one_icon.imageset/routine_creation_one_icon@1x.png new file mode 100644 index 00000000..b30a0b41 Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_one_icon.imageset/routine_creation_one_icon@1x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_one_icon.imageset/routine_creation_one_icon@2x.png b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_one_icon.imageset/routine_creation_one_icon@2x.png new file mode 100644 index 00000000..b3b1b656 Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_one_icon.imageset/routine_creation_one_icon@2x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_one_icon.imageset/routine_creation_one_icon@3x.png b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_one_icon.imageset/routine_creation_one_icon@3x.png new file mode 100644 index 00000000..9de63ecd Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_one_icon.imageset/routine_creation_one_icon@3x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_repeat_icon.imageset/Contents.json b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_repeat_icon.imageset/Contents.json new file mode 100644 index 00000000..a5a863c7 --- /dev/null +++ b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_repeat_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "routine_creation_repeat_icon@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "routine_creation_repeat_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "routine_creation_repeat_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_repeat_icon.imageset/routine_creation_repeat_icon@1x.png b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_repeat_icon.imageset/routine_creation_repeat_icon@1x.png new file mode 100644 index 00000000..8737c0b6 Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_repeat_icon.imageset/routine_creation_repeat_icon@1x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_repeat_icon.imageset/routine_creation_repeat_icon@2x.png b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_repeat_icon.imageset/routine_creation_repeat_icon@2x.png new file mode 100644 index 00000000..2e14a222 Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_repeat_icon.imageset/routine_creation_repeat_icon@2x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_repeat_icon.imageset/routine_creation_repeat_icon@3x.png b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_repeat_icon.imageset/routine_creation_repeat_icon@3x.png new file mode 100644 index 00000000..e5d2796b Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_repeat_icon.imageset/routine_creation_repeat_icon@3x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_three_icon.imageset/Contents.json b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_three_icon.imageset/Contents.json new file mode 100644 index 00000000..48a3a54a --- /dev/null +++ b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_three_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "routine_creation_three_icon@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "routine_creation_three_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "routine_creation_three_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_three_icon.imageset/routine_creation_three_icon@1x.png b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_three_icon.imageset/routine_creation_three_icon@1x.png new file mode 100644 index 00000000..3bbfa493 Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_three_icon.imageset/routine_creation_three_icon@1x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_three_icon.imageset/routine_creation_three_icon@2x.png b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_three_icon.imageset/routine_creation_three_icon@2x.png new file mode 100644 index 00000000..4bf64fd0 Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_three_icon.imageset/routine_creation_three_icon@2x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_three_icon.imageset/routine_creation_three_icon@3x.png b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_three_icon.imageset/routine_creation_three_icon@3x.png new file mode 100644 index 00000000..9f54c6b7 Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_three_icon.imageset/routine_creation_three_icon@3x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_time_icon.imageset/Contents.json b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_time_icon.imageset/Contents.json new file mode 100644 index 00000000..3a832765 --- /dev/null +++ b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_time_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "routine_creation_time_icon@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "routine_creation_time_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "routine_creation_time_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_time_icon.imageset/routine_creation_time_icon@1x.png b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_time_icon.imageset/routine_creation_time_icon@1x.png new file mode 100644 index 00000000..ba951cf1 Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_time_icon.imageset/routine_creation_time_icon@1x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_time_icon.imageset/routine_creation_time_icon@2x.png b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_time_icon.imageset/routine_creation_time_icon@2x.png new file mode 100644 index 00000000..6cbf723e Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_time_icon.imageset/routine_creation_time_icon@2x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_time_icon.imageset/routine_creation_time_icon@3x.png b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_time_icon.imageset/routine_creation_time_icon@3x.png new file mode 100644 index 00000000..ecce813d Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_time_icon.imageset/routine_creation_time_icon@3x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_two_icon.imageset/Contents.json b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_two_icon.imageset/Contents.json new file mode 100644 index 00000000..b3234ab2 --- /dev/null +++ b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_two_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "routine_creation_two_icon@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "routine_creation_two_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "routine_creation_two_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_two_icon.imageset/routine_creation_two_icon@1x.png b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_two_icon.imageset/routine_creation_two_icon@1x.png new file mode 100644 index 00000000..fbe47b04 Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_two_icon.imageset/routine_creation_two_icon@1x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_two_icon.imageset/routine_creation_two_icon@2x.png b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_two_icon.imageset/routine_creation_two_icon@2x.png new file mode 100644 index 00000000..4fc176bb Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_two_icon.imageset/routine_creation_two_icon@2x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_two_icon.imageset/routine_creation_two_icon@3x.png b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_two_icon.imageset/routine_creation_two_icon@3x.png new file mode 100644 index 00000000..697f5059 Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_two_icon.imageset/routine_creation_two_icon@3x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/asterisk_icon.imageset/asterisk_icon@1x.png b/Projects/Presentation/Resources/Images.xcassets/asterisk_icon.imageset/asterisk_icon@1x.png index c5e5addc..bba22547 100644 Binary files a/Projects/Presentation/Resources/Images.xcassets/asterisk_icon.imageset/asterisk_icon@1x.png and b/Projects/Presentation/Resources/Images.xcassets/asterisk_icon.imageset/asterisk_icon@1x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/asterisk_icon.imageset/asterisk_icon@2x.png b/Projects/Presentation/Resources/Images.xcassets/asterisk_icon.imageset/asterisk_icon@2x.png index 1e2f0c23..1e621231 100644 Binary files a/Projects/Presentation/Resources/Images.xcassets/asterisk_icon.imageset/asterisk_icon@2x.png and b/Projects/Presentation/Resources/Images.xcassets/asterisk_icon.imageset/asterisk_icon@2x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/asterisk_icon.imageset/asterisk_icon@3x.png b/Projects/Presentation/Resources/Images.xcassets/asterisk_icon.imageset/asterisk_icon@3x.png index e56e82db..88944f2f 100644 Binary files a/Projects/Presentation/Resources/Images.xcassets/asterisk_icon.imageset/asterisk_icon@3x.png and b/Projects/Presentation/Resources/Images.xcassets/asterisk_icon.imageset/asterisk_icon@3x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/clear_icon.imageset/Contents.json b/Projects/Presentation/Resources/Images.xcassets/clear_icon.imageset/Contents.json new file mode 100644 index 00000000..ebbf67f2 --- /dev/null +++ b/Projects/Presentation/Resources/Images.xcassets/clear_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "clear_icon@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "clear_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "clear_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Presentation/Resources/Images.xcassets/clear_icon.imageset/clear_icon@1x.png b/Projects/Presentation/Resources/Images.xcassets/clear_icon.imageset/clear_icon@1x.png new file mode 100644 index 00000000..a44a4f5d Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/clear_icon.imageset/clear_icon@1x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/clear_icon.imageset/clear_icon@2x.png b/Projects/Presentation/Resources/Images.xcassets/clear_icon.imageset/clear_icon@2x.png new file mode 100644 index 00000000..a07deaf4 Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/clear_icon.imageset/clear_icon@2x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/clear_icon.imageset/clear_icon@3x.png b/Projects/Presentation/Resources/Images.xcassets/clear_icon.imageset/clear_icon@3x.png new file mode 100644 index 00000000..b541a27e Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/clear_icon.imageset/clear_icon@3x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/divide_line_icon.imageset/Contents.json b/Projects/Presentation/Resources/Images.xcassets/divide_line_icon.imageset/Contents.json new file mode 100644 index 00000000..1c3455c8 --- /dev/null +++ b/Projects/Presentation/Resources/Images.xcassets/divide_line_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "divide_line_icon@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "divide_line_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "divide_line_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Presentation/Resources/Images.xcassets/divide_line_icon.imageset/divide_line_icon@1x.png b/Projects/Presentation/Resources/Images.xcassets/divide_line_icon.imageset/divide_line_icon@1x.png new file mode 100644 index 00000000..13047155 Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/divide_line_icon.imageset/divide_line_icon@1x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/divide_line_icon.imageset/divide_line_icon@2x.png b/Projects/Presentation/Resources/Images.xcassets/divide_line_icon.imageset/divide_line_icon@2x.png new file mode 100644 index 00000000..9bc45f3c Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/divide_line_icon.imageset/divide_line_icon@2x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/divide_line_icon.imageset/divide_line_icon@3x.png b/Projects/Presentation/Resources/Images.xcassets/divide_line_icon.imageset/divide_line_icon@3x.png new file mode 100644 index 00000000..50d625da Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/divide_line_icon.imageset/divide_line_icon@3x.png differ diff --git a/Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift b/Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift index c91bf739..64a713b1 100644 --- a/Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift +++ b/Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift @@ -48,6 +48,8 @@ enum BitnagilIcon { static let ellipsisIcon = UIImage(named: "ellipsis_icon", in: bundle, with: nil) static let informationIcon = UIImage(named: "information_icon", in: bundle, with: nil) static let addRoutineIcon = UIImage(named: "add_routine_icon", in: bundle, with: nil) + static let divideLineIcon = UIImage(named: "divide_line_icon", in: bundle, with: nil) + static let clearIcon = UIImage(named: "clear_icon", in: bundle, with: nil) // MARK: - Tab Bar Icons static let homeIcon = UIImage(named: "home_fill_icon", in: bundle, with: nil) @@ -72,6 +74,13 @@ enum BitnagilIcon { static let repeatIcon = UIImage(named: "repeat_icon", in: bundle, with: nil) static let editIcon = UIImage(named: "edit_icon", in: bundle, with: nil) static let trashIcon = UIImage(named: "trash_icon", in: bundle, with: nil) + static let routineTimeIcon = UIImage(named: "routine_creation_time_icon", in: bundle, with: nil) + static let routineListIcon = UIImage(named: "routine_creation_list_icon", in: bundle, with: nil) + static let routineDateIcon = UIImage(named: "routine_creation_date", in: bundle, with: nil) + static let routineRepeatIcon = UIImage(named: "routine_creation_repeat_icon", in: bundle, with: nil) + static let oneIcon = UIImage(named: "routine_creation_one_icon", in: bundle, with: nil) + static let twoIcon = UIImage(named: "routine_creation_two_icon", in: bundle, with: nil) + static let threeIcon = UIImage(named: "routine_creation_three_icon", in: bundle, with: nil) } enum Direction { diff --git a/Projects/Presentation/Sources/Home/View/HomeView.swift b/Projects/Presentation/Sources/Home/View/HomeView.swift index 824a2218..710a8944 100644 --- a/Projects/Presentation/Sources/Home/View/HomeView.swift +++ b/Projects/Presentation/Sources/Home/View/HomeView.swift @@ -166,7 +166,7 @@ final class HomeView: BaseViewController { guard let routineCreationViewModel = DIContainer.shared.resolve(type: RoutineCreationViewModel.self) else { fatalError("routineCreationViewModel 의존성이 등록되지 않았습니다.") } - let routineCreationView = RoutineCreationView(viewModel: routineCreationViewModel) + let routineCreationView = RoutineCreationViewController(viewModel: routineCreationViewModel) routineCreationView.hidesBottomBarWhenPushed = true self.navigationController?.pushViewController(routineCreationView, animated: true) } @@ -597,8 +597,9 @@ extension HomeView: FloatingMenuViewDelegate { guard let routineCreationViewModel = DIContainer.shared.resolve(type: RoutineCreationViewModel.self) else { fatalError("routineCreationViewModel 의존성이 등록되지 않았습니다.") } - let routineCreationView = RoutineCreationView(viewModel: routineCreationViewModel) - routineCreationView.hidesBottomBarWhenPushed = true - self.navigationController?.pushViewController(routineCreationView, animated: true) + let routineCreationViewController = RoutineCreationViewController(viewModel: routineCreationViewModel) + routineCreationViewController.hidesBottomBarWhenPushed = true + + self.navigationController?.pushViewController(routineCreationViewController, animated: true) } } diff --git a/Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift b/Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift index c35f123c..76fb1886 100644 --- a/Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift +++ b/Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift @@ -303,7 +303,7 @@ extension RecommendedRoutineView: RecommendedRoutineCardViewDelegate { fatalError("routineCreationViewModel 의존성이 등록되지 않았습니다.") } - let routineCreationView = RoutineCreationView(viewModel: routineCreationViewModel, recommendRoutineId: routine.id) + let routineCreationView = RoutineCreationViewController(viewModel: routineCreationViewModel, recommendRoutineId: routine.id) routineCreationView.hidesBottomBarWhenPushed = true self.navigationController?.pushViewController(routineCreationView, animated: true) } @@ -326,7 +326,7 @@ extension RecommendedRoutineView: FloatingMenuViewDelegate { guard let routineCreationViewModel = DIContainer.shared.resolve(type: RoutineCreationViewModel.self) else { fatalError("routineCreationViewModel 의존성이 등록되지 않았습니다.") } - let routineCreationView = RoutineCreationView(viewModel: routineCreationViewModel) + let routineCreationView = RoutineCreationViewController(viewModel: routineCreationViewModel) routineCreationView.hidesBottomBarWhenPushed = true self.navigationController?.pushViewController(routineCreationView, animated: true) } diff --git a/Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineView.swift b/Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineView.swift index 06003127..1e46be69 100644 --- a/Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineView.swift +++ b/Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineView.swift @@ -366,7 +366,7 @@ final class ResultRecommendedRoutineView: BaseViewController: UIView, UIGestureRecognizerDelegate { + private let titleImageView = UIImageView() + private let labelStackView = UIStackView() + private let titleLabel = UILabel() + private let placeHolderLabel = UILabel() + private let infoImageView = UIImageView() + private let asteriskImageView = UIImageView() + private let chevronImageView = UIImageView() + private let divideLine = UIView() + private let contentView = ContentView() + private var labelStackViewBottomConstraint: Constraint? + public var onAction: ((ContentView.Action) -> Void)? { + didSet { contentView.action = onAction } + } + + init( + title: String, + placeHolder: String, + titleImage: UIImage?, + withInfoImage: Bool, + withAsteriskImage: Bool + ) { + super.init(frame: .zero) + configureAttribute() + configureLayout() + + titleLabel.text = title + placeHolderLabel.text = placeHolder + titleImageView.image = titleImage + infoImageView.isHidden = !withInfoImage + asteriskImageView.isHidden = !withAsteriskImage + divideLine.isHidden = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure( + image: UIImage, + title: String, + contents: [String] + ) { + titleImageView.image = image + titleLabel.text = title + } + + private func configureAttribute() { + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))) + tapGesture.delegate = self + addGestureRecognizer(tapGesture) + + backgroundColor = BitnagilColor.gray99 + layer.cornerRadius = 12 + layer.masksToBounds = true + + labelStackView.spacing = Layout.contentLabelStackViewSpacing + labelStackView.axis = .vertical + labelStackView.distribution = .fill + labelStackView.alignment = .leading + + titleLabel.font = BitnagilFont.init(style: .body1, weight: .semiBold).font + titleLabel.textColor = .black + + placeHolderLabel.font = BitnagilFont.init(style: .body2, weight: .medium).font + placeHolderLabel.textColor = BitnagilColor.gray70 + + infoImageView.image = BitnagilIcon.informationIcon + asteriskImageView.image = BitnagilIcon.asteriskIcon + chevronImageView.image = BitnagilIcon.chevronIcon(direction: .down)?.withRenderingMode(.alwaysTemplate) + chevronImageView.tintColor = BitnagilColor.gray30 + + divideLine.backgroundColor = BitnagilColor.gray96 + + contentView.isHidden = true + contentView.action = { [weak self] in + self?.onAction?($0) + } + } + + private func configureLayout() { + addSubview(titleImageView) + addSubview(labelStackView) + addSubview(asteriskImageView) + addSubview(infoImageView) + addSubview(contentView) + addSubview(chevronImageView) + addSubview(divideLine) + + labelStackView.addArrangedSubview(titleLabel) + labelStackView.addArrangedSubview(placeHolderLabel) + + titleImageView.snp.makeConstraints { make in + make.centerY.equalTo(labelStackView) + make.leading.equalToSuperview().offset(Layout.edgeSpacing) + make.size.equalTo(Layout.titleImageSize) + } + + labelStackView.snp.makeConstraints { make in + make.top.equalToSuperview().offset(Layout.edgeSpacing) + make.leading.equalTo(titleImageView.snp.trailing).offset(Layout.titleImageTrailingSpacing) + make.trailing.equalToSuperview().offset(-Layout.edgeSpacing) + make.height.equalTo(Layout.titleLabelSmallHeight).priority(800) + labelStackViewBottomConstraint = make.bottom.equalToSuperview() + .offset(-Layout.edgeSpacing) + .priority(.medium) + .constraint + } + + titleLabel.snp.makeConstraints { make in + make.height.equalTo(Layout.titleLabelLargeHeight) + } + + placeHolderLabel.snp.makeConstraints { make in + make.height.equalTo(Layout.placeHolderLabelHeight) + } + + infoImageView.snp.makeConstraints { make in + make.leading.equalTo(titleLabel.snp.trailing).offset(Layout.titleLabelTrailingSpacing) + make.centerY.equalTo(titleLabel) + make.size.equalTo(Layout.infoImageSize) + } + + asteriskImageView.snp.makeConstraints { make in + make.leading.equalTo(titleLabel.snp.trailing).offset(Layout.titleLabelTrailingSpacing) + make.centerY.equalTo(titleLabel) + make.size.equalTo(Layout.asteriskImageSize) + } + + chevronImageView.snp.makeConstraints { make in + make.centerY.equalTo(titleImageView) + make.height.equalTo(Layout.chevronDownImageHeight) + make.width.equalTo(Layout.chevronDownImageWidth) + make.trailing.equalToSuperview().offset(-Layout.chevronDownImageTrailingSpacing) + } + + divideLine.snp.makeConstraints { make in + make.top.equalTo(titleImageView.snp.bottom).offset(Layout.divideLineTopSpacing) + make.horizontalEdges.equalToSuperview().inset(Layout.edgeSpacing) + make.height.equalTo(Layout.divideLineHeight) + } + + contentView.snp.makeConstraints { make in + make.top.equalTo(divideLine.snp.bottom) + make.horizontalEdges.equalToSuperview() + make.bottom.equalToSuperview().offset(-Layout.edgeSpacing) + } + + labelStackViewBottomConstraint?.isActive = true + } + + func configure(dependencies: ContentView.Dependencies) { + contentView.configure(dependencies: dependencies) + } + + @objc private func handleTap(_ gestureRecognizer: UIGestureRecognizer) { + let location = gestureRecognizer.location(in: self) + + if location.y < labelStackView.frame.maxY { + toggleExpand() + } + } + + private func configureContents(contents: [String]) { + guard !contents.isEmpty else { + placeHolderLabel.isHidden = false + titleLabel.snp.updateConstraints { make in + make.height.equalTo(Layout.titleLabelLargeHeight) + } + return + } + + placeHolderLabel.isHidden = true + + labelStackView + .arrangedSubviews + .dropFirst(2) // title, placeholder 제외한 label들 삭제 + .forEach { $0.removeFromSuperview() } + + contents.forEach { + let label = UILabel() + label.text = $0 + label.font = BitnagilFont.init(style: .body1, weight: .semiBold).font + label.textColor = .black + + labelStackView.addArrangedSubview(label) + label.snp.makeConstraints { make in + make.height.equalTo(Layout.contentLabelHeight) + } + } + + titleLabel.snp.updateConstraints { make in + make.height.equalTo(Layout.titleLabelSmallHeight) + } + } + + private func toggleExpand() { + let isExpanded = !(labelStackViewBottomConstraint?.isActive ?? false) + let nextExpandedState = !isExpanded + + if nextExpandedState { + labelStackViewBottomConstraint?.isActive = false + placeHolderLabel.isHidden = true + contentView.isHidden = false + divideLine.isHidden = false + chevronImageView.image = BitnagilIcon.chevronIcon(direction: .up) + } else { + labelStackViewBottomConstraint?.isActive = true + placeHolderLabel.isHidden = false + contentView.isHidden = true + divideLine.isHidden = true + chevronImageView.image = BitnagilIcon.chevronIcon(direction: .down) + } + + contentView.setExpanded(expanded: nextExpandedState) + + self.layoutIfNeeded() + } +} diff --git a/Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationInputView.swift b/Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationInputView.swift deleted file mode 100644 index b23da4d8..00000000 --- a/Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationInputView.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// RoutineCreationInputView.swift -// Presentation -// -// Created by 이동현 on 7/20/25. -// - -import SnapKit -import UIKit - -/// 루틴 등록 화면 텍스트 입력 delegate -protocol RoutineCreationInputViewDelegate: AnyObject { - /// 등록한 view를 삭제합니다. - func routineCreationInputViewDidTapDeleteButton(_ sender: RoutineCreationInputView) - - /// 입력 중인 text를 반환합니다. - func routineCreationInputView(_ sender: RoutineCreationInputView, didChangeText text: String) -} - -final class RoutineCreationInputView: UIView { - private enum Layout { - static let textFieldLeadingOffset: CGFloat = 24 - static let textFieldTrailingInset: CGFloat = 51 - static let textFieldHeight: CGFloat = 20 - static let deleteButtonSize: CGFloat = 24 - static let deleteButtonTrailingInset: CGFloat = 8 - } - - private let textField = UITextField() - private let deleteButton = UIButton() - weak var delegate: RoutineCreationInputViewDelegate? - - override init(frame: CGRect) { - super.init(frame: frame) - configureAttribute() - configureLayout() - - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func configureAttribute() { - backgroundColor = BitnagilColor.gray99 - layer.cornerRadius = 12 - - textField.delegate = self - textField.textColor = .black - textField.font = BitnagilFont.init(style: .body2, weight: .semiBold).font - textField.returnKeyType = .done - textField.addTarget(self, action: #selector(textFieldEditingChanged(_:)), for: .editingChanged) - - deleteButton.setImage(BitnagilIcon.deleteIcon, for: .normal) - deleteButton.addAction( - UIAction { [weak self] _ in - guard let self else { return } - self.delegate?.routineCreationInputViewDidTapDeleteButton(self) - }, for: .touchUpInside) - } - - private func configureLayout() { - addSubview(textField) - addSubview(deleteButton) - - textField.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(Layout.textFieldLeadingOffset) - make.trailing.equalToSuperview().inset(Layout.textFieldTrailingInset) - make.centerY.equalToSuperview() - make.height.equalTo(Layout.textFieldHeight) - } - - deleteButton.snp.makeConstraints { make in - make.size.equalTo(Layout.deleteButtonSize) - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().inset(Layout.deleteButtonTrailingInset) - } - } - - func configure(canDelete: Bool, placeholder: String?) { - deleteButton.isHidden = !canDelete - - guard - let placeholder, - let placeholderColor = BitnagilColor.gray90 - else { return } - - textField.attributedPlaceholder = NSAttributedString( - string: placeholder, - attributes: [ - .foregroundColor: placeholderColor, - .font: BitnagilFont.init(style: .body2, weight: .semiBold).font]) - } - - func configure(title: String) { - textField.text = title - } - - @objc private func textFieldEditingChanged(_ sender: UITextField) { - delegate?.routineCreationInputView(self, didChangeText: sender.text ?? "") - } -} - -extension RoutineCreationInputView: UITextFieldDelegate { - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - textField.resignFirstResponder() - return true - } -} diff --git a/Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineNameContentView.swift b/Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineNameContentView.swift new file mode 100644 index 00000000..e8166068 --- /dev/null +++ b/Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineNameContentView.swift @@ -0,0 +1,220 @@ +// +// RoutineNameContentView.swift +// Presentation +// +// Created by 이동현 on 8/11/25. +// + +import SnapKit +import UIKit + +final class RoutineNameContentView: UIView, RoutineCreationExpandable { + private enum Layout { + static let edgeSpacing: CGFloat = 19 + static let numberImageSize: CGFloat = 20 + static let numberCenterYFromTextField: CGFloat = 2.5 + static let textFieldLeadingSpacing: CGFloat = 12 + static let textFieldHeight: CGFloat = 20 + static let divideLineHeight: CGFloat = 1.3 + static let textFieldTopSpacing: CGFloat = 23 + static let checkButtonViewTopSpacing: CGFloat = 24 + static let checkButtonViewSize: CGFloat = 18 + static let checkButtonLabelHeight: CGFloat = 20 + static let checkButtonLabelTrailingSpacing: CGFloat = 6 + static let foldedHeight: CGFloat = 0 + } + + enum Action { + case subroutineChanged(index: Int, text: String) + case deleteAllSubroutines + } + + struct Dependencies { + let subRoutines: [String] + } + + private let oneImageView = UIImageView() + private let twoImageView = UIImageView() + private let threeImageView = UIImageView() + private let subRoutineTextField1 = UITextField() + private let subRoutineTextField2 = UITextField() + private let subRoutineTextField3 = UITextField() + private let divideLine1 = UIImageView() + private let divideLine2 = UIImageView() + private let divideLine3 = UIImageView() + private let checkButtonLabel = UILabel() + private let checkButtonView = UIView() + private let checkButton = UIButton() + var heightConstraint: Constraint? + var action: ((Action) -> Void)? + + private var isExpanded = true // 초기 상태 추적 + + init() { + super.init(frame: .zero) + configureAttribute() + configureLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setExpanded(expanded: Bool) { + guard heightConstraint?.isActive == expanded else { return } + + subviews.forEach { $0.alpha = 0 } + defer { self.subviews.forEach { $0.alpha = 1 } } + + self.heightConstraint?.isActive = !expanded + self.subviews.forEach { $0.isHidden = !expanded } + } + + func configure(dependencies: Dependencies) { + guard !dependencies.subRoutines.isEmpty else { return } + + let subRoutines = dependencies.subRoutines + let subRoutineTextFields = [ + subRoutineTextField1, + subRoutineTextField2, + subRoutineTextField3] + let minCount = min(subRoutines.count, subRoutineTextFields.count) + + zip(subRoutines, subRoutineTextFields).prefix(minCount).forEach { + $1.text = $0 + } + } + + private func configureAttribute() { + oneImageView.image = BitnagilIcon.oneIcon + twoImageView.image = BitnagilIcon.twoIcon + threeImageView.image = BitnagilIcon.threeIcon + + let placeholders = [ + "ex) 일어나자마자 이불 개기", + "ex) 일어나서 찬물 마시기", + "ex) 양치하기"] + + let textFields = [ + subRoutineTextField1, + subRoutineTextField2, + subRoutineTextField3] + + zip(textFields, placeholders).forEach { textField, placeholder in + let attributes: [NSAttributedString.Key: Any] = [ + .font: BitnagilFont(style: .body2, weight: .medium).font, + .foregroundColor: BitnagilColor.gray90 ?? .systemGray] + + textField.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: attributes) + textField.font = BitnagilFont(style: .body2, weight: .medium).font + textField.textColor = BitnagilColor.gray30 + } + + divideLine1.image = BitnagilIcon.divideLineIcon + divideLine2.image = BitnagilIcon.divideLineIcon + divideLine3.image = BitnagilIcon.divideLineIcon + + checkButtonLabel.text = "세부루틴 설정 안함" + checkButtonLabel.font = BitnagilFont.init(style: .body2, weight: .medium).font + + checkButtonView.layer.cornerRadius = 4 + checkButtonView.layer.borderWidth = 1 + checkButtonView.backgroundColor = .white + checkButtonView.layer.borderColor = BitnagilColor.gray95?.cgColor + } + + private func configureLayout() { + addSubview(oneImageView) + addSubview(twoImageView) + addSubview(threeImageView) + addSubview(subRoutineTextField1) + addSubview(subRoutineTextField2) + addSubview(subRoutineTextField3) + addSubview(divideLine1) + addSubview(divideLine2) + addSubview(divideLine3) + addSubview(checkButtonLabel) + addSubview(checkButtonView) + addSubview(checkButton) + + self.snp.makeConstraints { make in + heightConstraint = make.height.equalTo(Layout.foldedHeight).constraint + } + + oneImageView.snp.makeConstraints { make in + make.centerY.equalTo(subRoutineTextField1).offset(Layout.numberCenterYFromTextField) + make.leading.equalToSuperview().offset(Layout.edgeSpacing) + make.size.equalTo(Layout.numberImageSize).priority(999) + } + + twoImageView.snp.makeConstraints { make in + make.centerY.equalTo(subRoutineTextField2).offset(Layout.numberCenterYFromTextField) + make.leading.equalToSuperview().offset(Layout.edgeSpacing) + make.size.equalTo(Layout.numberImageSize).priority(999) + } + + threeImageView.snp.makeConstraints { make in + make.centerY.equalTo(subRoutineTextField3).offset(Layout.numberCenterYFromTextField) + make.leading.equalToSuperview().offset(Layout.edgeSpacing) + make.size.equalTo(Layout.numberImageSize).priority(999) + } + + subRoutineTextField1.snp.makeConstraints { make in + make.top.equalToSuperview().offset(Layout.edgeSpacing).priority(999) + make.leading.equalTo(oneImageView.snp.trailing).offset(Layout.textFieldLeadingSpacing) + make.trailing.equalToSuperview().offset(-Layout.edgeSpacing) + make.height.equalTo(Layout.textFieldHeight).priority(999) + } + + subRoutineTextField2.snp.makeConstraints { make in + make.top.equalTo(divideLine1.snp.bottom).offset(Layout.textFieldTopSpacing) + make.leading.equalTo(oneImageView.snp.trailing).offset(Layout.textFieldLeadingSpacing) + make.trailing.equalToSuperview().offset(-Layout.edgeSpacing) + make.height.equalTo(Layout.textFieldHeight).priority(999) + } + + subRoutineTextField3.snp.makeConstraints { make in + make.top.equalTo(divideLine2.snp.bottom).offset(Layout.textFieldTopSpacing) + make.leading.equalTo(oneImageView.snp.trailing).offset(Layout.textFieldLeadingSpacing) + make.trailing.equalToSuperview().offset(-Layout.edgeSpacing) + make.height.equalTo(Layout.textFieldHeight).priority(999) + } + + divideLine1.snp.makeConstraints { make in + make.horizontalEdges.equalTo(subRoutineTextField1) + make.top.equalTo(subRoutineTextField1.snp.bottom) + make.height.equalTo(Layout.divideLineHeight).priority(999) + } + + divideLine2.snp.makeConstraints { make in + make.horizontalEdges.equalTo(subRoutineTextField2) + make.top.equalTo(subRoutineTextField2.snp.bottom) + make.height.equalTo(Layout.divideLineHeight).priority(999) + } + + divideLine3.snp.makeConstraints { make in + make.horizontalEdges.equalTo(subRoutineTextField3) + make.top.equalTo(subRoutineTextField3.snp.bottom) + make.height.equalTo(Layout.divideLineHeight).priority(999) + } + + checkButtonView.snp.makeConstraints { make in + make.top.equalTo(divideLine3.snp.bottom).offset(Layout.checkButtonViewTopSpacing).priority(999) + make.bottom.equalToSuperview().offset(-Layout.edgeSpacing).priority(999) + make.trailing.equalToSuperview().offset(-Layout.edgeSpacing) + make.size.equalTo(Layout.checkButtonViewSize).priority(999) + } + + checkButtonLabel.snp.makeConstraints { make in + make.trailing.equalTo(checkButtonView.snp.leading).offset(-Layout.checkButtonLabelTrailingSpacing) + make.centerY.equalTo(checkButtonView) + } + + checkButton.snp.makeConstraints { make in + make.verticalEdges.equalTo(checkButtonView) + make.leading.equalTo(checkButtonLabel.snp.leading) + make.trailing.equalTo(checkButtonView.snp.trailing) + } + } + +} diff --git a/Projects/Presentation/Sources/RoutineCreation/View/Component/RoutinePeriodContentView.swift b/Projects/Presentation/Sources/RoutineCreation/View/Component/RoutinePeriodContentView.swift new file mode 100644 index 00000000..86e657de --- /dev/null +++ b/Projects/Presentation/Sources/RoutineCreation/View/Component/RoutinePeriodContentView.swift @@ -0,0 +1,152 @@ +// +// PeriodContentView.swift +// Presentation +// +// Created by 이동현 on 8/14/25. +// + +import Domain +import SnapKit +import UIKit + +final class RoutinePeriodContentView: UIView, RoutineCreationExpandable { + enum ButtonType { + case start + case end + } + + private enum Layout { + static let edgeSpacing: CGFloat = 19 + static let labelTopSpacing: CGFloat = 14 + static let labelHeight: CGFloat = 39 + static let dateButtonHeight: CGFloat = 39 + static let dateButtonWidth: CGFloat = 116 + static let foldedHeight: CGFloat = 0 + } + + enum Action { + case startDateSetTapped + case endDateSetTapped + } + + struct Dependencies { + let start: Date + let end: Date + } + + private let startLabel = UILabel() + private let endLabel = UILabel() + private let startButton = UIButton() + private let endButton = UIButton() + var heightConstraint: Constraint? + var action: ((Action) -> Void)? + + init() { + super.init(frame: .zero) + configureAttribute() + configureLayout() + + // TODO: - 추후 삭제 + configureButon(buttonType: .start, date: Date()) + configureButon(buttonType: .end, date: Date()) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setExpanded(expanded: Bool) { + guard heightConstraint?.isActive == expanded else { return } + + subviews.forEach { $0.alpha = 0 } + defer { self.subviews.forEach { $0.alpha = 1 } } + + self.heightConstraint?.isActive = !expanded + self.subviews.forEach { $0.isHidden = !expanded } + } + + func configure(dependencies: Dependencies) { + let startString = dependencies.start.convertToString(dateType: .yearMonthDate) + let endString = dependencies.end.convertToString(dateType: .yearMonthDate) + + startButton.setTitle(startString, for: .normal) + endButton.setTitle(endString, for: .normal) + } + + private func configureAttribute() { + startLabel.text = "시작일" + endLabel.text = "종료일" + + [startLabel, endLabel].forEach { + $0.font = BitnagilFont.init(style: .body2, weight: .semiBold).font + $0.textColor = BitnagilColor.gray30 + } + + [startButton, endButton].forEach { + $0.setTitleColor(BitnagilColor.gray30, for: .normal) + $0.titleLabel?.font = BitnagilFont.init(style: .body2, weight: .medium).font + $0.layer.cornerRadius = 12 + $0.layer.masksToBounds = true + $0.backgroundColor = .white + $0.layer.borderColor = BitnagilColor.gray97?.cgColor + $0.layer.borderWidth = 1 + } + + startButton.addAction( + UIAction { [weak self] _ in + self?.action?(.startDateSetTapped) + }, + for: .touchUpInside) + + endButton.addAction( + UIAction { [weak self] _ in + self?.action?(.endDateSetTapped) + }, + for: .touchUpInside) + } + + private func configureLayout() { + addSubview(startLabel) + addSubview(startButton) + addSubview(endLabel) + addSubview(endButton) + + self.snp.makeConstraints { make in + heightConstraint = make.height.equalTo(Layout.foldedHeight).constraint + } + + startLabel.snp.makeConstraints { make in + make.top.equalToSuperview().offset(Layout.labelTopSpacing).priority(999) + make.leading.equalTo(Layout.edgeSpacing) + make.height.equalTo(Layout.labelHeight) + } + + startButton.snp.makeConstraints { make in + make.top.equalTo(startLabel) + make.trailing.equalToSuperview().offset(-Layout.edgeSpacing) + make.height.equalTo(Layout.dateButtonHeight) + make.width.equalTo(Layout.dateButtonWidth) + } + + endLabel.snp.makeConstraints { make in + make.top.equalTo(startLabel.snp.bottom).offset(Layout.labelTopSpacing) + make.leading.equalTo(Layout.edgeSpacing) + make.height.equalTo(Layout.labelHeight) + make.bottom.equalToSuperview().offset(-Layout.edgeSpacing).priority(999) + } + + endButton.snp.makeConstraints { make in + make.top.equalTo(endLabel) + make.trailing.equalToSuperview().offset(-Layout.edgeSpacing) + make.height.equalTo(Layout.dateButtonHeight) + make.width.equalTo(Layout.dateButtonWidth) + } + } + + func configureButon(buttonType: ButtonType, date: Date) { + let button = buttonType == .start ? startButton : endButton + + let dateString = date.convertToString(dateType: .yearMonthDate) + button.setTitle(dateString, for: .normal) + } +} diff --git a/Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineRepeatContentView.swift b/Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineRepeatContentView.swift new file mode 100644 index 00000000..a8cb2b15 --- /dev/null +++ b/Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineRepeatContentView.swift @@ -0,0 +1,229 @@ +// +// RoutineRepeatContentView.swift +// Presentation +// +// Created by 이동현 on 8/12/25. +// + +import Domain +import SnapKit +import UIKit + +final class RoutineRepeatContentView: UIView, RoutineCreationExpandable { + private enum Layout { + static let edgeSpacing: CGFloat = 19 + static let repeatButtonTopSpacing: CGFloat = 14 + static let repeatButtonHeight: CGFloat = 39 + static let repeaetButtonBetweenSpacing: CGFloat = 13 + static let weekStackViewTopSpacing: CGFloat = 15 + static let weekStackViewHeight: CGFloat = 38 + static let weekStackViewSpacing: CGFloat = 5 + static let checkButtonImageTopSpacing: CGFloat = 24 + static let checkButtonImageSize: CGFloat = 18 + static let checkButtonLabelHeight: CGFloat = 20 + static let checkButtonLabelTrailingSpacing: CGFloat = 6 + static let foldedHeight: CGFloat = 0 + } + + enum Action { + case repeatDaily + case repeatWeekly + case setWeeks([Week]) + } + + struct Dependencies { + let repeatType: RepeatType? + let selectedWeeks: [Week] + } + + private let dailyButton = UIButton() + private let weeklyButton = UIButton() + private let weekStackView = UIStackView() + private var repeatButtonBottomConstraint: Constraint? + var heightConstraint: Constraint? + var action: ((Action) -> Void)? + + init() { + super.init(frame: .zero) + configureAttribute() + configureLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setExpanded(expanded: Bool) { + guard heightConstraint?.isActive == expanded else { return } + + subviews.forEach { $0.alpha = 0 } + defer { self.subviews.forEach { $0.alpha = 1 } } + + self.heightConstraint?.isActive = !expanded + self.subviews.forEach { $0.isHidden = !expanded } + + if expanded { + weekStackView.isHidden = !weeklyButton.isSelected + } + } + + func configure(dependencies: Dependencies) { + configureRepeatButton(selectedType: dependencies.repeatType) + + let selectedWeeks = dependencies.selectedWeeks + + weekStackView.subviews.forEach { button in + guard let button = button as? UIButton else { return } + button.isSelected = false + button.backgroundColor = .white + button.layer.borderColor = BitnagilColor.gray96?.cgColor + } + + selectedWeeks.forEach { + let index = $0.id + guard let button = weekStackView.arrangedSubviews[index] as? UIButton else { return } + + button.isSelected = true + button.backgroundColor = BitnagilColor.orange500 + button.layer.borderColor = BitnagilColor.orange500?.cgColor + } + } + + private func configureAttribute() { + configureWeekStackView() + weekStackView.isHidden = true + + dailyButton.setTitle("매일", for: .normal) + weeklyButton.setTitle("요일 선택", for: .normal) + [dailyButton, weeklyButton].forEach { button in + button.titleLabel?.font = BitnagilFont.init(style: .body2, weight: .medium).font + button.setTitleColor(BitnagilColor.gray30, for: .normal) + button.setTitleColor(.white, for: .selected) + button.layer.cornerRadius = 12 + button.layer.masksToBounds = true + button.layer.borderWidth = 1 + button.layer.borderColor = BitnagilColor.gray97?.cgColor + button.backgroundColor = .white + } + + dailyButton.addAction( + UIAction { [weak self] _ in + self?.configureRepeatButton(selectedType: .daily) + }, + for: .touchUpInside) + + weeklyButton.addAction( + UIAction { [weak self] _ in + self?.configureRepeatButton(selectedType: .weekly) + }, + for: .touchUpInside) + } + + private func configureLayout() { + addSubview(dailyButton) + addSubview(weeklyButton) + addSubview(weekStackView) + + self.snp.makeConstraints { make in + heightConstraint = make.height.equalTo(Layout.foldedHeight).constraint + } + + dailyButton.snp.makeConstraints { make in + make.top.equalToSuperview().offset(Layout.edgeSpacing).priority(999) + make.leading.equalToSuperview().offset(Layout.edgeSpacing) + make.height.equalTo(Layout.repeatButtonHeight) + repeatButtonBottomConstraint = make.bottom.equalToSuperview() + .offset(-Layout.edgeSpacing) + .priority(999) + .constraint + } + + weeklyButton.snp.makeConstraints { make in + make.top.equalTo(dailyButton) + make.leading.equalTo(dailyButton.snp.trailing).offset(Layout.repeaetButtonBetweenSpacing) + make.trailing.equalToSuperview().offset(-Layout.edgeSpacing) + make.height.equalTo(Layout.repeatButtonHeight) + make.width.equalTo(dailyButton) + make.bottom.equalTo(dailyButton) + } + + weekStackView.snp.makeConstraints { make in + make.top.equalTo(dailyButton.snp.bottom).offset(Layout.weekStackViewTopSpacing) + make.horizontalEdges.equalToSuperview().inset(Layout.edgeSpacing) + make.height.equalTo(Layout.weekStackViewHeight) + make.bottom.equalToSuperview().offset(-Layout.edgeSpacing).priority(999) + } + } + + private func configureWeekStackView() { + weekStackView.axis = .horizontal + weekStackView.spacing = 5 + weekStackView.distribution = .fillEqually + + Week.allCases.forEach { + let button = UIButton() + let normalLayerColor = BitnagilColor.gray96?.cgColor + button.layer.cornerRadius = 12 + button.layer.masksToBounds = true + button.titleLabel?.font = BitnagilFont.init(style: .body2, weight: .medium).font + button.setTitleColor(BitnagilColor.gray30, for: .normal) + button.setTitleColor(.white, for: .selected) + button.setTitle($0.koreanValue, for: .normal) + button.layer.borderWidth = 1 + button.layer.borderColor = normalLayerColor + button.backgroundColor = .white + + button.addAction( + UIAction { action in + guard let sender = action.sender as? UIButton else { return } + + sender.isSelected.toggle() + if sender.isSelected { + sender.backgroundColor = BitnagilColor.orange500 + sender.layer.borderColor = BitnagilColor.orange500?.cgColor + } else { + sender.backgroundColor = .white + sender.layer.borderColor = normalLayerColor + } + }, + for: .touchUpInside) + weekStackView.addArrangedSubview(button) + } + } + + private func configureRepeatButton(selectedType: RepeatType?) { + if let selectedType { + switch selectedType { + case .daily: + dailyButton.isSelected.toggle() + if dailyButton.isSelected { + weeklyButton.isSelected = false + } + case .weekly: + weeklyButton.isSelected.toggle() + if weeklyButton.isSelected { + dailyButton.isSelected = false + } + } + } + + [dailyButton, weeklyButton].forEach { + if $0.isSelected { + $0.backgroundColor = BitnagilColor.gray10 + $0.layer.borderColor = BitnagilColor.gray10?.cgColor + } else { + $0.backgroundColor = .white + $0.layer.borderColor = BitnagilColor.gray97?.cgColor + } + } + + if weeklyButton.isSelected { + let bottomSpacing = Layout.edgeSpacing + Layout.weekStackViewTopSpacing + Layout.weekStackViewHeight + weekStackView.isHidden = false + repeatButtonBottomConstraint?.update(offset: -bottomSpacing) + } else { + weekStackView.isHidden = true + repeatButtonBottomConstraint?.update(offset: -Layout.foldedHeight) + } + } +} diff --git a/Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimeContentView.swift b/Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimeContentView.swift new file mode 100644 index 00000000..f2d37d5e --- /dev/null +++ b/Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimeContentView.swift @@ -0,0 +1,157 @@ +// +// TimeContentView.swift +// Presentation +// +// Created by 이동현 on 8/15/25. +// + +import Domain +import SnapKit +import UIKit + +final class RoutineTimeContentView: UIView, RoutineCreationExpandable { + private enum Layout { + static let edgeSpacing: CGFloat = 19 + static let timeLabelTopSpacing: CGFloat = 14 + static let timeLabelHeight: CGFloat = 39 + static let timeButtonTopSpacing: CGFloat = 14 + static let timeButtonHeight: CGFloat = 39 + static let timeButtonWidth: CGFloat = 116 + static let checkButtonImageViewTopSpacing: CGFloat = 21 + static let checkButtonImageViewSize: CGFloat = 18 + static let checkButtonLabelHeight: CGFloat = 20 + static let checkButtonLabelTrailingSpacing: CGFloat = 6 + static let foldedHeight: CGFloat = 0 + } + + enum Action { + case allDayTapped + case timeSetTapped + } + + struct Dependencies { + let startTime: Date + } + + private let timeLabel = UILabel() + private let timeButton = UIButton() + private let checkButtonImageView = UIImageView() + private let checkLabel = UILabel() + private let checkButton = UIButton() + + var heightConstraint: Constraint? + var action: ((Action) -> Void)? + + init() { + super.init(frame: .zero) + configureAttribute() + configureLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setExpanded(expanded: Bool) { + guard heightConstraint?.isActive == expanded else { return } + + subviews.forEach { $0.alpha = 0 } + defer { self.subviews.forEach { $0.alpha = 1 } } + + self.heightConstraint?.isActive = !expanded + self.subviews.forEach { $0.isHidden = !expanded } + } + + func configure(dependencies: Dependencies) { + let time = dependencies.startTime + + if time.isMidnight { + timeButton.setTitle("하루종일", for: .normal) + checkButtonImageView.image = BitnagilIcon.checkedIcon + } else { + timeButton.setTitle(time.convertToString(dateType: .amPmTimeShort), for: .normal) + checkButtonImageView.image = UIImage() + } + } + + private func configureAttribute() { + timeLabel.text = "시작 시간" + timeLabel.font = BitnagilFont.init(style: .body2, weight: .semiBold).font + + timeButton.layer.cornerRadius = 12 + timeButton.layer.borderWidth = 1 + timeButton.layer.borderColor = BitnagilColor.gray97?.cgColor + timeButton.backgroundColor = .white + timeButton.setTitleColor(BitnagilColor.gray30, for: .normal) + timeButton.titleLabel?.font = BitnagilFont.init(style: .body2, weight: .medium).font + + checkLabel.text = "하루종일" + checkLabel.font = BitnagilFont.init(style: .body2, weight: .medium).font + + checkButtonImageView.layer.cornerRadius = 4 + checkButtonImageView.layer.borderWidth = 1 + checkButtonImageView.layer.masksToBounds = true + checkButtonImageView.backgroundColor = .white + checkButtonImageView.layer.borderColor = BitnagilColor.gray95?.cgColor + + timeButton.addAction( + UIAction { [weak self] _ in + self?.action?(.timeSetTapped) + }, + for: .touchUpInside) + + checkButton.addAction( + UIAction { [weak self] _ in + self?.action?(.allDayTapped) + }, + for: .touchUpInside) + } + + private func configureLayout() { + addSubview(timeLabel) + addSubview(timeButton) + addSubview(checkLabel) + addSubview(checkButtonImageView) + addSubview(checkButton) + + self.snp.makeConstraints { make in + heightConstraint = make.height.equalTo(Layout.foldedHeight).constraint + } + + timeLabel.snp.makeConstraints { make in + make.top.equalToSuperview().offset(Layout.timeLabelTopSpacing).priority(999) + make.height.equalTo(Layout.timeLabelHeight) + make.leading.equalTo(Layout.edgeSpacing) + } + + timeButton.snp.makeConstraints { make in + make.top.equalToSuperview().offset(Layout.timeButtonTopSpacing).priority(999) + make.trailing.equalToSuperview().offset(-Layout.edgeSpacing) + make.height.equalTo(Layout.timeButtonHeight) + make.width.equalTo(Layout.timeButtonWidth) + } + + checkButtonImageView.snp.makeConstraints { make in + make.top.equalTo(timeButton.snp.bottom).offset(Layout.checkButtonImageViewTopSpacing) + make.size.equalTo(Layout.checkButtonImageViewSize) + make.trailing.equalToSuperview().offset(-Layout.edgeSpacing) + make.bottom.equalToSuperview().offset(-Layout.edgeSpacing).priority(999) + } + + checkLabel.snp.makeConstraints { make in + make.centerY.equalTo(checkButtonImageView) + make.trailing.equalTo(checkButtonImageView.snp.leading).offset(-Layout.checkButtonLabelTrailingSpacing) + } + + checkButton.snp.makeConstraints { make in + make.leading.equalTo(checkLabel) + make.verticalEdges.equalTo(checkButtonImageView) + make.trailing.equalTo(checkButtonImageView) + } + } + + func configureTime(time: Date) { + let timeString = time.convertToString(dateType: .amPmTimeShort) + timeButton.setTitle(timeString, for: .normal) + } +} diff --git a/Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimePickerButton.swift b/Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimePickerButton.swift deleted file mode 100644 index dd546292..00000000 --- a/Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimePickerButton.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// RoutineTimePickerButton.swift -// Presentation -// -// Created by 이동현 on 7/27/25. -// - -import SnapKit -import UIKit - -final class RoutineTimePickerButton: UIButton { - private enum Layout { - static let chevronDownImageViewTrailingSpacing: CGFloat = 19 - static let chevronDownImageViewSize: CGFloat = 20 - static let labelLeadingSpacing: CGFloat = 24 - } - - private let chevronDownImageView = UIImageView() - private let label = UILabel() - - override init(frame: CGRect) { - super.init(frame: frame) - configureAttribute() - configureLayout() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func configureAttribute() { - layer.borderWidth = 1 - layer.borderColor = BitnagilColor.navy100?.cgColor - layer.cornerRadius = 12 - backgroundColor = .white - - chevronDownImageView.contentMode = .scaleAspectFit - chevronDownImageView.tintColor = BitnagilColor.navy400 - chevronDownImageView.image = BitnagilIcon - .chevronIcon(direction: .down)? - .withRenderingMode(.alwaysTemplate) - - label.text = "시간 선택" - label.textColor = BitnagilColor.gray40 - label.font = BitnagilFont.init(style: .body2, weight: .medium).font - } - - private func configureLayout() { - addSubview(chevronDownImageView) - addSubview(label) - - chevronDownImageView.snp.makeConstraints { make in - make.trailing.equalToSuperview().inset(Layout.chevronDownImageViewTrailingSpacing) - make.centerY.equalToSuperview() - make.size.equalTo(Layout.chevronDownImageViewSize) - } - - label.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(Layout.labelLeadingSpacing) - make.centerY.equalToSuperview() - } - } - - func configure(title: String) { - label.text = title - } -} diff --git a/Projects/Presentation/Sources/RoutineCreation/View/Component/SubroutineAdditionButton.swift b/Projects/Presentation/Sources/RoutineCreation/View/Component/SubroutineAdditionButton.swift deleted file mode 100644 index d5aaeaea..00000000 --- a/Projects/Presentation/Sources/RoutineCreation/View/Component/SubroutineAdditionButton.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// DetailRoutineAdditionButton.swift -// Presentation -// -// Created by 이동현 on 7/21/25. -// - -import SnapKit -import UIKit - -final class SubroutineAdditionButton: UIButton { - private enum Layout { - static let plusImageViewLeadingSpacing: CGFloat = 24 - static let plusImageViewSize: CGFloat = 20 - static let labelLeadingSpacing: CGFloat = 10 - } - - private let plusImageView = UIImageView() - private let label = UILabel() - - override init(frame: CGRect) { - super.init(frame: frame) - configureAttribute() - configureLayout() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func configureAttribute() { - layer.borderWidth = 1 - layer.borderColor = BitnagilColor.navy100?.cgColor - layer.cornerRadius = 12 - backgroundColor = .white - - plusImageView.contentMode = .scaleAspectFit - plusImageView.tintColor = BitnagilColor.navy400 - plusImageView.image = BitnagilIcon - .routineCreationIcon? - .withRenderingMode(.alwaysTemplate) - - label.text = "세부루틴 추가" - label.textColor = BitnagilColor.gray40 - label.font = BitnagilFont.init(style: .body2, weight: .medium).font - } - - private func configureLayout() { - addSubview(plusImageView) - addSubview(label) - - plusImageView.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(Layout.plusImageViewLeadingSpacing) - make.centerY.equalToSuperview() - make.size.equalTo(Layout.plusImageViewSize) - } - - label.snp.makeConstraints { make in - make.leading.equalTo(plusImageView.snp.trailing).offset(Layout.labelLeadingSpacing) - make.centerY.equalToSuperview() - } - } -} diff --git a/Projects/Presentation/Sources/RoutineCreation/View/Component/DatePickerView.swift b/Projects/Presentation/Sources/RoutineCreation/View/Component/TimePickerView.swift similarity index 74% rename from Projects/Presentation/Sources/RoutineCreation/View/Component/DatePickerView.swift rename to Projects/Presentation/Sources/RoutineCreation/View/Component/TimePickerView.swift index 2651ead6..37683ac1 100644 --- a/Projects/Presentation/Sources/RoutineCreation/View/Component/DatePickerView.swift +++ b/Projects/Presentation/Sources/RoutineCreation/View/Component/TimePickerView.swift @@ -8,21 +8,22 @@ import SnapKit import UIKit -protocol DatePickerViewDelegate: AnyObject { - func datePickerView(_ pickerView: DatePickerView, didSelectTime time: Date) +protocol TimePickerViewDelegate: AnyObject { + func timePickerView(_ sender: TimePickerView, didSelectTime time: Date) } -final class DatePickerView: UIViewController { +final class TimePickerView: UIViewController { private enum Layout { static let datePickerHeight: CGFloat = 195 static let horizontalSpacing: CGFloat = 20 static let registerButtonHeight: CGFloat = 54 - static let registerButtonVerticalSpacing: CGFloat = 14 + static let registerButtonTopSpacing: CGFloat = 36 + static let registerButtonBottomSpacing: CGFloat = 14 } private let datePicker = UIDatePicker() private let registerButton = UIButton() - weak var delegate: DatePickerViewDelegate? + weak var delegate: TimePickerViewDelegate? override func viewDidLoad() { super.viewDidLoad() @@ -33,20 +34,20 @@ final class DatePickerView: UIViewController { private func configureAttribute() { datePicker.preferredDatePickerStyle = .wheels datePicker.datePickerMode = .time - datePicker.locale = Locale(identifier: "en_US") + datePicker.backgroundColor = .white datePicker.tintColor = .black registerButton.layer.cornerRadius = 12 registerButton.layer.masksToBounds = true - registerButton.backgroundColor = BitnagilColor.navy500 + registerButton.backgroundColor = BitnagilColor.gray10 registerButton.titleLabel?.font = BitnagilFont.init(style: .body1, weight: .semiBold).font - registerButton.setTitle("등록하기", for: .normal) + registerButton.setTitle("확인", for: .normal) registerButton.setTitleColor(.white, for: .normal) registerButton.addAction( UIAction { [weak self] _ in guard let self else { return } - self.delegate?.datePickerView(self, didSelectTime: datePicker.date) + self.delegate?.timePickerView(self, didSelectTime: datePicker.date) dismiss(animated: true) }, for: .touchUpInside) @@ -59,14 +60,15 @@ final class DatePickerView: UIViewController { datePicker.snp.makeConstraints { make in make.top.horizontalEdges.equalToSuperview() - make.height.equalTo(Layout.datePickerHeight) + make.height.equalTo(Layout.datePickerHeight).priority(.medium) } registerButton.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview().inset(Layout.horizontalSpacing) - make.top.equalTo(datePicker.snp.bottom).offset(Layout.registerButtonVerticalSpacing) - make.bottom.equalTo(safeArea.snp.bottom).offset(-Layout.registerButtonVerticalSpacing) + make.top.equalTo(datePicker.snp.bottom).offset(Layout.registerButtonTopSpacing) + make.bottom.equalTo(safeArea.snp.bottom).offset(-Layout.registerButtonBottomSpacing) make.height.equalTo(Layout.registerButtonHeight) } + } } diff --git a/Projects/Presentation/Sources/RoutineCreation/View/Protocol/RoutineCreationExpandable.swift b/Projects/Presentation/Sources/RoutineCreation/View/Protocol/RoutineCreationExpandable.swift new file mode 100644 index 00000000..8dd6868d --- /dev/null +++ b/Projects/Presentation/Sources/RoutineCreation/View/Protocol/RoutineCreationExpandable.swift @@ -0,0 +1,22 @@ +// +// RoutineCreationExpandable.swift +// Presentation +// +// Created by 이동현 on 8/10/25. +// + +import SnapKit +import UIKit + +protocol RoutineCreationExpandable: UIView { + associatedtype Action + associatedtype Dependencies + + /// RoutineCreationExpandableContentView 에서 일어날 수 있는 action을 전달합니다. + var action: ((Action) -> Void)? { get set } + var heightConstraint: Constraint? { get set } + + func setExpanded(expanded: Bool) + func configure(dependencies: Dependencies) +} + diff --git a/Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationView.swift b/Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationView.swift deleted file mode 100644 index fd6d9caf..00000000 --- a/Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationView.swift +++ /dev/null @@ -1,652 +0,0 @@ -// -// RoutineCreationViewController.swift -// Presentation -// -// Created by 이동현 on 7/20/25. -// - -import Combine -import SnapKit -import UIKit - -final class RoutineCreationView: BaseViewController { - private enum Layout { - static let horizontalInset: CGFloat = 20 - static let nameTitleLabelTopSpacing: CGFloat = 32 - static let titleLabelHeight: CGFloat = 24 - static let titleLabelTopSpacing: CGFloat = 40 - static let titleLabelBottomSpacing: CGFloat = 14 - static let titleAsteriskTopSpacing: CGFloat = 2 - static let titleAsteriskLeadingSpacing: CGFloat = 2 - static let titleAsteriskHeight: CGFloat = 24 - static let titleAsteriskWidth: CGFloat = 9 - static let inputViewHeight: CGFloat = 52 - static let subroutineStackViewHeight: CGFloat = 0 - static let subroutineStackViewSpacing: CGFloat = 10 - static let warningLabelTopSpacing: CGFloat = 8 - static let warningLabelHeight: CGFloat = 18 - static let repeatButtonHeight: CGFloat = 52 - static let repeatWeekDayButtonLeadingSpacing: CGFloat = 13 - static let infoButtonSize: CGFloat = 16 - static let infoButtonLeadingSpacing: CGFloat = 6 - static let tooltipViewHeight: CGFloat = 47 - static let tooltipViewLeadingSpacing: CGFloat = 22 - static let tooltipViewBottomSpacing: CGFloat = 8 - static let tooltipViewTailLeadingSpacing: CGFloat = 16 - static let detailTooltipWidth: CGFloat = 209 - static let repeatTooltipWidth: CGFloat = 262 - static let weekDayStackViewTopSpacing: CGFloat = 16 - static let weekDayStackViewSpacing: CGFloat = 10 - static let weekDayStackViewHeight: CGFloat = 48 - static let allDayButtonSize: CGFloat = 20 - static let allDayButtonTrailingSpacing: CGFloat = 6 - static let allDayLabelWidth: CGFloat = 55 - static let registerButtonTopSpacing: CGFloat = 54 - static let registerButtonHeight: CGFloat = 54 - static let registerButtonBottomSpacing: CGFloat = 14 - static let datePickerBottomSheetHeight: CGFloat = 347 - } - - private let scrollView = UIScrollView() - private let contentView = UIView() - - private let nameTitleLabel = UILabel() - private let nameAsterisk = UIImageView() - private let nameInputView = RoutineCreationInputView() - - private let detailTitleLabel = UILabel() - private let detailInfoButton = UIButton() - private let subroutineAdditionButton = SubroutineAdditionButton() - private let subroutineStackView = UIStackView() - private let detailWarningLabel = UILabel() - private let detailToolTipView = TooltipView(tailPosition: .offsetFromLeading(Layout.tooltipViewTailLeadingSpacing)) - - private let repeatTitleLabel = UILabel() - private let repeatInfoButton = UIButton() - private let repeatToolTipView = TooltipView(tailPosition: .offsetFromLeading(Layout.tooltipViewTailLeadingSpacing)) - private let repeatDailyButton = UIButton() - private let repeatWeekButton = UIButton() - - private let startTimeTitleLabel = UILabel() - private let startTimeAsterisk = UIImageView() - private let timePickerButton = RoutineTimePickerButton() - private let allDayButton = UIButton() - private let allDayLabelButton = UIButton() - private let allDayLabel = UILabel() - private let weekdaysStackView = UIStackView() - - private let registerButton = UIButton() - - private let navigationTitle: String - private let registerButtonTitle: String - private var repeatRoutineTitleTopConstraint: Constraint? - private var startTimeTitleTopConstraint: Constraint? - - private var cancellables = Set() - - init(viewModel: RoutineCreationViewModel, routineId: String? = nil) { - navigationTitle = routineId == nil ? "루틴 등록" : "루틴 수정" - registerButtonTitle = routineId == nil ? "등록하기" : "수정하기" - - super.init(viewModel: viewModel) - if let routineId { - viewModel.action(input: .fetchRoutine(id: routineId)) - } - } - - init(viewModel: RoutineCreationViewModel, recommendRoutineId: Int) { - navigationTitle = "루틴 등록" - registerButtonTitle = "등록하기" - - super.init(viewModel: viewModel) - viewModel.action(input: .fetchRecommendedRoutine(id: recommendRoutineId)) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) -// configureNavigationBar(navigationStyle: .withBackButton(title: navigationTitle)) - } - - override func configureAttribute() { - configureSubroutineStackView() - configureWeekdaysStackView() - configureButtonActions() - - let titleLabels = [ - nameTitleLabel, - detailTitleLabel, - repeatTitleLabel, - startTimeTitleLabel] - let infoButtons = [detailInfoButton, repeatInfoButton] - let repeatButtons = [repeatDailyButton, repeatWeekButton] - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap)) - - view.addGestureRecognizer(tapGesture) - view.backgroundColor = .white - tapGesture.cancelsTouchesInView = false - - titleLabels.forEach { - $0.textColor = .black - $0.font = BitnagilFont.init(style: .body1, weight: .semiBold).font - } - - nameAsterisk.image = BitnagilIcon.asteriskIcon - startTimeAsterisk.image = BitnagilIcon.asteriskIcon - nameAsterisk.contentMode = .scaleAspectFit - startTimeAsterisk.contentMode = .scaleAspectFit - - infoButtons.forEach { - $0.setImage(BitnagilIcon.informationIcon, for: .normal) - } - - repeatButtons.forEach { - $0.layer.borderWidth = 1 - $0.layer.cornerRadius = 12 - $0.titleLabel?.font = BitnagilFont.init(style: .body2, weight: .semiBold).font - } - - nameTitleLabel.text = "루틴 이름" - detailTitleLabel.text = "세부 루틴" - repeatTitleLabel.text = "루틴 반복" - startTimeTitleLabel.text = "시작 시간" - - nameInputView.delegate = self - nameInputView.configure( - canDelete: false, - placeholder: "ex) \(RoutineCreationViewModel.RoutineExample.wakeUp.rawValue)") - - allDayButton.setImage(BitnagilIcon.uncheckedIcon, for: .normal) - - detailWarningLabel.text = "* 세부 루틴은 최대 3개까지 입력이 가능해요" - detailWarningLabel.textColor = BitnagilColor.error - detailWarningLabel.font = BitnagilFont(style: .caption1, weight: .medium).font - - detailToolTipView.configure(message: "어려운 루틴이라면 단계별로 나눠보세요!") - repeatToolTipView.configure(message: "선택하지 않을 경우, 당일 루틴으로만 자동 설정돼요.") - detailToolTipView.isHidden = true - repeatToolTipView.isHidden = true - - repeatDailyButton.setTitle("매일", for: .normal) - repeatWeekButton.setTitle("요일 선택", for: .normal) - - allDayLabel.text = "하루 종일" - allDayLabel.textColor = BitnagilColor.navy400 - allDayLabel.font = BitnagilFont.init(style: .body2, weight: .medium).font - - registerButton.backgroundColor = BitnagilColor.navy50 - registerButton.setTitleColor(.white, for: .normal) - registerButton.setTitleColor(BitnagilColor.gray70, for: .disabled) - - registerButton.layer.cornerRadius = 12 - registerButton.layer.masksToBounds = true - registerButton.setTitle(registerButtonTitle, for: .normal) - registerButton.isEnabled = false - registerButton.titleLabel?.font = BitnagilFont.init(style: .body1, weight: .semiBold).font - } - - override func configureLayout() { - let safeArea = view.safeAreaLayoutGuide - - view.addSubview(scrollView) - scrollView.addSubview(contentView) - - contentView.addSubview(nameTitleLabel) - contentView.addSubview(nameAsterisk) - contentView.addSubview(nameInputView) - - contentView.addSubview(detailTitleLabel) - contentView.addSubview(detailInfoButton) - contentView.addSubview(detailToolTipView) - contentView.addSubview(subroutineAdditionButton) - contentView.addSubview(subroutineStackView) - contentView.addSubview(detailWarningLabel) - - contentView.addSubview(repeatTitleLabel) - contentView.addSubview(repeatToolTipView) - contentView.addSubview(repeatInfoButton) - contentView.addSubview(repeatDailyButton) - contentView.addSubview(repeatWeekButton) - contentView.addSubview(weekdaysStackView) - - contentView.addSubview(startTimeTitleLabel) - contentView.addSubview(startTimeAsterisk) - contentView.addSubview(allDayButton) - contentView.addSubview(allDayLabelButton) - contentView.addSubview(allDayLabel) - contentView.addSubview(timePickerButton) - - contentView.addSubview(registerButton) - - scrollView.snp.makeConstraints { make in - make.edges.equalTo(safeArea) - } - contentView.snp.makeConstraints { make in - make.edges.equalTo(scrollView.contentLayoutGuide) - make.width.equalTo(scrollView.frameLayoutGuide) - make.height.equalTo(scrollView.frameLayoutGuide).priority(.low) - } - - nameTitleLabel.snp.makeConstraints { make in - make.top.equalToSuperview().offset(Layout.nameTitleLabelTopSpacing) - make.leading.equalToSuperview().offset(Layout.horizontalInset) - make.height.equalTo(Layout.titleLabelHeight) - } - - nameAsterisk.snp.makeConstraints { make in - make.top.equalTo(nameTitleLabel).offset(-Layout.titleAsteriskTopSpacing) - make.leading.equalTo(nameTitleLabel.snp.trailing).offset(Layout.titleAsteriskLeadingSpacing) - make.height.equalTo(Layout.titleAsteriskHeight) - make.width.equalTo(Layout.titleAsteriskWidth) - } - - nameInputView.snp.makeConstraints { make in - make.top.equalTo(nameTitleLabel.snp.bottom).offset(Layout.titleLabelBottomSpacing) - make.horizontalEdges.equalToSuperview().inset(Layout.horizontalInset) - make.height.equalTo(Layout.inputViewHeight) - } - - detailTitleLabel.snp.makeConstraints { make in - make.top.equalTo(nameInputView.snp.bottom).offset(Layout.titleLabelTopSpacing) - make.leading.equalToSuperview().offset(Layout.horizontalInset) - make.height.equalTo(Layout.titleLabelHeight) - } - - detailInfoButton.snp.makeConstraints { make in - make.leading.equalTo(detailTitleLabel.snp.trailing).offset(Layout.infoButtonLeadingSpacing) - make.size.equalTo(Layout.infoButtonSize) - make.centerY.equalTo(detailTitleLabel) - } - - detailToolTipView.snp.makeConstraints { make in - make.leading.equalTo(detailInfoButton.snp.centerX).offset(-Layout.tooltipViewLeadingSpacing) - make.bottom.equalTo(detailInfoButton.snp.top).offset(-Layout.tooltipViewBottomSpacing) - make.width.equalTo(Layout.detailTooltipWidth) - make.height.equalTo(Layout.tooltipViewHeight) - } - - subroutineAdditionButton.snp.makeConstraints { make in - make.horizontalEdges.equalToSuperview().inset(Layout.horizontalInset) - make.top.equalTo(detailTitleLabel.snp.bottom).offset(Layout.titleLabelBottomSpacing) - make.height.equalTo(Layout.inputViewHeight) - } - - subroutineStackView.snp.makeConstraints { make in - make.horizontalEdges.equalToSuperview().inset(Layout.horizontalInset) - make.top.equalTo(subroutineAdditionButton.snp.bottom).offset(Layout.subroutineStackViewSpacing) - make.height.equalTo(Layout.subroutineStackViewHeight).priority(.medium) - } - - detailWarningLabel.snp.makeConstraints { make in - make.horizontalEdges.equalToSuperview().inset(Layout.horizontalInset) - make.top.equalTo(subroutineStackView.snp.bottom).offset(Layout.warningLabelTopSpacing) - make.height.equalTo(Layout.warningLabelHeight) - } - - repeatTitleLabel.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(Layout.horizontalInset) - make.height.equalTo(Layout.titleLabelHeight) - repeatRoutineTitleTopConstraint = make.top - .equalTo(subroutineStackView.snp.bottom) - .offset(Layout.titleLabelTopSpacing) - .constraint - } - - repeatInfoButton.snp.makeConstraints { make in - make.leading.equalTo(repeatTitleLabel.snp.trailing).offset(Layout.infoButtonLeadingSpacing) - make.size.equalTo(Layout.infoButtonSize) - make.centerY.equalTo(repeatTitleLabel) - } - - repeatToolTipView.snp.makeConstraints { make in - make.leading.equalTo(repeatInfoButton.snp.centerX).offset(-Layout.tooltipViewLeadingSpacing) - make.bottom.equalTo(repeatInfoButton.snp.top).offset(-Layout.tooltipViewBottomSpacing) - make.width.equalTo(Layout.repeatTooltipWidth) - make.height.equalTo(Layout.tooltipViewHeight) - } - - repeatDailyButton.snp.makeConstraints { make in - make.top.equalTo(repeatTitleLabel.snp.bottom).offset(Layout.titleLabelBottomSpacing) - make.leading.equalToSuperview().offset(Layout.horizontalInset) - make.height.equalTo(Layout.repeatButtonHeight) - } - - repeatWeekButton.snp.makeConstraints { make in - make.top.equalTo(repeatTitleLabel.snp.bottom).offset(Layout.titleLabelBottomSpacing) - make.leading.equalTo(repeatDailyButton.snp.trailing).offset(Layout.repeatWeekDayButtonLeadingSpacing) - make.trailing.equalToSuperview().inset(Layout.horizontalInset) - make.height.equalTo(Layout.repeatButtonHeight) - make.width.equalTo(repeatDailyButton) - } - - weekdaysStackView.snp.makeConstraints { make in - make.top.equalTo(repeatDailyButton.snp.bottom).offset(Layout.weekDayStackViewTopSpacing) - make.horizontalEdges.equalToSuperview().inset(Layout.horizontalInset) - make.height.equalTo(Layout.weekDayStackViewHeight) - } - - startTimeTitleLabel.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(Layout.horizontalInset) - make.height.equalTo(Layout.titleLabelHeight) - startTimeTitleTopConstraint = make.top - .equalTo(repeatDailyButton.snp.bottom) - .offset(Layout.titleLabelTopSpacing) - .constraint - } - - startTimeAsterisk.snp.makeConstraints { make in - make.top.equalTo(startTimeTitleLabel).offset(-Layout.titleAsteriskTopSpacing) - make.leading.equalTo(startTimeTitleLabel.snp.trailing).offset(Layout.titleAsteriskLeadingSpacing) - make.height.equalTo(Layout.titleAsteriskHeight) - make.width.equalTo(Layout.titleAsteriskWidth) - } - - allDayLabel.snp.makeConstraints { make in - make.trailing.equalToSuperview().inset(Layout.horizontalInset) - make.width.equalTo(Layout.allDayLabelWidth) - make.centerY.equalTo(startTimeTitleLabel) - } - - allDayButton.snp.makeConstraints { make in - make.trailing.equalTo(allDayLabel.snp.leading).offset(-Layout.allDayButtonTrailingSpacing) - make.size.equalTo(Layout.allDayButtonSize) - make.centerY.equalTo(startTimeTitleLabel) - } - - allDayLabelButton.snp.makeConstraints { make in - make.edges.equalTo(allDayLabel) - } - - timePickerButton.snp.makeConstraints { make in - make.top.equalTo(startTimeTitleLabel.snp.bottom).offset(Layout.titleLabelBottomSpacing) - make.horizontalEdges.equalToSuperview().inset(Layout.horizontalInset) - make.height.equalTo(Layout.inputViewHeight) - } - - registerButton.snp.makeConstraints { make in - make.top.equalTo(timePickerButton.snp.bottom).offset(Layout.registerButtonTopSpacing) - make.horizontalEdges.equalToSuperview().inset(Layout.horizontalInset) - make.height.equalTo(Layout.registerButtonHeight) - make.bottom.equalToSuperview().inset(Layout.registerButtonBottomSpacing) - } - } - - override func bind() { - viewModel.output.namePublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] name in - self?.nameInputView.configure(title: name ?? "") - } - .store(in: &cancellables) - - viewModel.output.subRoutinesPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] routines in - var repeatRoutineTitleLabelTopSpacing = Layout.titleLabelTopSpacing - - self? - .subroutineStackView - .subviews - .enumerated() - .forEach { - guard let inputView = $0.element as? RoutineCreationInputView else { return } - - if $0.offset < routines.count { - inputView.configure(title: routines[$0.offset]) - inputView.isHidden = false - } else { - inputView.isHidden = true - } - } - - self?.detailWarningLabel.isHidden = routines.count < 3 - if !(self?.detailWarningLabel.isHidden ?? true) { - repeatRoutineTitleLabelTopSpacing += (Layout.warningLabelHeight + Layout.warningLabelTopSpacing) - self?.repeatRoutineTitleTopConstraint?.update(offset: repeatRoutineTitleLabelTopSpacing) - } - } - .store(in: &cancellables) - - viewModel.output.repeatTypePublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] repeatType in - var startTimeLabelTopSpacing = Layout.titleLabelTopSpacing - - switch repeatType { - case .none: - self?.repeatDailyButton.isSelected = false - self?.repeatWeekButton.isSelected = false - self?.weekdaysStackView.isHidden = true - case .daily: - self?.repeatDailyButton.isSelected = true - self?.repeatWeekButton.isSelected = false - self?.weekdaysStackView.isHidden = true - case .week: - self?.repeatDailyButton.isSelected = false - self?.repeatWeekButton.isSelected = true - self?.weekdaysStackView.isHidden = false - startTimeLabelTopSpacing += (Layout.weekDayStackViewHeight + Layout.weekDayStackViewTopSpacing) - } - - self?.startTimeTitleTopConstraint?.update(offset: startTimeLabelTopSpacing) - self?.configureRepeatButton() - } - .store(in: &cancellables) - - viewModel.output.weekDayPublisher - .receive(on: DispatchQueue.main) - .sink(receiveValue: { [weak self] weekDays in - let weekDaysIndex = weekDays.map { $0.id } - - self?.weekdaysStackView - .subviews - .enumerated() - .forEach { - let isContains = weekDaysIndex.contains($0.offset) - $0.element.backgroundColor = isContains - ? BitnagilColor.lightBlue100 - : .white - } - }) - .store(in: &cancellables) - - viewModel.output.executionTimePublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] executionType in - self?.timePickerButton.configure(title: executionType.description) - let allDayButtonImage = executionType == "하루종일" - ? BitnagilIcon.checkedIcon - : BitnagilIcon.uncheckedIcon - self?.allDayButton.setImage(allDayButtonImage, for: .normal) - } - .store(in: &cancellables) - - viewModel.output.isRoutineValid - .receive(on: DispatchQueue.main) - .sink { [weak self] isRegisterButtonEnabled in - self?.registerButton.isEnabled = isRegisterButtonEnabled - self?.registerButton.backgroundColor = isRegisterButtonEnabled - ? BitnagilColor.navy500 - : BitnagilColor.navy50 - } - .store(in: &cancellables) - } - - private func configureButtonActions() { - subroutineAdditionButton.addAction( - UIAction { [weak self] _ in - self?.viewModel.action(input: .addSubRoutine) - }, - for: .touchUpInside) - - detailInfoButton.addAction( - UIAction { [weak self] _ in - self?.detailInfoButton.isSelected.toggle() - if self?.detailInfoButton.isSelected ?? false { - self?.detailToolTipView.showTooltip() - } else { - self?.detailToolTipView.hideTooltip() - } - }, - for: .touchUpInside) - - repeatInfoButton.addAction( - UIAction { [weak self] _ in - self?.repeatInfoButton.isSelected.toggle() - if self?.repeatInfoButton.isSelected ?? false { - self?.repeatToolTipView.showTooltip() - } else { - self?.repeatToolTipView.hideTooltip() - } - }, - for: .touchUpInside) - - repeatDailyButton.addAction( - UIAction { [weak self] _ in - self?.viewModel.action(input: .configureRepeatType(type: .daily)) - }, - for: .touchUpInside) - - repeatWeekButton.addAction( - UIAction { [weak self] _ in - self?.viewModel.action(input: .configureRepeatType(type: .week)) - }, - for: .touchUpInside) - - [allDayButton, allDayLabelButton].forEach { - $0.addAction( - UIAction { [weak self] _ in - self?.viewModel.action(input: .configureExecution(type: .allDay)) - }, - for: .touchUpInside) - } - - timePickerButton.addAction( - UIAction { [weak self] _ in - let datePickerView = DatePickerView() - datePickerView.delegate = self - self?.presentCustomBottomSheet(contentViewController: datePickerView, maxHeight: Layout.datePickerBottomSheetHeight) - }, - for: .touchUpInside) - - registerButton.addAction( - UIAction { [weak self] _ in - self?.viewModel.action(input: .registerRoutine) - self?.navigationController?.popViewController(animated: true) - }, - for: .touchUpInside) - } - - private func configureSubroutineStackView() { - subroutineStackView.axis = .vertical - subroutineStackView.spacing = Layout.subroutineStackViewSpacing - - RoutineCreationViewModel.RoutineExample - .allCases - .dropFirst() - .enumerated() - .forEach { - let view = RoutineCreationInputView() - let placeholder = "\($0.offset + 1). ex) \($0.element.rawValue)" - view.delegate = self - view.configure(canDelete: true, placeholder: placeholder) - view.snp.makeConstraints { make in - make.height.equalTo(Layout.inputViewHeight) - } - subroutineStackView.addArrangedSubview(view) - } - } - - private func configureWeekdaysStackView() { - weekdaysStackView.isHidden = true - weekdaysStackView.axis = .horizontal - weekdaysStackView.spacing = Layout.weekDayStackViewSpacing - weekdaysStackView.distribution = .fillEqually - - Week.allCases.forEach { week in - let button = UIButton() - button.setTitle(week.koreanValue, for: .normal) - button.layer.cornerRadius = 12 - button.backgroundColor = .white - button.setTitleColor(.black, for: .normal) - button.titleLabel?.font = BitnagilFont.init(style: .body2, weight: .medium).font - button.addAction( - UIAction { [weak self] _ in - self?.viewModel.action(input: .toggleRepeatDay(weekDay: week)) - }, - for: .touchUpInside) - weekdaysStackView.addArrangedSubview(button) - } - } - - private func configureRepeatButton() { - [repeatDailyButton, repeatWeekButton].forEach { - let isSelected = $0.isSelected - let titleColor = isSelected - ? BitnagilColor.navy500 - : BitnagilColor.navy100 - let borderColor = isSelected - ? BitnagilColor.navy500?.cgColor - : BitnagilColor.navy100?.cgColor - - $0.setTitleColor(titleColor, for: .normal) - $0.layer.borderColor = borderColor - } - } - - @objc private func handleTap(_ gesture: UITapGestureRecognizer) { - let location = gesture.location(in: contentView) - - // 탭한 곳이 버튼 영역인 경우 - if detailInfoButton.frame.contains(location) { - repeatInfoButton.isSelected = false - repeatToolTipView.hideTooltip() - return - } - if repeatInfoButton.frame.contains(location) { - detailInfoButton.isSelected = false - detailToolTipView.hideTooltip() - return - } - - // 탭한 곳이 tooltip 영역 밖인 경우 - if !detailToolTipView.frame.contains(location) { - detailInfoButton.isSelected = false - detailToolTipView.hideTooltip() - } - if !repeatToolTipView.frame.contains(location) { - repeatInfoButton.isSelected = false - repeatToolTipView.hideTooltip() - } - - view.endEditing(true) - } -} - -extension RoutineCreationView: RoutineCreationInputViewDelegate { - func routineCreationInputViewDidTapDeleteButton(_ sender: RoutineCreationInputView) { - guard let index = subroutineStackView.subviews.firstIndex(of: sender) else { return } - - viewModel.action(input: .deleteSubRoutine(index: index)) - } - - func routineCreationInputView(_ sender: RoutineCreationInputView, didChangeText text: String) { - if sender === nameInputView { - viewModel.action(input: .configureName(name: text)) - return - } - - guard let index = subroutineStackView.subviews.firstIndex(of: sender) else { return } - - viewModel.action(input: .configureSubRoutine(name: text, index: index)) - } -} - -extension RoutineCreationView: DatePickerViewDelegate { - func datePickerView(_ pickerView: DatePickerView, didSelectTime time: Date) { - viewModel.action(input: .configureExecution(type: .time(startAt: time))) - } -} diff --git a/Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationViewController.swift b/Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationViewController.swift new file mode 100644 index 00000000..f72654b6 --- /dev/null +++ b/Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationViewController.swift @@ -0,0 +1,225 @@ +// +// RoutineCreationViewController.swift +// Presentation +// +// Created by 이동현 on 8/15/25. +// + +import Combine +import Domain +import SnapKit +import UIKit + +final class RoutineCreationViewController: BaseViewController { + private enum Layout { + static let horizontalSpacing: CGFloat = 20 + static let textFieldHeight: CGFloat = 24 + static let textFieldTopSpacing: CGFloat = 51 + static let textFieldUnderLineTopSpacing: CGFloat = 12 + static let textFieldUnderLineHeight: CGFloat = 2 + static let clearButtonSize: CGFloat = 16 + static let subroutineViewTopSpacing: CGFloat = 40 + static let routineCreationViewTopSpacing: CGFloat = 12 + static let registerButtonTopSpacing: CGFloat = 59 + static let registerButtonHeight: CGFloat = 54 + static let registerButtonBottomSpacing: CGFloat = 14 + static let timePickerBottomSheetHeight: CGFloat = 357 + static let calendarBottomSheetHeight: CGFloat = 517 + } + + private let scrollView = UIScrollView() + private let contentView = UIView() + private let routineTextField = UITextField() + private let textViewUnderLineView = UIView() + private let clearButton = UIButton() + + private let subRoutineNameView = RoutineCreationCardView( + title: "세부루틴", + placeHolder: "ex) 일어나자마자 이불 개기", + titleImage: BitnagilIcon.routineListIcon, + withInfoImage: true, + withAsteriskImage: false) + private let repeatView = RoutineCreationCardView( + title: "반복 요일", + placeHolder: "ex) 매주 월,화,수,목,금", + titleImage: BitnagilIcon.routineRepeatIcon, + withInfoImage: false, + withAsteriskImage: false) + private let periodView = RoutineCreationCardView( + title: "목표 기간", + placeHolder: "ex) 25.08.06 - 25.08.06", + titleImage: BitnagilIcon.routineDateIcon, + withInfoImage: false, + withAsteriskImage: true) + private let timeView = RoutineCreationCardView( + title: "시간", + placeHolder: "ex) 오전 9:40부터 시작", + titleImage: BitnagilIcon.routineTimeIcon, + withInfoImage: false, + withAsteriskImage: true) + + private let registerButton = UIButton() + private let navigationTitle: String + private let registerButtonTitle: String + private var cancellables = Set() + + init(viewModel: RoutineCreationViewModel, routineId: String? = nil) { + navigationTitle = routineId == nil ? "루틴 등록" : "루틴 수정" + registerButtonTitle = routineId == nil ? "등록하기" : "수정하기" + + super.init(viewModel: viewModel) + if let routineId { + viewModel.action(input: .fetchRoutine(id: routineId)) + } + } + + init(viewModel: RoutineCreationViewModel, recommendRoutineId: Int) { + navigationTitle = "루틴 등록" + registerButtonTitle = "등록하기" + + super.init(viewModel: viewModel) + viewModel.action(input: .fetchRecommendedRoutine(id: recommendRoutineId)) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + configureCustomNavigationBar(navigationBarStyle: .withBackButton(title: navigationTitle)) + } + + override func configureAttribute() { + let attributes: [NSAttributedString.Key: Any] = [ + .font: BitnagilFont(style: .title3, weight: .semiBold).font, + .foregroundColor: BitnagilColor.gray90 ?? .systemGray] + + view.backgroundColor = .white + + routineTextField.attributedPlaceholder = NSAttributedString(string: "루틴 제목을 입력해주세요.", attributes: attributes) + routineTextField.font = BitnagilFont(style: .title3, weight: .semiBold).font + routineTextField.textColor = BitnagilColor.gray10 + + textViewUnderLineView.backgroundColor = BitnagilColor.gray90 + + clearButton.setImage(BitnagilIcon.clearIcon, for: .normal) + + registerButton.layer.cornerRadius = 12 + registerButton.layer.masksToBounds = true + registerButton.titleLabel?.font = BitnagilFont.init(style: .body1, weight: .semiBold).font + registerButton.backgroundColor = BitnagilColor.gray95 + registerButton.setTitle("등록하기", for: .normal) + + subRoutineNameView.configure(dependencies: .init(subRoutines: ["ㄴㄴㄴ", "ㅋㅋㅋ"])) + bindCreationCardViews() + } + + override func configureLayout() { + let safeArea = view.safeAreaLayoutGuide + navigationController?.setNavigationBarHidden(true, animated: false) + + view.addSubview(scrollView) + scrollView.addSubview(contentView) + contentView.addSubview(routineTextField) + contentView.addSubview(clearButton) + contentView.addSubview(textViewUnderLineView) + contentView.addSubview(subRoutineNameView) + contentView.addSubview(repeatView) + contentView.addSubview(periodView) + contentView.addSubview(timeView) + contentView.addSubview(registerButton) + + scrollView.snp.makeConstraints { make in + make.edges.equalTo(safeArea) + } + + contentView.snp.makeConstraints { make in + make.edges.equalTo(scrollView.contentLayoutGuide) + make.width.equalTo(scrollView.frameLayoutGuide) + make.height.equalTo(scrollView.frameLayoutGuide).priority(.low) + } + + routineTextField.snp.makeConstraints { make in + make.top.equalToSuperview().offset(Layout.textFieldTopSpacing) + make.horizontalEdges.equalToSuperview().inset(Layout.horizontalSpacing) + make.height.equalTo(Layout.textFieldHeight) + } + + clearButton.snp.makeConstraints { make in + make.trailing.equalToSuperview().offset(-Layout.horizontalSpacing) + make.centerY.equalTo(routineTextField) + make.size.equalTo(Layout.clearButtonSize) + } + + textViewUnderLineView.snp.makeConstraints { make in + make.top.equalTo(routineTextField.snp.bottom).offset(Layout.textFieldUnderLineTopSpacing) + make.horizontalEdges.equalToSuperview().inset(Layout.horizontalSpacing) + make.height.equalTo(Layout.textFieldUnderLineHeight) + } + + subRoutineNameView.snp.makeConstraints { make in + make.top.equalTo(textViewUnderLineView.snp.bottom).offset(Layout.subroutineViewTopSpacing) + make.horizontalEdges.equalToSuperview().inset(Layout.horizontalSpacing) + } + + repeatView.snp.makeConstraints { make in + make.top.equalTo(subRoutineNameView.snp.bottom).offset(Layout.routineCreationViewTopSpacing) + make.horizontalEdges.equalToSuperview().inset(Layout.horizontalSpacing) + } + + periodView.snp.makeConstraints { make in + make.top.equalTo(repeatView.snp.bottom).offset(Layout.routineCreationViewTopSpacing) + make.horizontalEdges.equalToSuperview().inset(Layout.horizontalSpacing) + } + + timeView.snp.makeConstraints { make in + make.top.equalTo(periodView.snp.bottom).offset(Layout.routineCreationViewTopSpacing) + make.horizontalEdges.equalToSuperview().inset(Layout.horizontalSpacing) + } + + registerButton.snp.makeConstraints { make in + make.top.greaterThanOrEqualTo(timeView.snp.bottom).offset(Layout.registerButtonTopSpacing) + make.horizontalEdges.equalToSuperview().inset(Layout.horizontalSpacing) + make.height.equalTo(Layout.registerButtonHeight) + make.bottom.equalToSuperview().inset(Layout.registerButtonBottomSpacing) + } + } + + override func bind() { + + } + + private func bindCreationCardViews() { + periodView.onAction = { [weak self] action in + let datePickerView = BitnagilCalendarView() + datePickerView.delegate = self + + self?.presentCustomBottomSheet(contentViewController: datePickerView, maxHeight: Layout.calendarBottomSheetHeight) + } + + timeView.onAction = { [weak self] action in + switch action { + case .allDayTapped: + print() + case .timeSetTapped: + let datePickerView = TimePickerView() + datePickerView.delegate = self + self?.presentCustomBottomSheet(contentViewController: datePickerView, maxHeight: Layout.timePickerBottomSheetHeight) + } + } + } +} + +extension RoutineCreationViewController: TimePickerViewDelegate { + func timePickerView(_ pickerView: TimePickerView, didSelectTime time: Date) { + viewModel.action(input: .configureExecution(type: .time(startAt: time))) + timeView.configure(dependencies: .init(startTime: time)) + } +} + +extension RoutineCreationViewController: BitnagilCalendarViewDelegate { + func bitnagilCalendarView(_ sender: BitnagilCalendarView, didSelectDate date: Date) { + periodView.configure(dependencies: .init(start: date, end: date)) + } +} diff --git a/Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift b/Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift index 977c5af4..ab626f35 100644 --- a/Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift +++ b/Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift @@ -10,11 +10,6 @@ import Domain import Foundation final class RoutineCreationViewModel: ViewModel { - enum RepeatType { - case daily - case week - } - enum RoutineExample: String, CaseIterable { case wakeUp = "아침에 개운하게 일어나기" case foldBlanket = "일어나자마자 이불 개기" @@ -77,7 +72,7 @@ final class RoutineCreationViewModel: ViewModel { init(routineUseCase: RoutineUseCaseProtocol, recommenededRoutineUseCase: RecommendedRoutineUseCaseProtocol) { self.routineUseCase = routineUseCase self.recommenededRoutineUseCase = recommenededRoutineUseCase - + output = Output( namePublisher: nameSubject.eraseToAnyPublisher(), subRoutinesPublisher: subRoutinesSubject @@ -133,7 +128,7 @@ final class RoutineCreationViewModel: ViewModel { sortOrder: $0.sortOrder) } let weekDay = routine.repeatDay.compactMap { Week(rawValue: $0.rawValue) } - let repeatType: RepeatType = weekDay.count == Week.allCases.count ? .daily : .week + let repeatType: RepeatType = weekDay.count == Week.allCases.count ? .daily : .weekly let executionType: ExecutionType if routine.executionTime == "00:00:00" { @@ -235,7 +230,7 @@ final class RoutineCreationViewModel: ViewModel { switch selectedType { case .daily: weekDaySubject.send(Set(Week.allCases)) - case .week: + case .weekly: if repeatType == .daily { weekDaySubject.send([]) } diff --git a/Projects/Shared/Sources/Extension/Date+.swift b/Projects/Shared/Sources/Extension/Date+.swift index cbfeced5..6f5e3625 100644 --- a/Projects/Shared/Sources/Extension/Date+.swift +++ b/Projects/Shared/Sources/Extension/Date+.swift @@ -8,6 +8,11 @@ import Foundation extension Date { + public var isMidnight: Bool { + let components = Calendar.current.dateComponents([.hour, .minute, .second], from: self) + return components.hour == 0 && components.minute == 0 + } + public func convertToString(dateType: DateType) -> String { let formatter = DateFormatter() formatter.locale = Locale(identifier: "ko_KR") diff --git a/Tuist/Dependencies.swift b/Tuist/Dependencies.swift index 14979193..6cc6b8d3 100644 --- a/Tuist/Dependencies.swift +++ b/Tuist/Dependencies.swift @@ -4,7 +4,7 @@ let dependencies = Dependencies( swiftPackageManager: .init( [ .remote(url: "https://github.com/SnapKit/SnapKit.git", requirement: .upToNextMajor(from: "5.0.0")), - .remote(url: "https://github.com/devxoul/Then.git", requirement: .upToNextMajor(from: "3.0.0")), + .remote(url: "https://github.com/devxoul/Then.git", requirement: .upToNextMajor(from: "3.0.0")) ], productTypes: [:] ), diff --git a/Tuist/Package.resolved b/Tuist/Package.resolved index cff34c51..e5e60718 100644 --- a/Tuist/Package.resolved +++ b/Tuist/Package.resolved @@ -9,6 +9,15 @@ "version" : "5.10.2" } }, + { + "identity" : "fscalendar", + "kind" : "remoteSourceControl", + "location" : "https://github.com/WenchaoD/FSCalendar.git", + "state" : { + "revision" : "0fbdec5172fccb90f707472eeaea4ffe095278f6", + "version" : "2.8.4" + } + }, { "identity" : "kakao-ios-sdk", "kind" : "remoteSourceControl", diff --git a/Tuist/Package.swift b/Tuist/Package.swift index c0b0cff2..2c91fc53 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -7,6 +7,7 @@ let package = Package( .package(url: "https://github.com/SnapKit/SnapKit.git", from: "5.0.0"), .package(url: "https://github.com/kakao/kakao-ios-sdk", from: "2.23.0"), .package(url: "https://github.com/onevcat/Kingfisher.git", from: "8.0.0"), - .package(url: "https://github.com/airbnb/lottie-ios", from: "4.0.0") + .package(url: "https://github.com/airbnb/lottie-ios", from: "4.0.0"), + .package(url: "https://github.com/WenchaoD/FSCalendar.git", from: "2.8.4") ] )