diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml index 549a69d..71a1396 100644 --- a/.github/auto_assign.yml +++ b/.github/auto_assign.yml @@ -7,6 +7,7 @@ addAssignees: author reviewers: - minneee - Peter1119 + - Roy-wonji # A number of reviewers added to the pull request # Set 0 to add all the reviewers (default: 0) diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Contents.json b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Contents.json index 73c0059..7f73912 100644 --- a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Contents.json +++ b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Contents.json @@ -2,5 +2,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "compression-type" : "automatic" } } diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/TeamiIntroduce.imageset/Contents.json b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/TeamiIntroduce.imageset/Contents.json new file mode 100644 index 0000000..98b4695 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/TeamiIntroduce.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "TeamiIntroduce.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/TeamiIntroduce.imageset/TeamiIntroduce.png b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/TeamiIntroduce.imageset/TeamiIntroduce.png new file mode 100644 index 0000000..fffc4cb Binary files /dev/null and b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/TeamiIntroduce.imageset/TeamiIntroduce.png differ diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroduceAccident.imageset/Contents.json b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroduceAccident.imageset/Contents.json new file mode 100644 index 0000000..76bf3d6 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroduceAccident.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "TeamInfroduce_Accident.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroduceAccident.imageset/TeamInfroduce_Accident.png b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroduceAccident.imageset/TeamInfroduce_Accident.png new file mode 100644 index 0000000..0d4767d Binary files /dev/null and b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroduceAccident.imageset/TeamInfroduce_Accident.png differ diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroduceCircle.imageset/Contents.json b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroduceCircle.imageset/Contents.json new file mode 100644 index 0000000..a67eb54 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroduceCircle.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "TeamInfroduce_Circle.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroduceCircle.imageset/TeamInfroduce_Circle.png b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroduceCircle.imageset/TeamInfroduce_Circle.png new file mode 100644 index 0000000..3cb6fcf Binary files /dev/null and b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroduceCircle.imageset/TeamInfroduce_Circle.png differ diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroduceHeart.imageset/Contents.json b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroduceHeart.imageset/Contents.json new file mode 100644 index 0000000..c0f5096 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroduceHeart.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "TeamInfroduce_Heart.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroduceHeart.imageset/TeamInfroduce_Heart.png b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroduceHeart.imageset/TeamInfroduce_Heart.png new file mode 100644 index 0000000..600341c Binary files /dev/null and b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroduceHeart.imageset/TeamInfroduce_Heart.png differ diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroducePerson.imageset/Contents.json b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroducePerson.imageset/Contents.json new file mode 100644 index 0000000..d3af2dd --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroducePerson.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "TeamInfroduce_Person.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroducePerson.imageset/TeamInfroduce_Person.png b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroducePerson.imageset/TeamInfroduce_Person.png new file mode 100644 index 0000000..0635f4c Binary files /dev/null and b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/Images/teamInfroducePerson.imageset/TeamInfroduce_Person.png differ diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/Contents.json b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/blog.imageset/Contents.json b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/blog.imageset/Contents.json new file mode 100644 index 0000000..3e8c32f --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/blog.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "blog.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/blog.imageset/blog.svg b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/blog.imageset/blog.svg new file mode 100644 index 0000000..d3b5f50 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/blog.imageset/blog.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/check.imageset/Contents.json b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/check.imageset/Contents.json new file mode 100644 index 0000000..17203cc --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/check.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "check.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/check.imageset/check.svg b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/check.imageset/check.svg new file mode 100644 index 0000000..daadbb8 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/check.imageset/check.svg @@ -0,0 +1,4 @@ + + + + diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/glabal.imageset/Contents.json b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/glabal.imageset/Contents.json new file mode 100644 index 0000000..64594cb --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/glabal.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "glabal.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/glabal.imageset/glabal.svg b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/glabal.imageset/glabal.svg new file mode 100644 index 0000000..a1cd791 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/glabal.imageset/glabal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/leftArrow.imageset/Contents.json b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/leftArrow.imageset/Contents.json new file mode 100644 index 0000000..2a1fcd6 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/leftArrow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "leftArrow.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/leftArrow.imageset/leftArrow.svg b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/leftArrow.imageset/leftArrow.svg new file mode 100644 index 0000000..45f1991 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/leftArrow.imageset/leftArrow.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/link.imageset/Contents.json b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/link.imageset/Contents.json new file mode 100644 index 0000000..6060b67 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/link.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "link.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/link.imageset/link.svg b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/link.imageset/link.svg new file mode 100644 index 0000000..f15bf06 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/link.imageset/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/people.imageset/Contents.json b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/people.imageset/Contents.json new file mode 100644 index 0000000..1cc8d72 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/people.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "people.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/people.imageset/people.svg b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/people.imageset/people.svg new file mode 100644 index 0000000..3b74325 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/people.imageset/people.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/rightArrow.imageset/Contents.json b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/rightArrow.imageset/Contents.json new file mode 100644 index 0000000..bfef48d --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/rightArrow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "rightArrow.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/rightArrow.imageset/rightArrow.svg b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/rightArrow.imageset/rightArrow.svg new file mode 100644 index 0000000..894dae3 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Resources/Assets.xcassets/icon/rightArrow.imageset/rightArrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/TeamIntroduce/TeamIntroduce/Sources/DesignSytstem/Color/Extension+ShapeStyle.swift b/TeamIntroduce/TeamIntroduce/Sources/DesignSytstem/Color/Extension+ShapeStyle.swift index 15cfe68..01e3bfa 100644 --- a/TeamIntroduce/TeamIntroduce/Sources/DesignSytstem/Color/Extension+ShapeStyle.swift +++ b/TeamIntroduce/TeamIntroduce/Sources/DesignSytstem/Color/Extension+ShapeStyle.swift @@ -13,12 +13,14 @@ extension ShapeStyle where Self == Color { static var staticWhite: Color { .init(hex: "FFFFFF") } static var staticBlack: Color { .init(hex: "0C0E0F") } + static var shadowColor: Color { .init(hex: "000000")} // MARK: - Static Text static var textPrimary: Color { .init(hex: "0A0A0A") } static var textSecondary: Color { .init(hex: "717182") } static var textSecondary100: Color { .init(hex: "525252") } + static var textGray100: Color { .init(hex: "7D7E8C") } static var textInactive: Color { .init(hex: "70737C47").opacity(0.28) } // MARK: - Static Background @@ -47,6 +49,7 @@ extension ShapeStyle where Self == Color { static var gray90: Color { .init(hex: "202325") } static var grayError: Color { .init(hex: "FF5050") } static var grayWhite: Color { .init(hex: "FFFFFF") } + static var blueGray: Color { .init(hex: "7A7A89") } static var grayPrimary: Color { .init(hex: "0099FF") } // MARK: - Surface @@ -68,7 +71,7 @@ extension ShapeStyle where Self == Color { // MARK: - NatureBlue - static var blue10: Color { .init(hex: "F5F8FF") } + static var blue10: Color { .init(hex: "155DFC") } static var blue20: Color { .init(hex: "E1EAFF") } static var blue30: Color { .init(hex: "C1D3FF") } static var blue40: Color { .init(hex: "0D82F9") } @@ -99,6 +102,10 @@ extension ShapeStyle where Self == Color { static var gray600: Color { .init(hex: "808080") } static var gray800: Color { .init(hex: "4D4D4D") } + + + static var green: Color { .init(hex: "00A63E") } + static var lightPurple: Color { .init(hex: "9810FA") } static var error: Color { .init(hex: "FF5050") } static var basicBlue: Color { .init(hex: "0099FF") } diff --git a/TeamIntroduce/TeamIntroduce/Sources/DesignSytstem/Componet/Navigation/CustomNavigationBackBar.swift b/TeamIntroduce/TeamIntroduce/Sources/DesignSytstem/Componet/Navigation/CustomNavigationBackBar.swift new file mode 100644 index 0000000..42f7c2a --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Sources/DesignSytstem/Componet/Navigation/CustomNavigationBackBar.swift @@ -0,0 +1,49 @@ +// +// CustomNavigationBackBar.swift +// TeamIntroduce +// +// Created by Wonji Suh on 8/12/25. +// + +import SwiftUI + + struct CustomNavigationBackBar: View { + private var buttonAction: () -> Void = { } + private var text: String + + + init( + text: String = "", + buttonAction: @escaping () -> Void, + ) { + self.buttonAction = buttonAction + self.text = text + } + + var body: some View { + HStack { + Image(asset: .leftArrow) + .resizable() + .scaledToFit() + .frame(width: 10, height: 20) + .foregroundStyle(.staticWhite) + + Spacer() + .frame(width: 20) + + if !text.isEmpty { + Text(text) + .pretendardFont(family: .regular, size: 14) + .foregroundStyle(.textPrimary) + } + + Spacer() + + + } + .padding(.horizontal, 30) + .onTapGesture { + buttonAction() + } + } +} diff --git a/TeamIntroduce/TeamIntroduce/Sources/DesignSytstem/Image/Extension+Image.swift b/TeamIntroduce/TeamIntroduce/Sources/DesignSytstem/Image/Extension+Image.swift new file mode 100644 index 0000000..229a230 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Sources/DesignSytstem/Image/Extension+Image.swift @@ -0,0 +1,36 @@ +// +// Extension+Image.swift +// TeamIntroduce +// +// Created by Wonji Suh on 8/11/25. +// + +import SwiftUI + + extension UIImage { + convenience init?(_ asset: ImageAsset) { + self.init(named: asset.rawValue, in: Bundle.main, with: nil) + } + + convenience init?(assetName: String) { + self.init(named: assetName, in: Bundle.main, with: nil) + } +} + +extension Image { + init(asset: ImageAsset) { + if let uiImage = UIImage(asset) { + self.init(uiImage: uiImage) + } else { + self = Image(systemName: "questionmark") + } + } + + init(assetName: String) { + if let uiImage = UIImage(assetName: assetName) { + self.init(uiImage: uiImage) + } else { + self = Image(systemName: "questionmark") + } + } +} diff --git a/TeamIntroduce/TeamIntroduce/Sources/DesignSytstem/Image/ImageAsset.swift b/TeamIntroduce/TeamIntroduce/Sources/DesignSytstem/Image/ImageAsset.swift new file mode 100644 index 0000000..628858e --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Sources/DesignSytstem/Image/ImageAsset.swift @@ -0,0 +1,28 @@ +// +// ImageAsset.swift +// TeamIntroduce +// +// Created by Wonji Suh on 8/11/25. +// + +import Foundation + +enum ImageAsset: String { + case people + case rightArrow + case blog + case check + case leftArrow + case glabal + case link + case arrowRight + case missionLogo + case teamAgreementLogo + case teamBlogLogo + case teamIntroductionLogo + case teamiIntroduce + case teamInfroduceAccident + case teamInfroduceHeart + case teamInfroduceCircle + case teamInfroducePerson +} diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/Coordinator/Flow/IntroduceCoordinator.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/Coordinator/Flow/IntroduceCoordinator.swift index 4a69d93..d457bf8 100644 --- a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/Coordinator/Flow/IntroduceCoordinator.swift +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/Coordinator/Flow/IntroduceCoordinator.swift @@ -73,3 +73,5 @@ final class IntroduceCoordinator: NavigationControlling, ObservableObject { replaceStack([route], animated: animated) } } + + diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/Coordinator/Flow/IntroduceRoute.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/Coordinator/Flow/IntroduceRoute.swift index 6a67d16..4d9be14 100644 --- a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/Coordinator/Flow/IntroduceRoute.swift +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/Coordinator/Flow/IntroduceRoute.swift @@ -18,4 +18,7 @@ enum IntroduceRoute: Hashable { // 팀 블로그 case teamBlog + // webView + case webView(url: String) + } diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/Coordinator/View/IntorduceCoordinatorView.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/Coordinator/View/IntorduceCoordinatorView.swift index d7d94b9..eae6272 100644 --- a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/Coordinator/View/IntorduceCoordinatorView.swift +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/Coordinator/View/IntorduceCoordinatorView.swift @@ -9,7 +9,7 @@ import SwiftUI import SwiftData struct IntorduceCoordinatorView : View { - @EnvironmentObject private var coordinator: IntroduceCoordinator + @StateObject private var coordinator = IntroduceCoordinator() var sharedModelContainer: ModelContainer = { let schema = Schema([ Item.self, @@ -25,7 +25,7 @@ struct IntorduceCoordinatorView : View { var body: some View { NavigationStack(path: $coordinator.path) { - ContentView() + IntroductionMainView(viewModel: IntroductionViewModel(coordinator: coordinator)) .navigationDestination(for: IntroduceRoute.self, destination: makeDestination) } .modelContainer(sharedModelContainer) @@ -38,13 +38,18 @@ extension IntorduceCoordinatorView { private func makeDestination(for route: IntroduceRoute) -> some View { switch route { case .introduceMain: - ContentView() + IntroductionMainView(viewModel: IntroductionViewModel(coordinator: coordinator)) case .teamAgreement: ContentView() case .teamIntroduce: - EmptyView() + TeamIntroduceView(coordinator: coordinator) + .navigationBarBackButtonHidden() case .teamBlog: - EmptyView() + TeamBlogView(viewModel: .init(coordinator: coordinator)) + .navigationBarBackButtonHidden() + + case .webView(let url): + WebView(coordinator: coordinator, url: url) } } } diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/Components/IntroductionRow/IntroductionRowView.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/Components/IntroductionRow/IntroductionRowView.swift index 80652e8..b13c9b1 100644 --- a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/Components/IntroductionRow/IntroductionRowView.swift +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/Components/IntroductionRow/IntroductionRowView.swift @@ -8,48 +8,48 @@ import SwiftUI struct IntroductionRowView: View { - private let model: IntroductionRowModel - - init(model: IntroductionRowModel) { - self.model = model - } - - var body: some View { - HStack { - Circle() - .frame(width: 42, height: 42) - VStack(alignment: .leading, spacing: 4) { - HStack(spacing: 4) { - Text(model.name) - .pretendardFont(family: .semiBold, size: 14) - .foregroundStyle(.textPrimary) - - if model.isLeader { - Image(systemName: "crown.fill") - .foregroundStyle(Color.yellow) - .font(.system(size: 12)) - } - } - - Text(model.role) - .pretendardFont(family: .regular, size: 12) - .foregroundStyle(.textSecondary) - - MBTILabel(mbti: model.mbti) - } - - Spacer() - - Image("ArrowRight") - .foregroundStyle(Color(hex: "717182")) + private let model: IntroductionRowModel + + init(model: IntroductionRowModel) { + self.model = model + } + + var body: some View { + HStack { + Circle() + .frame(width: 42, height: 42) + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 4) { + Text(model.name) + .pretendardFont(family: .semiBold, size: 14) + .foregroundStyle(.textPrimary) + + if model.isLeader { + Image(systemName: "crown.fill") + .foregroundStyle(Color.yellow) + .font(.system(size: 12)) + } } - .padding(16) - .background(Color.staticWhite) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .shadow(radius: 1) + + Text(model.role) + .pretendardFont(family: .regular, size: 12) + .foregroundStyle(.textSecondary) + + MBTILabel(mbti: model.mbti) + } + + Spacer() + + Image(asset: .arrowRight) + .foregroundStyle(Color(hex: "717182")) } + .padding(16) + .background(.staticWhite) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .shadow(radius: 1) + } } #Preview { - IntroductionRowView(model: IntroductionRowModel.mockData[0]) + IntroductionRowView(model: IntroductionRowModel.mockData[0]) } diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/Components/MissionView.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/Components/MissionView.swift index 81bbc47..1564e9c 100644 --- a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/Components/MissionView.swift +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/Components/MissionView.swift @@ -10,7 +10,7 @@ import SwiftUI struct MissionView: View { var body: some View { VStack(alignment: .center, spacing: 12) { - Image("MissionLogo") + Image(asset: .missionLogo) .renderingMode(.original) Text("우리의 미션") diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/Components/SkeletonRowView.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/Components/SkeletonRowView.swift index efa7ce5..ac20e9b 100644 --- a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/Components/SkeletonRowView.swift +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/Components/SkeletonRowView.swift @@ -8,51 +8,51 @@ import SwiftUI struct SkeletonRowView: View { - @State private var isAnimating = false - - var body: some View { - HStack { - Circle() - .frame(width: 42, height: 42) - .foregroundColor(.gray.opacity(0.3)) - - VStack(alignment: .leading, spacing: 4) { - RoundedRectangle(cornerRadius: 4) - .frame(width: 80, height: 16) - .foregroundColor(.gray.opacity(0.3)) - - RoundedRectangle(cornerRadius: 4) - .frame(width: 120, height: 12) - .foregroundColor(.gray.opacity(0.2)) - - RoundedRectangle(cornerRadius: 8) - .frame(width: 50, height: 20) - .foregroundColor(.gray.opacity(0.2)) - } - - Spacer() - - RoundedRectangle(cornerRadius: 4) - .frame(width: 16, height: 16) - .foregroundColor(.gray.opacity(0.2)) - } - .padding(16) - .background(Color.staticWhite) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .shadow(radius: 1) - .opacity(isAnimating ? 0.5 : 1.0) - .animation(.easeInOut(duration: 1.0).repeatForever(autoreverses: true), value: isAnimating) - .onAppear { - isAnimating = true - } + @State private var isAnimating = false + + var body: some View { + HStack { + Circle() + .frame(width: 42, height: 42) + .foregroundColor(.gray.opacity(0.3)) + + VStack(alignment: .leading, spacing: 4) { + RoundedRectangle(cornerRadius: 4) + .frame(width: 80, height: 16) + .foregroundColor(.gray.opacity(0.3)) + + RoundedRectangle(cornerRadius: 4) + .frame(width: 120, height: 12) + .foregroundColor(.gray.opacity(0.2)) + + RoundedRectangle(cornerRadius: 8) + .frame(width: 50, height: 20) + .foregroundColor(.gray.opacity(0.2)) + } + + Spacer() + + RoundedRectangle(cornerRadius: 4) + .frame(width: 16, height: 16) + .foregroundColor(.gray.opacity(0.2)) } + .padding(16) + .background(Color.staticWhite) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .shadow(radius: 1) + .opacity(isAnimating ? 0.5 : 1.0) + .animation(.easeInOut(duration: 1.0).repeatForever(autoreverses: true), value: isAnimating) + .onAppear { + isAnimating = true + } + } } #Preview { - VStack { - SkeletonRowView() - SkeletonRowView() - SkeletonRowView() - } - .padding() + VStack { + SkeletonRowView() + SkeletonRowView() + SkeletonRowView() + } + .padding() } diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/Components/TeamExploreItem.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/Components/TeamExploreItem.swift new file mode 100644 index 0000000..e8dd8b1 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/Components/TeamExploreItem.swift @@ -0,0 +1,50 @@ +// +// TeamExploreItem.swift +// TeamIntroduce +// +// Created by Wonji Suh on 8/12/25. +// + +import Foundation + +enum TeamExploreItem: CaseIterable, Identifiable { + case introduction + case agreement + case blog + + var id: Self { self } + + + var title: String { + switch self { + case .introduction: + return "팀 소개" + case .agreement: + return "팀 약속" + case .blog: + return "팀 블로그" + } + } + + var subtitle: String { + switch self { + case .introduction: + return "우리 팀의 특징과 목표" + case .agreement: + return "함께 지켜나갈 소중한 약속들" + case .blog: + return "팀원들의 블로그 모음" + } + } + + var imageName: ImageAsset { + switch self { + case .introduction: + return .teamIntroductionLogo + case .agreement: + return .teamAgreementLogo + case .blog: + return .teamBlogLogo + } + } +} diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/Components/TeamExploreRowView.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/Components/TeamExploreRowView.swift index fa7649c..0586493 100644 --- a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/Components/TeamExploreRowView.swift +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/Components/TeamExploreRowView.swift @@ -7,86 +7,46 @@ import SwiftUI -enum TeamExploreItem: CaseIterable, Identifiable { - case introduction - case agreement - case blog - - var id: Self { self } - - - var title: String { - switch self { - case .introduction: - return "팀 소개" - case .agreement: - return "팀 약속" - case .blog: - return "팀 블로그" - } - } - - var subtitle: String { - switch self { - case .introduction: - return "우리 팀의 특징과 목표" - case .agreement: - return "함께 지켜나갈 소중한 약속들" - case .blog: - return "팀원들의 블로그 모음" - } - } - - var imageName: String { - switch self { - case .introduction: - return "TeamIntroductionLogo" - case .agreement: - return "TeamAgreementLogo" - case .blog: - return "TeamBlogLogo" - } - } -} struct TeamExploreRowView: View { - - private let item: TeamExploreItem - - init(item: TeamExploreItem) { - self.item = item - } - - var body: some View { - HStack { - Image(item.imageName) - .frame(width: 42, height: 42) - VStack(alignment: .leading, spacing: 4) { - Text(item.title) - .pretendardFont(family: .semiBold, size: 14) - .foregroundStyle(.textPrimary) - - Text(item.subtitle) - .pretendardFont(family: .regular, size: 12) - .foregroundStyle(.textSecondary) - } - - Spacer() - - Image("ArrowRight") - .foregroundStyle(Color(hex: "717182")) - } - .padding(16) - .background(Color.staticWhite) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .shadow(radius: 1) + + private let item: TeamExploreItem + + init(item: TeamExploreItem) { + self.item = item + } + + var body: some View { + HStack { + Image(asset: item.imageName) + .frame(width: 42, height: 42) + + VStack(alignment: .leading, spacing: 4) { + Text(item.title) + .pretendardFont(family: .semiBold, size: 14) + .foregroundStyle(.textPrimary) + + Text(item.subtitle) + .pretendardFont(family: .regular, size: 12) + .foregroundStyle(.textSecondary) + } + + Spacer() + + Image(asset: .arrowRight) + .foregroundStyle(Color(hex: "717182")) } + .padding(16) + .background(Color.staticWhite) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .shadow(radius: 1) + } } #Preview { - VStack { - TeamExploreRowView(item: .introduction) - TeamExploreRowView(item: .agreement) - TeamExploreRowView(item: .blog) - } + VStack { + TeamExploreRowView(item: .introduction) + TeamExploreRowView(item: .agreement) + TeamExploreRowView(item: .blog) + } } diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/ContentView.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/ContentView.swift index 6b8e037..0a8d72b 100644 --- a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/ContentView.swift +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/ContentView.swift @@ -13,13 +13,22 @@ struct ContentView: View { @Query private var items: [Item] @EnvironmentObject private var coordinator: IntroduceCoordinator - var body: some View { - VStack { - Text("main") - .onTapGesture { - coordinator.send(.present(.teamAgreement)) - } + ZStack { + Color.white + .edgesIgnoringSafeArea(.all) + + VStack { + Spacer() + + Text("main") + + Spacer() + + + Spacer() + .frame(height: 20) + } } } @@ -39,6 +48,12 @@ struct ContentView: View { } } + +extension ContentView { + + +} + #Preview { ContentView() .modelContainer(for: Item.self, inMemory: true) diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/IntroductionMainView.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/IntroductionMainView.swift index af42da3..6b793b1 100644 --- a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/IntroductionMainView.swift +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/IntroductionMainView.swift @@ -8,54 +8,58 @@ import SwiftUI struct IntroductionMainView: View { - @ObservedObject var viewModel: IntroductionViewModel - + @State private var viewModel: IntroductionViewModel + init(viewModel: IntroductionViewModel) { - self.viewModel = viewModel + _viewModel = State(initialValue: viewModel) } - var body: some View { - ScrollView { - VStack(spacing: 16) { - MissionView() - - VStack { - Text("팀원 소개") - .pretendardFont(family: .bold, size: 14) - .foregroundStyle(.textPrimary) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 8) - if viewModel.isLoading { - ForEach(0..<3, id: \.self) { _ in - SkeletonRowView() - } - } else { - ForEach(viewModel.introductions) { model in - IntroductionRowView(model: model) - } - } - } - - VStack { - Text("더 알아보기") - .pretendardFont(family: .bold, size: 14) - .foregroundStyle(.textPrimary) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 8) - - ForEach(TeamExploreItem.allCases) { item in - TeamExploreRowView(item: item) - } - } + + var body: some View { + ScrollView { + VStack(spacing: 16) { + MissionView() + + VStack { + Text("팀원 소개") + .pretendardFont(family: .bold, size: 14) + .foregroundStyle(.textPrimary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 8) + if viewModel.isLoading { + ForEach(0..<3, id: \.self) { _ in + SkeletonRowView() } - .padding(.horizontal, 16) - .onAppear { - viewModel.onAppear() + } else { + ForEach(viewModel.introductions) { model in + IntroductionRowView(model: model) } + } } + + VStack { + Text("더 알아보기") + .pretendardFont(family: .bold, size: 14) + .foregroundStyle(.textPrimary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 8) + + ForEach(TeamExploreItem.allCases) { item in + TeamExploreRowView(item: item) + .onTapGesture { + viewModel.send(.tapMoreInfo(item)) + } + } + } + } + .padding(.horizontal, 16) + } + .onAppear { + viewModel.send(.onAppear) } + } } #Preview { - IntroductionMainView(viewModel: IntroductionViewModel()) + IntroductionMainView(viewModel: IntroductionViewModel()) } diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/IntroductionViewModel.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/IntroductionViewModel.swift deleted file mode 100644 index 4fbf61a..0000000 --- a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/View/IntroductionViewModel.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// IntroductionViewModel.swift -// TeamIntroduce -// -// Created by 홍석현 on 8/11/25. -// - -import Foundation -import SwiftUI - -@MainActor -final class IntroductionViewModel: ObservableObject { - - // MARK: - Published Properties - @Published private(set) var introductions: [IntroductionRowModel] = [] - @Published private(set) var isLoading = false - - // MARK: - Public Methods - - /// onAppear 시 호출되는 데이터 페치 메서드 - func onAppear() { - Task { - await fetchIntroductions() - } - } - - /// 데이터를 새로고침하는 메서드 - func refresh() { - Task { - await fetchIntroductions() - } - } - - // MARK: - Private Methods - - /// 팀원 소개 데이터를 가져오는 메서드 - private func fetchIntroductions() async { - isLoading = true - - do { - // 실제 API 호출 시뮬레이션 (2초 딜레이) - try await Task.sleep(nanoseconds: 2_000_000_000) - - // Mock 데이터 로드 - introductions = IntroductionRowModel.mockData - - } catch { - introductions = [] - } - - isLoading = false - } -} diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/ViewModel/IntroductionViewModel.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/ViewModel/IntroductionViewModel.swift new file mode 100644 index 0000000..0bc1c23 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/IntroduceMain/ViewModel/IntroductionViewModel.swift @@ -0,0 +1,76 @@ +// +// IntroductionViewModel.swift +// TeamIntroduce +// +// Created by 홍석현 on 8/11/25. +// + +import Foundation +import SwiftUI +import Combine + +@MainActor +@Observable +final class IntroductionViewModel { + + // MARK: - State + private(set) var introductions: [IntroductionRowModel] = [] + private(set) var isLoading = false + + // 네비게이션을 코디네이터로 위임하는 라우팅 클로저 + private let route: (IntroduceCoordinator.Action) -> Void + + // MARK: - Action (뷰에서 이 enum만 쓰면 됩니다) + enum Action { + case onAppear + case refresh + case tapMoreInfo(TeamExploreItem) + } + + // MARK: - Init + /// 기본값: no-op(코디네이터 주입 안 해도 안전) + init(route: @escaping (IntroduceCoordinator.Action) -> Void = { _ in }) { + self.route = route + } + + /// 편의 이니셜라이저: 코디네이터 주입 + convenience init(coordinator: IntroduceCoordinator?) { + if let coordinator { + self.init(route: { [weak coordinator] action in coordinator?.send(action) }) + } else { + self.init() // no-op + } + } + + // MARK: - Single entrypoint + func send(_ action: Action) { + switch action { + case .onAppear, .refresh: + Task { await fetchIntroductions() } + + case .tapMoreInfo(let moreInfo): + switch moreInfo { + case .introduction: + route(.present(.teamIntroduce)) + case .agreement: + route(.present(.teamAgreement)) + case .blog: + route(.present(.teamBlog)) + } + } + } + + // MARK: - Private + private func fetchIntroductions() async { + isLoading = true + defer { isLoading = false } + + do { + // mock delay + try await Task.sleep(nanoseconds: 2_000_000_000) + introductions = IntroductionRowModel.mockData + } catch { + introductions = [] + } + } +} diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/Root/RootView.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/Root/RootView.swift index f1ce0c8..51db37e 100644 --- a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/Root/RootView.swift +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/Root/RootView.swift @@ -8,13 +8,15 @@ import SwiftUI struct RootView: View { - @StateObject var coordinator = IntroduceCoordinator() +// @StateObject var coordinator = IntroduceCoordinator() var body: some View { IntorduceCoordinatorView() - .environmentObject(coordinator) +// .environmentObject(coordinator) } } #Preview { RootView() } + + diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/TeamBlog/BlogItem.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/TeamBlog/BlogItem.swift new file mode 100644 index 0000000..49247a9 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/TeamBlog/BlogItem.swift @@ -0,0 +1,15 @@ +// +// BlogItem.swift +// TeamIntroduce +// +// Created by Wonji Suh on 8/12/25. +// + +import Foundation + +struct BlogItem: Identifiable { + let id = UUID() + let name: String + let blogTitle: String + let blogLink: String +} diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/TeamBlog/TeamBlogView.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/TeamBlog/TeamBlogView.swift new file mode 100644 index 0000000..ca13ab7 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/TeamBlog/TeamBlogView.swift @@ -0,0 +1,212 @@ +// +// TeamBlogView.swift +// TeamIntroduce +// +// Created by Wonji Suh on 8/12/25. +// + +import SwiftUI + +import SwiftUI + +struct TeamBlogView: View { + @Bindable var viewModel: TeamBlogViewModel + + init(viewModel: TeamBlogViewModel) { + self.viewModel = viewModel + } + + var body: some View { + ZStack { + Color.white + .edgesIgnoringSafeArea(.all) + + VStack { + Spacer().frame(height: 14) + + CustomNavigationBackBar(text: "팀블로그") { + viewModel.send(.backToRoot) + } + + Spacer().frame(height: 20) + + blogHeaderView() + + Spacer().frame(height: 10) + + blogList() + + Spacer() + + blogHintBanner() + + Spacer().frame(height: 30) + } + } + .onAppear { + viewModel.send(.onAppear) + } + } +} + +extension TeamBlogView { + + @ViewBuilder + private func blogHeaderView() -> some View { + VStack(alignment: .center) { + Spacer().frame(height: 16) + + Circle() + .fill(.gray40) + .frame(width: 56, height: 56) + .overlay { + Image(asset: .glabal) + .resizable() + .scaledToFit() + .frame(width: 30, height: 30) + } + + Spacer().frame(height: 10) + + HStack { + Spacer() + Text("팀원들의 블로그") + .pretendardFont(family: .regular, size: 13) + .foregroundStyle(.staticBlack) + Spacer() + } + + Spacer().frame(height: 10) + + Text("각자 공부한 내용및 경험을 공유 하는 공간입니다.") + .pretendardFont(family: .regular, size: 13) + .foregroundStyle(.blueGray) + + Spacer().frame(height: 16) + + } + .background( + RoundedRectangle(cornerRadius: 12) + .fill(.staticWhite) + .shadow(color: .shadowColor, radius: 2) + ) + .padding(.horizontal, 16) + } + + // 💡 순차 애니메이션 blog 리스트 + @ViewBuilder + private func blogList() -> some View { + if !viewModel.isLoading { + + + VStack(spacing: 12) { + ForEach(Array(viewModel.blogs.indices), id: \.self) { index in + let blog = viewModel.blogs[index] + + blogListitem( + name: blog.name, + blogTitle: blog.blogTitle, + blogLink: blog.blogLink + ) { link in + viewModel.send(.presentWebView(url: link)) + } + .opacity(index <= viewModel.currentMaxIndex ? 1 : 0) + .offset(y: index <= viewModel.currentMaxIndex ? 0 : 20) + .onAppear { + guard index > viewModel.currentMaxIndex else { return } + let delay = 0.25 + 0.12 * Double(index) // ⏱ 첫 대기, 카드 간 텀 조정 + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + withAnimation(.spring(response: 0.8, dampingFraction: 0.85)) { + viewModel.currentMaxIndex = index + } + } + } + } + } + } else { + ForEach(0..<3, id: \.self) { _ in + SkeletonRowView() + } + } + } + + @ViewBuilder + private func blogListitem( + name: String, + blogTitle: String, + blogLink: String, + action: @escaping (String) -> Void + ) -> some View { + VStack { + HStack { + Circle() + .fill(.gray.opacity(0.3)) + .frame(width: 40, height: 40) + + VStack(alignment: .leading) { + HStack { + Text(name) + .pretendardFont(family: .regular, size: 12) + .foregroundStyle(.textSecondary) + + Image(asset: .link) + .resizable() + .scaledToFit() + .frame(width: 15, height: 15) + .onTapGesture { + action(blogLink) + } + + Spacer() + } + + Text(blogTitle) + .pretendardFont(family: .regular, size: 12) + .foregroundStyle(.textGray100) + + Text(blogLink) + .pretendardFont(family: .light, size: 14) + .foregroundStyle(.basicBlack) + .onTapGesture { + action(blogLink) + } + } + + Spacer() + } + .padding(16) + } + .background( + RoundedRectangle(cornerRadius: 12) + .fill(.staticWhite) + .shadow(color: .shadowColor, radius: 2) + ) + .padding(.horizontal, 16) + } + + @ViewBuilder + private func blogHintBanner() -> some View { + VStack { + HStack { + Spacer() + + Text("💡 블로그 링크를 탭하면 새 탭에서 열립니다") + .pretendardFont(family: .light, size: 12) + .foregroundStyle(.basicBlack) + + Spacer() + } + .padding(.vertical, 16) + } + .background( + RoundedRectangle(cornerRadius: 12) + .fill(.staticWhite) + .shadow(color: .shadowColor, radius: 2) + ) + .padding(.horizontal, 16) + } +} + +#Preview { + TeamBlogView(viewModel: .init()) +} diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/TeamBlog/TeamBlogViewModel.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/TeamBlog/TeamBlogViewModel.swift new file mode 100644 index 0000000..a4db07d --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/TeamBlog/TeamBlogViewModel.swift @@ -0,0 +1,85 @@ +// +// TeamBlogViewModel.swift +// TeamIntroduce +// +// Created by Wonji Suh on 8/12/25. +// + +import SwiftUI +import Combine + +import SwiftUI +import Observation + +@MainActor +@Observable +final class TeamBlogViewModel { + private(set) var isLoading = false + var currentMaxIndex: Int = -1 + + // 라우팅 + private let route: (IntroduceCoordinator.Action) -> Void + private let goBack: () -> Void + + let blogs: [BlogItem] = [ + .init(name: "김민희", + blogTitle: "모바일개발과 크로스플랫폼 기술을 공유합니다", + blogLink: "https://0minnie0.tistory.com/"), + .init(name: "서원지", + blogTitle: "모바일개발과 크로스플랫폼 기술을 공유합니다", + blogLink: "https://velog.io/@suhwj/posts"), + .init(name: "홍석현", + blogTitle: "모바일개발과 크로스플랫폼 기술을 공유합니다", + blogLink: "https://velog.io/@gustjrghd/posts") + ] + + // MARK: - Init + init( + route: @escaping (IntroduceCoordinator.Action) -> Void = { _ in }, + goBack: @escaping () -> Void = {} + ) { + self.route = route + self.goBack = goBack + } + + // 코디네이터 주입 편의 생성자 + convenience init(coordinator: IntroduceCoordinator?) { + if let coordinator { + self.init( + route: { [weak coordinator] action in coordinator?.send(action) }, + goBack: { [weak coordinator] in coordinator?.goBack() } + ) + } else { + self.init() + } + } + + // MARK: - Action + enum Action { + case onAppear + case refresh + case presentWebView(url: String) + case backToRoot + } + + // MARK: - Single entrypoint + func send(_ action: Action) { + switch action { + case .onAppear, .refresh: + Task { await fetchIntroductions() } + + case .presentWebView(let url): + route(.present(.webView(url: url))) + + case .backToRoot: + goBack() + } + } + + // MARK: - Private + private func fetchIntroductions() async { + isLoading = true + defer { isLoading = false } + try? await Task.sleep(for: .seconds(0.3)) + } +} diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/TeamIntroduce/IntroduceItem.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/TeamIntroduce/IntroduceItem.swift new file mode 100644 index 0000000..7503d88 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/TeamIntroduce/IntroduceItem.swift @@ -0,0 +1,15 @@ +// +// IntroduceItem.swift +// TeamIntroduce +// +// Created by Wonji Suh on 8/12/25. +// + +import Foundation + +struct IntroduceItem: Identifiable { + let id = UUID() + let image: ImageAsset + let title: String + let subtitle: String +} diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/TeamIntroduce/TeamIntroduceView.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/TeamIntroduce/TeamIntroduceView.swift new file mode 100644 index 0000000..00b2c79 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/TeamIntroduce/TeamIntroduceView.swift @@ -0,0 +1,170 @@ +// +// TeamIntroduceView.swift +// TeamIntroduce +// +// Created by Wonji Suh on 8/12/25. +// + +import SwiftUI + + +struct TeamIntroduceView: View { + @ObservedObject var coordinator: IntroduceCoordinator + + // 현재까지 보여줄 수 있는 최대 인덱스 + @State private var currentMaxIndex: Int = -1 + + // 소개 아이템 배열 + private let introduceItems: [IntroduceItem] = [ + .init(image: .teamInfroducePerson, title: "다양성 존중", subtitle: "각자의 강점과 개성을 인정하고 서로 보완하며 성장합니다."), + .init(image: .teamInfroduceAccident, title: "창의적 사고", subtitle: "새로운 아이디어를 자유롭게 제안하고 실험하는 문화를 추구 합니다."), + .init(image: .teamInfroduceHeart, title: "따뜻한 소통", subtitle: "솔직하고 건설적인 피드백으로 서로를 도우며 성장합니다."), + .init(image: .teamInfroduceCircle, title: "목표지향", subtitle: "명확한 목표를 설정하고 함께 달성해나가는 팀워크를 발휘합니다.") + ] + + var body: some View { + ZStack { + Color.staticWhite + .edgesIgnoringSafeArea(.all) + + VStack { + Spacer().frame(height: 14) + + CustomNavigationBackBar(text: "팀소개") { + coordinator.goBack() + } + + Spacer().frame(height: 20) + + teamIntorduceHeader() + teamIntroduceList() + introduceList() + + Spacer() + } + } + } +} + +extension TeamIntroduceView { + // 타이틀 박스 + @ViewBuilder + private func teamIntorduceHeader() -> some View { + VStack { + Text("우리 팀의 궁극적인 목표") + .pretendardFont(family: .semiBold, size: 16) + .foregroundStyle(.gray60) + + Image(asset: .teamiIntroduce) + .resizable() + .scaledToFit() + .frame(width: 56, height: 56) + + Spacer().frame(height: 10) + + HStack { + Spacer() + TypingText( + text: "안녕하세요 1조입니다! 👋", + font: .pretendardFontFamily(family: .bold, size: 16), + perChar: 0.06, + startDelay: 0.15, + showsCursor: false + ) + Spacer() + } + } + .padding(16) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(.staticWhite) + .shadow(color: .shadowColor, radius: 2) + ) + .padding(.horizontal, 16) + } + + // 팀 특징 타이틀 + @ViewBuilder + fileprivate func teamIntroduceList() -> some View { + HStack { + Text("우리 팀만의 특징") + .pretendardFont(family: .regular, size: 16) + .foregroundStyle(.basicBlack) + Spacer() + } + .padding(16) + } + + // 애니메이션되며 등장하는 소개 리스트 + @ViewBuilder + private func introduceList() -> some View { + let indices = Array(introduceItems.indices) + + VStack(spacing: 12) { + ForEach(indices, id: \.self) { index in + let item = introduceItems[index] + + introduceItem( + image: item.image, + title: item.title, + subtitle: item.subtitle + ) + .opacity(index <= currentMaxIndex ? 1 : 0) + .offset(y: index <= currentMaxIndex ? 0 : 12) + .onAppear { + // 이미 등장한 인덱스는 무시 + guard index > currentMaxIndex else { return } + let delay = 0.25 + 0.15 * Double(index) + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + withAnimation(.spring(response: 0.5, dampingFraction: 0.85)) { + currentMaxIndex = index + } + } + } + } + } + } + + // 개별 아이템 뷰 + @ViewBuilder + fileprivate func introduceItem( + image: ImageAsset, + title: String, + subtitle: String + ) -> some View { + VStack { + HStack { + Image(asset: image) + .resizable() + .scaledToFit() + .frame(width: 35, height: 35) + + Spacer().frame(width: 10) + + VStack(alignment: .leading) { + Text(title) + .pretendardFont(family: .regular, size: 12) + .foregroundStyle(.textSecondary100) + Spacer().frame(height: 4) + Text(subtitle) + .pretendardFont(family: .regular, size: 14) + .foregroundStyle(.textPrimary) + } + + Spacer() + } + .padding(16) + } + .background( + RoundedRectangle(cornerRadius: 12) + .fill(.staticWhite) + .shadow(color: .shadowColor, radius: 2) + ) + .padding(.horizontal, 16) + } +} + +#Preview { + @Previewable @StateObject var coordinator: IntroduceCoordinator = IntroduceCoordinator() + TeamIntroduceView(coordinator: coordinator) +} diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/TeamIntroduce/TypingText.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/TeamIntroduce/TypingText.swift new file mode 100644 index 0000000..646fae4 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/TeamIntroduce/TypingText.swift @@ -0,0 +1,62 @@ +// +// TypingText.swift +// TeamIntroduce +// +// Created by Wonji Suh on 8/12/25. +// + +import SwiftUI + +struct TypingText: View { + let text: String + var font: Font = .pretendardFontFamily(family: .semiBold, size: 20) + var perChar: Double = 0.05 // 글자당 지연(초) + var startDelay: Double = 0.0 // 시작 지연(초) + var showsCursor: Bool = true // 커서 표시 여부 + + @Environment(\.accessibilityReduceMotion) private var reduceMotion + @State private var displayed: String = "" + @State private var blink = false + @State private var task: Task? + + var body: some View { + HStack(spacing: 0) { + Text(displayed).font(font) + + if showsCursor { + Text("|") + .font(font) + .opacity(blink ? 0 : 1) + .animation(.easeInOut(duration: 0.6).repeatForever(), value: blink) + .accessibilityHidden(true) + } + } + .onAppear { startTyping() } + .onChange(of: text) { _ , _ in startTyping() } + .onDisappear { task?.cancel() } + } + + private func startTyping() { + task?.cancel() + + guard !reduceMotion else { + displayed = text + blink.toggle() + return + } + + displayed = "" + blink = false + + task = Task { + if startDelay > 0 { + try? await Task.sleep(nanoseconds: UInt64(startDelay * 1_000_000_000)) + } + for ch in text { + displayed.append(ch) + try? await Task.sleep(nanoseconds: UInt64(perChar * 1_000_000_000)) + } + await MainActor.run { blink = true } + } + } +} diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/WebView/WebRepresentableView.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/WebView/WebRepresentableView.swift new file mode 100644 index 0000000..8ca5a64 --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/WebView/WebRepresentableView.swift @@ -0,0 +1,144 @@ +// +// WebRepresentableView.swift +// TeamIntroduce +// +// Created by Wonji Suh on 8/12/25. +// + +import SwiftUI +import WebKit + + +import SwiftUI +import WebKit + + struct WebRepresentableView: UIViewRepresentable { + + // MARK: - URL to load + var urlToLoad: String + + init(urlToLoad: String) { + self.urlToLoad = urlToLoad + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeUIView(context: Context) -> UIView { + // 컨테이너 + let containerView = UIView() + containerView.backgroundColor = .white + + // WKWebView + let configuration = WKWebViewConfiguration() + let webView = WKWebView(frame: .zero, configuration: configuration) + webView.scrollView.showsVerticalScrollIndicator = false + webView.scrollView.minimumZoomScale = 1.0 + webView.scrollView.maximumZoomScale = 1.0 + webView.navigationDelegate = context.coordinator + webView.uiDelegate = context.coordinator + webView.allowsLinkPreview = true + webView.backgroundColor = .white + webView.translatesAutoresizingMaskIntoConstraints = false + + let spinner = UIActivityIndicatorView(style: .large) + spinner.translatesAutoresizingMaskIntoConstraints = false + spinner.hidesWhenStopped = true + spinner.isHidden = false + + containerView.addSubview(webView) + containerView.addSubview(spinner) + + NSLayoutConstraint.activate([ + // WebView는 전체 + webView.topAnchor.constraint(equalTo: containerView.topAnchor), + webView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), + webView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + webView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + + // 스피너는 중앙 + spinner.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + spinner.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), + ]) + + // 코디네이터가 참조 보관 + context.coordinator.webView = webView + context.coordinator.spinner = spinner + + // 로드 직전에 스피너 시작 + spinner.startAnimating() + spinner.alpha = 1 + + // 로드 + _Concurrency.Task { + await loadURLInWebView(urlToLoad: urlToLoad, webView: webView) + } + + return containerView + } + + func loadURLInWebView(urlToLoad: String, webView: WKWebView) async { + guard let url = URL(string: urlToLoad) else { + print("INVALID URL") + return + } + let request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy) + + await MainActor.run { + webView.configuration.upgradeKnownHostsToHTTPS = true + webView.configuration.preferences.minimumFontSize = 16 + webView.load(request) + } + } + + func updateUIView(_ uiView: UIView, context: Context) { + // 필요 시 업데이트 + } + + // MARK: - Coordinator + class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate { + var parent: WebRepresentableView + weak var webView: WKWebView? + weak var spinner: UIActivityIndicatorView? + + init(_ parent: WebRepresentableView) { + self.parent = parent + } + + // MARK: - WKNavigationDelegate + + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + // 로딩 시작 → 스피너 표시 + DispatchQueue.main.async { [weak self ] in + self?.spinner?.alpha = 1 + self?.spinner?.startAnimating() + } + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + // 로딩 완료 → 스피너 숨김(페이드아웃) + hideSpinner() + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + hideSpinner() + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + hideSpinner() + } + + private func hideSpinner() { + DispatchQueue.main.async { [weak self ] in + guard let spinner = self?.spinner else { return } + UIView.animate(withDuration: 0.2, animations: { + spinner.alpha = 0 + }, completion: { _ in + spinner.stopAnimating() + spinner.alpha = 1 // 다음 로딩 대비 초기화 + }) + } + } + } +} diff --git a/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/WebView/WebView.swift b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/WebView/WebView.swift new file mode 100644 index 0000000..7c4180b --- /dev/null +++ b/TeamIntroduce/TeamIntroduce/Sources/Presnetaion/WebView/WebView.swift @@ -0,0 +1,46 @@ +// +// WebView.swift +// TeamIntroduce +// +// Created by Wonji Suh on 8/12/25. +// + +import SwiftUI + + struct WebView: View { + + @ObservedObject var coordinator: IntroduceCoordinator + var url: String + + init( + coordinator: IntroduceCoordinator, + url: String + ) { + self._coordinator = ObservedObject(wrappedValue: coordinator) + self.url = url + } + + var body: some View { + ZStack { + Color.white + .edgesIgnoringSafeArea(.all) + + VStack { + Spacer() + .frame(height: 14) + + CustomNavigationBackBar(text: ""){ + coordinator.goBack() + } + + Spacer() + .frame(height: 16) + + WebRepresentableView(urlToLoad: url) + } + .navigationBarBackButtonHidden(true) + } + } +} + +