Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b8f8eec
refactor: #99 화이트 배경 삽입
dev-domo Jan 6, 2026
7837cd3
setting: #99 백그라운드 권한 제거
dev-domo Jan 6, 2026
ff242b5
setting: #99 위치 권한 설정 문구 추가
dev-domo Jan 6, 2026
3a91055
feat: #99 에플 로그인 시 이름 입력 뷰 건너뛰기
dev-domo Jan 6, 2026
86aa93b
refactor: #99 애플 로그인 시 가져오는 이름 저장
dev-domo Jan 7, 2026
7992517
refactor: #99 scenarioNames가 없는 경우에도 중복 처리가 되지 않도록 방지
dev-domo Jan 7, 2026
b7ecbc9
feat: #99 weatherKit 로고 및 링크 삽입
dev-domo Jan 7, 2026
1c64b46
refactor: #99 홈 등록된 시나리오가 없을 때 뷰 수정
dev-domo Jan 7, 2026
df72143
refactor: #99 weatherKitButtonView에 탭 제스처 추가
dev-domo Jan 7, 2026
1c36676
refactor: #99 LoginResponseDTO 필드 추가
dev-domo Jan 11, 2026
908f141
refactor: #99 카카오 SDK 초기화 로직 함수 분리
dev-domo Jan 12, 2026
f2ddf4b
refactor: #99 동시 바인딩 적용을 통한 날씨 업데이트 속도 증진
dev-domo Jan 12, 2026
0dd1c5b
refactor: #99 메서드명 수정 및 메서드 분리
dev-domo Jan 13, 2026
b340462
refactor: #99 구조적 동시성 구현 방법 변경
dev-domo Jan 13, 2026
72ef72c
refactor: #99 WeatherKit 로고 탭 제스처 등록 로직을 setGesture 메서드로 이동
dev-domo Jan 13, 2026
4c6c6c6
refactor: #99 불필요한 메서드 분리 수정
dev-domo Jan 13, 2026
8ea0a97
refactor: UISwipeActionsConfiguration 생성 방식 수정
dev-domo Jan 13, 2026
3164fa2
refactor: #99 드래그 동작 추상화
dev-domo Jan 13, 2026
aaab10b
refactor: #99 드래그 동작 싱글턴 클래스 구현
dev-domo Jan 13, 2026
df0e633
refactor: #99 드래그 동작 핸들러 파일명 및 폴더링 수정
dev-domo Jan 13, 2026
506efde
refactor: #99 테이블뷰 Drop 동작 공통 메서드 분리
dev-domo Jan 13, 2026
1cd46ec
refactor: #99 UITableViewDragDelegate 확장으로 수정
dev-domo Jan 13, 2026
912a996
refactor: #99 테이블뷰의 스와이프 삭제 기능을 프로토콜화
dev-domo Jan 13, 2026
427fbae
feat: #99 유저 이름 조회 API 연동
dev-domo Jan 16, 2026
1aa1bda
refactor: #99 약관동의 완료 여부, 온보딩 완료 여부를 관리하지 않도록 수정
dev-domo Jan 16, 2026
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
12 changes: 8 additions & 4 deletions BeforeGoing.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -267,12 +267,14 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BeforeGoing/Resource/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.lifestyle";
INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "\"현재 위치 사용을 허용하시겠습니까?\"";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "\"앱을 사용하는 동안 위치 사용을 허용하시겠습니까?\"";
INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "앱 사용 여부와 관계없이 현재 위치의 날씨를 확인하고 정확한 알림을 제공하는 데 사용됩니다.";
INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "앱이 닫혀 있을 때에도 날씨 정보를 미리 업데이트하여 정확한 정보를 제공하기 위해 위치를 사용합니다.";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "현재 위치의 날씨 정보를 제공하고, 사용자가 직접 미션을 추가할 때 지리적 태그를 지정하기 위해 필요합니다.";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UIUserInterfaceStyle = Light;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
Expand Down Expand Up @@ -307,12 +309,14 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BeforeGoing/Resource/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.lifestyle";
INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "\"현재 위치 사용을 허용하시겠습니까?\"";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "\"앱을 사용하는 동안 위치 사용을 허용하시겠습니까?\"";
INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "앱 사용 여부와 관계없이 현재 위치의 날씨를 확인하고 정확한 알림을 제공하는 데 사용됩니다.";
INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "앱이 닫혀 있을 때에도 날씨 정보를 미리 업데이트하여 정확한 정보를 제공하기 위해 위치를 사용합니다.";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "현재 위치의 날씨 정보를 제공하고, 사용자가 직접 미션을 추가할 때 지리적 태그를 지정하기 위해 필요합니다.";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UIUserInterfaceStyle = Light;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
Expand Down
16 changes: 11 additions & 5 deletions BeforeGoing/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

