Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
a6fa968
fix/#298: 테스트 타깃 정상 작동을 위한 코드 제거
dongglehada Mar 17, 2026
92e52c4
feat/#298: logger 및 loggerStorage 생성
dongglehada Mar 17, 2026
65d12aa
test/#298: logger 테스트
dongglehada Mar 17, 2026
15691d2
fix/#298: dictionary, queue 기반 에서 NSCache로 수정
dongglehada Mar 17, 2026
36c8e8a
style/#298: Apply SwiftLint autocorrect
github-actions[bot] Mar 17, 2026
6853453
chore/#298: 버전 3.0.2로 수정
dongglehada Mar 17, 2026
7beafc0
Merge branch 'feat/#298-logger' of https://github.com/Team-Maple/MLS-…
dongglehada Mar 17, 2026
921e5dc
fix/#298: 접두사 제거
dongglehada Mar 17, 2026
bb1a5b9
fix/#298: info -> notice 로 메서드명 수정 및 debug privacy 적용
dongglehada Mar 18, 2026
04f7e49
Merge pull request #305 from Team-Maple/feat/#298-logger
dongglehada Mar 19, 2026
8691faa
chore/#296: shared 모듈 추가 및 core, core test, designsystem, designsyste…
dongglehada Mar 19, 2026
fc8618c
feat/#296: logger 이전
dongglehada Mar 19, 2026
d21cbd3
refactor/#296: DIC 이전 및 리팩토링
dongglehada Mar 19, 2026
6ba6452
refactor/#296: project 제거
dongglehada Mar 23, 2026
14e2a77
chore/#296: package 로 core 생성
dongglehada Mar 23, 2026
be3b77b
chore/#296: compositionalLayoutBuilder, Extension, ImageLoader, Modal…
dongglehada Mar 23, 2026
3151d51
chore/#296: 불필요 파일 제거
dongglehada Mar 23, 2026
7e14bb5
refactor/#296: swift 6 컴파일 오류 수정 및 의존성 설정
dongglehada Mar 23, 2026
0cc812b
refactor/#296: network layer 이전
dongglehada Mar 25, 2026
0400496
style/#296: Apply SwiftLint autocorrect
github-actions[bot] Mar 25, 2026
d4840c4
Merge pull request #307 from Team-Maple/feat/#296-sharedModule
dongglehada Mar 31, 2026
636697d
feat/#306: MLSDesignSystem 모듈 생성
pinocchio22 Apr 4, 2026
dac56c0
Merge branch 'dev' of github.com:Team-Maple/MLS-iOS into feat/#306-ML…
pinocchio22 Apr 4, 2026
c0b9eef
fix/#306: Core와 DesignSystem 모듈 의존성 분리
pinocchio22 Apr 4, 2026
1d65008
fix/#306: 오타 수정 및 주석 제거
pinocchio22 Apr 4, 2026
7ef1f6a
feat/#302: 강제업데이트 유즈케이스 생성 및 앱피처 모듈 생성
dongglehada Apr 6, 2026
67f496d
style/#302: Apply SwiftLint autocorrect
github-actions[bot] Apr 6, 2026
4c15e42
fix/#302: synchronize제거
dongglehada Apr 6, 2026
9477a1e
fix/#302: nonisolated(unsafe) 제거
dongglehada Apr 6, 2026
50ee87e
fix/#302: 파싱에러 전파 수정
dongglehada Apr 6, 2026
f33300b
fix/#306: Tabbar 숨김 처리를 각 Feature 모듈에서 직접 담당하도록 수정
pinocchio22 Apr 6, 2026
9e30686
style/#306: Apply SwiftLint autocorrect
github-actions[bot] Apr 6, 2026
a0db594
Merge pull request #313 from Team-Maple/feat/#306-MLSDesignSystem-Module
pinocchio22 Apr 7, 2026
f8ee5dd
fix/#315: MLSDesignSystem 위치를 App 밖으로 이동
pinocchio22 Apr 7, 2026
8f717ef
fix/#315: ToLoginView의 텍스트를 재사용 가능한 형태로 수정
pinocchio22 Apr 7, 2026
6e746f1
fix/#315: CharacterInputView의 타이틀도 재사용 가능한 형태로 수정
pinocchio22 Apr 7, 2026
055ecd2
Merge pull request #314 from Team-Maple/feat/#302-force-update
dongglehada Apr 8, 2026
ead01a6
fix/#315: CardList 컴포넌트에 recommended(rank: Int)타입 추가
pinocchio22 Apr 8, 2026
7690c24
Merge branch 'dev' into fix/#315-Components
pinocchio22 Apr 8, 2026
6615afd
style/#315: Apply SwiftLint autocorrect
github-actions[bot] Apr 8, 2026
f7f2e52
Merge pull request #317 from Team-Maple/fix/#315-Components
pinocchio22 Apr 8, 2026
a0d791f
feat/#316: auth feature example 타깃 추가
dongglehada Apr 8, 2026
8518c8d
feat/#316: authfeature pakage 생성
dongglehada Apr 8, 2026
70b7036
feat/#316: 빌드 가능상태로 이전
dongglehada Apr 8, 2026
2625b92
feat/#316: example app 빌드 가능 상태로 수정
dongglehada Apr 8, 2026
652e4f1
feat/#316: 파일 폴더링 수정
dongglehada Apr 8, 2026
6cabad5
feat/#315: TooptipView 및 TooltipFactory 생성
pinocchio22 Apr 8, 2026
e288405
Merge branch 'fix/#315-Components' of github.com:Team-Maple/MLS-iOS i…
pinocchio22 Apr 9, 2026
71f3d25
fix/#315: 로그인유도 UI / 레벨, 직업 입력 UI 수정
pinocchio22 Apr 9, 2026
062f7ee
style/#315: Apply SwiftLint autocorrect
github-actions[bot] Apr 9, 2026
6dc0277
feat/#316: 임포트 순서 배치, 불필요 유즈케이스 제거
dongglehada Apr 11, 2026
634fae2
rename/#316: 폴더링 수정
dongglehada Apr 11, 2026
59228bd
rename/#316: 소셜로그인 프로바이더 명 변경
dongglehada Apr 11, 2026
0f231eb
rename/#316: 도메인 인터페이스 분리
dongglehada Apr 11, 2026
9374b0a
rename/#316: 인터페이스 파일 폴더링
dongglehada Apr 11, 2026
92c0f2b
feat/#316: 유즈케이스 통합 및 비즈니스 로직 분리
dongglehada Apr 11, 2026
56ad416
feat/#316: 도메인 인터페이스 제거 후 피처 인터페이스로 이동
dongglehada Apr 11, 2026
519008c
test/#316: 테스트 코드 추가 및 유즈 케이스 불필요한 옵저버블 랩핑 제거
dongglehada Apr 11, 2026
5e84675
refactor/#316: 베이스 뷰컨 로깅 로거블 사용
dongglehada Apr 13, 2026
c4ade97
feat/#316: 예시앱 폰트 등록 코드 추가
dongglehada Apr 13, 2026
8a954ca
style/#316: Apply SwiftLint autocorrect
github-actions[bot] Apr 13, 2026
679b0a5
fix/#315: 제미나이 피드백 반영 수정
pinocchio22 Apr 19, 2026
8bb48e4
Merge branch 'fix/#315-Components' of github.com:Team-Maple/MLS-iOS i…
pinocchio22 Apr 19, 2026
7b4aefd
style/#315: Apply SwiftLint autocorrect
github-actions[bot] Apr 19, 2026
c360738
fix/#315: rxGesture 추가 + 접근제어자 설정
pinocchio22 Apr 20, 2026
81e6cd3
Merge branch 'fix/#315-Components' of github.com:Team-Maple/MLS-iOS i…
pinocchio22 Apr 20, 2026
40e949f
Merge pull request #320 from Team-Maple/fix/#315-Components
pinocchio22 Apr 20, 2026
2ed3ed7
Merge pull request #322 from Team-Maple/feat/#316-new-authFeature
pinocchio22 Apr 20, 2026
c254c10
fix/#319: API 도메인 수정
pinocchio22 Apr 21, 2026
1af1898
fix/#319: 공지사항 / 패치노트 페이징 수정
pinocchio22 Apr 22, 2026
df224db
fix/#319: 공지사항 / 패치노트 페이징 수정
pinocchio22 Apr 22, 2026
59eabc7
style/#319: Apply SwiftLint autocorrect
github-actions[bot] Apr 22, 2026
84d973c
Merge pull request #323 from Team-Maple/fix/#319-API-Domain
pinocchio22 Apr 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
56 changes: 56 additions & 0 deletions MLS/Core/Core/Logger/Loggable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import OSLog

