Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 프로필 변경 화면 VIP 구현 #259

Merged
merged 21 commits into from
Dec 9, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4d33d51
:recycle: 프로필 편집 뷰 사이클 리팩토링
loinsir Dec 7, 2023
05ebd07
:sparkles: 프로필 편집 validation 구현
loinsir Dec 7, 2023
09720d3
:bug: 완료 버튼 연결
loinsir Dec 7, 2023
33bf5e9
:bug: 프로필 수정 validation 필드 수정
loinsir Dec 7, 2023
e6b4d78
:sparkles: 프로필 이미지 편집 액션시트 구현
loinsir Dec 7, 2023
63a02a1
:bug: 닉네임 중복체크 버튼 활성화 조건 수정
loinsir Dec 8, 2023
84dd450
Merge branch 'iOS/dev' of https://github.com/boostcampwm2023/iOS09-La…
loinsir Dec 8, 2023
383780f
:bug: 프로필 변경 완료 후 완료 버튼 status 변경
loinsir Dec 8, 2023
248aa3a
:bug: 프로필 변경 후 프로필 탭 돌아올 경우 게시글 표시 안되는 버그 수정
loinsir Dec 8, 2023
e31e68f
:wrench: 프로필 변경 성공 여부 토스트 메시지 표시 수정
loinsir Dec 8, 2023
eb7afdb
:wrench: 프로필 이미지 변경 시도 수정
loinsir Dec 8, 2023
d6d84d3
Merge branch 'iOS/dev' of https://github.com/boostcampwm2023/iOS09-La…
loinsir Dec 8, 2023
54593b5
:wrench: 프로필 이미지 삭제 수정
loinsir Dec 8, 2023
3345df5
:bug: presignedURL body parameter key 수정
loinsir Dec 9, 2023
3ca6269
:bug: 이미지 업로드 버그 수정
loinsir Dec 9, 2023
50bceff
:sparkles: 기본 이미지로 변경 가능한 기능 수정
loinsir Dec 9, 2023
4954c48
:wrench: 코드 리뷰 반영 수정
loinsir Dec 9, 2023
777c666
:wrench: 닉네임 검증 결과에 따른 안내 문구 색상 표시 수정
loinsir Dec 9, 2023
90ec979
:wrench: 코드 리뷰 반영 수정
loinsir Dec 9, 2023
e3da5df
:wrench: 린트경고 수정
loinsir Dec 9, 2023
abda7b7
:wrench: 코드 리뷰 반영 네이밍 수정
loinsir Dec 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions iOS/Layover/Layover.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
198167A32B20583D0032F563 /* SettingModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1981679E2B20583D0032F563 /* SettingModels.swift */; };
198167A42B20583D0032F563 /* SettingInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1981679F2B20583D0032F563 /* SettingInteractor.swift */; };
198167A62B20593B0032F563 /* SettingConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 198167A52B20593B0032F563 /* SettingConfigurator.swift */; };
198167A82B20DD670032F563 /* PresignedURLDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 198167A72B20DD670032F563 /* PresignedURLDTO.swift */; };
19A169232B176C5F00DB34C0 /* TagPlayListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1691D2B176C5F00DB34C0 /* TagPlayListPresenter.swift */; };
19A169242B176C5F00DB34C0 /* TagPlayListWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1691E2B176C5F00DB34C0 /* TagPlayListWorker.swift */; };
19A169252B176C5F00DB34C0 /* TagPlayListRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1691F2B176C5F00DB34C0 /* TagPlayListRouter.swift */; };
Expand Down Expand Up @@ -271,6 +272,7 @@
1981679E2B20583D0032F563 /* SettingModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingModels.swift; sourceTree = "<group>"; };
1981679F2B20583D0032F563 /* SettingInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingInteractor.swift; sourceTree = "<group>"; };
198167A52B20593B0032F563 /* SettingConfigurator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingConfigurator.swift; sourceTree = "<group>"; };
198167A72B20DD670032F563 /* PresignedURLDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresignedURLDTO.swift; sourceTree = "<group>"; };
19A1691D2B176C5F00DB34C0 /* TagPlayListPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagPlayListPresenter.swift; sourceTree = "<group>"; };
19A1691E2B176C5F00DB34C0 /* TagPlayListWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagPlayListWorker.swift; sourceTree = "<group>"; };
19A1691F2B176C5F00DB34C0 /* TagPlayListRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagPlayListRouter.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -625,6 +627,7 @@
FC4084C52B1F1C5B00CE4727 /* UploadPostDTO.swift */,
FC4084C92B1F291200CE4727 /* UploadVideoDTO.swift */,
1915D6E42B1FB82000CE1DD0 /* CheckSignUpDTO.swift */,
198167A72B20DD670032F563 /* PresignedURLDTO.swift */,
);
path = DTOs;
sourceTree = "<group>";
Expand Down Expand Up @@ -1354,6 +1357,7 @@
FC68E2A52B0233D3001AABFF /* Provider.swift in Sources */,
FC930E7E2B0CDB2900AA48E3 /* ThumbnailCollectionViewCell.swift in Sources */,
194552112B03AF2B00299768 /* MainTabBarConfigurator.swift in Sources */,
198167A82B20DD670032F563 /* PresignedURLDTO.swift in Sources */,
8321A2F72B1E14A1000A12AF /* LOPopUpView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
17 changes: 17 additions & 0 deletions iOS/Layover/Layover/Network/DTOs/PresignedURLDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// PresignedURLDTO.swift
// Layover
//
// Created by 김인환 on 12/7/23.
// Copyright © 2023 CodeBomber. All rights reserved.
//

import Foundation

struct PresignedURLDTO: Decodable {
let preSignedURL: String

enum CodingKeys: String, CodingKey {
case preSignedURL = "preSignedUrl"
}
}
loinsir marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ protocol UserEndPointFactory {
func makeUserWithDrawEndPoint() -> EndPoint<Response<NicknameDTO>>
func makeUserInformationEndPoint(with id: Int?) -> EndPoint<Response<MemberDTO>>
func makeUserPostsEndPoint(at page: Int, of id: Int?) -> EndPoint<Response<[PostDTO]>>
func makeFetchUserProfilePresignedURL(of fileType: String) -> EndPoint<Response<PresignedURLDTO>>
}

final class DefaultUserEndPointFactory: UserEndPointFactory {
Expand Down Expand Up @@ -75,7 +76,6 @@ final class DefaultUserEndPointFactory: UserEndPointFactory {
}

func makeUserPostsEndPoint(at page: Int, of id: Int? = nil) -> EndPoint<Response<[PostDTO]>> {

var queryParameters = [String: String]()
queryParameters.updateValue(String(page), forKey: "page")

Expand All @@ -89,4 +89,15 @@ final class DefaultUserEndPointFactory: UserEndPointFactory {
queryParameters: queryParameters
)
}

func makeFetchUserProfilePresignedURL(of fileType: String) -> EndPoint<Response<PresignedURLDTO>> {
var bodyParameters = [String: String]()
bodyParameters.updateValue(fileType, forKey: "fileType")

return EndPoint(
path: "/member/profile-image/presigned-url",
method: .POST,
bodyParameters: bodyParameters
)
}
}
15 changes: 15 additions & 0 deletions iOS/Layover/Layover/Network/Provider/Provider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ protocol ProviderType {
func request(url: URL) async throws -> Data
func request(url: String) async throws -> Data
func upload(data: Data, to url: String, method: HTTPMethod) async throws -> Data
func upload(from fileURL: URL, to url: String, method: HTTPMethod) async throws -> Data
func upload(fromFile: URL,
loinsir marked this conversation as resolved.
Show resolved Hide resolved
to url: String,
method: HTTPMethod,
Expand All @@ -35,6 +36,10 @@ extension ProviderType {
method: method)
}

func upload(from fileURL: URL, to url: String, method: HTTPMethod = .PUT) async throws -> Data {
return try await upload(from: fileURL, to: url, method: method)
}

func upload(fromFile: URL,
to url: String,
method: HTTPMethod = .PUT,
Expand Down Expand Up @@ -136,6 +141,16 @@ class Provider: ProviderType {
return data
}

func upload(from fileURL: URL, to url: String, method: HTTPMethod = .PUT) async throws -> Data {
guard let url = URL(string: url) else { throw NetworkError.components }
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData)
request.httpMethod = method.rawValue

let (data, response) = try await session.upload(for: request, fromFile: fileURL)
try self.checkStatusCode(of: response)
return data
}

// 동영상 업로드용
func upload(fromFile: URL,
to url: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ final class EditProfileConfigurator: Configurator {
let viewController = viewController
let interactor = EditProfileInteractor()
let presenter = EditProfilePresenter()
let worker = MockUserWorker()
let worker = UserWorker()
let router = EditProfileRouter()

router.viewController = viewController
Expand Down
147 changes: 128 additions & 19 deletions iOS/Layover/Layover/Scenes/EditProfile/EditProfileInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import Foundation
import OSLog

protocol EditProfileBusinessLogic {
func fetchProfile()
func validateProfileInfo(with request: EditProfileModels.ValidateProfileInfo.Request)
func fetchProfile(with request: EditProfileModels.FetchProfile.Request)
func changeProfile(with request: EditProfileModels.ChangeProfile.Request)
func checkDuplication(with request: EditProfileModels.CheckNicknameDuplication.Request)
func editProfile(with requeset: EditProfileModels.EditProfile.Request)
@discardableResult
func editProfile(with request: EditProfileModels.EditProfile.Request) -> Task<Bool, Never>
}

protocol EditProfileDataStore {
Expand All @@ -31,6 +32,10 @@ final class EditProfileInteractor: EditProfileBusinessLogic, EditProfileDataStor
var userWorker: UserWorkerProtocol?
var presenter: EditProfilePresentationLogic?

private var didCheckedNicknameDuplicate = true
private var nicknameState: NicknameState = .valid
private var introduceState: Models.ChangeProfile.IntroduceLengthState = .valid

// MARK: - Data Store

var nickname: String?
Expand All @@ -39,20 +44,52 @@ final class EditProfileInteractor: EditProfileBusinessLogic, EditProfileDataStor

// MARK: - Use Case

func fetchProfile() {
presenter?.presentProfile(with: EditProfileModels.FetchProfile.Reponse(nickname: nickname,
introduce: introduce,
profileImageData: profileImageData))
func fetchProfile(with request: EditProfileModels.FetchProfile.Request) {
presenter?.presentProfile(with: EditProfileModels.FetchProfile.Response(nickname: nickname,
introduce: introduce,
profileImageData: profileImageData))
}

func validateProfileInfo(with request: Models.ValidateProfileInfo.Request) {
let nicknameChanged = nickname != request.nickname
let introduceChanged = introduce != request.introduce
let profileInfoChanged = nicknameChanged || request.profileImageChanged || introduceChanged
let profileInfoValiation = (userWorker?.validateNickname(request.nickname) == .valid) && validateIntroduce(request.introduce ?? "")
let response = EditProfileModels.ValidateProfileInfo.Response(isValid: profileInfoChanged && profileInfoValiation,
nicknameChanged: nicknameChanged)
presenter?.presentProfileInfoValidation(with: response)
func changeProfile(with request: Models.ChangeProfile.Request) {
loinsir marked this conversation as resolved.
Show resolved Hide resolved
let response: Models.ChangeProfile.Response

switch request.changedProfileComponent {
case .nickname(let changedNickname):
didCheckedNicknameDuplicate = changedNickname == nickname
nicknameState = userWorker?.validateNickname(changedNickname ?? "") ?? .invalidCharacter

let canCheckNicknameDuplication = changedNickname == nickname ? false : nicknameState == .valid

response = Models.ChangeProfile.Response(nicknameAlertDescription: nicknameState != .valid ? nicknameState.description : nil,
introduceAlertDescription: nil,
canCheckNicknameDuplication: canCheckNicknameDuplication,
canEditProfile: false)

case .introduce(let changedIntroduce):
introduceState = introduceLengthState(of: changedIntroduce ?? "", by: request.validIntroduceLength)
let canEditProfile = changedIntroduce != introduce
&& introduceState == .valid
&& nicknameState == .valid
&& didCheckedNicknameDuplicate

let introduceAlertDescription = introduce == changedIntroduce || introduceState == .valid ? nil : introduceState.description

response = Models.ChangeProfile.Response(nicknameAlertDescription: nil,
introduceAlertDescription: introduceAlertDescription,
canCheckNicknameDuplication: nil,
canEditProfile: canEditProfile)

case .profileImage(_):
let canEditProfile = didCheckedNicknameDuplicate
&& nicknameState == .valid
&& introduceState == .valid
response = Models.ChangeProfile.Response(nicknameAlertDescription: nil,
introduceAlertDescription: nil,
canCheckNicknameDuplication: nil,
canEditProfile: canEditProfile)
}

presenter?.presentProfileState(with: response)
}

func checkDuplication(with request: Models.CheckNicknameDuplication.Request) {
Expand All @@ -61,18 +98,90 @@ final class EditProfileInteractor: EditProfileBusinessLogic, EditProfileDataStor
os_log(.error, log: .data, "checkDuplication Server Error")
return
}
didCheckedNicknameDuplicate = response
let canEditProfile = didCheckedNicknameDuplicate && nicknameState == .valid && introduceState == .valid
await MainActor.run {
presenter?.presentNicknameDuplication(with: Models.CheckNicknameDuplication.Response(isValid: response))
presenter?.presentNicknameDuplication(with: Models.CheckNicknameDuplication.Response(isValid: response, canEditProfile: canEditProfile))
}
}
}

func editProfile(with requeset: Models.EditProfile.Request) {
private func profileImageEditRequest(into imageData: Data, fileExtension: String) async -> Bool {
guard let presignedUploadURL = await userWorker?.fetchImagePresignedURL(with: fileExtension),
await userWorker?.modifyProfileImage(data: imageData, to: presignedUploadURL) != nil else {
os_log(.error, log: .data, "Edit ProfileImage Error")
await MainActor.run {
presenter?.presentProfile(with: Models.EditProfile.Response(isSuccess: false))
}
profileImageData = imageData
return false
}

await MainActor.run {
presenter?.presentProfile(with: Models.EditProfile.Response(isSuccess: true))
}
return true
}

private func validateIntroduce(_ introduce: String) -> Bool {
return introduce.count <= 50
@discardableResult
func editProfile(with request: Models.EditProfile.Request) -> Task<Bool, Never> {
Task {
async let isSuccessNicknameEdit = nicknameEditRequest(into: request.nickname)
async let isSuccessIntroduceEdit = introduceEditRequest(into: request.introduce)

guard await isSuccessNicknameEdit, await isSuccessIntroduceEdit else {
await MainActor.run {
presenter?.presentProfile(with: Models.EditProfile.Response(isSuccess: false))
}
return false
}

// 이미지 변경 시도
if let profileImageData = request.profileImageData, let profileImageExtension = request.profileImageExtension {
guard await profileImageEditRequest(into: profileImageData, fileExtension: profileImageExtension) else {
await MainActor.run {
presenter?.presentProfile(with: Models.EditProfile.Response(isSuccess: false))
}
return false
}
} else { // 이미지 변경이 없는 경우 이미지 삭제 시도
guard await userWorker?.fetchImagePresignedURL(with: "jpeg") != nil else {
await MainActor.run {
presenter?.presentProfile(with: Models.EditProfile.Response(isSuccess: false))
}
return false
}
profileImageData = nil
}

await MainActor.run {
presenter?.presentProfile(with: Models.EditProfile.Response(isSuccess: true))
}
return true
}
}

private func nicknameEditRequest(into nickname: String) async -> Bool {
if self.nickname != nickname {
guard let response = await userWorker?.modifyNickname(to: nickname) else {
os_log(.error, log: .data, "Edit Profile Error")
return false
}
self.nickname = nickname
}
return true
}

private func introduceEditRequest(into introduce: String?) async -> Bool {
guard let modifyIntroduceResponse = await userWorker?.modifyIntroduce(to: introduce ?? "") else {
os_log(.error, log: .data, "Edit Profile Error")
return false
}
self.introduce = introduce
return true
}

private func introduceLengthState(of introduce: String, by length: Int) -> Models.ChangeProfile.IntroduceLengthState {
introduce.count <= length ? .valid : .overLength
}
}
Loading