DIContainer.shared.injectDependency()
initKakaoSDK()

if let appKey = Bundle.main.object(forInfoDictionaryKey: "KAKAO_NATIVE_APP_KEY") as? String {
KakaoSDK.initSDK(appKey: appKey)
} else {
fatalError("카카오 네이티브 앱 키 없음")
}
return true
}

Expand Down Expand Up @@ -49,3 +45,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationWillTerminate(_ application: UIApplication) {}
}

extension AppDelegate {

func initKakaoSDK() {
if let appKey = Bundle.main.object(forInfoDictionaryKey: "KAKAO_NATIVE_APP_KEY") as? String {
KakaoSDK.initSDK(appKey: appKey)
} else {
fatalError("카카오 네이티브 앱 키 없음")
}
}
}
1 change: 1 addition & 0 deletions BeforeGoing/Core/BeforeGoingError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ enum BeforeGoingError: Error, Equatable {
case missionLimitError
case tooManyRequset
case withdrawFailed
case notFoundProvider
}
4 changes: 3 additions & 1 deletion BeforeGoing/Data/Model/Auth/Response/LoginResponseDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ struct LoginResponseDTO: Decodable {
let accessTokenExpiresIn: Int
let refreshToken: String
let refreshTokenExpiresIn: Int
let isNewMember: Bool
}

extension LoginResponseDTO {
Expand All @@ -13,7 +14,8 @@ extension LoginResponseDTO {
accessToken: accessToken,
accessTokenExpiresIn: accessTokenExpiresIn,
refreshToken: refreshToken,
refreshTokenExpiresIn: refreshTokenExpiresIn
refreshTokenExpiresIn: refreshTokenExpiresIn,
isNewMember: isNewMember
)
}
}
11 changes: 7 additions & 4 deletions BeforeGoing/Data/Network/Service/API/MemberAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Alamofire
enum MemberAPI {
case updateNickname(accessToken: String, dto: UpdateNicknameRequestDTO)
case withdraw(accessToken: String)
case fetchMemberName(accessToken: String)
}