public protocol Loggable {}

public extension Loggable {
/// 모듈 이름을 자동으로 추출
/// - Returns: 타입이 정의된 모듈 이름 (예: "AuthFeature", "BookmarkFeature")
static var subsystem: String {
let fullName = String(reflecting: Self.self)
// "AuthFeature.LoginViewModel" -> "AuthFeature"
return fullName.components(separatedBy: ".").first ?? "Unknown"
}

/// 타입 이름을 카테고리로 사용
/// - Returns: 타입 이름 (예: "LoginViewModel", "BookmarkManager")
static var category: String {
String(describing: Self.self)
}

/// 타입별로 캐싱된 Logger 인스턴스
private static var logger: Logger {
LoggerStorage.logger(subsystem: subsystem, category: category)
}

/// 디버그 로그 (Debug 빌드에서만 출력)
/// - Parameter message: 로그 메시지 (.private로 보호되어 민감 정보도 안전하게 로깅)
func logDebug(_ message: String) {
#if DEBUG
Self.logger.debug("\(message, privacy: .private)")
#endif
}

/// 정보성 로그 (Notice 레벨)
/// - Parameter message: 로그 메시지
func logNotice(_ message: String) {
Self.logger.notice("\(message)")
}

/// 경고 로그 (비정상적이지만 처리 가능한 상황)
/// - Parameter message: 로그 메시지
func logWarning(_ message: String) {
Self.logger.warning("\(message)")
}

/// 에러 로그
/// - Parameter message: 로그 메시지
func logError(_ message: String) {
Self.logger.error("\(message)")
}

/// 치명적 에러 로그 (앱 크래시 직전 상황)
/// - Parameter message: 로그 메시지
func logCritical(_ message: String) {
Self.logger.critical("\(message)")
}
}
47 changes: 47 additions & 0 deletions MLS/Core/Core/Logger/LoggerStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import OSLog

