diff --git a/JLPTVoca/JLPTVoca/Assets.xcassets/Image/favouriteIconActivated.imageset/Contents.json b/JLPTVoca/JLPTVoca/Assets.xcassets/Image/favoriteIconActivated.imageset/Contents.json similarity index 100% rename from JLPTVoca/JLPTVoca/Assets.xcassets/Image/favouriteIconActivated.imageset/Contents.json rename to JLPTVoca/JLPTVoca/Assets.xcassets/Image/favoriteIconActivated.imageset/Contents.json diff --git a/JLPTVoca/JLPTVoca/Assets.xcassets/Image/favouriteIconActivated.imageset/favouriteIconActivated.png b/JLPTVoca/JLPTVoca/Assets.xcassets/Image/favoriteIconActivated.imageset/favouriteIconActivated.png similarity index 100% rename from JLPTVoca/JLPTVoca/Assets.xcassets/Image/favouriteIconActivated.imageset/favouriteIconActivated.png rename to JLPTVoca/JLPTVoca/Assets.xcassets/Image/favoriteIconActivated.imageset/favouriteIconActivated.png diff --git a/JLPTVoca/JLPTVoca/Assets.xcassets/Image/favouriteIconInactivated.imageset/Contents.json b/JLPTVoca/JLPTVoca/Assets.xcassets/Image/favoriteIconInactivated.imageset/Contents.json similarity index 100% rename from JLPTVoca/JLPTVoca/Assets.xcassets/Image/favouriteIconInactivated.imageset/Contents.json rename to JLPTVoca/JLPTVoca/Assets.xcassets/Image/favoriteIconInactivated.imageset/Contents.json diff --git a/JLPTVoca/JLPTVoca/Assets.xcassets/Image/favouriteIconInactivated.imageset/favouriteIconInactivated.png b/JLPTVoca/JLPTVoca/Assets.xcassets/Image/favoriteIconInactivated.imageset/favouriteIconInactivated.png similarity index 100% rename from JLPTVoca/JLPTVoca/Assets.xcassets/Image/favouriteIconInactivated.imageset/favouriteIconInactivated.png rename to JLPTVoca/JLPTVoca/Assets.xcassets/Image/favoriteIconInactivated.imageset/favouriteIconInactivated.png diff --git a/JLPTVoca/JLPTVoca/Assets.xcassets/Image/wordCardBlue.imageset/Contents.json b/JLPTVoca/JLPTVoca/Assets.xcassets/Image/wordCardBlue.imageset/Contents.json index a859662..59ff646 100644 --- a/JLPTVoca/JLPTVoca/Assets.xcassets/Image/wordCardBlue.imageset/Contents.json +++ b/JLPTVoca/JLPTVoca/Assets.xcassets/Image/wordCardBlue.imageset/Contents.json @@ -1,11 +1,11 @@ { "images" : [ { + "filename" : "wordCardBlue.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "wordCardBlue.png", "idiom" : "universal", "scale" : "2x" }, diff --git a/JLPTVoca/JLPTVoca/Assets.xcassets/Image/wordCardBlue.imageset/wordCardBlue.png b/JLPTVoca/JLPTVoca/Assets.xcassets/Image/wordCardBlue.imageset/wordCardBlue.png index fcc0b76..7cf7c0f 100644 Binary files a/JLPTVoca/JLPTVoca/Assets.xcassets/Image/wordCardBlue.imageset/wordCardBlue.png and b/JLPTVoca/JLPTVoca/Assets.xcassets/Image/wordCardBlue.imageset/wordCardBlue.png differ diff --git a/JLPTVoca/JLPTVoca/Assets.xcassets/Image/wordCardRed.imageset/Contents.json b/JLPTVoca/JLPTVoca/Assets.xcassets/Image/wordCardRed.imageset/Contents.json index 1ee208d..bfa353b 100644 --- a/JLPTVoca/JLPTVoca/Assets.xcassets/Image/wordCardRed.imageset/Contents.json +++ b/JLPTVoca/JLPTVoca/Assets.xcassets/Image/wordCardRed.imageset/Contents.json @@ -1,11 +1,11 @@ { "images" : [ { + "filename" : "wordCardRed.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "wordCardRed.png", "idiom" : "universal", "scale" : "2x" }, diff --git a/JLPTVoca/JLPTVoca/Assets.xcassets/Image/wordCardRed.imageset/wordCardRed.png b/JLPTVoca/JLPTVoca/Assets.xcassets/Image/wordCardRed.imageset/wordCardRed.png index 28b2650..de7599f 100644 Binary files a/JLPTVoca/JLPTVoca/Assets.xcassets/Image/wordCardRed.imageset/wordCardRed.png and b/JLPTVoca/JLPTVoca/Assets.xcassets/Image/wordCardRed.imageset/wordCardRed.png differ diff --git a/JLPTVoca/JLPTVoca/Constants/StringConstants.swift b/JLPTVoca/JLPTVoca/Constants/StringConstants.swift index e849667..8aedb17 100644 --- a/JLPTVoca/JLPTVoca/Constants/StringConstants.swift +++ b/JLPTVoca/JLPTVoca/Constants/StringConstants.swift @@ -22,3 +22,7 @@ enum DetailChart { enum SpeechBubble { static let main = "단어의 시작은 집중력!" } + +enum QuitStudy { + static let title = "오늘의 단어 학습을 마쳤어요!" +} diff --git a/JLPTVoca/JLPTVoca/ContentView.swift b/JLPTVoca/JLPTVoca/ContentView.swift index 6cb6090..73b1d43 100644 --- a/JLPTVoca/JLPTVoca/ContentView.swift +++ b/JLPTVoca/JLPTVoca/ContentView.swift @@ -20,7 +20,7 @@ struct ContentView: View { Label("학습", systemImage: "house.fill") } - DictionaryView() + SelectDictionaryView() .tabItem { Label("사전", systemImage: "book.fill") } diff --git a/JLPTVoca/JLPTVoca/Views/Dictionary/DictionaryView.swift b/JLPTVoca/JLPTVoca/Views/Dictionary/DictionaryView.swift index 2459ee6..a5d3088 100644 --- a/JLPTVoca/JLPTVoca/Views/Dictionary/DictionaryView.swift +++ b/JLPTVoca/JLPTVoca/Views/Dictionary/DictionaryView.swift @@ -10,23 +10,22 @@ import SwiftUI struct DictionaryView: View { @Environment(WordManager.self) private var wordManager + let type: DictionaryType + var body: some View { - NavigationStack { - List(wordManager.studyStateDeck) { state in - VStack(alignment: .leading) { - Text(state.word.plainJapanese) - .font(.headline) - Text(state.word.korean) - .font(.subheadline) - .foregroundStyle(.secondary) - } - } - .navigationTitle("JLPT Dictionary") - } + DictionaryWordCard( + japanese: "四字熟語", + furigana: "よじじゅくご", + korean: "사자성어", + level: 111, + maturity: 2222222, + isFavorite: true + ) + .overlay( + RoundedRectangle(cornerRadius: 15) + .stroke( + Color.black50, + lineWidth: 1 + )) } } - -#Preview { - DictionaryView() - .environment(WordManager()) -} diff --git a/JLPTVoca/JLPTVoca/Views/Dictionary/DictionaryWordCard.swift b/JLPTVoca/JLPTVoca/Views/Dictionary/DictionaryWordCard.swift new file mode 100644 index 0000000..cc349c8 --- /dev/null +++ b/JLPTVoca/JLPTVoca/Views/Dictionary/DictionaryWordCard.swift @@ -0,0 +1,84 @@ +// +// DictionaryWordCard.swift +// JLPTVoca +// +// Created by Rama on 9/20/25. +// + +import SwiftUI + +struct DictionaryWordCard: View { + @State private var isFavorited = false + + var japanese: String + var furigana: String + var korean: String + var level: Int + var maturity: Int + var isFavorite: Bool + + var body: some View { + Button(action: { }) { + HStack { + wordContent() + Spacer() + wordInfo() + } + .padding(12) + } + } + + private func wordContent() -> some View { + VStack(alignment: .leading) { + HStack { + Text("\(japanese)") + .font(.wdl) + .foregroundStyle(Color.black100) + .padding(.trailing, 12) + + Text("\(furigana)") + .font(.furi) + .foregroundStyle(Color.black50) + } + .padding(.bottom, 8) + + Text("\(korean)") + .font(.b4) + .foregroundStyle(Color.black100) + } + } + + private func wordInfo() -> some View { + VStack(alignment: .trailing, spacing: 12) { + HStack { + Text("N\(level)") + .font(.min) + .foregroundStyle(Color.black70) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.black.opacity(0.05)) + .clipShape(RoundedRectangle(cornerRadius: 6)) + + HStack(spacing: 4) { + Circle() + .fill(Color.cyan) + .frame(width: 8, height: 8) + + Text("\(maturity)") + .font(.min) + .foregroundStyle(Color.black70) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.black.opacity(0.05)) + .clipShape(RoundedRectangle(cornerRadius: 6)) + } + + Spacer().frame(height: 12) + + FavoriteButton( + isFavorited: isFavorited, + action: { }) + } + } +} diff --git a/JLPTVoca/JLPTVoca/Views/Dictionary/DictionaryWordListView.swift b/JLPTVoca/JLPTVoca/Views/Dictionary/DictionaryWordListView.swift deleted file mode 100644 index 9d3f2ae..0000000 --- a/JLPTVoca/JLPTVoca/Views/Dictionary/DictionaryWordListView.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// DictionaryWordListView.swift -// JLPTVoca -// -// Created by Rama on 8/27/25. -// - -import SwiftUI - -struct DictionaryWordListView: View { - var body: some View { - Text("DictionaryWordListView") - } -} - -#Preview { - DictionaryWordListView() -} diff --git a/JLPTVoca/JLPTVoca/Views/Dictionary/SelectDictionaryView.swift b/JLPTVoca/JLPTVoca/Views/Dictionary/SelectDictionaryView.swift new file mode 100644 index 0000000..adb872c --- /dev/null +++ b/JLPTVoca/JLPTVoca/Views/Dictionary/SelectDictionaryView.swift @@ -0,0 +1,113 @@ +// +// SelectDictionaryView.swift +// JLPTVoca +// +// Created by Rama on 9/19/25. +// + +import SwiftUI + +enum SelectDictConstants { + static let imageSize: CGFloat = 48 + static let imageLeadingPadding: CGFloat = 14 + static let imageTrailingPadding: CGFloat = 24 + static let imageVerticalPadding: CGFloat = 10 + + static let iconWidth: CGFloat = 10 + static let iconHeight: CGFloat = 18 + static let iconTrailingPadding: CGFloat = 30 + + static let buttonPadding: CGFloat = 16 + static let buttonRadius: CGFloat = 10 + + static let tableVStackSpacing: CGFloat = 16 +} + +enum DictionaryType { + case entire + case favorite + + var title: String { + switch self { + case .entire: + return "전체 단어" + case .favorite: + return "즐겨찾기 한 단어" + } + } + + var image: Image { + switch self { + case .entire: + return Image(.wordCardRed) + case .favorite: + return Image(.wordCardBlue) + } + } +} + +struct SelectDictionaryView: View { + @State private var router = NavigationManager() + + var body: some View { + NavigationStack(path: $router.path) { + ZStack(alignment: .topLeading) { + Color.black5.ignoresSafeArea() + + Group { + VStack(alignment: .leading ,spacing: SelectDictConstants.tableVStackSpacing) { + dictionaryTypeButton(type: .entire) + dictionaryTypeButton(type: .favorite) + } + } + .padding(.horizontal, SelectDictConstants.buttonPadding) + } + .navigationDestination(for: DictionaryRoute.self) { route in + switch route { + case .entireWords: + DictionaryView(type: .entire) + case .favoriteWords: + DictionaryView(type: .favorite) + } + } + } + .environment(router) + } + + private func dictionaryTypeButton(type: DictionaryType) -> some View { + return Button(action: { + switch type { + case .entire: + router.navigate(.entireWords) + case .favorite: + router.navigate(.favoriteWords) + } + }) { + HStack { + type.image + .resizable() + .frame(width: SelectDictConstants.imageSize, height: SelectDictConstants.imageSize) + .padding(.leading, SelectDictConstants.imageLeadingPadding) + .padding(.trailing, SelectDictConstants.imageTrailingPadding) + .padding(.vertical, SelectDictConstants.imageVerticalPadding) + + Text(type.title) + .font(.b4) + .foregroundStyle(Color.black100) + + Spacer() + + Image(systemName: "chevron.right") + .foregroundStyle(Color.black50) + .frame(width: SelectDictConstants.iconWidth, height: SelectDictConstants.iconHeight) + .padding(.trailing, SelectDictConstants.iconTrailingPadding) + } + } + .background(Color.white0) + .clipShape(RoundedRectangle(cornerRadius: SelectDictConstants.buttonRadius)) + } +} + +#Preview { + SelectDictionaryView() +} diff --git a/JLPTVoca/JLPTVoca/Views/Study/CustomBackButton.swift b/JLPTVoca/JLPTVoca/Views/Study/CustomBackButton.swift deleted file mode 100644 index 48f15a6..0000000 --- a/JLPTVoca/JLPTVoca/Views/Study/CustomBackButton.swift +++ /dev/null @@ -1,15 +0,0 @@ -import SwiftUI - -struct CustomBackButton: ToolbarContent { - @Binding var showAlert: Bool - - var body: some ToolbarContent { - ToolbarItem(placement: .navigationBarLeading) { - Button { - showAlert = true - } label: { - Image(systemName: "chevron.backward") - } - } - } -} diff --git a/JLPTVoca/JLPTVoca/Views/Study/FavoriteButton.swift b/JLPTVoca/JLPTVoca/Views/Study/FavoriteButton.swift new file mode 100644 index 0000000..bf6e87a --- /dev/null +++ b/JLPTVoca/JLPTVoca/Views/Study/FavoriteButton.swift @@ -0,0 +1,25 @@ +// +// FavoriteButton.swift +// JLPTVoca +// +// Created by Rama on 9/20/25. +// + +import SwiftUI + +enum FavoriteButtonConstant { + static let ImageSize: CGFloat = 30 +} + +struct FavoriteButton: View { + let isFavorited: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + Image(isFavorited ? .favoriteIconActivated : .favoriteIconInactivated) + .resizable() + .frame(width: FavoriteButtonConstant.ImageSize, height: FavoriteButtonConstant.ImageSize) + } + } +} diff --git a/JLPTVoca/JLPTVoca/Views/Study/StudyAlertButtons.swift b/JLPTVoca/JLPTVoca/Views/Study/StudyAlertButtons.swift deleted file mode 100644 index 5679b94..0000000 --- a/JLPTVoca/JLPTVoca/Views/Study/StudyAlertButtons.swift +++ /dev/null @@ -1,17 +0,0 @@ -import SwiftUI - -struct StudyAlertButtons: View { - @Environment(NavigationManager.self) private var router - - var body: some View { - Button("취소", role: .cancel) { } - Button("종료", role: .destructive) { - router.pop() - } - } -} - -#Preview { - StudyAlertButtons() - .environment(NavigationManager()) -} diff --git a/JLPTVoca/JLPTVoca/Views/Study/StudyCompletionView.swift b/JLPTVoca/JLPTVoca/Views/Study/StudyCompletionView.swift deleted file mode 100644 index b04ffbc..0000000 --- a/JLPTVoca/JLPTVoca/Views/Study/StudyCompletionView.swift +++ /dev/null @@ -1,23 +0,0 @@ -import SwiftUI - -struct StudyCompletionView: View { - @Environment(NavigationManager.self) private var router - - var body: some View { - ZStack { - Color.black.opacity(0.5) - .ignoresSafeArea() - - Button(action: { - router.path.removeLast() - }) { - Text("홈으로 돌아가기") - } - } - } -} - -#Preview { - StudyCompletionView() - .environment(NavigationManager()) -} diff --git a/JLPTVoca/JLPTVoca/Views/Study/StudyPopupType.swift b/JLPTVoca/JLPTVoca/Views/Study/StudyPopupType.swift new file mode 100644 index 0000000..e0504dd --- /dev/null +++ b/JLPTVoca/JLPTVoca/Views/Study/StudyPopupType.swift @@ -0,0 +1,61 @@ +// +// StudyPopupType.swift +// JLPTVoca +// +// Created by Rama on 9/18/25. +// + +import SwiftUI + +enum PopupType: Equatable { + case exitConfirmation + case studyCompletion( + studiedCount: Int, + uncertainCount: Int + ) + + var title: String { + switch self { + case .exitConfirmation: + return "학습을 중단할까요?" + case .studyCompletion(_, _): + return "오늘의 단어 학습을 마쳤어요!" + } + } + + var image: Image { + switch self { + case .exitConfirmation: + return Image(.catSad) + case .studyCompletion(_, _): + return Image(.catHappy) + } + } + + var message: String { + switch self { + case .exitConfirmation: + return "지금 중단하면 학습한 내용이 저장되지 않아요" + case .studyCompletion(let studiedCount, let uncertainCount): + return "외운 단어: \(studiedCount)\n학습할 단어: \(uncertainCount)" + } + } + + var primaryButtonTitle: String { + switch self { + case .exitConfirmation: + return "이어서 하기" + case .studyCompletion(_, _): + return "홈으로 돌아가기" + } + } + + var primaryButtonRole: ButtonRole? { + switch self { + case .exitConfirmation: + return .destructive + case .studyCompletion(_, _): + return .none + } + } +} diff --git a/JLPTVoca/JLPTVoca/Views/Study/StudyPopupView.swift b/JLPTVoca/JLPTVoca/Views/Study/StudyPopupView.swift new file mode 100644 index 0000000..97a7174 --- /dev/null +++ b/JLPTVoca/JLPTVoca/Views/Study/StudyPopupView.swift @@ -0,0 +1,89 @@ +import SwiftUI + +struct StudyPopupView: View { + @Binding var isPresented: Bool + let popupType: PopupType + + let completeAction: () -> Void + let cancelAction: (() -> Void)? + + var body: some View { + ZStack { + Color.black.opacity(0.7) + .ignoresSafeArea() + .onTapGesture { + if case .studyCompletion = popupType { + isPresented = false + } + } + + VStack(spacing: 0) { + popupType.image + .resizable() + .frame( + width: 173, + height: 173 + ) + .padding(.top, 6) + .padding(.bottom, 25) + + Text(popupType.title) + .font(.b3) + .foregroundStyle(Color.black100) + .padding(.bottom, 14) + + Text(popupType.message) + .font(.b4) + .foregroundStyle(Color.black50) + .multilineTextAlignment(.center) + .padding(.bottom, 32) + + HStack(spacing: 25) { + if let cancelAction = cancelAction { + Button(action: { + cancelAction() + }) { + Text("나가기") + .padding(.horizontal, 20) + .padding(.vertical, 16) + .font(.b2) + .foregroundStyle(Color.black70) + } + .overlay( + RoundedRectangle(cornerRadius: 15) + .stroke( + Color.black50, + lineWidth: 1 + ) + ) + } + + Button(action: { + completeAction() + }) { + Text(popupType.primaryButtonTitle) + .padding(.horizontal, 20) + .padding(.vertical, 16) + .font(.b2) + .foregroundStyle(Color.white0) + } + .background(Color.coral50) + .clipShape(RoundedRectangle(cornerRadius: 15)) + } + } + .padding(30) + .background(Color.white) + .cornerRadius(15) + .padding(.horizontal, 24) + } + } +} + +#Preview { + StudyPopupView( + isPresented: .constant(true), + popupType: .studyCompletion(studiedCount: 30, uncertainCount: 10), + completeAction: { print("Primary Action") }, + cancelAction: nil + ) +} diff --git a/JLPTVoca/JLPTVoca/Views/Study/WordCardView.swift b/JLPTVoca/JLPTVoca/Views/Study/WordCardView.swift index 88a866e..09089e2 100644 --- a/JLPTVoca/JLPTVoca/Views/Study/WordCardView.swift +++ b/JLPTVoca/JLPTVoca/Views/Study/WordCardView.swift @@ -91,13 +91,9 @@ struct WordCardView: View { VStack { HStack { - Button(action: { - isFavorited.toggle() - }) { - Image(isFavorited ? "favouriteIconActivated" : "favouriteIconInactivated") - .resizable() - .frame(width: 30, height: 30) - } + FavoriteButton( + isFavorited: isFavorited, + action: { }) .padding(24) Spacer() diff --git a/JLPTVoca/JLPTVoca/Views/Study/WordStudyView.swift b/JLPTVoca/JLPTVoca/Views/Study/WordStudyView.swift index 08c758b..6c6222b 100644 --- a/JLPTVoca/JLPTVoca/Views/Study/WordStudyView.swift +++ b/JLPTVoca/JLPTVoca/Views/Study/WordStudyView.swift @@ -12,8 +12,7 @@ struct WordStudyView: View { @Environment(WordManager.self) private var wordManager @Environment(NavigationManager.self) private var router - @State private var showAlert = false - @State private var showCompletionModal = false + @State private var isPopupPresented: PopupType? var body: some View { ZStack { @@ -31,24 +30,38 @@ struct WordStudyView: View { FilterButtons() } - if showCompletionModal { - StudyCompletionView() + if let popupType = isPopupPresented { + StudyPopupView( + isPresented: Binding( + get: { isPopupPresented != nil }, + set: { if !$0 { isPopupPresented = nil } } + ), + popupType: popupType, + completeAction: { router.pop() }, + cancelAction: popupType == .exitConfirmation ? { isPopupPresented = nil } : nil + ) } } + .navigationBarBackButtonHidden(true) .toolbar(.hidden, for: .tabBar) - .toolbar { CustomBackButton(showAlert: $showAlert) } - .alert( - "학습 종료", - isPresented: $showAlert, - actions: { - StudyAlertButtons() - }, message: { - Text("7년 연습생 하고 집에 갈래?") - }) - .onChange(of: wordManager.studyStateDeck) { _, newDeck in + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + isPopupPresented = .exitConfirmation + } label: { + Image(systemName: "chevron.backward") + } + } + } + .onChange(of: wordManager.studyStateDeck) { + _, + newDeck in if newDeck.isEmpty { - showCompletionModal = true + isPopupPresented = .studyCompletion( + studiedCount: 30, + uncertainCount: 10 + ) } } }