Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
3b87103
[Add] #294 - 퀴즈 화면 ViewController 파일 추가
youz2me Oct 18, 2024
3ab4400
[Feat] #294 - QuizCoordinator 구현 및 퀴즈 완료 상태에 따른 화면 분기 로직 추가
youz2me Oct 19, 2024
2fdc131
[Feat] #294 - TabBarController에 QuizCoordinator 연동 및 탭 추가
youz2me Oct 20, 2024
2f26a57
[Fix] #294 - 퀴즈 탭 선택 시 현재 화면에서 push되도록 네비게이션 로직 수정
youz2me Oct 22, 2025
4ace7b1
[Feat] #294 - 퀴즈 결과 화면 구현 및 보상 수령 후 탭 전환 로직 추가
youz2me Oct 22, 2025
f178172
[Add] #294 - 퀴즈 관련 이미지 및 버튼 에셋 추가
youz2me Oct 22, 2025
590f887
[Chore] #294 - 프로젝트 파일에 퀴즈 관련 파일 추가
youz2me Oct 22, 2025
9cc0715
Merge remote-tracking branch 'refs/remotes/origin/setting/#298-meta'
youz2me Oct 22, 2025
3fc8c51
Merge remote-tracking branch 'origin/develop' into feat/#294-quiz
youz2me Oct 23, 2025
d0347b5
[Add] #294 - 퀴즈 API 관련 DTO 및 TargetType 파일 추가
youz2me Oct 24, 2025
e9f99c4
[Feat] #294 - 퀴즈 도메인 엔티티 및 Repository 구현
youz2me Oct 24, 2025
fabc822
feat: #294 - 퀴즈 화면 UI 구현 및 네비게이션 설정
youz2me Oct 25, 2025
ff44a28
feat: #294 - 퀴즈 뷰모델 Input-Output 패턴 구현
youz2me Oct 25, 2025
9eb5119
feat: #294 - 퀴즈 Repository Mock 구현 및 DI 설정
youz2me Oct 25, 2025
943b82c
[Feat] #294 - QuizResult 엔티티 및 도메인 레이어 구현
youz2me Oct 25, 2025
30b2ae7
[Feat] #294 - 퀴즈 시간 측정 및 MVVM 패턴 개선
youz2me Oct 25, 2025
036fda0
[Feat] #294 - 퀴즈 화면 바인딩 완성 및 UI 구현
youz2me Oct 25, 2025
4d5787a
[Chore] #294 - 불필요한 코드 정리 및 프로젝트 파일 업데이트
youz2me Oct 25, 2025
57cb8cd
[Feat] #294 - 퀴즈 탭 재로드 시 시간 표시 버그 수정
youz2me Oct 25, 2025
8bcb48a
[Feat] #294 - NextQuizInfoViewController UI 구현
youz2me Oct 25, 2025
8a18068
[Feat] #294 - 퀴즈 관련 StringLiterals 추가
youz2me Oct 25, 2025
d3869ff
[Feat] #294 - Pricedown 폰트 지원 추가
youz2me Oct 25, 2025
9edc5ae
[Feat] #294 - Pricedown 폰트 리소스 Info.plist에 등록
youz2me Oct 25, 2025
61457cd
[Feat] #294 - 퀴즈 관련 파일들 프로젝트에 추가
youz2me Oct 25, 2025
24a00fa
[Feat] #294 - UserSession에 quizCompletedAt 프로퍼티 추가
youz2me Oct 25, 2025
bc1823f
[Feat] #294 - UserSessionRepository에 퀴즈 완료 시간 관리 기능 추가
youz2me Oct 25, 2025
70857e3
[Feat] #294 - Login 및 Agreement ViewModel에 quizCompletedAt 매개변수 추가
youz2me Oct 25, 2025
602b484
[Feat] #294 - TabBarController에 퀴즈 완료 상태 확인 로직 구현
youz2me Oct 25, 2025
7803894
[Feat] #294 - QuizViewModel에 퀴즈 완료 시간 저장 기능 추가
youz2me Oct 25, 2025
5731b9d
[Feat] #294 - UseCase 및 NavigationView 퀴즈 지원 추가
youz2me Oct 25, 2025
5c860d3
[Chore] #294 - 퀴즈 파일 구조 변경
youz2me Oct 26, 2025
07620c8
[Fix] #294 - 퀴즈 풀이 완료 후 지난 시간을 반영할 수 있도록 구현
youz2me Oct 26, 2025
5fc3d3d
[Fix] #294 - 리뷰 반영
youz2me Oct 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 148 additions & 1 deletion Wable-iOS.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion Wable-iOS/App/AppDelegate+InjectDependency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,21 @@ extension AppDelegate {
}