/// Logger를 NSCache에 저장하기 위한 Wrapper 클래스
/// Logger는 struct이므로 class로 감싸야 NSCache에 저장 가능
private final class LoggerBox {
let logger: Logger

init(logger: Logger) {
self.logger = logger
}
}

/// Logger 인스턴스를 캐싱하여 재사용하는 저장소
/// NSCache를 사용하여 메모리 압박 시 자동으로 해제되며, Thread-safe하게 동작
final class LoggerStorage {
static let shared = LoggerStorage()

/// 캐싱된 Logger 인스턴스들
/// NSCache는 메모리 부족 시 자동으로 오래된 항목을 제거
private let cache = NSCache<NSString, LoggerBox>()

private init() {}

/// Logger 인스턴스를 가져오거나 생성
/// - Parameters:
/// - subsystem: 모듈 이름 (예: "AuthFeature")
/// - category: 카테고리 이름 (예: "LoginViewModel")
/// - Returns: 캐싱되거나 새로 생성된 Logger 인스턴스
static func logger(subsystem: String, category: String) -> Logger {
shared.getOrCreateLogger(subsystem: subsystem, category: category)
}

private func getOrCreateLogger(subsystem: String, category: String) -> Logger {
let key = "\(subsystem).\(category)" as NSString

// 캐시에서 확인
if let box = cache.object(forKey: key) {
return box.logger
}

// 없으면 새로 생성 (NSCache는 thread-safe)
let newLogger = Logger(subsystem: subsystem, category: category)
let box = LoggerBox(logger: newLogger)
cache.setObject(box, forKey: key)
return newLogger
}
}
24 changes: 0 additions & 24 deletions MLS/Core/DIContainerTests/DIContainerTests.swift
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
import XCTest

@testable import Core
@testable import Data
@testable import DomainInterface

class DIContainerTests: XCTestCase {
override func setUp() {
super.setUp()
DIContainer.resetForTesting()
}

override func tearDown() {
DIContainer.resetForTesting()
super.tearDown()
}

/// 객체 등록 및 가져오기 테스트
func testRegisterAndResolve() {
Expand Down Expand Up @@ -122,16 +111,3 @@ extension DIContainerTests {
}
}
}

// 테스트를 위한 서비스 초기화 함수
extension DIContainer {
public static func resetForTesting() {
shared.resetForTesting()
}

private func resetForTesting() {
serviceQueue.sync {
services.removeAll()
}
}
}
119 changes: 119 additions & 0 deletions MLS/Core/DIContainerTests/LoggerTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
@testable import Core
import Testing

struct TestViewModel: Loggable {
func testLogs() {
logDebug("Debug 로그 테스트")
logNotice("Notice 로그 테스트")
logWarning("Warning 로그 테스트")
logError("Error 로그 테스트")
logCritical("Critical 로그 테스트")
}
}

class TestManager: Loggable {
func testLogs() {
logDebug("Debug 로그 (class)")
logNotice("Notice 로그 (class)")
logWarning("Warning 로그 (class)")
logError("Error 로그 (class)")
logCritical("Critical 로그 (class)")
}
}

