diff --git a/CERTI-iOS/Application/DIContainer/AppDIContainer.swift b/CERTI-iOS/Application/DIContainer/AppDIContainer.swift index 15b35bd3..911f235a 100644 --- a/CERTI-iOS/Application/DIContainer/AppDIContainer.swift +++ b/CERTI-iOS/Application/DIContainer/AppDIContainer.swift @@ -148,6 +148,10 @@ extension AppDIContainer { return DefaultFetchCareersListUseCase(repository: careersRepository) } + func makeEditCareerUseCase() -> EditCareersUseCase { + return DefaultEditCareersUseCase(repository: careersRepository) + } + func makeAddActivityUseCase() -> AddActivityUseCase { return DefaultAddActivityUseCase(repository: activityRepository) } @@ -156,6 +160,10 @@ extension AppDIContainer { return DefaultDeleteActivityUseCase(repository: activityRepository) } + func makeEditActivityUseCase() -> EditActivityUseCase { + return DefaultEditActivityUseCase(repository: activityRepository) + } + func makeFetchActivityListUseCase() -> FetchActivityListUseCase { return DefaultFetchActivityListUseCase(repository: activityRepository) } @@ -264,9 +272,11 @@ extension AppDIContainer { addCareersUseCase: makeAddCareersUseCase(), deleteCareersUseCase: makeDeleteCareersUseCase(), fetchCareersListUseCase: makeFetchCareersListUseCase(), + editCareerUseCase: makeEditCareerUseCase(), addActivityUseCase: makeAddActivityUseCase(), deleteActivityUseCase: makeDeleteActivityUseCase(), - fetchActivityListUseCase: makeFetchActivityListUseCase() + fetchActivityListUseCase: makeFetchActivityListUseCase(), + editActivityUseCase: makeEditActivityUseCase() ) } diff --git a/CERTI-iOS/Data/Network/Activity/ActivityAPI.swift b/CERTI-iOS/Data/Network/Activity/ActivityAPI.swift index 645999c0..92a49ee0 100644 --- a/CERTI-iOS/Data/Network/Activity/ActivityAPI.swift +++ b/CERTI-iOS/Data/Network/Activity/ActivityAPI.swift @@ -13,6 +13,7 @@ enum ActivityAPI { case fetchActivityList case addActivity(request: AddActivityRequestDTO) case deleteActivity(id: Int) + case editActivity(activityId:Int, request: EditActivityRequestDTO) } extension ActivityAPI: BaseTargetType { @@ -31,6 +32,8 @@ extension ActivityAPI: BaseTargetType { return "activity" case .deleteActivity(let id): return "activity/\(id)" + case .editActivity(let activityId, _): + return "activity/\(activityId)" } } @@ -42,6 +45,8 @@ extension ActivityAPI: BaseTargetType { return .post case .deleteActivity: return .delete + case .editActivity: + return .put } } @@ -53,6 +58,8 @@ extension ActivityAPI: BaseTargetType { return .requestJSONEncodable(request) case .deleteActivity: return .requestPlain + case .editActivity(_, let request): + return .requestJSONEncodable(request) } } diff --git a/CERTI-iOS/Data/Network/Activity/ActivityService.swift b/CERTI-iOS/Data/Network/Activity/ActivityService.swift index 96d9da5f..a141e51e 100644 --- a/CERTI-iOS/Data/Network/Activity/ActivityService.swift +++ b/CERTI-iOS/Data/Network/Activity/ActivityService.swift @@ -13,6 +13,7 @@ protocol ActivityServiceProtocol { func fetchActivityList() async -> Result func deleteActivity(id: Int) async -> Result func addActivity(request: AddActivityRequestDTO) async -> Result + func editActivity(activityId: Int, request: EditActivityRequestDTO) async -> Result } final class ActivityService: BaseService, ActivityServiceProtocol { @@ -29,4 +30,8 @@ final class ActivityService: BaseService, ActivityServiceProtocol { func addActivity(request: AddActivityRequestDTO) async -> Result { return await requestVoid(provider, .addActivity(request: request)) } + + func editActivity(activityId: Int, request: EditActivityRequestDTO) async -> Result { + return await requestVoid(provider, .editActivity(activityId: activityId, request: request)) + } } diff --git a/CERTI-iOS/Data/Network/Activity/DTO/Request/EditActivityRequestDTO.swift b/CERTI-iOS/Data/Network/Activity/DTO/Request/EditActivityRequestDTO.swift new file mode 100644 index 00000000..fda28f50 --- /dev/null +++ b/CERTI-iOS/Data/Network/Activity/DTO/Request/EditActivityRequestDTO.swift @@ -0,0 +1,16 @@ +// +// EditActivityRequestDTO.swift +// CERTI-iOS +// +// Created by 이상엽 on 1/26/26. +// + +import Foundation + +struct EditActivityRequestDTO: Encodable { + let startAt: String + let endAt: String + let place: String + let name: String + let description: String +} diff --git a/CERTI-iOS/Data/Network/Activity/DTO/Response/ActivityListResponseDTO.swift b/CERTI-iOS/Data/Network/Activity/DTO/Response/ActivityListResponseDTO.swift index 03df1ba3..2ba36e21 100644 --- a/CERTI-iOS/Data/Network/Activity/DTO/Response/ActivityListResponseDTO.swift +++ b/CERTI-iOS/Data/Network/Activity/DTO/Response/ActivityListResponseDTO.swift @@ -16,7 +16,7 @@ struct ActivityListData: Decodable { extension ActivityListData { func toActivityListEntity() -> ActivityListEntity { return ActivityListEntity( - list: activityDetailResponses.map{ $0.toResumeEntityData() } + list: activityDetailResponses.map{ $0.toActivityEntity() } ) } } @@ -31,8 +31,8 @@ struct Activity: Decodable, Identifiable { let description: String let place: String - func toResumeEntityData() -> ResumeEntityData { - return ResumeEntityData( + func toActivityEntity() -> ActivityEntity { + return ActivityEntity( activityId: activityId, startAt: startAt, endAt: endAt, diff --git a/CERTI-iOS/Data/Network/Careers/CareersAPI.swift b/CERTI-iOS/Data/Network/Careers/CareersAPI.swift index 010155a8..1c37fe83 100644 --- a/CERTI-iOS/Data/Network/Careers/CareersAPI.swift +++ b/CERTI-iOS/Data/Network/Careers/CareersAPI.swift @@ -13,6 +13,7 @@ enum CareersAPI { case fetchCareersList case deleteCareers(id: Int) case addCareer(request: AddCareerRequestDTO) + case editCareer(careerId:Int, request: EditCareerRequestDTO) } extension CareersAPI: BaseTargetType { @@ -31,6 +32,8 @@ extension CareersAPI: BaseTargetType { return "careers/\(id)" case .addCareer: return "careers" + case .editCareer(let careerId, _): + return "careers/\(careerId)" } } @@ -42,6 +45,8 @@ extension CareersAPI: BaseTargetType { return .delete case .addCareer: return .post + case .editCareer: + return .put } } @@ -53,6 +58,8 @@ extension CareersAPI: BaseTargetType { return .requestPlain case .addCareer(let request): return .requestJSONEncodable(request) + case .editCareer(_, let request): + return .requestJSONEncodable(request) } } diff --git a/CERTI-iOS/Data/Network/Careers/CareersService.swift b/CERTI-iOS/Data/Network/Careers/CareersService.swift index 7d3c279b..f54eb630 100644 --- a/CERTI-iOS/Data/Network/Careers/CareersService.swift +++ b/CERTI-iOS/Data/Network/Careers/CareersService.swift @@ -12,7 +12,8 @@ import Moya protocol CareersServiceProtocol { func fetchCareersList() async -> Result func deledteCareers(id: Int) async -> Result - func addCareer(request: AddCareerRequestDTO) async -> Result + func addCareer(request: AddCareerRequestDTO) async -> Result + func editCareer(careerId:Int, request: EditCareerRequestDTO) async -> Result } final class CareersService: BaseService, CareersServiceProtocol { @@ -26,7 +27,11 @@ final class CareersService: BaseService, CareersServiceProtocol { return await requestVoid(provider, .deleteCareers(id: id)) } - func addCareer(request: AddCareerRequestDTO) async -> Result { - return await requestDecodable(provider, .addCareer(request: request)) + func addCareer(request: AddCareerRequestDTO) async -> Result { + return await requestVoid(provider, .addCareer(request: request)) + } + + func editCareer(careerId:Int, request: EditCareerRequestDTO) async -> Result { + return await requestVoid(provider, .editCareer(careerId: careerId, request: request)) } } diff --git a/CERTI-iOS/Data/Network/Careers/DTO/Request/EditCareerRequestDTO.swift b/CERTI-iOS/Data/Network/Careers/DTO/Request/EditCareerRequestDTO.swift new file mode 100644 index 00000000..447c42e2 --- /dev/null +++ b/CERTI-iOS/Data/Network/Careers/DTO/Request/EditCareerRequestDTO.swift @@ -0,0 +1,16 @@ +// +// EditCareerRequestDTO.swift +// CERTI-iOS +// +// Created by 이상엽 on 1/15/26. +// + +import Foundation + +struct EditCareerRequestDTO: Encodable { + let startAt: String + let endAt: String + let place: String + let name: String + let description: String +} diff --git a/CERTI-iOS/Data/Network/Careers/DTO/Response/CareersListResponseDTO.swift b/CERTI-iOS/Data/Network/Careers/DTO/Response/CareersListResponseDTO.swift index 887ebe0c..03e8793d 100644 --- a/CERTI-iOS/Data/Network/Careers/DTO/Response/CareersListResponseDTO.swift +++ b/CERTI-iOS/Data/Network/Careers/DTO/Response/CareersListResponseDTO.swift @@ -14,9 +14,9 @@ struct CareersListData: Decodable { } extension CareersListData { - func toCareersListEntity() -> CareersListEntity { - return CareersListEntity( - list: careerDetailResponseList.map{ $0.toResumeEntityData() } + func toCareersListEntity() -> CareerListEntity { + return CareerListEntity( + list: careerDetailResponseList.map{ $0.toCareerEntity() } ) } } @@ -31,8 +31,8 @@ struct Career: Decodable, Identifiable { let description: String let place: String - func toResumeEntityData() -> ResumeEntityData { - return ResumeEntityData( + func toCareerEntity() -> CareerEntity { + return CareerEntity( careerId: careerId, startAt: startAt, endAt: endAt, diff --git a/CERTI-iOS/Data/Repositories/DefaultActivityRepository.swift b/CERTI-iOS/Data/Repositories/DefaultActivityRepository.swift index 8e36cc6e..4b46ba49 100644 --- a/CERTI-iOS/Data/Repositories/DefaultActivityRepository.swift +++ b/CERTI-iOS/Data/Repositories/DefaultActivityRepository.swift @@ -38,4 +38,9 @@ final class DefaultActivityRepository: ActivityRepository { let requestDTO = request.toAddActivityRequestDTO() return await service.addActivity(request: requestDTO) } + + func editActivity(activityId: Int, request: ActivityEntity) async -> Result { + let requestDTO = request.toEditActivityRequestDTO() + return await service.editActivity(activityId: activityId, request: requestDTO) + } } diff --git a/CERTI-iOS/Data/Repositories/DefaultCareersRepository.swift b/CERTI-iOS/Data/Repositories/DefaultCareersRepository.swift index bcad3206..2beb6272 100644 --- a/CERTI-iOS/Data/Repositories/DefaultCareersRepository.swift +++ b/CERTI-iOS/Data/Repositories/DefaultCareersRepository.swift @@ -17,7 +17,7 @@ final class DefaultCareersRepository: CareersRepository { self.service = service } - func fetchCareersList() async -> Result { + func fetchCareersList() async -> Result { let result = await service.fetchCareersList() switch result { case .success(let dto): @@ -34,8 +34,13 @@ final class DefaultCareersRepository: CareersRepository { return await service.deledteCareers(id: id) } - func addCareer(request: CareersEntity) async -> Result { + func addCareer(request: CareerEntity) async -> Result { let requestDTO = request.toAddCareerRequestDTO() return await service.addCareer(request: requestDTO) } + + func editCareer(careerId: Int, request: CareerEntity) async -> Result { + let requestDTO = request.toEditCareerRequestDTO() + return await service.editCareer(careerId: careerId, request: requestDTO) + } } diff --git a/CERTI-iOS/Domain/Entities/ActivityEntity.swift b/CERTI-iOS/Domain/Entities/ActivityEntity.swift new file mode 100644 index 00000000..34055b66 --- /dev/null +++ b/CERTI-iOS/Domain/Entities/ActivityEntity.swift @@ -0,0 +1,54 @@ +// +// ActivityEntity.swift +// CERTI-iOS +// +// Created by 이상엽 on 1/17/26. +// + +import Foundation + +struct ActivityEntity { + let activityId: Int? + let startAt: String + let endAt: String + let name: String + let place: String + let description: String +} + +// MARK: - Func + +extension ActivityEntity { + func toAddActivityRequestDTO() -> AddActivityRequestDTO { + AddActivityRequestDTO( + startAt: startAt, + endAt: endAt, + place: place, + name: name, + description: description + ) + } + + func toEditActivityRequestDTO() -> EditActivityRequestDTO { + EditActivityRequestDTO( + startAt: startAt, + endAt: endAt, + place: place, + name: name, + description: description + ) + } + + func toActivityModel() -> ActivityModel? { + guard let activityId else { return nil } + + return ActivityModel( + activityId: activityId, + startAt: startAt, + endAt: endAt, + name: name, + place: place, + description: description + ) + } +} diff --git a/CERTI-iOS/Domain/Entities/ActivityListEntity.swift b/CERTI-iOS/Domain/Entities/ActivityListEntity.swift new file mode 100644 index 00000000..11f8b9f9 --- /dev/null +++ b/CERTI-iOS/Domain/Entities/ActivityListEntity.swift @@ -0,0 +1,18 @@ +// +// ActivityListEntity.swift +// CERTI-iOS +// +// Created by 이상엽 on 1/17/26. +// + +import Foundation + +struct ActivityListEntity { + let list: [ActivityEntity] +} + +extension ActivityListEntity { + func toActivityModels() -> [ActivityModel] { + list.compactMap { $0.toActivityModel() } + } +} diff --git a/CERTI-iOS/Domain/Entities/CareerEntity.swift b/CERTI-iOS/Domain/Entities/CareerEntity.swift new file mode 100644 index 00000000..ac498fc7 --- /dev/null +++ b/CERTI-iOS/Domain/Entities/CareerEntity.swift @@ -0,0 +1,54 @@ +// +// CareerEntity.swift +// CERTI-iOS +// +// Created by 이상엽 on 1/17/26. +// + +import Foundation + +struct CareerEntity { + let careerId: Int? + let startAt: String + let endAt: String + let name: String + let place: String + let description: String +} + +// MARK: - Func + +extension CareerEntity { + func toAddCareerRequestDTO() -> AddCareerRequestDTO { + AddCareerRequestDTO( + startAt: startAt, + endAt: endAt, + place: place, + name: name, + description: description + ) + } + + func toEditCareerRequestDTO() -> EditCareerRequestDTO { + EditCareerRequestDTO( + startAt: startAt, + endAt: endAt, + place: place, + name: name, + description: description + ) + } + + func toCareerModel() -> CareerModel? { + guard let careerId else { return nil } + + return CareerModel( + careerId: careerId, + startAt: startAt, + endAt: endAt, + name: name, + place: place, + description: description + ) + } +} diff --git a/CERTI-iOS/Domain/Entities/CareerListEntity.swift b/CERTI-iOS/Domain/Entities/CareerListEntity.swift new file mode 100644 index 00000000..6f5c5e3c --- /dev/null +++ b/CERTI-iOS/Domain/Entities/CareerListEntity.swift @@ -0,0 +1,18 @@ +// +// CareerListEntity.swift +// CERTI-iOS +// +// Created by 이상엽 on 1/17/26. +// + +import Foundation + +struct CareerListEntity { + let list: [CareerEntity] +} + +extension CareerListEntity { + func toCareerModels() -> [CareerModel] { + list.compactMap { $0.toCareerModel() } + } +} diff --git a/CERTI-iOS/Domain/Entities/ResumeEntity.swift b/CERTI-iOS/Domain/Entities/ResumeEntity.swift deleted file mode 100644 index 8168526a..00000000 --- a/CERTI-iOS/Domain/Entities/ResumeEntity.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// ResumeEntity.swift -// CERTI-iOS -// -// Created by 이상엽 on 8/26/25. -// - -import Foundation - -struct ActivityEntity { - let data: ResumeEntityData - - - // MARK: - Func - - func toAddActivityRequestDTO() -> AddActivityRequestDTO { - return data.toAddActivityRequestDTO() - } -} - -struct ActivityListEntity { - let list: [ResumeEntityData] - - - // MARK: - Func - - func toResumeModel() -> [ResumeModel] { - return list.map { $0.toResumeModel() } - } -} - -struct CareersEntity { - let data: ResumeEntityData - - - // MARK: - Func - - func toResumeModel() -> ResumeModel { - return data.toResumeModel() - } - - func toAddCareerRequestDTO() -> AddCareerRequestDTO { - return data.toAddCareerRequestDTO() - } -} - -struct CareersListEntity { - let list: [ResumeEntityData] - - - // MARK: - Func - - func toResumeModel() -> [ResumeModel] { - return list.map { $0.toResumeModel() } - } -} - -struct ResumeEntityData { - var activityId: Int? - var careerId: Int? - var startAt: String - var endAt: String - var name: String - var place: String - var description: String - - init(activityId: Int? = nil, careerId: Int? = nil, startAt: String, endAt: String, name: String, place: String, description: String) { - self.activityId = activityId - self.careerId = careerId - self.startAt = startAt - self.endAt = endAt - self.name = name - self.place = place - self.description = description - } - - - // MARK: - Func - - func toAddActivityRequestDTO() -> AddActivityRequestDTO { - return AddActivityRequestDTO( - startAt: startAt, - endAt: endAt, - place: place, - name: name, - description: description - ) - } - - func toResumeModel() -> ResumeModel { - return ResumeModel( - activityId: activityId, - careerId: careerId, - startAt: startAt, - endAt: endAt, - name: name, - place: place, - description: description - ) - } - - func toAddCareerRequestDTO() -> AddCareerRequestDTO { - return AddCareerRequestDTO( - startAt: startAt, - endAt: endAt, - place: place, - name: name, - description: description - ) - } -} diff --git a/CERTI-iOS/Domain/Interfaces/Repositories/ActivityRepository.swift b/CERTI-iOS/Domain/Interfaces/Repositories/ActivityRepository.swift index 15259a90..fa43d744 100644 --- a/CERTI-iOS/Domain/Interfaces/Repositories/ActivityRepository.swift +++ b/CERTI-iOS/Domain/Interfaces/Repositories/ActivityRepository.swift @@ -13,4 +13,5 @@ protocol ActivityRepository { func fetchActivityList() async -> Result func deleteActivity(id: Int) async -> Result func addActivity(request: ActivityEntity) async -> Result + func editActivity(activityId: Int, request: ActivityEntity) async -> Result } diff --git a/CERTI-iOS/Domain/Interfaces/Repositories/CareersRepository.swift b/CERTI-iOS/Domain/Interfaces/Repositories/CareersRepository.swift index d7a045f6..6367cd3c 100644 --- a/CERTI-iOS/Domain/Interfaces/Repositories/CareersRepository.swift +++ b/CERTI-iOS/Domain/Interfaces/Repositories/CareersRepository.swift @@ -10,7 +10,8 @@ import Foundation import Moya protocol CareersRepository { - func fetchCareersList() async -> Result + func fetchCareersList() async -> Result func deleteCareers(id: Int) async -> Result - func addCareer(request: CareersEntity) async -> Result + func addCareer(request: CareerEntity) async -> Result + func editCareer(careerId:Int, request: CareerEntity) async -> Result } diff --git a/CERTI-iOS/Domain/UseCases/Activity/EditActivityUseCase.swift b/CERTI-iOS/Domain/UseCases/Activity/EditActivityUseCase.swift new file mode 100644 index 00000000..89f659c6 --- /dev/null +++ b/CERTI-iOS/Domain/UseCases/Activity/EditActivityUseCase.swift @@ -0,0 +1,24 @@ +// +// EditActivityUseCase.swift +// CERTI-iOS +// +// Created by 이상엽 on 1/26/26. +// + +import Foundation + +protocol EditActivityUseCase { + func execute(activityId:Int, request: ActivityEntity) async -> Result +} + +final class DefaultEditActivityUseCase: EditActivityUseCase { + private let repository: ActivityRepository + + init(repository: ActivityRepository) { + self.repository = repository + } + + func execute(activityId: Int, request: ActivityEntity) async -> Result { + return await repository.editActivity(activityId: activityId, request: request) + } +} diff --git a/CERTI-iOS/Domain/UseCases/Careers/AddCareersUseCase.swift b/CERTI-iOS/Domain/UseCases/Careers/AddCareersUseCase.swift index 10445b63..fa252760 100644 --- a/CERTI-iOS/Domain/UseCases/Careers/AddCareersUseCase.swift +++ b/CERTI-iOS/Domain/UseCases/Careers/AddCareersUseCase.swift @@ -8,7 +8,7 @@ import Foundation protocol AddCareersUseCase { - func execute(request: CareersEntity) async -> Result + func execute(request: CareerEntity) async -> Result } final class DefaultAddCareersUseCase: AddCareersUseCase { @@ -18,7 +18,7 @@ final class DefaultAddCareersUseCase: AddCareersUseCase { self.repository = repository } - func execute(request: CareersEntity) async -> Result { + func execute(request: CareerEntity) async -> Result { return await repository.addCareer(request: request) } } diff --git a/CERTI-iOS/Domain/UseCases/Careers/EditCareersUseCase.swift b/CERTI-iOS/Domain/UseCases/Careers/EditCareersUseCase.swift new file mode 100644 index 00000000..01b3df57 --- /dev/null +++ b/CERTI-iOS/Domain/UseCases/Careers/EditCareersUseCase.swift @@ -0,0 +1,24 @@ +// +// EditCareersUseCase.swift +// CERTI-iOS +// +// Created by 이상엽 on 1/15/26. +// + +import Foundation + +protocol EditCareersUseCase { + func execute(careerId:Int, request: CareerEntity) async -> Result +} + +final class DefaultEditCareersUseCase: EditCareersUseCase { + private let repository: CareersRepository + + init(repository: CareersRepository) { + self.repository = repository + } + + func execute(careerId: Int, request: CareerEntity) async -> Result { + return await repository.editCareer(careerId: careerId, request: request) + } +} diff --git a/CERTI-iOS/Domain/UseCases/Careers/FetchCareersListUseCase.swift b/CERTI-iOS/Domain/UseCases/Careers/FetchCareersListUseCase.swift index 24b58759..e427d36f 100644 --- a/CERTI-iOS/Domain/UseCases/Careers/FetchCareersListUseCase.swift +++ b/CERTI-iOS/Domain/UseCases/Careers/FetchCareersListUseCase.swift @@ -8,7 +8,7 @@ import Foundation protocol FetchCareersListUseCase { - func execute() async -> Result + func execute() async -> Result } final class DefaultFetchCareersListUseCase: FetchCareersListUseCase { @@ -18,7 +18,7 @@ final class DefaultFetchCareersListUseCase: FetchCareersListUseCase { self.repository = repository } - func execute() async -> Result { + func execute() async -> Result { return await repository.fetchCareersList() } } diff --git a/CERTI-iOS/Domain/UseCases/PreviewUseCases/ResumePreviewUseCase.swift b/CERTI-iOS/Domain/UseCases/PreviewUseCases/ResumePreviewUseCase.swift index 79ac1b28..771feec8 100644 --- a/CERTI-iOS/Domain/UseCases/PreviewUseCases/ResumePreviewUseCase.swift +++ b/CERTI-iOS/Domain/UseCases/PreviewUseCases/ResumePreviewUseCase.swift @@ -47,35 +47,41 @@ struct PreviewFetchAcquisitionDetailUseCase: FetchAcquisitionDetailUseCase { struct PreviewFetchActivityListUseCase: FetchActivityListUseCase { func execute() async -> Result { let dummyActivities = ActivityListEntity(list:[ - ResumeEntityData(startAt: "2022.03", endAt: "2022.12", name: "SOPT 35기 iOS", place: "SOPT", description: "CERTI 앱 개발 프로젝트 진행"), - ResumeEntityData(startAt: "2023.03", endAt: "2023.07", name: "학교 창업동아리", place: "성균관대", description: "서비스 아이디어 기획 및 발표") + ActivityEntity(activityId: 0, startAt: "2022.03", endAt: "2022.12", name: "SOPT 35기 iOS", place: "SOPT", description: "CERTI 앱 개발 프로젝트 진행"), + ActivityEntity(activityId: 1, startAt: "2023.03", endAt: "2023.07", name: "학교 창업동아리", place: "성균관대", description: "서비스 아이디어 기획 및 발표") ]) return .success(dummyActivities) } } struct PreviewFetchCareersListUseCase: FetchCareersListUseCase { - func execute() async -> Result { - let dummyCareers = CareersListEntity(list:[ - ResumeEntityData(careerId: 1, startAt: "2021.11", endAt: "2022.01", name: "패션디자이너 인턴", place: "서티그룹", description: "트렌드 리서치"), - ResumeEntityData(careerId: 2, startAt: "2023.02", endAt: "2023.07", name: "iOS 개발 인턴", place: "CERTI", description: "CERTI 앱 개발 참여") + func execute() async -> Result { + let dummyCareers = CareerListEntity(list:[ + CareerEntity(careerId: 1, startAt: "2021.11", endAt: "2022.01", name: "패션디자이너 인턴", place: "서티그룹", description: "트렌드 리서치"), + CareerEntity(careerId: 2, startAt: "2023.02", endAt: "2023.07", name: "iOS 개발 인턴", place: "CERTI", description: "CERTI 앱 개발 참여") ]) return .success(dummyCareers) } } struct PreviewAddCareersUseCase: AddCareersUseCase { - func execute(request: CareersEntity) async -> Result { - .success(true) + func execute(request: CareerEntity) async -> Result { + return .success(()) } } -struct PreviewDeleteCareersUserCase: DeleteCareersUseCase { +struct PreviewDeleteCareersUseCase: DeleteCareersUseCase { func execute(id: Int) async -> Result { return .success(()) } } +struct PreviewEditCareerUseCase: EditCareersUseCase { + func execute(careerId: Int, request: CareerEntity) async -> Result { + return .success(()) + } +} + struct PreviewAddActivityUseCase: AddActivityUseCase { func execute(request: ActivityEntity) async -> Result { return .success(()) diff --git a/CERTI-iOS/Global/Extensions/Date+.swift b/CERTI-iOS/Global/Extensions/Date+.swift index 00d9d944..2205049c 100644 --- a/CERTI-iOS/Global/Extensions/Date+.swift +++ b/CERTI-iOS/Global/Extensions/Date+.swift @@ -24,4 +24,22 @@ extension Date { formatter.locale = Locale(identifier: "ko_KR") return formatter.string(from: self) } + + static func stringToDate(_ value: String) -> Date? { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "ko_KR") + + let formats = [ + "yyyy.MM.dd" + ] + + for format in formats { + formatter.dateFormat = format + if let date = formatter.date(from: value) { + return date + } + } + + return nil + } } diff --git a/CERTI-iOS/Global/Extensions/String+.swift b/CERTI-iOS/Global/Extensions/String+.swift index 541e2a59..3e1191a0 100644 --- a/CERTI-iOS/Global/Extensions/String+.swift +++ b/CERTI-iOS/Global/Extensions/String+.swift @@ -32,7 +32,7 @@ extension String { // 숫자만 남기기 let filteredPrice = self.filter { $0.isNumber } guard let price = Int(filteredPrice) else { return 0 } - + return price } @@ -48,11 +48,11 @@ extension String { guard let date = inputFormatter.date(from: self) else { return self } - + let outputFormatter = DateFormatter() outputFormatter.locale = Locale(identifier: "ko_KR") outputFormatter.dateFormat = "yyyy년 M월 d일" - + return "\(outputFormatter.string(from: date))" } @@ -79,5 +79,20 @@ extension String { return "\(self.prefix(count))..." } } - + + func toYearMonth() -> String { + let toDateFormatter = DateFormatter() + toDateFormatter.locale = Locale(identifier: "ko_KR") + toDateFormatter.dateFormat = "yyyy.MM.dd" + + let yearMonthFormatter = DateFormatter() + yearMonthFormatter.locale = Locale(identifier: "ko_KR") + yearMonthFormatter.dateFormat = "yyyy.MM" + + guard let date = toDateFormatter.date(from: self) else { + return self + } + + return yearMonthFormatter.string(from: date) + } } diff --git a/CERTI-iOS/Presentation/Resume/Components/PeriodInputComponent.swift b/CERTI-iOS/Presentation/Resume/Components/PeriodInputComponent.swift index 79950627..91938057 100644 --- a/CERTI-iOS/Presentation/Resume/Components/PeriodInputComponent.swift +++ b/CERTI-iOS/Presentation/Resume/Components/PeriodInputComponent.swift @@ -15,7 +15,7 @@ struct PeriodInputComponent: View { @Binding var isFilled: Bool @Binding var startAt: String @Binding var endAt: String - + var body: some View { VStack(alignment: .leading, spacing: 16) { HStack(alignment: .center, spacing: 0) { @@ -25,14 +25,14 @@ struct PeriodInputComponent: View { placeholder: "시작일" ) .padding(.leading, 20) - + Text("부터") .applyCertiFont(.caption_semibold_14) .foregroundStyle(.grayscale600) .frame(height: 20) .padding(.leading, 8) .padding(.trailing, 10) - + customDatePicker( selectedDate: $endDate, isExpanded: $isEndDateExpanded, @@ -109,6 +109,17 @@ struct PeriodInputComponent: View { .padding(.trailing, 55) } } + .onAppear { + if startDate == nil, !startAt.isEmpty { + startDate = Date.stringToDate(startAt) + } + + if endDate == nil, !endAt.isEmpty { + endDate = Date.stringToDate(endAt) + } + + isFilled = startDate != nil && endDate != nil + } } private var dateFormatter: DateFormatter { @@ -145,7 +156,7 @@ extension PeriodInputComponent { } } label: { HStack(alignment: .center, spacing: 0) { - Text(selectedDate.wrappedValue != nil ? dateFormatter.string(from: selectedDate.wrappedValue!) : placeholder) + Text(selectedDate.wrappedValue != nil ? formatDateToString(selectedDate.wrappedValue!) : placeholder) .applyCertiFont(.caption_semibold_12) .frame(width: 72,height: 18, alignment: .leading) .foregroundColor(selectedDate.wrappedValue != nil ? .grayscale600 : .grayscale300) diff --git a/CERTI-iOS/Presentation/Resume/Components/ResumeActivityListComponent.swift b/CERTI-iOS/Presentation/Resume/Components/ResumeActivityListComponent.swift index 520e5419..a8bafb42 100644 --- a/CERTI-iOS/Presentation/Resume/Components/ResumeActivityListComponent.swift +++ b/CERTI-iOS/Presentation/Resume/Components/ResumeActivityListComponent.swift @@ -8,13 +8,15 @@ import SwiftUI struct ResumeActivityListComponent: View { - let model: ResumeModel - + let model: ActivityModel + let onTapCard: () -> Void + var body: some View { HStack(alignment: .center, spacing: 0) { VStack(alignment: .leading, spacing: 0) { - Text("\(model.startAt) ~ \(model.endAt)") - .applyCertiFont(.caption_regular_12) + let periodText = "\(model.startAt.toYearMonth()) ~ \(model.endAt.toYearMonth())" + + Text(periodText) .applyCertiFont(.caption_regular_12) .foregroundStyle(.grayscale500) .frame(height: 18) @@ -44,15 +46,19 @@ struct ResumeActivityListComponent: View { Spacer() } + .onTapGesture { + onTapCard() + } } } #Preview { - ResumeActivityListComponent(model: ResumeModel( + ResumeActivityListComponent(model: ActivityModel( + activityId: 1, startAt: "2021.11", endAt: "2022.01", - name: "패션디자이너 인턴", - place: "서티그룹", - description: "트렌드 리서치 및 소재 조사" - )) + name: "sopt", + place: "동아리 36기 기획", + description: "서비스 기획 및 아이디어 도출" + ), onTapCard: {}) } diff --git a/CERTI-iOS/Presentation/Resume/Components/ResumeCareerListComponent.swift b/CERTI-iOS/Presentation/Resume/Components/ResumeCareerListComponent.swift new file mode 100644 index 00000000..bf867db4 --- /dev/null +++ b/CERTI-iOS/Presentation/Resume/Components/ResumeCareerListComponent.swift @@ -0,0 +1,65 @@ +// +// ResumeCareerListComponent.swift +// CERTI-iOS +// +// Created by 이상엽 on 1/17/26. +// + +import SwiftUI + +struct ResumeCareerListComponent: View { + let model: CareerModel + let onTapCard: () -> Void + + var body: some View { + HStack(alignment: .center, spacing: 0) { + VStack(alignment: .leading, spacing: 0) { + let periodText = "\(model.startAt.toYearMonth()) ~ \(model.endAt.toYearMonth())" + + Text(periodText) + .applyCertiFont(.caption_regular_12) + .foregroundStyle(.grayscale500) + .frame(height: 18) + + Text(model.place) + .applyCertiFont(.caption_regular_12) + .foregroundStyle(.grayscale500) + .frame(height: 18) + .padding(.top, 12) + } + .frame(width: 104, height: 48) + + VStack(alignment: .leading, spacing: 0) { + Text(model.name) + .applyCertiFont(.body_semibold_16) + .foregroundStyle(.grayscale600) + .frame(width: 137, height: 22, alignment: .leading) + + Text(model.description) + .applyCertiFont(.caption_regular_12) + .foregroundStyle(.grayscale600) + .lineLimit(1) + .frame(width: 137, height: 18, alignment: .leading) + .padding(.top, 10) + } + .frame(width: 137) + .padding(.leading, 29) + + Spacer() + } + .onTapGesture { + onTapCard() + } + } +} + +#Preview { + ResumeCareerListComponent(model: CareerModel( + careerId: 1, + startAt: "2021.11", + endAt: "2022.01", + name: "패션디자이너 인턴", + place: "서티그룹", + description: "트렌드 리서치 및 소재 조사" + ), onTapCard: {}) +} diff --git a/CERTI-iOS/Presentation/Resume/Components/ResumeWriteButton.swift b/CERTI-iOS/Presentation/Resume/Components/ResumeWriteButton.swift index d5c1ccd7..7edf6677 100644 --- a/CERTI-iOS/Presentation/Resume/Components/ResumeWriteButton.swift +++ b/CERTI-iOS/Presentation/Resume/Components/ResumeWriteButton.swift @@ -9,13 +9,15 @@ import SwiftUI struct ResumeWriteButton: View { let action: () -> Void + let buttonText: String + @Binding var textEmpty: Bool var body: some View { Button { action() } label: { - Text("추가하기") + Text(buttonText) .applyCertiFont(.body_semibold_16) .foregroundStyle(textEmpty ? .white : .grayscale400) .frame(maxWidth: .infinity) @@ -27,8 +29,3 @@ struct ResumeWriteButton: View { .disabled(!textEmpty) } } - -private func testButtonClicked() { - print("testButtonClicked") -} - diff --git a/CERTI-iOS/Presentation/Resume/Coordinator/ResumeCoordinator.swift b/CERTI-iOS/Presentation/Resume/Coordinator/ResumeCoordinator.swift index a1fd3e54..c143848c 100644 --- a/CERTI-iOS/Presentation/Resume/Coordinator/ResumeCoordinator.swift +++ b/CERTI-iOS/Presentation/Resume/Coordinator/ResumeCoordinator.swift @@ -9,10 +9,10 @@ import SwiftUI enum ResumeRoute: Hashable { case myCertificateEdit - case myCareerEdit - case myCareerWriteView - case myExtracurricularActivityEditView - case myExtracurricularActivityWriteView + case myCareerManageView + case myCareerWriteView(mode: CareerWriteMode) + case myActivityManageView + case myActivityWriteView(mode: ActivityWriteMode) } final class ResumeCoordinator: ObservableObject { diff --git a/CERTI-iOS/Presentation/Resume/Factory/ResumeFactory.swift b/CERTI-iOS/Presentation/Resume/Factory/ResumeFactory.swift index f79d6280..3d65302a 100644 --- a/CERTI-iOS/Presentation/Resume/Factory/ResumeFactory.swift +++ b/CERTI-iOS/Presentation/Resume/Factory/ResumeFactory.swift @@ -21,10 +21,12 @@ final class DefaultResumeFactory: ResumeFactory { let addCareersUseCase: AddCareersUseCase let deleteCareersUseCase: DeleteCareersUseCase let fetchCareersListUseCase: FetchCareersListUseCase + let editCareerUseCase: EditCareersUseCase let addActivityUseCase: AddActivityUseCase let deleteActivityUseCase: DeleteActivityUseCase let fetchActivityListUseCase: FetchActivityListUseCase + let editActivityUseCase: EditActivityUseCase init( fetchJobUseCase: FetchJobUseCase, @@ -34,9 +36,11 @@ final class DefaultResumeFactory: ResumeFactory { addCareersUseCase: AddCareersUseCase, deleteCareersUseCase: DeleteCareersUseCase, fetchCareersListUseCase: FetchCareersListUseCase, + editCareerUseCase: EditCareersUseCase, addActivityUseCase: AddActivityUseCase, deleteActivityUseCase: DeleteActivityUseCase, - fetchActivityListUseCase: FetchActivityListUseCase + fetchActivityListUseCase: FetchActivityListUseCase, + editActivityUseCase: EditActivityUseCase ) { self.fetchJobUseCase = fetchJobUseCase self.fetchAcquisitionListUseCase = fetchAcquisitionListUseCase @@ -45,9 +49,11 @@ final class DefaultResumeFactory: ResumeFactory { self.addCareersUseCase = addCareersUseCase self.deleteCareersUseCase = deleteCareersUseCase self.fetchCareersListUseCase = fetchCareersListUseCase + self.editCareerUseCase = editCareerUseCase self.addActivityUseCase = addActivityUseCase self.deleteActivityUseCase = deleteActivityUseCase self.fetchActivityListUseCase = fetchActivityListUseCase + self.editActivityUseCase = editActivityUseCase } @MainActor @@ -60,9 +66,11 @@ final class DefaultResumeFactory: ResumeFactory { addCareersUseCase: addCareersUseCase, deleteCareersUseCase: deleteCareersUseCase, fetchCareersListUseCase: fetchCareersListUseCase, + editCareerUseCase: editCareerUseCase, addActivityUseCase: addActivityUseCase, deleteActivityUseCase: deleteActivityUseCase, - fetchActivityListUseCase: fetchActivityListUseCase + fetchActivityListUseCase: fetchActivityListUseCase, + editActivityUseCase: editActivityUseCase ) } } diff --git a/CERTI-iOS/Presentation/Resume/Models/ActivityModel.swift b/CERTI-iOS/Presentation/Resume/Models/ActivityModel.swift new file mode 100644 index 00000000..4bc8ca29 --- /dev/null +++ b/CERTI-iOS/Presentation/Resume/Models/ActivityModel.swift @@ -0,0 +1,19 @@ +// +// ActivityModel.swift +// CERTI-iOS +// +// Created by 이상엽 on 1/16/26. +// + +import Foundation + +struct ActivityModel: Identifiable { + var activityId: Int + var startAt: String + var endAt: String + var name: String + var place: String + var description: String + + var id: Int { activityId } +} diff --git a/CERTI-iOS/Presentation/Resume/Models/ActivityWriteModel.swift b/CERTI-iOS/Presentation/Resume/Models/ActivityWriteModel.swift new file mode 100644 index 00000000..cc65304b --- /dev/null +++ b/CERTI-iOS/Presentation/Resume/Models/ActivityWriteModel.swift @@ -0,0 +1,33 @@ +// +// ActivityWriteModel.swift +// CERTI-iOS +// +// Created by 이상엽 on 1/17/26. +// + +import Foundation + +struct ActivityWriteModel { + var startAt: String = "" + var endAt: String = "" + var name: String = "" + var place: String = "" + var description: String = "" +} + +extension ActivityWriteModel { + + + // MARK: - Func + + func toActivityEntity() -> ActivityEntity { + ActivityEntity( + activityId: nil, + startAt: startAt, + endAt: endAt, + name: name, + place: place, + description: description + ) + } +} diff --git a/CERTI-iOS/Presentation/Resume/Models/CareerModel.swift b/CERTI-iOS/Presentation/Resume/Models/CareerModel.swift new file mode 100644 index 00000000..5feb3b95 --- /dev/null +++ b/CERTI-iOS/Presentation/Resume/Models/CareerModel.swift @@ -0,0 +1,35 @@ +// +// CareerModel.swift +// CERTI-iOS +// +// Created by 이상엽 on 1/16/26. +// + +import Foundation + +struct CareerModel: Identifiable { + var careerId: Int + var startAt: String + var endAt: String + var name: String + var place: String + var description: String + + var id: Int { careerId } +} + + +// MARK: - Func + +extension CareerModel { + func toCareersEntity() -> CareerEntity { + CareerEntity( + careerId: careerId, + startAt: startAt, + endAt: endAt, + name: name, + place: place, + description: description + ) + } +} diff --git a/CERTI-iOS/Presentation/Resume/Models/CareerWriteModel.swift b/CERTI-iOS/Presentation/Resume/Models/CareerWriteModel.swift new file mode 100644 index 00000000..c72ecea3 --- /dev/null +++ b/CERTI-iOS/Presentation/Resume/Models/CareerWriteModel.swift @@ -0,0 +1,33 @@ +// +// CareerWriteModel.swift +// CERTI-iOS +// +// Created by 이상엽 on 1/17/26. +// + +import Foundation + +struct CareerWriteModel { + var startAt: String = "" + var endAt: String = "" + var name: String = "" + var place: String = "" + var description: String = "" +} + +extension CareerWriteModel { + + + // MARK: - Func + + func toCareerEntity() -> CareerEntity { + CareerEntity( + careerId: nil, + startAt: startAt, + endAt: endAt, + name: name, + place: place, + description: description + ) + } +} diff --git a/CERTI-iOS/Presentation/Resume/Models/ResumeListItem.swift b/CERTI-iOS/Presentation/Resume/Models/ResumeListItem.swift new file mode 100644 index 00000000..eea4318f --- /dev/null +++ b/CERTI-iOS/Presentation/Resume/Models/ResumeListItem.swift @@ -0,0 +1,16 @@ +// +// ResumeListItem.swift +// CERTI-iOS +// +// Created by 이상엽 on 1/17/26. +// + +import Foundation + +protocol ResumeListItem { + var startAt: String { get } + var endAt: String { get } + var name: String { get } + var place: String { get } + var description: String { get } +} diff --git a/CERTI-iOS/Presentation/Resume/Models/ResumeModel.swift b/CERTI-iOS/Presentation/Resume/Models/ResumeModel.swift deleted file mode 100644 index f272bff9..00000000 --- a/CERTI-iOS/Presentation/Resume/Models/ResumeModel.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// ResumeModel.swift -// CERTI-iOS -// -// Created by 이상엽 on 7/12/25. -// - -import SwiftUI - -struct ResumeModel: Identifiable { - var id: UUID = UUID() - - var activityId: Int? - var careerId: Int? - var startAt: String - var endAt: String - var name: String - var place: String - var description: String -} - -extension ResumeModel { - - - // MARK: - Func - - func toCareersEntity() -> CareersEntity { - return CareersEntity( - data: ResumeEntityData( - startAt: startAt, - endAt: endAt, - name: name, - place: place, - description: description - ) - ) - } - - func toActivityEntity() -> ActivityEntity { - return ActivityEntity( - data: ResumeEntityData( - startAt: startAt, - endAt: endAt, - name: name, - place: place, - description: description - ) - ) - } -} diff --git a/CERTI-iOS/Presentation/Resume/View/MyExtracurricularActivityEditView.swift b/CERTI-iOS/Presentation/Resume/View/MyActivityManageView.swift similarity index 88% rename from CERTI-iOS/Presentation/Resume/View/MyExtracurricularActivityEditView.swift rename to CERTI-iOS/Presentation/Resume/View/MyActivityManageView.swift index 00418685..d4780fd3 100644 --- a/CERTI-iOS/Presentation/Resume/View/MyExtracurricularActivityEditView.swift +++ b/CERTI-iOS/Presentation/Resume/View/MyActivityManageView.swift @@ -1,5 +1,5 @@ // -// MyExtracurricularActivityEditView.swift +// MyActivityManageView.swift // CERTI-iOS // // Created by 이상엽 on 7/13/25. @@ -7,7 +7,7 @@ import SwiftUI -struct MyExtracurricularActivityEditView: View { +struct MyActivityManageView: View { @ObservedObject var viewModel: ResumeViewModel @State var isDeleteAlertPresented = false @State var selectedActivityIndex : Int? = nil @@ -41,7 +41,7 @@ struct MyExtracurricularActivityEditView: View { .background(.purplewhite) .clipShape(RoundedRectangle(cornerRadius: 8)) } - .padding(.top, 44) + .padding(.top, 16) .padding(.leading, 20) @@ -49,13 +49,16 @@ struct MyExtracurricularActivityEditView: View { .applyCertiFont(.sub_semibold_20) .foregroundStyle(.grayscale600) .frame(height: 26) - .padding(.top, 32) + .padding(.top, 56) .padding(.leading, 20) LazyVGrid(columns: columns, spacing: 36) { - ForEach(viewModel.activityList) { item in + ForEach(viewModel.activitiesList) { item in HStack(alignment: .center, spacing: 0) { - ResumeActivityListComponent(model: item) + ResumeActivityListComponent(model: item, onTapCard: { + viewModel.selectActivity(id: item.activityId) + viewModel.navigateToActivityEdit() + }) .frame(height: 50) Button { diff --git a/CERTI-iOS/Presentation/Resume/View/MyExtracurricularActivityWriteView.swift b/CERTI-iOS/Presentation/Resume/View/MyActivityWriteView.swift similarity index 73% rename from CERTI-iOS/Presentation/Resume/View/MyExtracurricularActivityWriteView.swift rename to CERTI-iOS/Presentation/Resume/View/MyActivityWriteView.swift index e7bf150f..ce3bd483 100644 --- a/CERTI-iOS/Presentation/Resume/View/MyExtracurricularActivityWriteView.swift +++ b/CERTI-iOS/Presentation/Resume/View/MyActivityWriteView.swift @@ -1,5 +1,5 @@ // -// MyExtracurricularActivityWriteView.swift +// MyActivityWriteView.swift // CERTI-iOS // // Created by 이상엽 on 7/13/25. @@ -7,9 +7,10 @@ import SwiftUI -struct MyExtracurricularActivityWriteView: View { +struct MyActivityWriteView: View { @ObservedObject var viewModel: ResumeViewModel - + let mode: ActivityWriteMode + var body: some View { VStack (alignment: .leading, spacing: 0) { BackButton() { @@ -22,8 +23,8 @@ struct MyExtracurricularActivityWriteView: View { activityPeriodView PeriodInputComponent( isFilled: $viewModel.isPeriodFilled, - startAt: $viewModel.resumeModel.startAt, - endAt: $viewModel.resumeModel.endAt + startAt: $viewModel.activityWriteModel.startAt, + endAt: $viewModel.activityWriteModel.endAt ) organizeView activityView @@ -31,18 +32,27 @@ struct MyExtracurricularActivityWriteView: View { ResumeWriteButton( action: { Task { - await viewModel.addActivity(resumeModel: viewModel.resumeModel) - viewModel.clearResumeModel() + switch mode { + case .add: + await viewModel.addActivity(activityWriteModel: viewModel.activityWriteModel) + case .edit(let activityId): + await viewModel.editActivity(activityId: activityId, activityWriteModel: viewModel.activityWriteModel) + } viewModel.resumeViewRoutePop() } - }, - textEmpty: .constant(viewModel.isWriteButtonEnabled) + }, buttonText: mode == .add ? "추가하기" : "수정하기", + textEmpty: .constant(viewModel.isActivityWriteButtonEnabled) ) .padding(.top, 40) } } .onAppear{ - viewModel.clearResumeModel() + switch mode { + case .add: + viewModel.clearActivityWriteModel() + case .edit(let activityId): + viewModel.prepareActivityEdit(activityId: activityId) + } } .navigationBarBackButtonHidden() .scrollIndicators(.hidden) @@ -51,11 +61,11 @@ struct MyExtracurricularActivityWriteView: View { } } -extension MyExtracurricularActivityWriteView { +extension MyActivityWriteView { private var MyExtracurricularActivityTitleView: some View { Group { HStack(alignment: .center, spacing: 0) { - Text("대내외 활동 추가") + Text(mode == .add ? "대내외 활동 추가" : "대내외 활동 수정") .applyCertiFont(.sub_semibold_20) .foregroundStyle(.grayscale600) .frame(height: 26) @@ -105,7 +115,7 @@ extension MyExtracurricularActivityWriteView { .padding(.bottom, 24) .padding(.top, 36) - CharLimitTextField(text: $viewModel.resumeModel.name, maxLength: 10) + CharLimitTextField(text: $viewModel.activityWriteModel.name, maxLength: 10) .padding(.horizontal, 20) } } @@ -128,7 +138,7 @@ extension MyExtracurricularActivityWriteView { .padding(.bottom, 24) .padding(.top, 36) - CharLimitTextField(text: $viewModel.resumeModel.place, maxLength: 10) + CharLimitTextField(text: $viewModel.activityWriteModel.place, maxLength: 10) .padding(.horizontal, 20) } } @@ -151,13 +161,9 @@ extension MyExtracurricularActivityWriteView { .padding(.bottom, 24) .padding(.top, 36) - CharLimitTextField(text: $viewModel.resumeModel.description, maxLength: 16) + CharLimitTextField(text: $viewModel.activityWriteModel.description, maxLength: 16) .padding(.horizontal, 20) .padding(.bottom, 16) } } - - private func testButtonClicked() { - print("testButtonClicked") - } } diff --git a/CERTI-iOS/Presentation/Resume/View/MyCareerEditView.swift b/CERTI-iOS/Presentation/Resume/View/MyCareerManageView.swift similarity index 88% rename from CERTI-iOS/Presentation/Resume/View/MyCareerEditView.swift rename to CERTI-iOS/Presentation/Resume/View/MyCareerManageView.swift index 38de0cfe..95cb3294 100644 --- a/CERTI-iOS/Presentation/Resume/View/MyCareerEditView.swift +++ b/CERTI-iOS/Presentation/Resume/View/MyCareerManageView.swift @@ -1,13 +1,13 @@ // -// MyCareerEditView.swift +// MyCareerManageView.swift // CERTI-iOS // -// Created by 이상엽 on 7/12/25. +// Created by 이상엽 on 1/15/26. // import SwiftUI -struct MyCareerEditView: View { +struct MyCareerManageView: View { @ObservedObject var viewModel: ResumeViewModel @State var isDeleteAlertPresented = false @@ -41,20 +41,23 @@ struct MyCareerEditView: View { .background(.purplewhite) .clipShape(RoundedRectangle(cornerRadius: 8)) } - .padding(.top, 44) + .padding(.top, 16) .padding(.leading, 20) Text("경력사항 수정") .applyCertiFont(.sub_semibold_20) .foregroundStyle(.grayscale600) .frame(height: 26) - .padding(.top, 32) + .padding(.top, 56) .padding(.leading, 20) LazyVGrid(columns: columns, spacing: 36) { ForEach(viewModel.careersList) { item in HStack(alignment: .center, spacing: 0) { - ResumeActivityListComponent(model: item) + ResumeCareerListComponent(model: item, onTapCard: { + viewModel.selectCareer(id: item.careerId) + viewModel.navigateToCareerEdit() + }) .frame(height: 50) Button { @@ -84,10 +87,8 @@ struct MyCareerEditView: View { await viewModel.deleteCareers(id: deleteIndex) } isDeleteAlertPresented = false - print("확인 버튼 클릭") } onCancel: { isDeleteAlertPresented = false - print("취소버튼 클릭") } } } diff --git a/CERTI-iOS/Presentation/Resume/View/MyCareerWriteView.swift b/CERTI-iOS/Presentation/Resume/View/MyCareerWriteView.swift index a4144c99..73c68f52 100644 --- a/CERTI-iOS/Presentation/Resume/View/MyCareerWriteView.swift +++ b/CERTI-iOS/Presentation/Resume/View/MyCareerWriteView.swift @@ -9,44 +9,55 @@ import SwiftUI struct MyCareerWriteView: View { @ObservedObject var viewModel: ResumeViewModel + let mode: CareerWriteMode var body: some View { - VStack (alignment: .leading, spacing: 0) { - BackButton() { - viewModel.resumeViewRoutePop() - } - - ScrollView { - VStack(alignment: .leading, spacing: 0) { - MyCareerWriteTitleView - workingPeriodView - PeriodInputComponent( - isFilled: $viewModel.isPeriodFilled, - startAt: $viewModel.resumeModel.startAt, - endAt: $viewModel.resumeModel.endAt - ) - workingCompany - dutyView - dutyDetailView - ResumeWriteButton( - action: { - Task { - await viewModel.addCareer(resumeModel: viewModel.resumeModel) - viewModel.resumeViewRoutePop() + VStack (alignment: .leading, spacing: 0) { + BackButton() { + viewModel.resumeViewRoutePop() + } + + ScrollView { + VStack(alignment: .leading, spacing: 0) { + MyCareerWriteTitleView + workingPeriodView + PeriodInputComponent( + isFilled: $viewModel.isPeriodFilled, + startAt: $viewModel.careerWriteModel.startAt, + endAt: $viewModel.careerWriteModel.endAt + ) + workingCompany + dutyView + dutyDetailView + ResumeWriteButton( + action: { + Task { + switch mode { + case .add: + await viewModel.addCareer(careerWriteModel: viewModel.careerWriteModel) + case .edit(let careerId): + await viewModel.editCareer(careerId: careerId, careerWriteModel: viewModel.careerWriteModel) } - }, - textEmpty: .constant(viewModel.isWriteButtonEnabled) - ) - .padding(.top, 40) - } + viewModel.resumeViewRoutePop() + } + }, buttonText: mode == .add ? "추가하기" : "수정하기", + textEmpty: .constant(viewModel.isCareerWriteButtonEnabled) + ) + .padding(.top, 40) } - .onAppear{ - viewModel.clearResumeModel() + } + .onAppear{ + switch mode { + case .add: + viewModel.clearCareerWriteModel() + case .edit(let careerId): + viewModel.prepareCareerEdit(careerId: careerId) } - .scrollIndicators(.hidden) - .navigationBarBackButtonHidden() - .scrollDismissesKeyboard(.immediately) } + .scrollIndicators(.hidden) + .navigationBarBackButtonHidden() + .scrollDismissesKeyboard(.immediately) + } } } @@ -54,7 +65,7 @@ extension MyCareerWriteView { private var MyCareerWriteTitleView: some View { Group { HStack(alignment: .center, spacing: 0) { - Text("경력사항 추가") + Text(mode == .add ? "경력사항 추가" : "경력사항 수정") .applyCertiFont(.sub_semibold_20) .foregroundStyle(.grayscale600) .frame(height: 26) @@ -101,7 +112,7 @@ extension MyCareerWriteView { .padding(.bottom, 24) .padding(.top, 36) - CharLimitTextField(text: $viewModel.resumeModel.name, maxLength: 10) + CharLimitTextField(text: $viewModel.careerWriteModel.name, maxLength: 10) .padding(.horizontal, 20) } } @@ -123,7 +134,7 @@ extension MyCareerWriteView { .padding(.bottom, 24) .padding(.top, 36) - CharLimitTextField(text: $viewModel.resumeModel.place, maxLength: 10) + CharLimitTextField(text: $viewModel.careerWriteModel.place, maxLength: 10) .padding(.horizontal, 20) } } @@ -145,12 +156,8 @@ extension MyCareerWriteView { .padding(.bottom, 24) .padding(.top, 36) - CharLimitTextField(text: $viewModel.resumeModel.description, maxLength: 16) + CharLimitTextField(text: $viewModel.careerWriteModel.description, maxLength: 16) .padding(.horizontal, 20) } } - - private func testButtonClicked() { - print("testButtonClicked") - } } diff --git a/CERTI-iOS/Presentation/Resume/View/ResumeCoordinatorView.swift b/CERTI-iOS/Presentation/Resume/View/ResumeCoordinatorView.swift index d52a60ca..0233bffb 100644 --- a/CERTI-iOS/Presentation/Resume/View/ResumeCoordinatorView.swift +++ b/CERTI-iOS/Presentation/Resume/View/ResumeCoordinatorView.swift @@ -27,16 +27,16 @@ struct ResumeCoordinatorView: View { .onChange(of: resumeViewModel.resumeViewRoute) { route in guard let route = route else { return } switch route { - case .navigateToActivityEdit: - resumeCoordinator.push(next: .myExtracurricularActivityEditView) - case .navigateToCareerWrite: - resumeCoordinator.push(next: .myCareerWriteView) - case .navigateToActivityWrite: - resumeCoordinator.push(next: .myExtracurricularActivityWriteView) + case .navigateToActivityManage: + resumeCoordinator.push(next: .myActivityManageView) + case .navigateToCareerWrite(let mode): + resumeCoordinator.push(next: .myCareerWriteView(mode: mode)) + case .navigateToActivityWrite(let mode): + resumeCoordinator.push(next: .myActivityWriteView(mode: mode)) case .navigateToCertificatedEdit: resumeCoordinator.push(next: .myCertificateEdit) - case .navigateToCareerEdit: - resumeCoordinator.push(next: .myCareerEdit) + case .navigateToCareerManage: + resumeCoordinator.push(next: .myCareerManageView) case .resumeViewRoutePop: resumeCoordinator.pop() } @@ -46,14 +46,14 @@ struct ResumeCoordinatorView: View { switch route { case .myCertificateEdit: MyCertificateEditView(viewModel: resumeViewModel) - case .myCareerEdit: - MyCareerEditView(viewModel: resumeViewModel) - case .myCareerWriteView: - MyCareerWriteView(viewModel: resumeViewModel) - case .myExtracurricularActivityEditView: - MyExtracurricularActivityEditView(viewModel: resumeViewModel) - case .myExtracurricularActivityWriteView: - MyExtracurricularActivityWriteView(viewModel: resumeViewModel) + case .myCareerManageView: + MyCareerManageView(viewModel: resumeViewModel) + case .myCareerWriteView(let mode): + MyCareerWriteView(viewModel: resumeViewModel, mode: mode) + case .myActivityManageView: + MyActivityManageView(viewModel: resumeViewModel) + case .myActivityWriteView(let mode): + MyActivityWriteView(viewModel: resumeViewModel, mode: mode) } } } diff --git a/CERTI-iOS/Presentation/Resume/View/ResumeView.swift b/CERTI-iOS/Presentation/Resume/View/ResumeView.swift index b6fe7249..e9e8a508 100644 --- a/CERTI-iOS/Presentation/Resume/View/ResumeView.swift +++ b/CERTI-iOS/Presentation/Resume/View/ResumeView.swift @@ -23,8 +23,8 @@ struct ResumeView: View { ResumeMyCertificateView ResumeMyCareerTitleView ResumeMyCareerView - ResumeMyExtracurricularActivityTitleView - ResumeMyExtracurricularActivityView + ResumeMyActivityTitleView + ResumeMyActivityView } } .scrollIndicators(.hidden) @@ -203,7 +203,7 @@ extension ResumeView { Spacer() Button { - viewModel.navigateToCareerEdit() + viewModel.navigateToCareerManage() } label: { Image(.iconArrowright36) } @@ -238,7 +238,7 @@ extension ResumeView { .padding(.top, 20.5) .padding(.bottom, 29.5) - ResumeActivityListComponent(model: item) + ResumeCareerListComponent(model: item, onTapCard: {}) .frame(height: 74) } .padding(.horizontal, 20) @@ -254,7 +254,7 @@ extension ResumeView { } } - private var ResumeMyExtracurricularActivityTitleView: some View { + private var ResumeMyActivityTitleView: some View { HStack(alignment: .center, spacing: 0){ Text("대내외 활동") .applyCertiFont(.sub_semibold_20) @@ -264,7 +264,7 @@ extension ResumeView { Spacer() Button { - viewModel.navigateToActivityEdit() + viewModel.navigateToActivityManage() } label: { Image(.iconArrowright36) } @@ -274,9 +274,9 @@ extension ResumeView { .padding(.bottom, 16) } - private var ResumeMyExtracurricularActivityView: some View { + private var ResumeMyActivityView: some View { VStack(alignment: .leading, spacing: 0) { - if viewModel.activityList.isEmpty { + if viewModel.activitiesList.isEmpty { VStack(alignment: .center, spacing: 0) { Image(.imageEmpty) .padding(.top, 60) @@ -291,7 +291,7 @@ extension ResumeView { .frame(maxWidth: .infinity) } else { LazyVGrid(columns: columns, spacing: 16) { - ForEach(viewModel.activityList.prefix(4)) { item in + ForEach(viewModel.activitiesList.prefix(4)) { item in HStack(alignment: .center, spacing: 0) { Image(.resumeList) .frame(width: 24, height: 24) @@ -299,7 +299,7 @@ extension ResumeView { .padding(.top, 20.5) .padding(.bottom, 29.5) - ResumeActivityListComponent(model: item) + ResumeActivityListComponent(model: item, onTapCard: {}) .frame(height: 74) } .padding(.horizontal, 20) @@ -310,20 +310,3 @@ extension ResumeView { } } } - -#Preview { - ResumeView( - viewModel: ResumeViewModel( - fetchJobUseCase: PreviewFetchJobUseCase(), - fetchAcquisitionListUseCase: PreviewFetchAcquisitionListUseCase(), - fetchAcquisitionDetailUseCase: PreviewFetchAcquisitionDetailUseCase(), - deleteAcquisitionUseCase: PreviewDeleteAcquisitionUseCase(), - addCareersUseCase: PreviewAddCareersUseCase(), - deleteCareersUseCase: PreviewDeleteCareersUserCase(), - fetchCareersListUseCase: PreviewFetchCareersListUseCase(), - addActivityUseCase: PreviewAddActivityUseCase(), - deleteActivityUseCase: PreviewDeleteActivityUseCase(), - fetchActivityListUseCase: PreviewFetchActivityListUseCase() - ) - ) -} diff --git a/CERTI-iOS/Presentation/Resume/ViewModel/ResumeViewModel.swift b/CERTI-iOS/Presentation/Resume/ViewModel/ResumeViewModel.swift index 34e6a9f1..bae2d0df 100644 --- a/CERTI-iOS/Presentation/Resume/ViewModel/ResumeViewModel.swift +++ b/CERTI-iOS/Presentation/Resume/ViewModel/ResumeViewModel.swift @@ -9,36 +9,47 @@ import Foundation import os -enum ResumeViewRoute { - case navigateToCareerWrite - case navigateToActivityWrite +enum ResumeViewRoute: Equatable { + case navigateToCareerWrite(mode: CareerWriteMode) + case navigateToActivityWrite(mode: ActivityWriteMode) case navigateToCertificatedEdit - case navigateToCareerEdit - case navigateToActivityEdit + case navigateToCareerManage + case navigateToActivityManage case resumeViewRoutePop } +enum CareerWriteMode: Hashable { + case add + case edit(careerId: Int) +} + +enum ActivityWriteMode: Hashable { + case add + case edit(activityId: Int) +} + @MainActor final class ResumeViewModel: ObservableObject { @Published var resumeViewRoute: ResumeViewRoute? @Published var jobList: [String] = [] @Published var acquisitionList: [CertificatedModel] = [] @Published var acquisitionDetail: CertificatedDetailModel? = nil - @Published var careersList: [ResumeModel] = [] - @Published var activityList: [ResumeModel] = [] + @Published var careersList: [CareerModel] = [] + @Published var activitiesList: [ActivityModel] = [] @Published var isPeriodFilled: Bool = false - @Published var resumeModel = ResumeModel( - startAt: "", - endAt: "", - name: "", - place: "", - description: "" - ) + @Published var careerWriteModel = CareerWriteModel() + @Published var activityWriteModel = ActivityWriteModel() @Published var isCardDetailPresented = false + @Published var selectCareerId: Int = 0 + @Published var selectActivityId: Int = 0 + + var isCareerWriteButtonEnabled: Bool { + !careerWriteModel.name.isBlank && !careerWriteModel.place.isBlank && !careerWriteModel.description.isBlank && isPeriodFilled + } - var isWriteButtonEnabled: Bool { - !resumeModel.name.isBlank && !resumeModel.place.isBlank && !resumeModel.description.isBlank && isPeriodFilled + var isActivityWriteButtonEnabled: Bool { + !activityWriteModel.name.isBlank && !activityWriteModel.place.isBlank && !activityWriteModel.description.isBlank && isPeriodFilled } private let fetchJobUseCase: FetchJobUseCase @@ -50,10 +61,12 @@ final class ResumeViewModel: ObservableObject { private let addCareersUseCase: AddCareersUseCase private let deleteCareersUseCase: DeleteCareersUseCase private let fetchCareersListUseCase: FetchCareersListUseCase + private let editCareerUseCase: EditCareersUseCase private let addActivityUseCase: AddActivityUseCase private let deleteActivityUseCase: DeleteActivityUseCase private let fetchActivityListUseCase: FetchActivityListUseCase + private let editActivityUseCase: EditActivityUseCase init( fetchJobUseCase: FetchJobUseCase, @@ -63,9 +76,11 @@ final class ResumeViewModel: ObservableObject { addCareersUseCase: AddCareersUseCase, deleteCareersUseCase: DeleteCareersUseCase, fetchCareersListUseCase: FetchCareersListUseCase, + editCareerUseCase: EditCareersUseCase, addActivityUseCase: AddActivityUseCase, deleteActivityUseCase: DeleteActivityUseCase, - fetchActivityListUseCase: FetchActivityListUseCase + fetchActivityListUseCase: FetchActivityListUseCase, + editActivityUseCase: EditActivityUseCase ) { self.fetchJobUseCase = fetchJobUseCase self.fetchAcquisitionListUseCase = fetchAcquisitionListUseCase @@ -74,13 +89,15 @@ final class ResumeViewModel: ObservableObject { self.addCareersUseCase = addCareersUseCase self.deleteCareersUseCase = deleteCareersUseCase self.fetchCareersListUseCase = fetchCareersListUseCase + self.editCareerUseCase = editCareerUseCase self.addActivityUseCase = addActivityUseCase self.deleteActivityUseCase = deleteActivityUseCase self.fetchActivityListUseCase = fetchActivityListUseCase + self.editActivityUseCase = editActivityUseCase } - - func clearResumeModel() { - resumeModel = ResumeModel( + + func clearCareerWriteModel() { + careerWriteModel = CareerWriteModel( startAt: "", endAt: "", name: "", @@ -89,6 +106,18 @@ final class ResumeViewModel: ObservableObject { ) isPeriodFilled = false } + + func clearActivityWriteModel() { + activityWriteModel = ActivityWriteModel( + startAt: "", + endAt: "", + name: "", + place: "", + description: "" + ) + isPeriodFilled = false + } + private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "CERTI", category: "resume") } @@ -98,23 +127,31 @@ final class ResumeViewModel: ObservableObject { extension ResumeViewModel { func navigateToCareerWrite() { - resumeViewRoute = .navigateToCareerWrite + resumeViewRoute = .navigateToCareerWrite(mode: .add) + } + + func navigateToCareerEdit() { + resumeViewRoute = .navigateToCareerWrite(mode: .edit(careerId: selectCareerId)) } func navigateToActivityWrite() { - resumeViewRoute = .navigateToActivityWrite + resumeViewRoute = .navigateToActivityWrite(mode: .add) } - + + func navigateToActivityEdit() { + resumeViewRoute = .navigateToActivityWrite(mode: .edit(activityId: selectActivityId)) + } + func navigateToCertificatedEdit() { resumeViewRoute = .navigateToCertificatedEdit } - func navigateToCareerEdit() { - resumeViewRoute = .navigateToCareerEdit + func navigateToCareerManage() { + resumeViewRoute = .navigateToCareerManage } - func navigateToActivityEdit() { - resumeViewRoute = .navigateToActivityEdit + func navigateToActivityManage() { + resumeViewRoute = .navigateToActivityManage } func resumeViewRoutePop() { @@ -172,19 +209,19 @@ extension ResumeViewModel { case .success(_): logger.info("✅ 취득한 자격증 삭제 성공") acquisitionList.removeAll { $0.acquisitionId == id } - + case .failure(let error): logger.error("취득한 자격증 삭제 failed: \(error.localizedDescription)") } } - + func getCareersList() async { let result = await fetchCareersListUseCase.execute() switch result { case .success(let response): - self.careersList = response.toResumeModel() + self.careersList = response.toCareerModels() logger.debug("✅ 경력사항 조회 성공") case .failure(let error): @@ -192,9 +229,9 @@ extension ResumeViewModel { } } - func addCareer(resumeModel: ResumeModel) async { - let result = await addCareersUseCase.execute(request: resumeModel.toCareersEntity()) - + func addCareer(careerWriteModel: CareerWriteModel) async { + let result = await addCareersUseCase.execute(request: careerWriteModel.toCareerEntity()) + switch result { case .success: logger.info("✅ 경력 추가 성공") @@ -210,26 +247,37 @@ extension ResumeViewModel { case .success(_): logger.info("✅ 취득한 자격증 삭제 성공") careersList.removeAll { $0.careerId == id } - + case .failure(let error): logger.error("취득한 자격증 삭제 failed: \(error.localizedDescription)") } } - - + + func editCareer(careerId: Int, careerWriteModel: CareerWriteModel) async { + let result = await editCareerUseCase.execute(careerId: careerId, request: careerWriteModel.toCareerEntity()) + + switch result { + case .success(_): + logger.info("✅ 경력 수정 성공") + + case .failure(let error): + logger.error("❌경력 수정 failed: \(error.localizedDescription)") + } + } + func getActivityList() async { let result = await fetchActivityListUseCase.execute() switch result { case .success(let response): - self.activityList = response.toResumeModel() + self.activitiesList = response.toActivityModels() logger.debug("✅ 대내외활동 조회 성공") case .failure(let error): logger.error("❌ 대내외활동 조회 실패: \(error.localizedDescription)") } } - + func deleteActivity(id: Int) async { let result = await deleteActivityUseCase.execute(id: id) @@ -237,16 +285,16 @@ extension ResumeViewModel { switch result { case .success(_): logger.info("✅ 대내외 활동 삭제 성공") - activityList.removeAll { $0.activityId == id } - + activitiesList.removeAll { $0.activityId == id } + case .failure(let error): logger.error("대내외 활동 삭제 failed: \(error.localizedDescription)") } } - - func addActivity(resumeModel: ResumeModel) async { - let result = await addActivityUseCase.execute(request: resumeModel.toActivityEntity()) - + + func addActivity(activityWriteModel: ActivityWriteModel) async { + let result = await addActivityUseCase.execute(request: activityWriteModel.toActivityEntity()) + switch result { case .success: logger.info("✅ 활동 추가 성공") @@ -254,4 +302,64 @@ extension ResumeViewModel { logger.error("❌ 활동 추가 실패: \(error.localizedDescription)") } } + + func editActivity(activityId: Int, activityWriteModel: ActivityWriteModel) async { + let result = await editActivityUseCase.execute(activityId: activityId, request: activityWriteModel.toActivityEntity()) + + switch result { + case .success(_): + logger.info("✅ 대내외활동 수정 성공") + + case .failure(let error): + logger.error("❌ 대내외활동 수정 실패: \(error.localizedDescription)") + } + } +} + +extension ResumeViewModel { + + + // MARK: - Data Func + + func prepareCareerEdit(careerId: Int) { + guard let career = careersList.first(where: { $0.careerId == careerId }) else { + return + } + + careerWriteModel = CareerWriteModel( + startAt: career.startAt, + endAt: career.endAt, + name: career.name, + place: career.place, + description: career.description + ) + + isPeriodFilled = true + selectCareerId = careerId + } + + func selectCareer(id: Int) { + selectCareerId = id + } + + func prepareActivityEdit(activityId: Int) { + guard let activity = activitiesList.first(where: { $0.activityId == activityId }) else { + return + } + + activityWriteModel = ActivityWriteModel( + startAt: activity.startAt, + endAt: activity.endAt, + name: activity.name, + place: activity.place, + description: activity.description + ) + + isPeriodFilled = true + selectActivityId = activityId + } + + func selectActivity(id: Int) { + selectActivityId = id + } }