// MARK: - Community

diContainer.register(for: CommunityRepository.self) { env in
switch env {
case .mock: MockCommunityRepository()
case .production: CommunityRepositoryImpl()
}
}

// MARK: - Quiz

diContainer.register(for: QuizRepository.self) { env in
switch env {
case .mock: MockQuizRepository()
case .production: QuizRepositoryImpl()
}
}
}
}
26 changes: 26 additions & 0 deletions Wable-iOS/Core/Literals/String/StringLiterals+Quiz.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// StringLiterals+Quiz.swift
// Wable-iOS
//
// Created by Youjin Lee on 10/26/25.
//

import Foundation

extension StringLiterals {

// MARK: - Quiz

enum Quiz {
static let correctTitle = "걸어다니는 LCK 백과사전"
static let correctDescription = "정답을 맞추셨어요. 대단해요!"
static let incorrectTitle = "LCK의 평범한 시청자!"
static let incorrectDescription = "정답은 틀렸지만, 좋은 시도였어요!\n다음 기회를 노려봐요"
static let nextQuizTitle = "다음 퀴즈 참여까지\n남은 시간"
static let feedbackTitle = "와블에 대한 새로운 의견이 있다면? →"
static let loadingErrorTitle = "퀴즈 로딩 중 오류가 발생했어요"
static let rewardErrorTitle = "리워드 처리 중 오류가 발생했어요"
static let loadingErrorMessage = "다시 시도해주세요."

}
}
29 changes: 29 additions & 0 deletions Wable-iOS/Data/Mapper/QuizMapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// QuizMapper.swift
// Wable-iOS
//
// Created by Youjin Lee on 10/25/25.
//

import Foundation

enum QuizMapper { }

extension QuizMapper {
static func toDomain(_ response: DTO.Response.FetchQuiz) -> Quiz {
return Quiz(
id: response.id,
imageURL: response.imageURL,
text: response.text,
answer: response.answer
)
}

static func toDomain(_ response: DTO.Response.UpdateQuizGradeResponse) -> QuizResult {
return QuizResult(
isCorrect: response.answer,
topPercent: response.topPercent,
continueDay: response.continueDay
)
}
}
67 changes: 67 additions & 0 deletions Wable-iOS/Data/RepositoryImpl/QuizRepositoryImpl.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// QuizRepositoryImpl.swift
// Wable-iOS
//
// Created by Youjin Lee on 10/25/25.
//

import Combine
import Foundation

import CombineMoya
import Moya

final class QuizRepositoryImpl {
private let provider: APIProvider<QuizTargetType>

init(provider: APIProvider<QuizTargetType> = .init()) {
self.provider = provider
}
}

extension QuizRepositoryImpl: QuizRepository {
func updateQuizAnswer(id: Int, answer: Bool, totalTime: Int) -> AnyPublisher<QuizResult, WableError> {
let request = DTO.Request.UpdateQuizGradeRequest(
id: id,
answer: answer,
totalTime: totalTime
)

return provider.request(.updateQuizGrade(request: request), for: DTO.Response.UpdateQuizGradeResponse.self)
.map(QuizMapper.toDomain)
.mapWableError()
}

func fetchQuiz() -> AnyPublisher<Quiz, WableError> {
return provider.request(.fetchQuiz, for: DTO.Response.FetchQuiz.self)
.map(QuizMapper.toDomain)
.mapWableError()
}
}

