Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2f0d022
refactor: 각 도메인 서비스의 AF.request 호출을 삭제하고, NetworkService의 메서드를 호출하도록 변경
hgjwilly-koreatech Feb 12, 2026
c0bc1f7
refactor: RefreshToken 로직을 Interceptor Retry로 리팩토링
hgjwilly-koreatech Feb 12, 2026
c643297
refactor: 헤더 Access Token 로직을 Interceptor Adapt로 리팩토링
hgjwilly-koreatech Feb 12, 2026
5189d16
design: ErrorViewController UI 구현
hgjwilly-koreatech Feb 13, 2026
db990c3
feat: 5XX 오류시 ErrorViewController를 띄움
hgjwilly-koreatech Feb 13, 2026
b067550
fix: Interceptor 로직 개선
hgjwilly-koreatech Feb 13, 2026
e6e9d2f
feat: 키체인 캐싱
hgjwilly-koreatech Feb 13, 2026
735b367
fix: 점검페이지 present 로직 개선
hgjwilly-koreatech Feb 13, 2026
d433a77
refactor: NetworkService 파일 업로드, 다운로드 메서드에 Interceptor 추가
hgjwilly-koreatech Feb 14, 2026
d8ba11b
fix: NetworkService 에러처리 개선
hgjwilly-koreatech Feb 14, 2026
8586c47
chore: 오타 수정
hgjwilly-koreatech Feb 14, 2026
546a62c
chore: 사소한 수정
hgjwilly-koreatech Feb 14, 2026
30f1dde
refactor: 오류 타입이 Error 인 NetworkService의 request 메서드를 삭제
hgjwilly-koreatech Feb 14, 2026
06a77d4
chore: 사용하지 않는 메서드 삭제
hgjwilly-koreatech Feb 14, 2026
d9fff33
chore: API 실패 로깅을 NetworkService으로 분리
hgjwilly-koreatech Feb 14, 2026
1590f06
chore: 오타 수정
hgjwilly-koreatech Feb 14, 2026
09aba0b
refactor: ErrorResponse 리팩토링
hgjwilly-koreatech Feb 15, 2026
1d0c1f3
chore: 오타 수정, 사소한 변경
hgjwilly-koreatech Feb 15, 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
41 changes: 39 additions & 2 deletions Koin/Apps/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,16 @@ import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {

var window: UIWindow?
var urlParameters: [String: String]?
private var isPresentingErrorViewController = false

override init() {
super.init()
NotificationCenter.default.addObserver(self, selector: #selector(presentErrorViewController), name: NSNotification.Name("ServerError"), object: nil)
}

Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

The observer is added in init but never removed. Even if SceneDelegate is long-lived, it’s safer to remove the observer (e.g. in deinit) or use the block-based API that returns a token you can store and invalidate.

Suggested change
deinit {
NotificationCenter.default.removeObserver(self, name: NSNotification.Name("ServerError"), object: nil)
}

Copilot uses AI. Check for mistakes.
deinit {
NotificationCenter.default.removeObserver(self)
}

func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
Expand Down Expand Up @@ -115,5 +124,33 @@ extension SceneDelegate {
return viewController
}


@objc private func presentErrorViewController() {

guard isPresentingErrorViewController == false else {
return
}
isPresentingErrorViewController = true

if let navigationController = window?.rootViewController as? UINavigationController {

let homeViewController = makeHomeViewController()
let completion: ()->Void = { [weak self] in
navigationController.setViewControllers([homeViewController], animated: false)
navigationController.dismiss(animated: true) {
self?.isPresentingErrorViewController = false
}
}
let errorViewController = ErrorViewController(completion: completion).then {
$0.modalPresentationStyle = .fullScreen
}
Comment on lines +143 to +145
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

ErrorViewController(...).then { ... } requires the Then library, but this file only imports UIKit. Unless there’s a module-wide re-export (none found), this will not compile. Add import Then here or avoid .then and set modalPresentationStyle directly.

Copilot uses AI. Check for mistakes.

if let _ = navigationController.presentedViewController {
navigationController.dismiss(animated: true) {
navigationController.present(errorViewController, animated: true)
}
} else {
navigationController.present(errorViewController, animated: true)
}
}
}
}
66 changes: 48 additions & 18 deletions Koin/Core/Workers/KeychainWorker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,58 +19,89 @@ final class KeychainWorker {
}
static let shared = KeychainWorker()

private init() { }
private init() {}
private var keychains: [TokenType: String?] = [:]
private let lock = NSLock()

func create(key: TokenType, token: String) {
lock.lock()
defer {
lock.unlock()
}

let query: NSDictionary = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: keyType(key: key),
kSecValueData: token.data(using: .utf8, allowLossyConversion: false) as Any
]
SecItemDelete(query)
keychains.removeValue(forKey: key)

let status = SecItemAdd(query, nil)
if status != errSecSuccess {
print("Failed to save token, status code: \(status)")
if status == errSecSuccess {
keychains.updateValue(token, forKey: key)
} else {
print("Failed to save \(key) token,", SecCopyErrorMessageString(status, nil) ?? "")
}
}

func read(key: TokenType) -> String? {
lock.lock()
defer {
lock.unlock()
}

if let token: String? = keychains[key] {
return token
}

let query: NSDictionary = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: keyType(key: key),
kSecReturnData: kCFBooleanTrue as Any,
kSecMatchLimit: kSecMatchLimitOne
]

var dataTypeRef: AnyObject?
let status = SecItemCopyMatching(query, &dataTypeRef)

if status == errSecSuccess {
if let retrievedData: Data = dataTypeRef as? Data {
let value = String(data: retrievedData, encoding: String.Encoding.utf8)
return value
} else { return nil }
if status == errSecSuccess,
let retrievedData: Data = dataTypeRef as? Data,
let value = String(data: retrievedData, encoding: String.Encoding.utf8) {
keychains.updateValue(value, forKey: key)
return value
} else if status == errSecItemNotFound {
keychains.updateValue(nil, forKey: key)
return nil
Comment on lines +72 to +74
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

Dictionaries can’t store nil values; keychains.updateValue(nil, forKey:) removes the key. This means the errSecItemNotFound path isn’t actually cached and read will query the Keychain again next time. If you want to cache misses, use a non-optional value type (e.g. enum) or track not-found keys separately.

Copilot uses AI. Check for mistakes.
} else {
print("failed to loading, status code = \(status)")
print("Failed to load \(key) token,", SecCopyErrorMessageString(status, nil) ?? "")
return nil
}
}

func delete(key: TokenType) {
lock.lock()
defer {
lock.unlock()
}

keychains.removeValue(forKey: key)

let query: NSDictionary = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: keyType(key: key)
]
let status = SecItemDelete(query)
if status == errSecSuccess {
print("Item successfully deleted")
} else if status == errSecItemNotFound {
print("Item not found")
} else {
print("Error deleting the item, status code: \(status)")
}
if status == errSecSuccess {
return
} else if status == errSecItemNotFound {
print("\(key) token not found")
} else {
print("Error deleting \(key) token", SecCopyErrorMessageString(status, nil) ?? "")
}
}
}