struct LoggerTests {

@Test("Subsystem 자동 추출 검증 - Struct")
func testSubsystemExtractionStruct() async throws {
// subsystem은 타입이 정의된 모듈 이름이어야 함
let subsystem = TestViewModel.subsystem
let category = TestViewModel.category

// DIContainerTests 모듈에 정의되어 있으므로
#expect(subsystem == "DIContainerTests")
#expect(category == "TestViewModel")
}

@Test("Subsystem 자동 추출 검증 - Class")
func testSubsystemExtractionClass() async throws {
let subsystem = TestManager.subsystem
let category = TestManager.category

#expect(subsystem == "DIContainerTests")
#expect(category == "TestManager")
}

@Test("Struct에서 Loggable 사용 테스트")
func testLoggableWithStruct() async throws {
let viewModel = TestViewModel()

// 로그 출력 (Console.app에서 확인 가능)
// subsystem: "DIContainerTests", category: "TestViewModel"
viewModel.testLogs()

// 에러 없이 실행되면 성공
#expect(true)
}

@Test("Class에서 Loggable 사용 테스트")
func testLoggableWithClass() async throws {
let manager = TestManager()

// 로그 출력 (Console.app에서 확인 가능)
// subsystem: "DIContainerTests", category: "TestManager"
manager.testLogs()

// 에러 없이 실행되면 성공
#expect(true)
}

@Test("동시성 안전성 테스트 - 같은 Logger 동시 접근")
func testConcurrentLoggerAccess() async throws {
// 100개 스레드가 동시에 같은 Logger 사용
await withTaskGroup(of: Void.self) { group in
for i in 0..<100 {
group.addTask {
let vm = TestViewModel()
vm.logNotice("동시 로그 \(i)")
}
}
}

// 크래시 없이 완료되면 성공
#expect(true)
}

@Test("여러 타입에서 동시에 로깅")
func testMultipleTypesConcurrent() async throws {
let viewModel = TestViewModel()
let manager = TestManager()

// 동시에 로그 출력
await withTaskGroup(of: Void.self) { group in
group.addTask {
viewModel.testLogs()
}
group.addTask {
manager.testLogs()
}
}

// 에러 없이 실행되면 성공
#expect(true)
}

@Test("NSCache 캐싱 검증 - 반복 접근 시 정상 동작")
func testLoggerCaching() async throws {
let vm1 = TestViewModel()
let vm2 = TestViewModel()

// 같은 타입에서 여러 번 로깅 (NSCache에서 캐싱된 Logger 재사용)
vm1.logNotice("첫 번째 인스턴스")
vm2.logNotice("두 번째 인스턴스")
vm1.logNotice("다시 첫 번째")
vm2.logNotice("다시 두 번째")

// 크래시 없이 정상 동작하면 성공
#expect(true)
}
}
4 changes: 4 additions & 0 deletions MLS/Data/Data/Network/DTO/AlarmDTO/AlarmResponseDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public struct AlarmResponseDTO: Decodable {
}

public struct NormalContent: Decodable {
public let id: Int
public let type: String
public let title: String
public let link: String
Expand All @@ -50,13 +51,15 @@ public extension AlarmResponseDTO {
switch content {
case .normal(let normal):
return AlarmResponse(
id: normal.id,
type: normal.type,
title: normal.title,
link: normal.link,
date: normal.date
)
case .all(let all):
return AlarmResponse(
id: all.alrim.id,
type: all.alrim.type,
title: all.alrim.title,
link: all.alrim.link,
Expand All @@ -72,6 +75,7 @@ public extension AlarmResponseDTO {
switch content {
case .all(let all):
return AllAlarmResponse(
id: all.alrim.id,
type: all.alrim.type,
title: all.alrim.title,
link: all.alrim.link,
Expand Down
10 changes: 5 additions & 5 deletions MLS/Data/Data/Network/Endpoints/AlarmEndPoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public enum AlarmEndPoint {
public static func fetchPatchNotes(query: Encodable) -> ResponsableEndPoint<AlarmResponseDTO> {
.init(
baseURL: base,
path: "/api/v1/alrim/list/patch-notes",
path: "/api/v2/alrim/list/patch-notes",
method: .GET,
query: query
)
Expand All @@ -15,7 +15,7 @@ public enum AlarmEndPoint {
public static func fetchNotices(query: Encodable) -> ResponsableEndPoint<AlarmResponseDTO> {
.init(
baseURL: base,
path: "/api/v1/alrim/list/notices",
path: "/api/v2/alrim/list/notices",
method: .GET,
query: query
)
Expand All @@ -24,7 +24,7 @@ public enum AlarmEndPoint {
public static func fetchOutdatedEvents(query: Encodable) -> ResponsableEndPoint<AlarmResponseDTO> {
.init(
baseURL: base,
path: "/api/v1/alrim/list/events/outdated",
path: "/api/v2/alrim/list/events/outdated",
method: .GET,
query: query
)
Expand All @@ -33,7 +33,7 @@ public enum AlarmEndPoint {
public static func fetchOngoingEvents(query: Encodable) -> ResponsableEndPoint<AlarmResponseDTO> {
.init(
baseURL: base,
path: "/api/v1/alrim/list/events/ongoing",
path: "/api/v2/alrim/list/events/ongoing",
method: .GET,
query: query
)
Expand All @@ -42,7 +42,7 @@ public enum AlarmEndPoint {
public static func fetchAll(query: Encodable) -> ResponsableEndPoint<AlarmResponseDTO> {
.init(
baseURL: base,
path: "/api/v1/alrim/all",
path: "/api/v2/alrim/all",
method: .GET,
query: query
)
Expand Down
12 changes: 6 additions & 6 deletions MLS/Data/Data/Repository/AlarmAPIRepositoryImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,31 @@ public class AlarmAPIRepositoryImpl: AlarmAPIRepository {
self.tokenInterceptor = interceptor
}

public func fetchPatchNotes(cursor: String?, pageSize: Int) -> Observable<PagedEntity<AlarmResponse>> {
public func fetchPatchNotes(cursor: Int?, pageSize: Int) -> Observable<PagedEntity<AlarmResponse>> {
let endpoint = AlarmEndPoint.fetchPatchNotes(query: AlarmQuery(cursor: cursor, pageSize: pageSize))
return provider.requestData(endPoint: endpoint, interceptor: tokenInterceptor)
.map { $0.toAlarmDomain() }
}

public func fetchNotices(cursor: String?, pageSize: Int) -> Observable<PagedEntity<AlarmResponse>> {
public func fetchNotices(cursor: Int?, pageSize: Int) -> Observable<PagedEntity<AlarmResponse>> {
let endpoint = AlarmEndPoint.fetchNotices(query: AlarmQuery(cursor: cursor, pageSize: pageSize))
return provider.requestData(endPoint: endpoint, interceptor: tokenInterceptor)
.map { $0.toAlarmDomain() }
}

public func fetchOutdatedEvents(cursor: String?, pageSize: Int) -> Observable<PagedEntity<AlarmResponse>> {
public func fetchOutdatedEvents(cursor: Int?, pageSize: Int) -> Observable<PagedEntity<AlarmResponse>> {
let endpoint = AlarmEndPoint.fetchOutdatedEvents(query: AlarmQuery(cursor: cursor, pageSize: pageSize))
return provider.requestData(endPoint: endpoint, interceptor: tokenInterceptor)
.map { $0.toAlarmDomain() }
}

public func fetchOngoingEvents(cursor: String?, pageSize: Int) -> Observable<PagedEntity<AlarmResponse>> {
public func fetchOngoingEvents(cursor: Int?, pageSize: Int) -> Observable<PagedEntity<AlarmResponse>> {
let endpoint = AlarmEndPoint.fetchOngoingEvents(query: AlarmQuery(cursor: cursor, pageSize: pageSize))
return provider.requestData(endPoint: endpoint, interceptor: tokenInterceptor)
.map { $0.toAlarmDomain() }
}

public func fetchAll(cursor: String?, pageSize: Int) -> Observable<PagedEntity<AllAlarmResponse>> {
public func fetchAll(cursor: Int?, pageSize: Int) -> Observable<PagedEntity<AllAlarmResponse>> {
let endpoint = AlarmEndPoint.fetchAll(query: AlarmQuery(cursor: cursor, pageSize: pageSize))
return provider.requestData(endPoint: endpoint, interceptor: tokenInterceptor)
.map { $0.toAllAlarmDomain() }
Expand All @@ -52,7 +52,7 @@ public class AlarmAPIRepositoryImpl: AlarmAPIRepository {

private extension AlarmAPIRepositoryImpl {
struct AlarmQuery: Encodable {
let cursor: String?
let cursor: Int?
let pageSize: Int
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class FetchAllAlarmUseCaseImpl: FetchAllAlarmUseCase {
self.repository = repository
}

public func execute(cursor: String?, pageSize: Int) -> Observable<PagedEntity<AllAlarmResponse>> {
return repository.fetchAll(cursor: cursor, pageSize: pageSize)
public func execute(id: Int?, pageSize: Int) -> Observable<PagedEntity<AllAlarmResponse>> {
return repository.fetchAll(cursor: id, pageSize: pageSize)
}
}
Loading
Loading