struct MockQuizRepository: QuizRepository {
private var randomDelay: TimeInterval { Double.random(in: 0.7...1.3) }

func updateQuizAnswer(id: Int, answer: Bool, totalTime: Int) -> AnyPublisher<QuizResult, WableError> {
return .just(QuizResult(
isCorrect: false,
topPercent: Int.random(in: 1...99),
continueDay: Int.random(in: 1...99)
))
.delay(for: .seconds(randomDelay), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}

func fetchQuiz() -> AnyPublisher<Quiz, WableError> {
return .just(
Quiz(
id: Int.random(in: 1...9999),
imageURL: "https://i.ytimg.com/vi/yebNIHKAC4A/hqdefault.jpg",
text: "이 룬은 칼날비라는 룬으로 적에게 기본 공격을 3번 가하면 일정 시간 동안 공격 속도가 크게 증가하는 룬이다.",
answer: true
)
)
.delay(for: .seconds(randomDelay), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
6 changes: 4 additions & 2 deletions Wable-iOS/Data/RepositoryImpl/UserSessionRepositoryImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ extension UserSessionRepositoryImpl: UserSessionRepository {
isPushAlarmAllowed: Bool? = nil,
isAdmin: Bool? = nil,
isAutoLoginEnabled: Bool? = nil,
notificationBadgeCount: Int? = nil
notificationBadgeCount: Int? = nil,
quizCompletedAt: Date? = nil
) {
var sessions = fetchAllUserSessions()
let existingSession = sessions[userID]
Expand All @@ -66,7 +67,8 @@ extension UserSessionRepositoryImpl: UserSessionRepository {
isPushAlarmAllowed: isPushAlarmAllowed ?? existingSession?.isPushAlarmAllowed ?? false,
isAdmin: isAdmin ?? existingSession?.isAdmin ?? false,
isAutoLoginEnabled: isAutoLoginEnabled ?? existingSession?.isAutoLoginEnabled ?? true,
notificationBadgeCount: notificationBadgeCount ?? existingSession?.notificationBadgeCount ?? 0
notificationBadgeCount: notificationBadgeCount ?? existingSession?.notificationBadgeCount ?? 0,
quizCompletedAt: quizCompletedAt ?? existingSession?.quizCompletedAt
)

sessions[userID] = updatedSession
Expand Down
15 changes: 15 additions & 0 deletions Wable-iOS/Domain/Entity/Quiz.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// Quiz.swift
// Wable-iOS
//
// Created by Youjin Lee on 10/25/25.
//

import Foundation

struct Quiz: Hashable {
let id: Int
let imageURL: String
let text: String
let answer: Bool
}
14 changes: 14 additions & 0 deletions Wable-iOS/Domain/Entity/QuizResult.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// QuizResult.swift
// Wable-iOS
//
// Created by Youjin Lee on 10/26/25.
//

import Foundation

public struct QuizResult {
let isCorrect: Bool
let topPercent: Int
let continueDay: Int
}
1 change: 1 addition & 0 deletions Wable-iOS/Domain/Entity/UserSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ struct UserSession: Codable, Identifiable {
let isAdmin: Bool
let isAutoLoginEnabled: Bool?
let notificationBadgeCount: Int?
var quizCompletedAt: Date?
}
14 changes: 14 additions & 0 deletions Wable-iOS/Domain/RepositoryInterface/QuizRepository.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// QuizRepository.swift
// Wable-iOS
//
// Created by Youjin Lee on 10/25/25.
//

import Combine
import Foundation

protocol QuizRepository {
func updateQuizAnswer(id: Int, answer: Bool, totalTime: Int) -> AnyPublisher<QuizResult, WableError>
func fetchQuiz() -> AnyPublisher<Quiz, WableError>
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ protocol UserSessionRepository {
isPushAlarmAllowed: Bool?,
isAdmin: Bool?,
isAutoLoginEnabled: Bool?,
notificationBadgeCount: Int?)
notificationBadgeCount: Int?,
quizCompletedAt: Date?)
func updateActiveUserID(_ userID: Int?)
func removeUserSession(forUserID userID: Int)
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ extension FetchUserInformationUseCase {
isPushAlarmAllowed: Bool? = nil,
isAdmin: Bool? = nil,
isAutoLoginEnabled: Bool? = nil,
notificationBadgeCount: Int? = nil
notificationBadgeCount: Int? = nil,
quizCompletedAt: Date? = nil
) -> AnyPublisher<Void, Never> {
return Just(
repository.updateUserSession(
Expand All @@ -55,7 +56,8 @@ extension FetchUserInformationUseCase {
isPushAlarmAllowed: isPushAlarmAllowed,
isAdmin: isAdmin,
isAutoLoginEnabled: isAutoLoginEnabled,
notificationBadgeCount: notificationBadgeCount
notificationBadgeCount: notificationBadgeCount,
quizCompletedAt: quizCompletedAt
)
)
.eraseToAnyPublisher()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// UpdateQuizGradeRequest.swift
// Wable-iOS
//
// Created by Youjin Lee on 10/24/25.
//

import Foundation

extension DTO.Request {
struct UpdateQuizGradeRequest: Encodable {
let id: Int
let answer: Bool
let totalTime: Int

enum CodingKeys: String, CodingKey {
case id = "quizId"
case answer = "userAnswer"
case totalTime = "quizTime"
}
}
}
24 changes: 24 additions & 0 deletions Wable-iOS/Infra/Network/DTO/Response/Quiz/FetchQuiz.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// FetchQuiz.swift
// Wable-iOS
//
// Created by Youjin Lee on 10/24/25.
//

import Foundation

extension DTO.Response {
struct FetchQuiz: Decodable {
let id: Int
let imageURL: String
let text: String
let answer: Bool

enum CodingKeys: String, CodingKey {
case id = "quizId"
case imageURL = "quizImage"
case text = "quizText"
case answer = "quizAnswer"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// UpdateQuizGradeResponse.swift
// Wable-iOS
//
// Created by Youjin Lee on 10/24/25.
//

import Foundation

extension DTO.Response {
struct UpdateQuizGradeResponse: Decodable {
let answer: Bool
let topPercent: Int
let continueDay: Int

enum CodingKeys: String, CodingKey {
case answer = "quizResult"
case topPercent = "userPercent"
case continueDay = "continueNumber"
}
}
}
54 changes: 54 additions & 0 deletions Wable-iOS/Infra/Network/TargetType/QuizTargetType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// QuizTargetType.swift
// Wable-iOS
//
// Created by Youjin Lee on 10/24/25.
//

import Combine
import Foundation

import Moya

enum QuizTargetType {
case fetchQuiz
case updateQuizGrade(request: DTO.Request.UpdateQuizGradeRequest)
}

extension QuizTargetType: BaseTargetType {
var endPoint: String? {
switch self {
case .fetchQuiz:
"/v1/quiz"
case .updateQuizGrade:
"/v1/quiz/grade"
}
}

var query: [String : Any]? {
return .none
}

var requestBody: (any Encodable)? {
switch self {
case .fetchQuiz:
return .none
case .updateQuizGrade(request: let request):
return request
}
}

var multipartFormData: [Moya.MultipartFormData]? {
return .none
}

var method: Moya.Method {
switch self {
case .fetchQuiz:
return .get
case .updateQuizGrade:
return .patch
}
}
}

Loading