extension KeychainWorker {

private func keyType(key: TokenType) -> String {
let keyType: String
Expand All @@ -85,5 +116,4 @@ final class KeychainWorker {
}
return keyType
}

}
13 changes: 0 additions & 13 deletions Koin/Data/DTOs/Decodable/ErrorResponse.swift

This file was deleted.

19 changes: 19 additions & 0 deletions Koin/Data/DTOs/Decodable/ErrorResponseDto.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// ErrorResponseDto.swift
// koin
//
// Created by 김나훈 on 3/18/24.
//

import Foundation

struct ErrorResponseDto: Decodable {
let code: String
let message: String
}

extension ErrorResponseDto {
func toDomain(withStatusCode statusCode: Int) -> ErrorResponse {
return ErrorResponse(statusCode: statusCode, code: self.code, message: self.message)
}
}
2 changes: 1 addition & 1 deletion Koin/Data/Repository/DefaultAbTestRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ final class DefaultAbTestRepository: AbTestRepository {
}

func assignAbTest(requestModel: AssignAbTestRequest) -> AnyPublisher<AssignAbTestResponse, ErrorResponse> {
return service.assignAbTest(requestModel: requestModel, retry: false)
return service.assignAbTest(requestModel: requestModel)
}
}
12 changes: 6 additions & 6 deletions Koin/Data/Repository/DefaultBusRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,27 @@ final class DefaultBusRepository: BusRepository {
self.service = service
}

