diff --git a/dogether/Data/DataSources/ChallengeGroupsDataSource.swift b/dogether/Data/DataSources/ChallengeGroupsDataSource.swift index ffbb43b0..5facb687 100644 --- a/dogether/Data/DataSources/ChallengeGroupsDataSource.swift +++ b/dogether/Data/DataSources/ChallengeGroupsDataSource.swift @@ -17,18 +17,18 @@ final class ChallengeGroupsDataSource { } func getMemberTodos(groupId: String, memberId: String) async throws -> GetMemberTodosResponse { - try await NetworkManager.shared.request(ChallengeGroupsRouter.getMemberTodos(groupId: groupId, memberId: memberId)) + try await NetworkManager.shared.request( + ChallengeGroupsRouter.getMemberTodos(groupId: groupId, memberId: memberId) + ) } func createTodos(groupId: String, createTodosRequest: CreateTodosRequest) async throws { - try await NetworkManager.shared.request(ChallengeGroupsRouter.createTodos(groupId: groupId, createTodosRequest: createTodosRequest)) + try await NetworkManager.shared.request( + ChallengeGroupsRouter.createTodos(groupId: groupId, createTodosRequest: createTodosRequest) + ) } - func certifyTodo(todoId: String, certifyTodoRequest: CertifyTodoRequest) async throws { - try await NetworkManager.shared.request(ChallengeGroupsRouter.certifyTodo(todoId: todoId, certifyTodoRequest: certifyTodoRequest)) - } - - func readTodo(todoId: String) async throws { - try await NetworkManager.shared.request(ChallengeGroupsRouter.readTodo(todoId: todoId)) + func readTodo(todoHistoryId: String) async throws { + try await NetworkManager.shared.request(ChallengeGroupsRouter.readTodo(todoHistoryId: todoHistoryId)) } } diff --git a/dogether/Data/DataSources/TodosDataSource.swift b/dogether/Data/DataSources/TodosDataSource.swift new file mode 100644 index 00000000..25e2c247 --- /dev/null +++ b/dogether/Data/DataSources/TodosDataSource.swift @@ -0,0 +1,26 @@ +// +// TodosDataSource.swift +// dogether +// +// Created by seungyooooong on 1/3/26. +// + +import Foundation + +final class TodosDataSource { + static let shared = TodosDataSource() + + private init() { } + + func certifyTodo(todoId: String, certifyTodoRequest: CertifyTodoRequest) async throws { + try await NetworkManager.shared.request( + TodosRouter.certifyTodo(todoId: todoId, certifyTodoRequest: certifyTodoRequest) + ) + } + + func remindTodo(todoId: String, remindTodoRequest: RemindTodoRequest) async throws { + try await NetworkManager.shared.request( + TodosRouter.remindTodo(todoId: todoId, remindTodoRequest: remindTodoRequest) + ) + } +} diff --git a/dogether/Data/DataSources/UserDataSource.swift b/dogether/Data/DataSources/UserDataSource.swift index 138dec39..2a37f413 100644 --- a/dogether/Data/DataSources/UserDataSource.swift +++ b/dogether/Data/DataSources/UserDataSource.swift @@ -24,6 +24,11 @@ final class UserDataSource { try await NetworkManager.shared.request(UserRouter.getMyCertificationStats(groupId: groupId)) } + + func getMyActivityFromTodo(todoId: Int, sort: String) async throws -> GetMyActivityFromTodoResponse { + try await NetworkManager.shared.request(UserRouter.getMyActivityFromTodo(todoId: todoId, sort: sort)) + } + func getMyProfile() async throws -> GetMyProfileResponse { try await NetworkManager.shared.request(UserRouter.getMyProfile) } diff --git a/dogether/Data/Network/Request/RemindTodoRequest.swift b/dogether/Data/Network/Request/RemindTodoRequest.swift new file mode 100644 index 00000000..9422385c --- /dev/null +++ b/dogether/Data/Network/Request/RemindTodoRequest.swift @@ -0,0 +1,12 @@ +// +// RemindTodoRequest.swift +// dogether +// +// Created by seungyooooong on 1/3/26. +// + +import Foundation + +struct RemindTodoRequest: Encodable { + let reminderType: String +} diff --git a/dogether/Data/Network/Response/GetMemberTodosResponse.swift b/dogether/Data/Network/Response/GetMemberTodosResponse.swift index 7f5b9cab..25ca01af 100644 --- a/dogether/Data/Network/Response/GetMemberTodosResponse.swift +++ b/dogether/Data/Network/Response/GetMemberTodosResponse.swift @@ -8,31 +8,41 @@ import Foundation struct GetMemberTodosResponse: Decodable { + let isMine: Bool let currentTodoHistoryToReadIndex: Int - let todos: [MemberTodo] + let todos: [TodoEntityInGetMemberTodos] } -struct MemberTodo: Decodable { - let id: Int +struct TodoEntityInGetMemberTodos: Decodable { + let historyId: Int + let todoId: Int let content: String let status: String + let canRequestCertification: Bool + let canRequestCertificationReview: Bool let certificationContent: String? let certificationMediaUrl: String? let isRead: Bool let reviewFeedback: String? init( - id: Int, + historyId: Int, + todoId: Int, content: String, status: String, + canRequestCertification: Bool, + canRequestCertificationReview: Bool, certificationContent: String? = nil, certificationMediaUrl: String? = nil, isRead: Bool, reviewFeedback: String? = nil ) { - self.id = id + self.historyId = historyId + self.todoId = todoId self.content = content self.status = status + self.canRequestCertification = canRequestCertification + self.canRequestCertificationReview = canRequestCertificationReview self.certificationContent = certificationContent self.certificationMediaUrl = certificationMediaUrl self.isRead = isRead diff --git a/dogether/Data/Network/Response/GetMyActivityFromTodoResponse.swift b/dogether/Data/Network/Response/GetMyActivityFromTodoResponse.swift new file mode 100644 index 00000000..6e0c35d7 --- /dev/null +++ b/dogether/Data/Network/Response/GetMyActivityFromTodoResponse.swift @@ -0,0 +1,23 @@ +// +// GetMyActivityFromTodoResponse.swift +// dogether +// +// Created by seungyooooong on 1/20/26. +// + +import Foundation + +struct GetMyActivityFromTodoResponse: Decodable { + let certifications: [CertificationEntityInGetMyActivityFromTodoResponse] +} + + +struct CertificationEntityInGetMyActivityFromTodoResponse: Decodable { + let id: Int + let content: String + var status: String + var canRequestCertificationReview: Bool + var certificationContent: String + var certificationMediaUrl: String + var reviewFeedback: String? +} diff --git a/dogether/Data/Network/Response/GetMyTodosResponse.swift b/dogether/Data/Network/Response/GetMyTodosResponse.swift index d3dbed9e..439dc35b 100644 --- a/dogether/Data/Network/Response/GetMyTodosResponse.swift +++ b/dogether/Data/Network/Response/GetMyTodosResponse.swift @@ -15,6 +15,7 @@ struct TodoEntityInGetMyTodos: Decodable { let id: Int let content: String var status: String + var canRequestCertificationReview: Bool var certificationContent: String? var certificationMediaUrl: String? var reviewFeedback: String? diff --git a/dogether/Data/Network/Router/ChallengeGroupsRouter.swift b/dogether/Data/Network/Router/ChallengeGroupsRouter.swift index 6329310b..c50a3438 100644 --- a/dogether/Data/Network/Router/ChallengeGroupsRouter.swift +++ b/dogether/Data/Network/Router/ChallengeGroupsRouter.swift @@ -9,26 +9,23 @@ import Foundation enum ChallengeGroupsRouter: NetworkEndpoint { case createTodos(groupId: String, createTodosRequest: CreateTodosRequest) - case certifyTodo(todoId: String, certifyTodoRequest: CertifyTodoRequest) case getMyTodos(groupId: String, date: String) case getMyYesterdayTodos case getMemberTodos(groupId: String, memberId: String) - case readTodo(todoId: String) + case readTodo(todoHistoryId: String) var path: String { switch self { case .createTodos(let groupId, _): return Path.api + Path.v1 + Path.challengeGroups + "/\(groupId)/todos" - case .certifyTodo(let todoId, _): // FIXME: 추후 TodosRouter 분리 - return Path.api + Path.v1 + Path.todos + "/\(todoId)/certify" case .getMyTodos(let groupId, _): - return Path.api + Path.v1 + Path.challengeGroups + "/\(groupId)/my-todos" + return Path.api + Path.v2 + Path.challengeGroups + "/\(groupId)/my-todos" case .getMyYesterdayTodos: return Path.api + Path.v1 + Path.challengeGroups + "/my/yesterday" case .getMemberTodos(let groupId, let memberId): - return Path.api + Path.v1 + Path.challengeGroups + "/\(groupId)/challenge-group-members/\(memberId)/today-todo-history" - case .readTodo(let todoId): // FIXME: 추후 TodoHistoryRouter 분리 - return Path.api + Path.v1 + Path.todoHistory + "/\(todoId)" + return Path.api + Path.v2 + Path.challengeGroups + "/\(groupId)/challenge-group-members/\(memberId)/today-todo-history" + case .readTodo(let todoHistoryId): // FIXME: 추후 TodoHistoryRouter 분리 + return Path.api + Path.v1 + Path.todoHistory + "/\(todoHistoryId)" } } @@ -36,7 +33,7 @@ enum ChallengeGroupsRouter: NetworkEndpoint { switch self { case .getMyTodos, .getMyYesterdayTodos, .getMemberTodos: return .get - case .createTodos, .certifyTodo, .readTodo: + case .createTodos, .readTodo: return .post } } @@ -64,8 +61,6 @@ enum ChallengeGroupsRouter: NetworkEndpoint { switch self { case .createTodos(_, let createTodosRequest): return createTodosRequest - case .certifyTodo(_, let certifyTodoRequest): - return certifyTodoRequest default: return nil } diff --git a/dogether/Data/Network/Router/TodosRouter.swift b/dogether/Data/Network/Router/TodosRouter.swift new file mode 100644 index 00000000..f027bbcf --- /dev/null +++ b/dogether/Data/Network/Router/TodosRouter.swift @@ -0,0 +1,55 @@ +// +// TodosRouter.swift +// dogether +// +// Created by seungyooooong on 1/2/26. +// + +import Foundation + +enum TodosRouter: NetworkEndpoint { + case certifyTodo(todoId: String, certifyTodoRequest: CertifyTodoRequest) + case remindTodo(todoId: String, remindTodoRequest: RemindTodoRequest) + + var path: String { + switch self { + case .certifyTodo(let todoId, _): + return Path.api + Path.v1 + Path.todos + "/\(todoId)/certify" + case .remindTodo(let todoId, _): + return Path.api + Path.v1 + Path.todos + "/\(todoId)/reminders" + } + } + + var method: NetworkMethod { + switch self { + case .certifyTodo, .remindTodo: + return .post + } + } + + var parameters: [URLQueryItem]? { + switch self { + default: + return nil + } + } + + var header: [String : String]? { + switch self { + default: + return [ + Header.Key.contentType: Header.Value.applicationJson, + Header.Key.authorization: Header.Value.bearer + (UserDefaultsManager.shared.accessToken ?? "") + ] + } + } + + var body: (any Encodable)? { + switch self { + case .certifyTodo(_, let certifyTodoRequest): + return certifyTodoRequest + case .remindTodo(_, let remindTodoRequest): + return remindTodoRequest + } + } +} diff --git a/dogether/Data/Network/Router/UserRouter.swift b/dogether/Data/Network/Router/UserRouter.swift index 564ddab6..18cb9470 100644 --- a/dogether/Data/Network/Router/UserRouter.swift +++ b/dogether/Data/Network/Router/UserRouter.swift @@ -11,6 +11,7 @@ enum UserRouter: NetworkEndpoint { case getMyGroupActivity(groupId: Int) case getMyActivity(sort: String, page: String) case getMyCertificationStats(groupId: Int?) + case getMyActivityFromTodo(todoId: Int, sort: String) case getMyProfile var path: String { @@ -21,6 +22,8 @@ enum UserRouter: NetworkEndpoint { return Path.api + Path.v2 + Path.my + "/certifications" case .getMyCertificationStats: return Path.api + Path.v2 + Path.my + "/certification-stats" + case .getMyActivityFromTodo(let todoId, _): + return Path.api + Path.v1 + Path.my + "/activity" + "/todos/\(todoId)" + "/group-certifications" case .getMyProfile: return Path.api + Path.v1 + Path.my + "/profile" } @@ -28,7 +31,7 @@ enum UserRouter: NetworkEndpoint { var method: NetworkMethod { switch self { - case .getMyGroupActivity, .getMyActivity, .getMyCertificationStats, .getMyProfile: + case .getMyGroupActivity, .getMyActivity, .getMyCertificationStats, .getMyActivityFromTodo, .getMyProfile: return .get } } @@ -45,6 +48,10 @@ enum UserRouter: NetworkEndpoint { return [ .init(name: "groupId", value: String(groupId)) ] + case let.getMyActivityFromTodo(_, sort): + return [ + .init(name: "sortBy", value: sort) + ] default: return nil } diff --git a/dogether/Data/Repository/ChallengeGroupsRepository.swift b/dogether/Data/Repository/ChallengeGroupsRepository.swift index 56ac1c9f..ee2911f5 100644 --- a/dogether/Data/Repository/ChallengeGroupsRepository.swift +++ b/dogether/Data/Repository/ChallengeGroupsRepository.swift @@ -25,6 +25,7 @@ final class ChallengeGroupsRepository: ChallengeGroupsProtocol { id: $0.id, content: $0.content, status: TodoStatus(rawValue: $0.status) ?? .waitCertification, + canRemindReview: $0.canRequestCertificationReview, certificationContent: $0.certificationContent, certificationMediaUrl: $0.certificationMediaUrl, reviewFeedback: $0.reviewFeedback @@ -32,31 +33,33 @@ final class ChallengeGroupsRepository: ChallengeGroupsProtocol { } } - func getMemberTodos(groupId: Int, memberId: Int) async throws -> (index: Int, todos: [TodoEntity]) { + func getMemberTodos(groupId: Int, memberId: Int) async throws -> (index: Int, isMine: Bool, todos: [TodoEntity]) { let response = try await challengeGroupsDataSource.getMemberTodos( groupId: String(groupId), memberId: String(memberId) ) + if response.todos.isEmpty { throw NetworkError.noData } + let currentIndex = response.currentTodoHistoryToReadIndex + let isMine = response.isMine let memberTodos = response.todos.map { TodoEntity( - id: $0.id, + historyId: $0.historyId, + id: $0.todoId, content: $0.content, status: TodoStatus(rawValue: $0.status) ?? .waitCertification, thumbnailStatus: $0.isRead ? .done : .yet, + canRemindCertification: $0.canRequestCertification, + canRemindReview: $0.canRequestCertificationReview, certificationContent: $0.certificationContent, certificationMediaUrl: $0.certificationMediaUrl, reviewFeedback: $0.reviewFeedback ) } - return (currentIndex, memberTodos) - } - - func readTodo(todoId: String) async throws { - try await challengeGroupsDataSource.readTodo(todoId: todoId) + return (currentIndex, isMine, memberTodos) } - func certifyTodo(todoId: String, certifyTodoRequest: CertifyTodoRequest) async throws { - try await challengeGroupsDataSource.certifyTodo(todoId: todoId, certifyTodoRequest: certifyTodoRequest) + func readTodo(todoHistoryId: String) async throws { + try await challengeGroupsDataSource.readTodo(todoHistoryId: todoHistoryId) } } diff --git a/dogether/Data/Repository/TodosRepository.swift b/dogether/Data/Repository/TodosRepository.swift new file mode 100644 index 00000000..d08b53c5 --- /dev/null +++ b/dogether/Data/Repository/TodosRepository.swift @@ -0,0 +1,24 @@ +// +// TodosRepository.swift +// dogether +// +// Created by seungyooooong on 1/3/26. +// + +import Foundation + +final class TodosRepository: TodosProtocol { + private let todosDataSource: TodosDataSource + + init(todosDataSource: TodosDataSource = .shared) { + self.todosDataSource = todosDataSource + } + + func certifyTodo(todoId: String, certifyTodoRequest: CertifyTodoRequest) async throws { + try await todosDataSource.certifyTodo(todoId: todoId, certifyTodoRequest: certifyTodoRequest) + } + + func remindTodo(todoId: String, remindTodoRequest: RemindTodoRequest) async throws { + try await todosDataSource.remindTodo(todoId: todoId, remindTodoRequest: remindTodoRequest) + } +} diff --git a/dogether/Data/Repository/UserRepository.swift b/dogether/Data/Repository/UserRepository.swift index ef20cb2c..4dea59db 100644 --- a/dogether/Data/Repository/UserRepository.swift +++ b/dogether/Data/Repository/UserRepository.swift @@ -92,6 +92,21 @@ final class UserRepository: UserProtocol { return (statsViewDatas, certificationListViewDatas) } + func getMyCertifications(todoId: Int, sortOption: SortOptions) async throws -> [TodoEntity] { + let response = try await userDataSource.getMyActivityFromTodo(todoId: todoId, sort: sortOption.sortString) + return response.certifications.map { + TodoEntity( + id: $0.id, + content: $0.content, + status: TodoStatus(rawValue: $0.status) ?? .waitCertification, + canRemindReview: $0.canRequestCertificationReview, + certificationContent: $0.certificationContent, + certificationMediaUrl: $0.certificationMediaUrl, + reviewFeedback: $0.reviewFeedback + ) + } + } + func getProfileViewDatas() async throws -> ProfileViewDatas { let response = try await userDataSource.getMyProfile() return ProfileViewDatas( diff --git a/dogether/Data/RepositoryTest/ChallengeGroupsRepositoryTest.swift b/dogether/Data/RepositoryTest/ChallengeGroupsRepositoryTest.swift index 52fe8dd2..6f61689e 100644 --- a/dogether/Data/RepositoryTest/ChallengeGroupsRepositoryTest.swift +++ b/dogether/Data/RepositoryTest/ChallengeGroupsRepositoryTest.swift @@ -29,8 +29,8 @@ final class ChallengeGroupsRepositoryTest: ChallengeGroupsProtocol { return certify_pendings + review_pendings + approves + rejects } - func getMemberTodos(groupId: Int, memberId: Int) async throws -> (index: Int, todos: [TodoEntity]) { - return (index: 3, todos: [ + func getMemberTodos(groupId: Int, memberId: Int) async throws -> (index: Int, isMine: Bool, todos: [TodoEntity]) { + return (index: 3, isMine: false, todos: [ TodoEntity(id: 1, content: "신규 기능 개발", status: .waitCertification, thumbnailStatus: .done), TodoEntity(id: 2, content: "치킨 먹기", status: .waitCertification, thumbnailStatus: .done, certificationContent: "치킨 냠냠", reviewFeedback: ""), TodoEntity(id: 1, content: "신규 기능 개발", status: .waitCertification, thumbnailStatus: .done, reviewFeedback: "test"), @@ -50,7 +50,5 @@ final class ChallengeGroupsRepositoryTest: ChallengeGroupsProtocol { ]) } - func readTodo(todoId: String) async throws { } - - func certifyTodo(todoId: String, certifyTodoRequest: CertifyTodoRequest) async throws { } + func readTodo(todoHistoryId: String) async throws { } } diff --git a/dogether/Data/RepositoryTest/TodosRepositoryTest.swift b/dogether/Data/RepositoryTest/TodosRepositoryTest.swift new file mode 100644 index 00000000..4e321ae3 --- /dev/null +++ b/dogether/Data/RepositoryTest/TodosRepositoryTest.swift @@ -0,0 +1,14 @@ +// +// TodosRepositoryTest.swift +// dogether +// +// Created by seungyooooong on 1/3/26. +// + +import Foundation + +final class TodosRepositoryTest: TodosProtocol { + func certifyTodo(todoId: String, certifyTodoRequest: CertifyTodoRequest) async throws { } + + func remindTodo(todoId: String, remindTodoRequest: RemindTodoRequest) async throws { } +} diff --git a/dogether/Data/RepositoryTest/UserRepositoryTest.swift b/dogether/Data/RepositoryTest/UserRepositoryTest.swift index 77dde783..838791fd 100644 --- a/dogether/Data/RepositoryTest/UserRepositoryTest.swift +++ b/dogether/Data/RepositoryTest/UserRepositoryTest.swift @@ -110,7 +110,11 @@ final class UserRepositoryTest: UserProtocol { return (statsViewDatas, certificationListViewDatas) } - + + func getMyCertifications(todoId: Int, sortOption: SortOptions) async throws -> [TodoEntity] { + return [] + } + func getProfileViewDatas() async throws -> ProfileViewDatas { return ProfileViewDatas(name: "두식", imageUrl: "") } diff --git a/dogether/Domain/Entity/Enum/RemindTypes.swift b/dogether/Domain/Entity/Enum/RemindTypes.swift new file mode 100644 index 00000000..b77fd5ff --- /dev/null +++ b/dogether/Domain/Entity/Enum/RemindTypes.swift @@ -0,0 +1,13 @@ +// +// RemindTypes.swift +// dogether +// +// Created by seungyooooong on 1/2/26. +// + +import Foundation + +enum RemindTypes: String { + case certification = "TODO_CERTIFICATION" + case review = "TODO_CERTIFICATION_REVIEW" +} diff --git a/dogether/Presentation/Features/CertificationList/Components/CertificationSortOption.swift b/dogether/Domain/Entity/Enum/SortOptions.swift similarity index 87% rename from dogether/Presentation/Features/CertificationList/Components/CertificationSortOption.swift rename to dogether/Domain/Entity/Enum/SortOptions.swift index cf284543..e19371d9 100644 --- a/dogether/Presentation/Features/CertificationList/Components/CertificationSortOption.swift +++ b/dogether/Domain/Entity/Enum/SortOptions.swift @@ -1,8 +1,8 @@ // -// CertificationSortOption.swift +// SortOptions.swift // dogether // -// Created by yujaehong on 5/19/25. +// Created by seungyooooong on 1/19/26. // import Foundation diff --git a/dogether/Domain/Entity/TodoEntity.swift b/dogether/Domain/Entity/TodoEntity.swift index 0168f0bf..ccf1cd23 100644 --- a/dogether/Domain/Entity/TodoEntity.swift +++ b/dogether/Domain/Entity/TodoEntity.swift @@ -8,32 +8,49 @@ import Foundation struct TodoEntity: BaseEntity { + let historyId: Int? // MARK: readTodo에서 사용 let id: Int let content: String var status: TodoStatus var thumbnailStatus: ThumbnailStatus + var canRemindCertification: Bool + var canRemindReview: Bool var certificationContent: String? var certificationMediaUrl: String? var reviewFeedback: String? var createdAt: String? init( + historyId: Int? = nil, id: Int, content: String, status: TodoStatus, thumbnailStatus: ThumbnailStatus = .yet, + canRemindCertification: Bool = false, + canRemindReview: Bool = false, certificationContent: String? = nil, certificationMediaUrl: String? = nil, reviewFeedback: String? = nil, createdAt: String? = nil ) { + self.historyId = historyId self.id = id self.content = content self.status = status self.thumbnailStatus = thumbnailStatus + self.canRemindCertification = canRemindCertification + self.canRemindReview = canRemindReview self.certificationContent = certificationContent self.certificationMediaUrl = certificationMediaUrl self.reviewFeedback = reviewFeedback self.createdAt = createdAt } } + +extension TodoEntity { + func with(createdAt: String) -> Self { + var todo = self + todo.createdAt = createdAt + return todo + } +} diff --git a/dogether/Domain/Entity/ViewDatas/CertificationViewDatas.swift b/dogether/Domain/Entity/ViewDatas/CertificationViewDatas.swift index 99b801ba..9e4fc797 100644 --- a/dogether/Domain/Entity/ViewDatas/CertificationViewDatas.swift +++ b/dogether/Domain/Entity/ViewDatas/CertificationViewDatas.swift @@ -9,22 +9,19 @@ import Foundation struct CertificationViewDatas: BaseEntity { var title: String + var isMine: Bool? var todos: [TodoEntity] var index: Int - var groupId: Int? - var rankingEntity: RankingEntity? init( title: String = "", + isMine: Bool? = nil, todos: [TodoEntity] = [], index: Int = 0, - groupId: Int? = nil, - rankingEntity: RankingEntity? = nil ) { self.title = title + self.isMine = isMine self.todos = todos self.index = index - self.groupId = groupId - self.rankingEntity = rankingEntity } } diff --git a/dogether/Domain/Entity/ViewDatas/PreCertificationViewDatas.swift b/dogether/Domain/Entity/ViewDatas/PreCertificationViewDatas.swift new file mode 100644 index 00000000..0534b8c0 --- /dev/null +++ b/dogether/Domain/Entity/ViewDatas/PreCertificationViewDatas.swift @@ -0,0 +1,14 @@ +// +// PreCertificationViewDatas.swift +// dogether +// +// Created by seungyooooong on 1/19/26. +// + +import Foundation + +enum PreCertificationViewDatas: BaseEntity { + case main(title: String, date: String, groupId: Int, todoId: Int, filter: FilterTypes) + case ranking(title: String, groupId: Int, memberId: Int) + case certificationList(title: String, todoId: Int, sortOption: SortOptions, filter: FilterTypes) +} diff --git a/dogether/Domain/Protocol/ChallengeGroupsProtocol.swift b/dogether/Domain/Protocol/ChallengeGroupsProtocol.swift index 6a5787b2..b9f162dc 100644 --- a/dogether/Domain/Protocol/ChallengeGroupsProtocol.swift +++ b/dogether/Domain/Protocol/ChallengeGroupsProtocol.swift @@ -10,7 +10,6 @@ import Foundation protocol ChallengeGroupsProtocol { func createTodos(groupId: String, createTodosRequest: CreateTodosRequest) async throws func getMyTodos(groupId: String, date: String) async throws -> [TodoEntity] - func getMemberTodos(groupId: Int, memberId: Int) async throws -> (index: Int, todos: [TodoEntity]) - func readTodo(todoId: String) async throws - func certifyTodo(todoId: String, certifyTodoRequest: CertifyTodoRequest) async throws + func getMemberTodos(groupId: Int, memberId: Int) async throws -> (index: Int, isMine: Bool, todos: [TodoEntity]) + func readTodo(todoHistoryId: String) async throws } diff --git a/dogether/Domain/Protocol/TodosProtocol.swift b/dogether/Domain/Protocol/TodosProtocol.swift new file mode 100644 index 00000000..f2c7767a --- /dev/null +++ b/dogether/Domain/Protocol/TodosProtocol.swift @@ -0,0 +1,13 @@ +// +// TodosProtocol.swift +// dogether +// +// Created by seungyooooong on 1/3/26. +// + +import Foundation + +protocol TodosProtocol { + func certifyTodo(todoId: String, certifyTodoRequest: CertifyTodoRequest) async throws + func remindTodo(todoId: String, remindTodoRequest: RemindTodoRequest) async throws +} diff --git a/dogether/Domain/Protocol/UserProtocol.swift b/dogether/Domain/Protocol/UserProtocol.swift index 5889438a..1109ce52 100644 --- a/dogether/Domain/Protocol/UserProtocol.swift +++ b/dogether/Domain/Protocol/UserProtocol.swift @@ -19,5 +19,7 @@ protocol UserProtocol { certificationListViewDatas: CertificationListViewDatas ) + func getMyCertifications(todoId: Int, sortOption: SortOptions) async throws -> [TodoEntity] + func getProfileViewDatas() async throws -> ProfileViewDatas } diff --git a/dogether/Domain/UseCase/ChallengeGroupUseCase.swift b/dogether/Domain/UseCase/ChallengeGroupsUseCase.swift similarity index 69% rename from dogether/Domain/UseCase/ChallengeGroupUseCase.swift rename to dogether/Domain/UseCase/ChallengeGroupsUseCase.swift index 9468f4c1..54eec0d8 100644 --- a/dogether/Domain/UseCase/ChallengeGroupUseCase.swift +++ b/dogether/Domain/UseCase/ChallengeGroupsUseCase.swift @@ -1,5 +1,5 @@ // -// ChallengeGroupUseCase.swift +// ChallengeGroupsUseCase.swift // dogether // // Created by seungyooooong on 3/30/25. @@ -7,7 +7,7 @@ import Foundation -final class ChallengeGroupUseCase { +final class ChallengeGroupsUseCase { private let repository: ChallengeGroupsProtocol init(repository: ChallengeGroupsProtocol) { @@ -20,21 +20,17 @@ final class ChallengeGroupUseCase { } func getMyTodos(groupId: Int, date: String) async throws -> [TodoEntity] { - try await repository.getMyTodos(groupId: String(groupId), date: date) + try await repository.getMyTodos(groupId: String(groupId), date: date).map { $0.with(createdAt: date) } } - func getMemberTodos(groupId: Int, memberId: Int) async throws -> (index: Int, todos: [TodoEntity]) { + func getMemberTodos(groupId: Int, memberId: Int) async throws -> (index: Int, isMine: Bool, todos: [TodoEntity]) { try await repository.getMemberTodos(groupId: groupId, memberId: memberId) } func readTodo(todo: TodoEntity) async throws { + guard let todoHistoryId = todo.historyId else { return } // MARK: 이미 thumbnailStatus 가 done인 투두는 read API 호출할 필요 x if todo.thumbnailStatus == .done { return } - try await repository.readTodo(todoId: String(todo.id)) - } - - func certifyTodo(todoId: Int, content: String, mediaUrl: String) async throws { - let certifyTodoRequest = CertifyTodoRequest(content: content, mediaUrl: mediaUrl) - try await repository.certifyTodo(todoId: String(todoId), certifyTodoRequest: certifyTodoRequest) + try await repository.readTodo(todoHistoryId: String(todoHistoryId)) } } diff --git a/dogether/Domain/UseCase/TodosUseCase.swift b/dogether/Domain/UseCase/TodosUseCase.swift new file mode 100644 index 00000000..52411514 --- /dev/null +++ b/dogether/Domain/UseCase/TodosUseCase.swift @@ -0,0 +1,26 @@ +// +// TodosUseCase.swift +// dogether +// +// Created by seungyooooong on 1/3/26. +// + +import Foundation + +final class TodosUseCase { + private let repository: TodosProtocol + + init(repository: TodosProtocol) { + self.repository = repository + } + + func certifyTodo(todoId: Int, content: String, mediaUrl: String) async throws { + let certifyTodoRequest = CertifyTodoRequest(content: content, mediaUrl: mediaUrl) + try await repository.certifyTodo(todoId: String(todoId), certifyTodoRequest: certifyTodoRequest) + } + + func remindTodo(remindType: RemindTypes, todoId: Int) async throws { + let remindTodoRequest = RemindTodoRequest(reminderType: remindType.rawValue) + try await repository.remindTodo(todoId: String(todoId), remindTodoRequest: remindTodoRequest) + } +} diff --git a/dogether/Domain/UseCase/UserUseCase.swift b/dogether/Domain/UseCase/UserUseCase.swift index df16cd54..3c8e8b70 100644 --- a/dogether/Domain/UseCase/UserUseCase.swift +++ b/dogether/Domain/UseCase/UserUseCase.swift @@ -29,6 +29,10 @@ final class UserUseCase { try await repository.getCertificationListViewDatas(option: option, page: page) } + func getMyCertifications(todoId: Int, sortOption: SortOptions) async throws -> [TodoEntity] { + try await repository.getMyCertifications(todoId: todoId, sortOption: sortOption) + } + func getProfileViewDatas() async throws -> ProfileViewDatas { try await repository.getProfileViewDatas() } diff --git a/dogether/Presentation/Common/DogetherButton.swift b/dogether/Presentation/Common/DogetherButton.swift index 579a32a7..594622d2 100644 --- a/dogether/Presentation/Common/DogetherButton.swift +++ b/dogether/Presentation/Common/DogetherButton.swift @@ -23,6 +23,9 @@ final class DogetherButton: BaseButton { setTitle(title, for: .normal) titleLabel?.font = Fonts.body1B layer.cornerRadius = 8 + + setTitleColor(ButtonStatus.enabled.textColor, for: .normal) + backgroundColor = ButtonStatus.enabled.backgroundColor } override func configureAction() { } @@ -45,8 +48,6 @@ final class DogetherButton: BaseButton { setTitleColor(datas.status.textColor, for: .normal) backgroundColor = datas.status.backgroundColor isEnabled = datas.status == .enabled - - isHidden = datas.isHidden } } } @@ -54,10 +55,8 @@ final class DogetherButton: BaseButton { // MARK: - ViewDatas struct DogetherButtonViewDatas: BaseEntity { var status: ButtonStatus - var isHidden: Bool - init(status: ButtonStatus = .enabled, isHidden: Bool = false) { + init(status: ButtonStatus = .enabled) { self.status = status - self.isHidden = isHidden } } diff --git a/dogether/Presentation/Common/ReviewFeedbackView.swift b/dogether/Presentation/Common/ReviewFeedbackView.swift index a2254a07..b5e2d792 100644 --- a/dogether/Presentation/Common/ReviewFeedbackView.swift +++ b/dogether/Presentation/Common/ReviewFeedbackView.swift @@ -11,6 +11,7 @@ final class ReviewFeedbackView: BaseView { private let reviewFeedbackLabel = UILabel() override func configureView() { + isHidden = true backgroundColor = .grey700 layer.cornerRadius = 8 diff --git a/dogether/Presentation/Features/Certificate/CertificateViewModel.swift b/dogether/Presentation/Features/Certificate/CertificateViewModel.swift index 65d2ac4e..217a373b 100644 --- a/dogether/Presentation/Features/Certificate/CertificateViewModel.swift +++ b/dogether/Presentation/Features/Certificate/CertificateViewModel.swift @@ -10,7 +10,7 @@ import UIKit import RxRelay final class CertificateViewModel { - private let challengeGroupUseCase: ChallengeGroupUseCase + private let todosUseCase: TodosUseCase private(set) var certificateViewDatas = BehaviorRelay(value: CertificateViewDatas()) private(set) var certificateTextViewDatas = BehaviorRelay(value: DogetherTextViewDatas()) @@ -19,8 +19,8 @@ final class CertificateViewModel { ) init() { - let repository = DIManager.shared.getChallengeGroupsRepository() - self.challengeGroupUseCase = ChallengeGroupUseCase(repository: repository) + let todosRepository = DIManager.shared.getTodosRepository() + self.todosUseCase = TodosUseCase(repository: todosRepository) } } @@ -58,6 +58,6 @@ extension CertificateViewModel { let todo = certificateViewDatas.value.todo guard let content = todo.certificationContent, let mediaUrl = todo.certificationMediaUrl else { return } - try await challengeGroupUseCase.certifyTodo(todoId: todo.id, content: content, mediaUrl: mediaUrl) + try await todosUseCase.certifyTodo(todoId: todo.id, content: content, mediaUrl: mediaUrl) } } diff --git a/dogether/Presentation/Features/Certification/CertificationViewController.swift b/dogether/Presentation/Features/Certification/CertificationViewController.swift index 64694b94..d00f15cc 100644 --- a/dogether/Presentation/Features/Certification/CertificationViewController.swift +++ b/dogether/Presentation/Features/Certification/CertificationViewController.swift @@ -17,13 +17,19 @@ final class CertificationViewController: BaseViewController { pages = [certificationPage] super.viewDidLoad() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + loadCertificationView() - onAppear() + coordinator?.updateViewController = loadCertificationView } override func setViewDatas() { - if let datas = datas as? CertificationViewDatas { - viewModel.certificationViewDatas.accept(datas) + if let datas = datas as? PreCertificationViewDatas { + viewModel.preCertificationViewDatas.accept(datas) } bind(viewModel.certificationViewDatas) @@ -31,10 +37,17 @@ final class CertificationViewController: BaseViewController { } extension CertificationViewController { - private func onAppear() { + private func loadCertificationView() { Task { [weak self] in guard let self else { return } - try await viewModel.readTodo() + do { + try await viewModel.loadCertificationView() + try await viewModel.readTodo() + } catch let error as NetworkError { + if case .noData = error { + coordinator?.popViewController() + } + } } } } @@ -45,6 +58,7 @@ protocol CertificationDelegate { func certificationTapAction(_ scrollView: UIScrollView, _ stackView: UIStackView, _ gesture: UITapGestureRecognizer) func certificationListScrollEndAction(index: Int) func goCertificateViewAction(todo: TodoEntity) + func remindTodoAction(remindType: RemindTypes, todoId: Int) } extension CertificationViewController: CertificationDelegate { @@ -98,4 +112,13 @@ extension CertificationViewController: CertificationDelegate { let certificateViewDatas = CertificateViewDatas(todo: todo) coordinator?.pushViewController(certificateImageViewController, datas: certificateViewDatas) } + + func remindTodoAction(remindType: RemindTypes, todoId: Int) { + Task { [weak self] in + guard let self else { return } + try await viewModel.remindTodo(remindType: remindType, todoId: todoId) + // TODO: 재촉 메시지가 전송되었어요 토스트 메시지 노출 + viewModel.updateButtonStatus(remindType: remindType, todoId: todoId) + } + } } diff --git a/dogether/Presentation/Features/Certification/CertificationViewModel.swift b/dogether/Presentation/Features/Certification/CertificationViewModel.swift index 47f4d188..4c6c6a64 100644 --- a/dogether/Presentation/Features/Certification/CertificationViewModel.swift +++ b/dogether/Presentation/Features/Certification/CertificationViewModel.swift @@ -8,19 +8,74 @@ import RxRelay final class CertificationViewModel { - private let challengeGroupsUseCase: ChallengeGroupUseCase + private let challengeGroupsUseCase: ChallengeGroupsUseCase + private let todosUseCase: TodosUseCase + private let userUseCase: UserUseCase + private(set) var preCertificationViewDatas = BehaviorRelay(value: nil) private(set) var certificationViewDatas = BehaviorRelay(value: CertificationViewDatas()) init() { let challengeGroupsRepository = DIManager.shared.getChallengeGroupsRepository() - self.challengeGroupsUseCase = ChallengeGroupUseCase(repository: challengeGroupsRepository) + let todosRepository = DIManager.shared.getTodosRepository() + let userRepository = DIManager.shared.getUserRepository() + + self.challengeGroupsUseCase = ChallengeGroupsUseCase(repository: challengeGroupsRepository) + self.todosUseCase = TodosUseCase(repository: todosRepository) + self.userUseCase = UserUseCase(repository: userRepository) } } extension CertificationViewModel { + func loadCertificationView() async throws { + guard let datas = preCertificationViewDatas.value else { return } + + switch datas { + case .main(let title, let date, let groupId, let todoId, let filter): + let todos = try await getMyTodos(groupId: groupId, date: date, filter: filter) + certificationViewDatas.update { + $0.title = title + $0.index = todos.firstIndex { $0.id == todoId } ?? 0 + $0.todos = todos + } + + case .ranking(let title, let groupId, let memberId): + let (index, isMine, todos) = try await getMemberTodos(groupId: groupId, memberId: memberId) + certificationViewDatas.update { + $0.title = title + $0.isMine = isMine + $0.index = index + $0.todos = todos + } + + case .certificationList(let title, let todoId, let sortOption, let filter): + let todos = try await getMyCertifications(todoId: todoId, sortOption: sortOption, filter: filter) + certificationViewDatas.update { + $0.title = title + $0.index = todos.firstIndex { $0.id == todoId } ?? 0 + $0.todos = todos + } + } + } + + func getMyTodos(groupId: Int, date: String, filter: FilterTypes) async throws -> [TodoEntity] { + try await challengeGroupsUseCase.getMyTodos(groupId: groupId, date: date).filter { + filter == .all || filter == FilterTypes(status: $0.status.rawValue) + } + } + + func getMemberTodos(groupId: Int, memberId: Int) async throws -> (Int, Bool, [TodoEntity]) { + try await challengeGroupsUseCase.getMemberTodos(groupId: groupId, memberId: memberId) + } + + func getMyCertifications(todoId: Int, sortOption: SortOptions, filter: FilterTypes) async throws -> [TodoEntity] { + try await userUseCase.getMyCertifications(todoId: todoId, sortOption: sortOption).filter { + filter == .all || filter == FilterTypes(status: $0.status.rawValue) + } + } + func setIndex(index: Int) async throws { - if certificationViewDatas.value.rankingEntity == nil { + if certificationViewDatas.value.isMine == nil { certificationViewDatas.update { $0.index = index } } else { // MARK: 이전에 보고 있던 thumbnailStatus를 수정하고 이동한 todo의 read API를 호출 @@ -31,9 +86,23 @@ extension CertificationViewModel { } func readTodo(index: Int? = nil) async throws { - if certificationViewDatas.value.rankingEntity == nil { return } - try await challengeGroupsUseCase.readTodo( - todo: certificationViewDatas.value.todos[index ?? certificationViewDatas.value.index] - ) + let index = index ?? certificationViewDatas.value.index + guard let todo = certificationViewDatas.value.todos[safe: index], + case .ranking = preCertificationViewDatas.value else { return } + try await challengeGroupsUseCase.readTodo(todo: todo) + } + + func remindTodo(remindType: RemindTypes, todoId: Int) async throws { + try await todosUseCase.remindTodo(remindType: remindType, todoId: todoId) + } + + func updateButtonStatus(remindType: RemindTypes, todoId: Int) { + guard let index = certificationViewDatas.value.todos.firstIndex(where: { $0.id == todoId }) else { return } + switch remindType { + case .certification: + certificationViewDatas.update { $0.todos[index].canRemindCertification = false } + case .review: + certificationViewDatas.update { $0.todos[index].canRemindReview = false } + } } } diff --git a/dogether/Presentation/Features/Certification/Components/CertificationListView.swift b/dogether/Presentation/Features/Certification/Components/CertificationListView.swift index 62512b44..85642186 100644 --- a/dogether/Presentation/Features/Certification/Components/CertificationListView.swift +++ b/dogether/Presentation/Features/Certification/Components/CertificationListView.swift @@ -19,6 +19,7 @@ final class CertificationListView: BaseView { private let scrollView = UIScrollView() private let stackView = UIStackView() + private let skeletonView = SkeletonView() private var isFirst: Bool = true private var currentIndex: Int? @@ -40,6 +41,7 @@ final class CertificationListView: BaseView { override func configureHierarchy() { [scrollView].forEach { addSubview($0) } [stackView].forEach { scrollView.addSubview($0) } + stackView.addArrangedSubview(skeletonView) } override func configureConstraints() { @@ -49,15 +51,25 @@ final class CertificationListView: BaseView { stackView.snp.makeConstraints { $0.horizontalEdges.equalToSuperview().inset(16) + $0.width.equalToSuperview().offset(-32) + $0.height.equalToSuperview() + } + + skeletonView.snp.makeConstraints { + $0.edges.equalToSuperview() } } // MARK: - updateView override func updateView(_ data: (any BaseEntity)?) { if let datas = data as? CertificationViewDatas { + if datas.todos.isEmpty { return } + if isFirst { isFirst = false + stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + layoutIfNeeded() datas.todos @@ -78,7 +90,7 @@ final class CertificationListView: BaseView { stackView.addArrangedSubview($0) } - stackView.snp.makeConstraints { + stackView.snp.remakeConstraints { $0.horizontalEdges.equalToSuperview().inset(16) $0.width.equalTo(frame.width * CGFloat(datas.todos.count) - 32) $0.height.equalToSuperview() diff --git a/dogether/Presentation/Features/Certification/Components/CertificationPage.swift b/dogether/Presentation/Features/Certification/Components/CertificationPage.swift index 41af8f01..c0b8ad43 100644 --- a/dogether/Presentation/Features/Certification/Components/CertificationPage.swift +++ b/dogether/Presentation/Features/Certification/Components/CertificationPage.swift @@ -12,12 +12,24 @@ final class CertificationPage: BasePage { didSet { thumbnailListView.delegate = delegate certificationListView.delegate = delegate -// certificateButton.addAction( -// UIAction { [weak self] _ in -// guard let self, let currentTodo else { return } -// delegate?.goCertificateViewAction(todo: currentTodo) -// }, for: .touchUpInside -// ) + certificateButton.addAction( + UIAction { [weak self] _ in + guard let self, let currentTodo else { return } + delegate?.goCertificateViewAction(todo: currentTodo) + }, for: .touchUpInside + ) + remindCertificationButton.addAction( + UIAction { [weak self] _ in + guard let self, let currentTodo else { return } + delegate?.remindTodoAction(remindType: .certification, todoId: currentTodo.id) + }, for: .touchUpInside + ) + remindReviewButton.addAction( + UIAction { [weak self] _ in + guard let self, let currentTodo else { return } + delegate?.remindTodoAction(remindType: .review, todoId: currentTodo.id) + }, for: .touchUpInside + ) } } @@ -27,11 +39,16 @@ final class CertificationPage: BasePage { private let certificationStackView = UIStackView() private let certificationListView = CertificationListView() private let statusView = TodoStatusButton() + private let statusSkeletonView = SkeletonView() private let contentLabel = UILabel() + private let contentLabelSkeletonView = SkeletonView() private let reviewFeedbackView = ReviewFeedbackView() -// private let certificateButton = DogetherButton("인증하기") + private let certificateButton = DogetherButton("인증하기") + private let remindCertificationButton = DogetherButton("인증 재촉하기") + private let remindReviewButton = DogetherButton("검사 재촉하기") private var currentTodo: TodoEntity? + private var isFirst: Bool = true override func configureView() { certificationScrollView.showsVerticalScrollIndicator = false @@ -47,6 +64,8 @@ final class CertificationPage: BasePage { contentLabel.textColor = .grey0 contentLabel.numberOfLines = 0 + + [certificateButton, remindCertificationButton, remindReviewButton].forEach { $0.isHidden = true } } override func configureAction() { @@ -54,8 +73,13 @@ final class CertificationPage: BasePage { } override func configureHierarchy() { - [navigationHeader, thumbnailListView, certificationScrollView/*, certificateButton*/].forEach { addSubview($0) } + [ navigationHeader, thumbnailListView, certificationScrollView, + certificateButton, remindCertificationButton, remindReviewButton + ].forEach { addSubview($0) } certificationScrollView.addSubview(certificationStackView) + + statusView.addSubview(statusSkeletonView) + contentLabel.addSubview(contentLabelSkeletonView) } override func configureConstraints() { @@ -88,44 +112,86 @@ final class CertificationPage: BasePage { statusView.snp.makeConstraints { $0.centerX.equalToSuperview() + $0.width.equalTo(100) + $0.height.equalTo(36) } contentLabel.snp.makeConstraints { $0.horizontalEdges.equalToSuperview().inset(16) + $0.height.equalTo(36) } reviewFeedbackView.snp.makeConstraints { $0.horizontalEdges.equalToSuperview().inset(16) } -// certificateButton.snp.makeConstraints { -// $0.bottom.horizontalEdges.equalToSuperview().inset(16) -// } + certificateButton.snp.makeConstraints { + $0.bottom.horizontalEdges.equalToSuperview().inset(16) + } + + remindCertificationButton.snp.makeConstraints { + $0.bottom.horizontalEdges.equalToSuperview().inset(16) + } + + remindReviewButton.snp.makeConstraints { + $0.bottom.horizontalEdges.equalToSuperview().inset(16) + } + + [statusSkeletonView, contentLabelSkeletonView].forEach { + $0.snp.makeConstraints { $0.edges.equalToSuperview() } + } } // MARK: - updateView override func updateView(_ data: (any BaseEntity)?) { guard let datas = data as? CertificationViewDatas else { return } + navigationHeader.updateView(datas) thumbnailListView.updateView(datas) certificationListView.updateView(datas) - if currentTodo != datas.todos[datas.index] { - currentTodo = datas.todos[datas.index] + if let todo = datas.todos[safe: datas.index], currentTodo != todo { + if isFirst { + isFirst = false + + statusView.snp.remakeConstraints { + $0.centerX.equalToSuperview() + } + + contentLabel.snp.remakeConstraints { + $0.horizontalEdges.equalToSuperview().inset(16) + } + + [statusSkeletonView, contentLabelSkeletonView].forEach { $0.removeFromSuperview() } + } + + currentTodo = todo - statusView.updateView(datas.todos[datas.index].status) + statusView.updateView(todo.status) contentLabel.attributedText = NSAttributedString( - string: datas.todos[datas.index].content, + string: todo.content, attributes: Fonts.getAttributes(for: Fonts.head1B, textAlignment: .center) ) - reviewFeedbackView.updateView(datas.todos[datas.index].reviewFeedback ?? "") + reviewFeedbackView.updateView(todo.reviewFeedback ?? "") - // FIXME: 추후 수정 -// var dogetherButtonViewDatas = certificateButton.currentViewDatas ?? DogetherButtonViewDatas() -// dogetherButtonViewDatas.isHidden = datas.rankingEntity != nil || datas.todos[datas.index].status != .waitCertification -// certificateButton.updateView(dogetherButtonViewDatas) + let date = DateFormatterManager.formattedDate().translateDateFormatForServer() + let isWaitCertification = todo.status == .waitCertification + let isWaitExamination = todo.status == .waitExamination + let isToday = todo.createdAt ?? date == date + let isMine = datas.isMine ?? true + + certificateButton.isHidden = !(isWaitCertification && isToday && isMine) + remindCertificationButton.isHidden = !(isWaitCertification && !isMine) + remindReviewButton.isHidden = !isWaitExamination + + remindCertificationButton.updateView( + DogetherButtonViewDatas(status: todo.canRemindCertification ? .enabled : .disabled) + ) + remindReviewButton.updateView( + DogetherButtonViewDatas(status: todo.canRemindReview ? .enabled : .disabled) + ) } } } diff --git a/dogether/Presentation/Features/Certification/Components/ThumbnailListView.swift b/dogether/Presentation/Features/Certification/Components/ThumbnailListView.swift index 2dd67e8e..097e2ea7 100644 --- a/dogether/Presentation/Features/Certification/Components/ThumbnailListView.swift +++ b/dogether/Presentation/Features/Certification/Components/ThumbnailListView.swift @@ -19,6 +19,7 @@ final class ThumbnailListView: BaseView { private let scrollView = UIScrollView() private let stackView = UIStackView() + private let skeletonViews = (0 ..< 6).map { _ in SkeletonView() } private var isFirst: Bool = true private var currentIndex: Int? @@ -37,6 +38,7 @@ final class ThumbnailListView: BaseView { override func configureHierarchy() { [scrollView].forEach { addSubview($0) } [stackView].forEach { scrollView.addSubview($0) } + skeletonViews.forEach { stackView.addArrangedSubview($0) } } override func configureConstraints() { @@ -47,14 +49,22 @@ final class ThumbnailListView: BaseView { stackView.snp.makeConstraints { $0.horizontalEdges.equalToSuperview().inset(16) } + + skeletonViews.forEach { $0.snp.makeConstraints { + $0.width.height.equalTo(54) + } } } // MARK: - updateView override func updateView(_ data: (any BaseEntity)?) { if let datas = data as? CertificationViewDatas { + if datas.todos.isEmpty { return } + if isFirst { isFirst = false + stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + datas.todos .enumerated().map { let thumbnailView = ThumbnailView() diff --git a/dogether/Presentation/Features/CertificationList/CertificationListViewController.swift b/dogether/Presentation/Features/CertificationList/CertificationListViewController.swift index 264389e2..e7f21250 100644 --- a/dogether/Presentation/Features/CertificationList/CertificationListViewController.swift +++ b/dogether/Presentation/Features/CertificationList/CertificationListViewController.swift @@ -51,7 +51,7 @@ protocol CertificationListPageDelegate { func updateBottomSheetVisibleAction(isShowSheet: Bool) func selectSortAction(index: Int) func selectFilterAction(filterType: FilterTypes) - func selectCertificationAction(title: String, todos: [TodoEntity], index: Int) + func selectCertificationAction(title: String, todo: TodoEntity) func didScrollToBottom() } @@ -70,10 +70,15 @@ extension CertificationListViewController: CertificationListPageDelegate { viewModel.updateFilter(filter: filterType) } - func selectCertificationAction(title: String, todos: [TodoEntity], index: Int) { + func selectCertificationAction(title: String, todo: TodoEntity) { let certificationViewController = CertificationViewController() - let certificationViewDatas = CertificationViewDatas(title: title, todos: todos, index: index) - coordinator?.pushViewController(certificationViewController, datas: certificationViewDatas) + let preCertificationViewDatas = PreCertificationViewDatas.certificationList( + title: title, + todoId: todo.id, + sortOption: viewModel.sortViewDatas.value.options[viewModel.sortViewDatas.value.index], + filter: viewModel.certificationListViewDatas.value.filter + ) + coordinator?.pushViewController(certificationViewController, datas: preCertificationViewDatas) } func didScrollToBottom() { diff --git a/dogether/Presentation/Features/CertificationList/Components/CertificationListContentView.swift b/dogether/Presentation/Features/CertificationList/Components/CertificationListContentView.swift index fb39d5b1..4d2c6534 100644 --- a/dogether/Presentation/Features/CertificationList/Components/CertificationListContentView.swift +++ b/dogether/Presentation/Features/CertificationList/Components/CertificationListContentView.swift @@ -211,7 +211,7 @@ extension CertificationListContentView: UICollectionViewDelegate { title = groupName } - delegate?.selectCertificationAction(title: title, todos: section.todos, index: indexPath.item) + delegate?.selectCertificationAction(title: title, todo: section.todos[indexPath.item]) } } diff --git a/dogether/Presentation/Features/CertificationList/Components/CertificationListViewStatus.swift b/dogether/Presentation/Features/CertificationList/Components/CertificationListViewStatus.swift deleted file mode 100644 index e24fa49e..00000000 --- a/dogether/Presentation/Features/CertificationList/Components/CertificationListViewStatus.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// CertificationListViewStatus.swift -// dogether -// -// Created by yujaehong on 4/28/25. -// - -import UIKit - -enum CertificationListViewStatus { - case empty - case hasData -} diff --git a/dogether/Presentation/Features/Main/Components/TodoListItemButton.swift b/dogether/Presentation/Features/Main/Components/TodoListItemButton.swift index 5e966d2f..111929a7 100644 --- a/dogether/Presentation/Features/Main/Components/TodoListItemButton.swift +++ b/dogether/Presentation/Features/Main/Components/TodoListItemButton.swift @@ -19,20 +19,18 @@ final class TodoListItemButton: BaseButton { delegate?.goCertificateViewAction(todo: todo) } else { return } } else { - delegate?.goCertificationViewAction(index: index) + delegate?.goCertificationViewAction(todo: todo) } }, for: .touchUpInside ) } } - private(set) var index: Int private(set) var todo: TodoEntity private(set) var isToday: Bool private(set) var isUncertified: Bool - init(index: Int, todo: TodoEntity, isToday: Bool) { - self.index = index + init(todo: TodoEntity, isToday: Bool) { self.todo = todo self.isToday = isToday self.isUncertified = todo.status == .waitCertification diff --git a/dogether/Presentation/Features/Main/Components/TodoListView.swift b/dogether/Presentation/Features/Main/Components/TodoListView.swift index 423ca54a..c337f4ff 100644 --- a/dogether/Presentation/Features/Main/Components/TodoListView.swift +++ b/dogether/Presentation/Features/Main/Components/TodoListView.swift @@ -127,8 +127,8 @@ final class TodoListView: BaseView { todoListStackView.subviews.forEach { todoListStackView.removeArrangedSubview($0) } currentTodoList - .enumerated().map { - let todoListItemButton = TodoListItemButton(index: $0, todo: $1, isToday: isToday) + .map { + let todoListItemButton = TodoListItemButton(todo: $0, isToday: isToday) todoListItemButton.delegate = delegate return todoListItemButton } diff --git a/dogether/Presentation/Features/Main/MainViewController.swift b/dogether/Presentation/Features/Main/MainViewController.swift index 3d76d713..e768b94c 100644 --- a/dogether/Presentation/Features/Main/MainViewController.swift +++ b/dogether/Presentation/Features/Main/MainViewController.swift @@ -115,7 +115,7 @@ protocol MainDelegate { func goWriteTodoViewAction(todos: [TodoEntity]) func selectFilterAction(filterType: FilterTypes) func goCertificateViewAction(todo: TodoEntity) - func goCertificationViewAction(index: Int) + func goCertificationViewAction(todo: TodoEntity) } extension MainViewController: MainDelegate { @@ -235,15 +235,18 @@ extension MainViewController: MainDelegate { coordinator?.pushViewController(certificateImageViewController, datas: certificateViewDatas) } - func goCertificationViewAction(index: Int) { + func goCertificationViewAction(todo: TodoEntity) { let certificationViewController = CertificationViewController() - let certificationViewDatas = CertificationViewDatas( + let date = DateFormatterManager.formattedDate( + viewModel.sheetViewDatas.value.dateOffset + ).translateDateFormatForServer() + let preCertificationViewDatas = PreCertificationViewDatas.main( title: "내 인증 정보", - todos: viewModel.sheetViewDatas.value.todoList.filter { - viewModel.sheetViewDatas.value.filter == .all || viewModel.sheetViewDatas.value.filter == FilterTypes(status: $0.status.rawValue) - }, - index: index + date: date, + groupId: viewModel.currentGroup.id, + todoId: todo.id, + filter: viewModel.sheetViewDatas.value.filter ) - coordinator?.pushViewController(certificationViewController, datas: certificationViewDatas) + coordinator?.pushViewController(certificationViewController, datas: preCertificationViewDatas) } } diff --git a/dogether/Presentation/Features/Main/MainViewModel.swift b/dogether/Presentation/Features/Main/MainViewModel.swift index 41b7af5b..13475efb 100644 --- a/dogether/Presentation/Features/Main/MainViewModel.swift +++ b/dogether/Presentation/Features/Main/MainViewModel.swift @@ -11,7 +11,7 @@ import RxRelay final class MainViewModel { private let groupUseCase: GroupUseCase - private let challengeGroupsUseCase: ChallengeGroupUseCase + private let challengeGroupsUseCase: ChallengeGroupsUseCase private let todoCertificationsUseCase: TodoCertificationsUseCase private(set) var bottomSheetViewDatas = BehaviorRelay(value: BottomSheetViewDatas()) @@ -30,7 +30,7 @@ final class MainViewModel { let todoCertificationsRepository = DIManager.shared.getTodoCertificationsRepository() self.groupUseCase = GroupUseCase(repository: groupRepository) - self.challengeGroupsUseCase = ChallengeGroupUseCase(repository: challengeGroupsRepository) + self.challengeGroupsUseCase = ChallengeGroupsUseCase(repository: challengeGroupsRepository) self.todoCertificationsUseCase = TodoCertificationsUseCase(repository: todoCertificationsRepository) } } @@ -42,7 +42,7 @@ extension MainViewModel { } func getTodoList(dateOffset: Int, groupId: Int) async throws -> [TodoEntity] { - let date = DateFormatterManager.formattedDate(dateOffset).split(separator: ".").joined(separator: "-") + let date = DateFormatterManager.formattedDate(dateOffset).translateDateFormatForServer() return try await challengeGroupsUseCase.getMyTodos(groupId: groupId, date: date) } diff --git a/dogether/Presentation/Features/Ranking/RankingViewController.swift b/dogether/Presentation/Features/Ranking/RankingViewController.swift index adaa414b..4022adf5 100644 --- a/dogether/Presentation/Features/Ranking/RankingViewController.swift +++ b/dogether/Presentation/Features/Ranking/RankingViewController.swift @@ -51,20 +51,12 @@ protocol RankingDelegate { extension RankingViewController: RankingDelegate { func goCertificationViewAction(rankingEntity: RankingEntity) { - Task { - let (index, todos) = try await viewModel.getMemberTodos(memberId: rankingEntity.memberId) - - await MainActor.run { - let certificationViewController = CertificationViewController() - let certificationViewDatas = CertificationViewDatas( - title: "\(rankingEntity.name)님의 인증 정보", - todos: todos, - index: index, - groupId: viewModel.rankingViewDatas.value.groupId, - rankingEntity: rankingEntity - ) - coordinator?.pushViewController(certificationViewController, datas: certificationViewDatas) - } - } + let certificationViewController = CertificationViewController() + let preCertificationViewDatas = PreCertificationViewDatas.ranking( + title: "\(rankingEntity.name)님의 인증 정보", + groupId: viewModel.rankingViewDatas.value.groupId, + memberId: rankingEntity.memberId + ) + coordinator?.pushViewController(certificationViewController, datas: preCertificationViewDatas) } } diff --git a/dogether/Presentation/Features/Ranking/RankingViewModel.swift b/dogether/Presentation/Features/Ranking/RankingViewModel.swift index a6a3aee2..3895e3fb 100644 --- a/dogether/Presentation/Features/Ranking/RankingViewModel.swift +++ b/dogether/Presentation/Features/Ranking/RankingViewModel.swift @@ -11,16 +11,12 @@ import RxRelay final class RankingViewModel { private let groupUseCase: GroupUseCase - private let challengeGroupsUseCase: ChallengeGroupUseCase private(set) var rankingViewDatas = BehaviorRelay(value: RankingViewDatas()) init() { let groupRepository = DIManager.shared.getGroupRepository() - let challengeGroupsRepository = DIManager.shared.getChallengeGroupsRepository() - self.groupUseCase = GroupUseCase(repository: groupRepository) - self.challengeGroupsUseCase = ChallengeGroupUseCase(repository: challengeGroupsRepository) } } @@ -35,8 +31,4 @@ extension RankingViewModel { let rankings = try await groupUseCase.getRankings(groupId: rankingViewDatas.value.groupId) rankingViewDatas.update { $0.rankings = rankings } } - - func getMemberTodos(memberId: Int) async throws -> (Int, [TodoEntity]) { - try await challengeGroupsUseCase.getMemberTodos(groupId: rankingViewDatas.value.groupId, memberId: memberId) - } } diff --git a/dogether/Presentation/Features/TodoWrite/TodoWriteViewModel.swift b/dogether/Presentation/Features/TodoWrite/TodoWriteViewModel.swift index 4f085dda..e0322530 100644 --- a/dogether/Presentation/Features/TodoWrite/TodoWriteViewModel.swift +++ b/dogether/Presentation/Features/TodoWrite/TodoWriteViewModel.swift @@ -10,13 +10,13 @@ import Foundation import RxRelay final class TodoWriteViewModel { - private let challengeGroupsUseCase: ChallengeGroupUseCase + private let challengeGroupsUseCase: ChallengeGroupsUseCase private(set) var todoWriteViewDatas = BehaviorRelay(value: TodoWriteViewDatas()) init() { let challengeGroupsRepository = DIManager.shared.getChallengeGroupsRepository() - self.challengeGroupsUseCase = ChallengeGroupUseCase(repository: challengeGroupsRepository) + self.challengeGroupsUseCase = ChallengeGroupsUseCase(repository: challengeGroupsRepository) } } diff --git a/dogether/Utility/Extension/StringExt.swift b/dogether/Utility/Extension/StringExt.swift new file mode 100644 index 00000000..20e1cd5e --- /dev/null +++ b/dogether/Utility/Extension/StringExt.swift @@ -0,0 +1,14 @@ +// +// StringExt.swift +// dogether +// +// Created by seungyooooong on 2/20/26. +// + +import Foundation + +extension String { + func translateDateFormatForServer() -> String { + self.split(separator: ".").joined(separator: "-") + } +} diff --git a/dogether/Utility/Manager/DIManager.swift b/dogether/Utility/Manager/DIManager.swift index a26f96e4..5223f0a5 100644 --- a/dogether/Utility/Manager/DIManager.swift +++ b/dogether/Utility/Manager/DIManager.swift @@ -66,6 +66,15 @@ extension DIManager { } } + func getTodosRepository(buildMode: BuildModes? = nil) -> TodosProtocol { + switch buildMode ?? defaultBuildMode { + case .debug: + return TodosRepositoryTest() + case .live: + return TodosRepository() + } + } + func getUserRepository(buildMode: BuildModes? = nil) -> UserProtocol { switch buildMode ?? defaultBuildMode { case .debug: