Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
3b98f95
[#39] CodvieAPI 패키지 세팅
Hrepay Dec 22, 2025
063880d
[#39] CodvieAPI 패키지 오류 수정
Hrepay Dec 22, 2025
6da559d
Merge branch 'develop' into feat/#39
Hrepay Jan 3, 2026
f570892
[#39] 카카오 OIDC 방식으로 로그인 세팅
Hrepay Jan 3, 2026
c9d2392
Merge branch 'develop' into feat/#39
Hrepay Jan 3, 2026
f2f772b
[#39] 로그인 후, 약관동의 API 연결
Hrepay Jan 4, 2026
9ac04c7
[#39] 카카오 로그인 웹뷰로 딥링크 처리하여 로그인 구현 완료
Hrepay Jan 11, 2026
4326209
[#39] CodvieAPI 업데이트
Hrepay Jan 12, 2026
d3e6a91
[#39] 옷 추가 API 1차 구현
Hrepay Jan 12, 2026
65b8ee6
[#39] 옷 목록 조회/상세 조회 API 연결
Hrepay Jan 12, 2026
8b18f7b
[#39] 옷 수정, 삭제 API 연결
Hrepay Jan 12, 2026
5317142
[#39] 옷 추가 API 2차 구현
Hrepay Jan 13, 2026
4f64684
[#39] 약관 동의 API 구현
Hrepay Jan 13, 2026
1764581
[#39] 자동 로그인 및 토큰 재발급 구현
Hrepay Jan 13, 2026
3c9c51f
[#39] 기록 생성 API 구현
Hrepay Jan 13, 2026
091a0c2
[#39] 옷장 썸네일 목록 API 및 상세 조회 API 구현
Hrepay Jan 14, 2026
6067625
[#39] 옷 편집, 삭제 API 구현
Hrepay Jan 14, 2026
e351eda
[#39] 옷 편집 이미지 url이 누락되던 버그 수정 완료
Hrepay Jan 14, 2026
aa5b70e
[#39] 옷 전체보기 조회, 삭제 API 구현
Hrepay Jan 14, 2026
88ce488
[#39] 옷 추가 후 스플래시 뷰 연결 완료
Hrepay Jan 14, 2026
0b7545c
[#39] 옷 세부카테고리 기본 값 변경
Hrepay Jan 14, 2026
e6d97c9
[#39] JSONDecoder 공통 유틸로 분리
Hrepay Jan 17, 2026
4fc0da0
[#39] Auth API Result 타입을 async throws로 통일
Hrepay Jan 17, 2026
1b2eed9
[#39] SplashViewModel 분리 및 TokenConfig 상수화
Hrepay Jan 17, 2026
413858e
[#39] 보안: JWT 토큰 콘솔 출력 제거
Hrepay Jan 17, 2026
1307ae4
[#39] ClothDataSource 리팩토링 - 중복 제거 및 의존성 분리
Hrepay Jan 17, 2026
6c973f7
[#39] ViewModel 코드 정리 - MainActor.run 제거 및 TODO 주석 추가
Hrepay Jan 17, 2026
1a3b6dc
[#39] NavigationRouter DispatchQueue를 Task.sleep으로 변경
Hrepay Jan 17, 2026
6e82087
[#39] HistoryClothTag를 RecordClothTag로 통일
Hrepay Jan 17, 2026
a6ac145
[#39] 약관 동의 뷰 뒤로가기 버튼 제거
Hrepay Jan 17, 2026
5419df5
[#39] 옷 추가 API 요청에 season 필드 배열로 변경
Hrepay Jan 18, 2026
8a33631
[#39] 자동 로그인 임시 해제
Hrepay Jan 18, 2026
c4ada2c
Merge branch 'develop' into feat/#39
Hrepay Jan 18, 2026
b178b29
[#39] develop 브랜치와 병합에 따른 버그 해결
Hrepay Jan 18, 2026
43e7057
[#39] 이모지 정리
Hrepay Jan 18, 2026
461dca0
[#39] 로그인 에러 시, 메인 화면으로 이동되게 수정
Hrepay Jan 18, 2026
3ebf112
[#39] 서버 url config 파일로 이동
Hrepay Jan 18, 2026
3192f78
[#39] OnboardingViewModel에서 SocialAuthService를 사용하여 동작할 수 있게 로직 수정
Hrepay Jan 18, 2026
897ea91
[#39] 윈도우 에러시, 강종되는 버그 수정
Hrepay Jan 18, 2026
519b758
[#39] 약관동의 하드코딩 제거 및 API 데이터로 연결
Hrepay Jan 18, 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
90 changes: 83 additions & 7 deletions Codive/Application/AppRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import SwiftUI
import Combine

/// 앱의 최상위 RootView
struct AppRootView: View {
Expand All @@ -14,22 +15,97 @@ struct AppRootView: View {
private let authDIContainer: AuthDIContainer
private let appDIContainer: AppDIContainer

@State private var authRepository: AuthRepository

init(appDIContainer: AppDIContainer) {
self._appRouter = StateObject(wrappedValue: appDIContainer.appRouter)
self.authDIContainer = appDIContainer.makeAuthDIContainer()
self.appDIContainer = appDIContainer
self._authRepository = State(wrappedValue: self.authDIContainer.authRepository)
}

var body: some View {
switch appRouter.currentAppState {
case .splash:
SplashContainerView(appRouter: appRouter)
ZStack {
Group {
switch appRouter.currentAppState {
case .splash:
SplashContainerView(appRouter: appRouter)

case .auth:
authDIContainer.makeAuthFlowView()

case .termsAgreement:
TermsAgreementView(
onComplete: {
appRouter.navigateToMain()
}
)

case .main:
MainTabView(appDIContainer: appDIContainer)
}
}

// 로딩 오버레이
if appRouter.isLoading {
Color.black.opacity(0.3)
.ignoresSafeArea()
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(1.5)
}
}
.onOpenURL { url in
handleDeepLink(url: url)
}
}

// MARK: - Deep Link Handler
private func handleDeepLink(url: URL) {
guard url.scheme == "codive",
url.host == "oauth",
url.path == "/callback" else {
print("Invalid deep link format: \(url)")
return
}

guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems else {
print("Failed to parse URL components or query items.")
return
}

let accessToken = queryItems.first(where: { $0.name == "accessToken" })?.value
let refreshToken = queryItems.first(where: { $0.name == "refreshToken" })?.value

guard let unwrappedAccessToken = accessToken,
let unwrappedRefreshToken = refreshToken else {
print("Access token or refresh token missing in deep link.")
// Potentially show an error to the user or log
return
}

Task {
do {
try await authRepository.saveTokens(accessToken: unwrappedAccessToken, refreshToken: unwrappedRefreshToken)

// 로딩 표시
appRouter.showLoading()

case .auth:
authDIContainer.makeAuthFlowView()
// 회원 상태 확인
let status = try await authRepository.checkAuthStatus()

case .main:
MainTabView(appDIContainer: appDIContainer)
switch status {
case .notAgreed:
appRouter.navigateToTerms()
case .registered:
appRouter.navigateToMain()
}
} catch {
appRouter.hideLoading()
// 실패 시 인증 화면으로 이동
appRouter.finishSplash()
}
}
}
}
5 changes: 4 additions & 1 deletion Codive/DIContainer/AuthDIContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ final class AuthDIContainer {

// MARK: - Services (Data Layer)
lazy var socialAuthService: SocialAuthServiceProtocol = SocialAuthService()
lazy var authAPIService: AuthAPIServiceProtocol = AuthAPIService()

// MARK: - Repositories (Domain Layer)
lazy var authRepository: AuthRepository = AuthRepositoryImpl(
socialAuthService: socialAuthService
socialAuthService: socialAuthService,
authAPIService: authAPIService,
keychainTokenProvider: KeychainTokenProvider()
)

// MARK: - Initializer
Expand Down
13 changes: 10 additions & 3 deletions Codive/DIContainer/ClosetDIContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ final class ClosetDIContainer {
self.navigationRouter = navigationRouter
}

// MARK: - Services
private lazy var clothAPIService: ClothAPIServiceProtocol = {
return ClothAPIService()
}()

// MARK: - DataSources
private lazy var clothDataSource: ClothDataSource = {
return DefaultClothDataSource()
return DefaultClothDataSource(apiService: clothAPIService)
}()

// MARK: - Repositories
Expand Down Expand Up @@ -67,14 +72,16 @@ final class ClosetDIContainer {
return ClothDetailViewModel(
cloth: cloth,
navigationRouter: navigationRouter,
deleteClothItemsUseCase: makeDeleteClothItemsUseCase()
deleteClothItemsUseCase: makeDeleteClothItemsUseCase(),
clothRepository: clothRepository
)
}

func makeClothEditViewModel(cloth: Cloth) -> ClothEditViewModel {
return ClothEditViewModel(
cloth: cloth,
navigationRouter: navigationRouter
navigationRouter: navigationRouter,
clothRepository: clothRepository
)
}

Expand Down
126 changes: 126 additions & 0 deletions Codive/Features/Auth/Data/AuthAPIService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//
// AuthAPIService.swift
// Codive
//
// Created by 황상환 on 1/4/26.
//

import Foundation
import CodiveAPI
import OpenAPIRuntime

// MARK: - Auth API Service Protocol
protocol AuthAPIServiceProtocol {
func checkAuthStatus() async throws -> RegisterStatus
func reissueTokens(refreshToken: String) async throws -> TokenPair
func renewDeviceToken(deviceToken: String) async throws
}

// MARK: - Token Pair
struct TokenPair {
let accessToken: String
let refreshToken: String
}

// MARK: - Auth API Service Implementation
final class AuthAPIService: AuthAPIServiceProtocol {

private let client: Client
private let jsonDecoder: JSONDecoder

init(tokenProvider: TokenProvider = KeychainTokenProvider()) {
self.client = CodiveAPIProvider.createClient(
middlewares: [CodiveAuthMiddleware(provider: tokenProvider)]
)
self.jsonDecoder = JSONDecoderFactory.makeAPIDecoder()
}

func checkAuthStatus() async throws -> RegisterStatus {
let response = try await client.Auth_getUserStatus(
Operations.Auth_getUserStatus.Input()
)

switch response {
case .ok(let okResponse):
let httpBody = try okResponse.body.any
let data = try await Data(collecting: httpBody, upTo: .max)

let apiResponse = try jsonDecoder.decode(
Components.Schemas.BaseResponseUserStatusResponse.self,
from: data
)

guard let result = apiResponse.result,
let registerStatus = result.registerStatus else {
throw AuthError.networkError("회원 상태 정보 없음")
}

switch registerStatus {
case .NOT_AGREED:
return .notAgreed
case .REGISTERED:
return .registered
}

case .undocumented(statusCode: let statusCode, _):
throw AuthError.networkError("예상치 못한 응답 코드: \(statusCode)")
}
}

// MARK: - Token Reissue

func reissueTokens(refreshToken: String) async throws -> TokenPair {
let requestBody = Components.Schemas.TokenReissueRequest(refreshToken: refreshToken)
let input = Operations.Auth_reissueTokens.Input(body: .json(requestBody))
let response = try await client.Auth_reissueTokens(input)

switch response {
case .ok(let okResponse):
let httpBody = try okResponse.body.any
let data = try await Data(collecting: httpBody, upTo: .max)

let apiResponse = try jsonDecoder.decode(TokenReissueResponse.self, from: data)

guard let result = apiResponse.result,
let accessToken = result.accessToken,
let newRefreshToken = result.refreshToken else {
throw AuthError.networkError("토큰 재발급 응답 파싱 실패")
}

return TokenPair(accessToken: accessToken, refreshToken: newRefreshToken)

case .undocumented(statusCode: let statusCode, _):
throw AuthError.networkError("토큰 재발급 실패: \(statusCode)")
}
}

// MARK: - Device Token

func renewDeviceToken(deviceToken: String) async throws {
let requestBody = Components.Schemas.DeviceTokenRenewRequest(deviceToken: deviceToken)
let input = Operations.Auth_renewDeviceToken.Input(body: .json(requestBody))
let response = try await client.Auth_renewDeviceToken(input)

switch response {
case .ok:
return

case .undocumented(statusCode: let statusCode, _):
throw AuthError.networkError("디바이스 토큰 갱신 실패: \(statusCode)")
}
}
}

// MARK: - Custom Response Types

private struct TokenReissueResponse: Decodable {
let isSuccess: Bool?
let code: String?
let message: String?
let result: TokenResult?

struct TokenResult: Decodable {
let accessToken: String?
let refreshToken: String?
}
}
23 changes: 23 additions & 0 deletions Codive/Features/Auth/Data/KeychainTokenProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// KeychainTokenProvider.swift
// Codive
//
// Created by 황상환 on 1/4/26.
//

import Foundation
import CodiveAPI

// MARK: - Keychain Token Provider
final class KeychainTokenProvider: TokenProvider {

func getValidToken() async -> String? {
do {
let token = try KeychainManager.shared.getAccessToken()
return token
} catch {
print("토큰 조회 실패: \(error)")
return nil
}
}
}
35 changes: 21 additions & 14 deletions Codive/Features/Auth/Data/Repositories/AuthRepositoryImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,26 @@
//

import Foundation
import CodiveAPI

// MARK: - Auth Repository Implementation
@MainActor
final class AuthRepositoryImpl: AuthRepository {

// MARK: - Properties
private let socialAuthService: SocialAuthServiceProtocol

private let authAPIService: AuthAPIServiceProtocol
private let keychainTokenProvider: TokenProvider

// MARK: - Initializer
init(socialAuthService: SocialAuthServiceProtocol) {
init(
socialAuthService: SocialAuthServiceProtocol,
authAPIService: AuthAPIServiceProtocol = AuthAPIService(),
keychainTokenProvider: TokenProvider = KeychainTokenProvider()
) {
self.socialAuthService = socialAuthService
self.authAPIService = authAPIService
self.keychainTokenProvider = keychainTokenProvider
}

// MARK: - AuthRepository Implementation
Expand All @@ -27,20 +36,18 @@ final class AuthRepositoryImpl: AuthRepository {
case .apple:
return await socialAuthService.appleLogin()
}

// 향후 서버 연결 시 확장될 로직:
// 1. 소셜 로그인 성공
// 2. 서버에 사용자 정보 전송
// 3. JWT 토큰 받아서 로컬 저장
// 4. 최종 AuthResult 반환
}

func checkAuthStatus() async throws -> RegisterStatus {
return try await authAPIService.checkAuthStatus()
}

func logout() async {
await socialAuthService.logout()
// 향후 서버 연결 시 추가될 로직:
// 1. 서버에 로그아웃 요청
// 2. 로컬 토큰 삭제
// 3. 소셜 플랫폼 로그아웃
}

func saveTokens(accessToken: String, refreshToken: String) async throws {
try KeychainManager.shared.saveAccessToken(accessToken)
try KeychainManager.shared.saveRefreshToken(refreshToken)
}
}
Loading