func searchBusInformation(requestModel: SearchBusInfoRequest) -> AnyPublisher<BusSearchDto, Error> {
func searchBusInformation(requestModel: SearchBusInfoRequest) -> AnyPublisher<BusSearchDto, ErrorResponse> {
return service.searchBusInformation(requestModel: requestModel)
}

func fetchShuttleRouteList() -> AnyPublisher<ShuttleRouteDto, Error> {
func fetchShuttleRouteList() -> AnyPublisher<ShuttleRouteDto, ErrorResponse> {
return service.fetchShuttleRouteList()
}

func fetchExpressBusTimetableList(requestModel: FetchBusTimetableRequest) -> AnyPublisher<ExpressTimetableDto, Error> {
func fetchExpressBusTimetableList(requestModel: FetchBusTimetableRequest) -> AnyPublisher<ExpressTimetableDto, ErrorResponse> {
return service.fetchExpressTimetableList(requestModel: requestModel)
}

func fetchCityBusTimetableList(requestModel: FetchCityBusTimetableRequest) -> AnyPublisher<CityBusTimetableDto, Error> {
func fetchCityBusTimetableList(requestModel: FetchCityBusTimetableRequest) -> AnyPublisher<CityBusTimetableDto, ErrorResponse> {
return service.fetchCityTimetableList(requestModel: requestModel)
}

func fetchEmergencyNotice() -> AnyPublisher<BusNoticeDto, Error> {
func fetchEmergencyNotice() -> AnyPublisher<BusNoticeDto, ErrorResponse> {
return service.fetchEmergencyNotice()
}

func fetchShuttleBusTimetable(id: String) -> AnyPublisher<ShuttleBusTimetable, Error> {
func fetchShuttleBusTimetable(id: String) -> AnyPublisher<ShuttleBusTimetable, ErrorResponse> {
service.fetchShuttleBusTimetable(id: id)
.map { $0.toDomain() }
.eraseToAnyPublisher()
Expand Down
8 changes: 4 additions & 4 deletions Koin/Data/Repository/DefaultCoreRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@ final class DefaultCoreRepository: CoreRepository {
self.service = service
}

func fetchVersion() -> AnyPublisher<ForceUpdateResponse, Error> {
func fetchVersion() -> AnyPublisher<ForceUpdateResponse, ErrorResponse> {
return service.fetchVersion()
}

func fetBanner() -> AnyPublisher<BannerDto, Error> {
func fetBanner() -> AnyPublisher<BannerDto, ErrorResponse> {
return service.fetchBanner()
}
func fetchClubCategories() -> AnyPublisher<ClubCategoriesDto, Error> {
func fetchClubCategories() -> AnyPublisher<ClubCategoriesDto, ErrorResponse> {
return service.fetchClubCategories()
}

func fetchHotClubs() -> AnyPublisher<HotClubDto, Error> {
func fetchHotClubs() -> AnyPublisher<HotClubDto, ErrorResponse> {
return service.fetchHotClubs()
}

Expand Down
4 changes: 2 additions & 2 deletions Koin/Data/Repository/DefaultDiningRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ final class DefaultDiningRepository: DiningRepository {
}

func fetchDiningList(requestModel: FetchDiningListRequest) -> AnyPublisher<[DiningDto], ErrorResponse> {
return diningService.fetchDiningList(requestModel: requestModel, retry: false)
return diningService.fetchDiningList(requestModel: requestModel)
}

func fetchCoopShopList() -> AnyPublisher<CoopShopDto, Error> {
func fetchCoopShopList() -> AnyPublisher<CoopShopDto, ErrorResponse> {
return diningService.fetchCoopShopList()
}

Expand Down
4 changes: 2 additions & 2 deletions Koin/Data/Repository/DefaultLandRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ final class DefaultLandRepository: LandRepository {
self.service = service
}

func fetchLandList() -> AnyPublisher<LandDto, Error> {
func fetchLandList() -> AnyPublisher<LandDto, ErrorResponse> {
return service.fetchLandList()
}

func fetchLandDetail(requestModel: FetchLandDetailRequest) -> AnyPublisher<LandDetailDto, Error> {
func fetchLandDetail(requestModel: FetchLandDetailRequest) -> AnyPublisher<LandDetailDto, ErrorResponse> {
return service.fetchLandDetail(requestModel: requestModel)
}

Expand Down
8 changes: 4 additions & 4 deletions Koin/Data/Repository/DefaultLostItemRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ final class DefaultLostItemRepository: LostItemRepository {
self.service = service
}

func fetchLostItemList(requestModel: FetchLostItemListRequest) -> AnyPublisher<LostItemList, any Error> {
func fetchLostItemList(requestModel: FetchLostItemListRequest) -> AnyPublisher<LostItemList, ErrorResponse> {
return service.fetchLostItemList(requestModel: requestModel)
.map { $0.toDomain() }
.eraseToAnyPublisher()
}

func fetchLostItemData(id: Int) -> AnyPublisher<LostItemData, Error> {
func fetchLostItemData(id: Int) -> AnyPublisher<LostItemData, ErrorResponse> {
return service.fetchLostItemData(id: id)
.map { $0.toDomain() }
.eraseToAnyPublisher()
Expand All @@ -32,7 +32,7 @@ final class DefaultLostItemRepository: LostItemRepository {
return service.changeLostItemState(id: id)
}

func deleteLostItem(id: Int) -> AnyPublisher<Void, Error> {
func deleteLostItem(id: Int) -> AnyPublisher<Void, ErrorResponse> {
return service.deleteLostItem(id: id)
}

Expand All @@ -42,7 +42,7 @@ final class DefaultLostItemRepository: LostItemRepository {
.eraseToAnyPublisher()
}

func fetchLostItemStats() -> AnyPublisher<LostItemStats, Error> {
func fetchLostItemStats() -> AnyPublisher<LostItemStats, ErrorResponse> {
return service.fetchLostItemStats()
.map { $0.toDomain() }
.eraseToAnyPublisher()
Expand Down
18 changes: 9 additions & 9 deletions Koin/Data/Repository/DefaultNoticeListRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ final class DefaultNoticeListRepository: NoticeListRepository {
self.service = service
}

func fetchLostItemArticles(requestModel: FetchLostItemsRequest) -> AnyPublisher<NoticeListDto, Error> {
service.fetchLostItemArticles(requestModel: requestModel, retry: false)
func fetchLostItemArticles(requestModel: FetchLostItemsRequest) -> AnyPublisher<NoticeListDto, ErrorResponse> {
service.fetchLostItemArticles(requestModel: requestModel)
}


Expand All @@ -33,27 +33,27 @@ final class DefaultNoticeListRepository: NoticeListRepository {
}

func fetchLostItem(id: Int) -> AnyPublisher<LostArticleDetailDto, ErrorResponse> {
service.fetchLostItem(id: id, retry: false)
service.fetchLostItem(id: id)
}


func fetchNoticeArticles(requestModel: FetchNoticeArticlesRequest) -> AnyPublisher<NoticeListDto, Error> {
func fetchNoticeArticles(requestModel: FetchNoticeArticlesRequest) -> AnyPublisher<NoticeListDto, ErrorResponse> {
return service.fetchNoticeArticles(requestModel: requestModel)
}

func fetchLostItemList(requestModel: FetchNoticeArticlesRequest) -> AnyPublisher<NoticeListDto, Error> {
func fetchLostItemList(requestModel: FetchNoticeArticlesRequest) -> AnyPublisher<NoticeListDto, ErrorResponse> {
return service.fetchLostItemList(requestModel: requestModel)
}

func searchNoticeArticle(requestModel: SearchNoticeArticleRequest) -> AnyPublisher<NoticeListDto, Error> {
func searchNoticeArticle(requestModel: SearchNoticeArticleRequest) -> AnyPublisher<NoticeListDto, ErrorResponse> {
return service.searchNoticeArticle(requestModel: requestModel)
}

func fetchNoticeData(requestModel: FetchNoticeDataRequest) -> AnyPublisher<NoticeArticleDto, Error> {
func fetchNoticeData(requestModel: FetchNoticeDataRequest) -> AnyPublisher<NoticeArticleDto, ErrorResponse> {
return service.fetchNoticeData(requestModel: requestModel)
}

func fetchHotNoticeArticle() -> AnyPublisher<[NoticeArticleDto], Error> {
func fetchHotNoticeArticle() -> AnyPublisher<[NoticeArticleDto], ErrorResponse> {
return service.fetchHotNoticeArticles()
}

Expand All @@ -69,7 +69,7 @@ final class DefaultNoticeListRepository: NoticeListRepository {
return service.fetchMyNotificationKeyword()
}

func fetchRecommendedKeyword(count: Int?) -> AnyPublisher<NoticeRecommendedKeywordDto, Error> {
func fetchRecommendedKeyword(count: Int?) -> AnyPublisher<NoticeRecommendedKeywordDto, ErrorResponse> {
return service.fetchRecommendedKeyword(count: count)
}

Expand Down
Loading