extension MemberAPI: EndPoint {
Expand All @@ -22,7 +23,7 @@ extension MemberAPI: EndPoint {
let basePath = Environment.baseURL + basePath

switch self {
case .updateNickname:
case .updateNickname, .fetchMemberName:
return basePath + "/nickname"
case .withdraw:
return basePath
Expand All @@ -35,6 +36,8 @@ extension MemberAPI: EndPoint {
return .patch
case .withdraw:
return .delete
case .fetchMemberName:
return .get
}
}

Expand All @@ -44,7 +47,7 @@ extension MemberAPI: EndPoint {

var headers: HTTPHeaders? {
switch self {
case .updateNickname(let accessToken, _), .withdraw(let accessToken):
case .updateNickname(let accessToken, _), .withdraw(let accessToken), .fetchMemberName(let accessToken):
return [
"Content-Type": "application/json",
"Authorization": "Bearer \(accessToken)"
Expand All @@ -54,7 +57,7 @@ extension MemberAPI: EndPoint {

var parameterEncoding: any ParameterEncoding {
switch self {
case .updateNickname:
case .updateNickname, .fetchMemberName:
return JSONEncoding.default
case .withdraw:
return URLEncoding.default
Expand All @@ -69,7 +72,7 @@ extension MemberAPI: EndPoint {
switch self {
case .updateNickname(_, let dto):
return try? dto.toBodyParameters()
case .withdraw:
case .withdraw, .fetchMemberName:
return nil
}
}
Expand Down
54 changes: 34 additions & 20 deletions BeforeGoing/Data/Network/Service/NetworkService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ protocol APIManaging {
responseType: T.Type
) async throws -> T
func request(endPoint: any EndPoint) async throws
func requestString(endPoint: EndPoint) async throws -> String
func requestKakaoIDToken(nonce: String?) async throws -> String
}

Expand Down Expand Up @@ -45,6 +46,23 @@ final class NetworkService: APIManaging {
let response = try await dataRequest.serializingData().value
writeLog(response: response)
} catch {
if let afError = error.asAFError {
throw handleError(afError: afError)
}
throw error
}
}

func requestString(endPoint: EndPoint) async throws -> String {
do {
let dataRequest = createDataRequest(endPoint: endPoint)
let response = try await dataRequest.serializingString().value
writeLog(response: response)
return response
} catch {
if let afError = error.asAFError {
throw handleError(afError: afError)
}
throw error
}
}
Expand Down Expand Up @@ -73,28 +91,24 @@ final class NetworkService: APIManaging {
}
}

switch afError {
case .responseValidationFailed(let reason):
if case .unacceptableStatusCode(let statuscode) = reason {
switch statuscode {
case 304:
return .notModifiedError
case 400:
return .badRequestError
case 404:
return .notFoundError
case 429:
return .tooManyRequset
case (500...599):
return .serviceUnavailable
default:
return .unknownError
}
if let statusCode = afError.responseCode {
switch statusCode {
case 304:
return .notModifiedError
case 400:
return .badRequestError
case 404:
return .notFoundError
case 429:
return .tooManyRequset
case 500...599:
return .serviceUnavailable
default:
break
}
return .unknownError
default:
return .unknownError
}

return .unknownError
}
}

Expand Down
6 changes: 0 additions & 6 deletions BeforeGoing/Data/Persistence/Service/UserDefaultsKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@
//

enum UserDefaultsKey: String, CaseIterable {
case isKakaoCompletedAgreeTerms
case isAppleCompletedAgreeTerms
case isKakaoCompletedOnboarding
case isAppleCompletedOnboarding
case kakaoMemberName
case appleMemberName
case provider
case lastProvider
}
42 changes: 32 additions & 10 deletions BeforeGoing/Data/Repository/AuthRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ struct AuthRepository: AuthInterface {
return try await requestLogin(provider: provider, idToken: idToken)
}

func requestLogin(provider: Provider, idToken: String) async throws -> Bool {
private func requestLogin(provider: Provider, idToken: String) async throws -> Bool {
let requestDTO = loginRequestMapper.map((provider.rawValue, idToken))
let response = try await networkService.request(
endPoint: AuthAPI.login(dto: requestDTO),
Expand All @@ -60,16 +60,35 @@ struct AuthRepository: AuthInterface {
saveKeyChain(response: response)
saveProvider(provider)

return isCompletedOnboarding(provider: provider)
let isAgreedTerms = try await isAgreedTerms(accessToken: response.accessToken)
let isCompletedJoin = !response.isNewMember && isAgreedTerms
return isCompletedJoin
}

func requestLogin(provider: Provider, idToken: String, name: String?) async throws -> Bool {
let isCompletedJoin = try await requestLogin(provider: provider, idToken: idToken)

if let name,
!name.isBlank,
let accessToken = keyChainService.load(key: .accessToken) {
try await networkService.request(
endPoint: MemberAPI.updateNickname(
accessToken: accessToken,
dto: .init(nickname: name)
)
)
}

return isCompletedJoin
}

func autoLogin() async throws -> Bool {
guard let providerString: String = userDefaultsService.load(key: .provider),
let provider = Provider(rawValue: providerString) else {
guard let accessToken = keyChainService.load(key: .accessToken) else {
return false
}

guard isTokenExists, isCompletedOnboarding(provider: provider) else {
guard isTokenExists,
try await isAgreedTerms(accessToken: accessToken) else {
return false
}

Expand Down Expand Up @@ -148,13 +167,16 @@ struct AuthRepository: AuthInterface {
return false
}

private func isCompletedOnboarding(provider: Provider) -> Bool {
let key: UserDefaultsKey = (provider == .apple) ? .isAppleCompletedOnboarding : .isKakaoCompletedOnboarding

guard let isCompleted: Bool = userDefaultsService.load(key: key) else {
private func isAgreedTerms(accessToken: String) async throws -> Bool {
do {
let _ = try await networkService.request(
endPoint: TermsAPI.getTerms(accessToken: accessToken),
responseType: TermsResponseDTO.self
)
return true
} catch {
return false
}
return isCompleted
}
Comment on lines +170 to 180

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

isAgreedTerms(accessToken:) 함수가 TermsRepository에도 동일하게 구현되어 있어 코드 중복이 발생했습니다. 약관 관련 로직은 TermsRepository에서 관리하는 것이 역할 분리 측면에서 더 적절해 보입니다. TermsRepositoryisAgreedTerms 함수를 public으로 변경하고, AuthRepository에서는 TermsInterface를 통해 해당 함수를 호출하여 사용하는 방식으로 리팩토링하면 중복을 제거하고 코드 유지보수성을 높일 수 있습니다.


private func deleteUserInformation() {
Expand Down
Loading
Loading