.claude/skills/ios-repository-pattern/SKILL.md
iOS Repository 패턴 정의. Protocol + Impl 구조 및 API enum + RequestType 확장 패턴으로 네트워크 레이어를 구현합니다. async/await와 Result 타입을 사용하며, 다른 iOS 프로젝트에서도 재사용 가능합니다.
npx skillsauth add 3dollar-in-my-pocket/3dollars-in-my-pocket-ios ios-repository-patternInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Protocol + Impl 구조로 Repository를 구현하고, API enum + RequestType 확장으로 네트워크 요청을 정의합니다. async/await와 Result 타입을 사용하여 비동기 처리를 간결하게 합니다.
public protocol MyRepository {
func fetchData(input: FetchDataInput) async -> Result<MyDataResponse, Error>
func saveData(id: String, isDelete: Bool) async -> Result<String, Error>
}
규칙:
public struct MyRepositoryImpl: MyRepository {
public init() { }
public func fetchData(input: FetchDataInput) async -> Result<MyDataResponse, Error> {
let request = MyApi.fetchData(input: input)
return await NetworkManager.shared.request(requestType: request)
}
public func saveData(id: String, isDelete: Bool) async -> Result<String, Error> {
let request = MyApi.saveData(id: id, isDelete: isDelete)
return await NetworkManager.shared.request(requestType: request)
}
}
규칙:
enum MyApi {
case fetchData(input: FetchDataInput)
case saveData(id: String, isDelete: Bool)
}
규칙:
extension MyApi: RequestType {
var param: Encodable? {
switch self {
case .fetchData(let input):
return input
case .saveData:
return nil
}
}
var method: RequestMethod {
switch self {
case .fetchData:
return .get
case .saveData(_, let isDelete):
return isDelete ? .delete : .put
}
}
var header: HTTPHeaderType {
switch self {
case .fetchData(let input):
return .custom([
"X-Custom-Header": input.value
])
default:
return .default
}
}
var path: String {
switch self {
case .fetchData(let input):
return "/api/v1/data/\(input.id)"
case .saveData(let id, _):
return "/api/v1/data/\(id)"
}
}
}
규칙:
1. API enum 정의
↓
2. RequestType 확장 (param, method, header, path)
↓
3. Repository Protocol 정의
↓
4. Repository Impl 구현 (NetworkManager 호출)
import Foundation
import Model
// 1. API enum 정의
enum MyApi {
case fetchData(input: FetchDataInput)
case saveData(id: String, isDelete: Bool)
case updateData(id: Int, input: UpdateDataInput)
}
// 2. RequestType 확장
extension MyApi: RequestType {
var param: Encodable? {
switch self {
case .fetchData(let input):
return input
case .saveData:
return nil
case .updateData(_, let input):
return input
}
}
var method: RequestMethod {
switch self {
case .fetchData:
return .get
case .saveData(_, let isDelete):
return isDelete ? .delete : .put
case .updateData:
return .patch
}
}
var header: HTTPHeaderType {
switch self {
case .fetchData:
return .json
case .saveData:
return .json
case .updateData:
return .custom(["X-Nonce-Token": "token"])
}
}
var path: String {
switch self {
case .fetchData(let input):
return "/api/v1/data/\(input.id)"
case .saveData(let id, _):
return "/api/v1/data/\(id)"
case .updateData(let id, _):
return "/api/v1/data/\(id)"
}
}
}
// 3. Repository Protocol
public protocol MyRepository {
func fetchData(input: FetchDataInput) async -> Result<MyDataResponse, Error>
func saveData(id: String, isDelete: Bool) async -> Result<String, Error>
func updateData(id: Int, input: UpdateDataInput) async -> Result<MyDataResponse, Error>
}
// 4. Repository Impl
public struct MyRepositoryImpl: MyRepository {
public init() { }
public func fetchData(input: FetchDataInput) async -> Result<MyDataResponse, Error> {
let request = MyApi.fetchData(input: input)
return await NetworkManager.shared.request(requestType: request)
}
public func saveData(id: String, isDelete: Bool) async -> Result<String, Error> {
let request = MyApi.saveData(id: id, isDelete: isDelete)
return await NetworkManager.shared.request(requestType: request)
}
public func updateData(id: Int, input: UpdateDataInput) async -> Result<MyDataResponse, Error> {
let request = MyApi.updateData(id: id, input: input)
return await NetworkManager.shared.request(requestType: request)
}
}
case .fetchData(let input):
return input // Encodable 구조체
case .saveData:
return nil
case .reportStore(_, let reportReason):
return ["deleteReasonType": reportReason]
case .fetchStorePhotos(let storeId, let cursor):
var params = ["storeId": "\(storeId)"]
if let cursor {
params["cursor"] = cursor
}
return params
case .fetchDisplayItems(_, let itemTypes):
return ["itemTypes": itemTypes.map { $0.rawValue }]
case .fetchData:
return .get
case .saveData(_, let isDelete):
return isDelete ? .delete : .put
case .fetchData:
return .json // 또는 .default
case .fetchAroundStores:
return .location
case .createStore(_, let token):
return .custom(["X-Nonce-Token": token])
case .fetchBossStoreDetail(let input):
return .custom([
"X-Device-Latitude": String(input.latitude),
"X-Device-Longitude": String(input.longitude)
])
case .fetchData(let input):
return "/api/v1/data/\(input.id)"
case .togglePostSticker(let storeId, let postId, _):
return "/api/v1/store/\(storeId)/news-post/\(postId)/stickers"
// Repository Protocol
public protocol StoreRepository {
func fetchStoreContributorHistories(storeId: Int, cursor: String?) async -> Result<StoreContributorHistoriesSection, Error>
func saveStore(storeId: String, isDelete: Bool) async -> Result<String, Error>
}
// Repository Impl
public struct StoreRepositoryImpl: StoreRepository {
public init() { }
public func fetchStoreContributorHistories(storeId: Int, cursor: String?) async -> Result<StoreContributorHistoriesSection, Error> {
let request = StoreApi.fetchStoreContributorHistories(storeId: storeId, cursor: cursor)
return await NetworkManager.shared.request(requestType: request)
}
public func saveStore(storeId: String, isDelete: Bool) async -> Result<String, Error> {
let request = StoreApi.saveStore(storeId: storeId, isDelete: isDelete)
return await NetworkManager.shared.request(requestType: request)
}
}
// API enum
enum StoreApi {
case fetchStoreContributorHistories(storeId: Int, cursor: String?)
case saveStore(storeId: String, isDelete: Bool)
}
// RequestType 확장
extension StoreApi: RequestType {
var param: Encodable? {
switch self {
case .fetchStoreContributorHistories(_, let cursor):
if let cursor {
return ["cursor": cursor]
} else {
return nil
}
case .saveStore:
return nil
}
}
var method: RequestMethod {
switch self {
case .fetchStoreContributorHistories:
return .get
case .saveStore(_, let isDelete):
return isDelete ? .delete : .put
}
}
var header: HTTPHeaderType {
switch self {
case .fetchStoreContributorHistories:
return .json
case .saveStore:
return .json
}
}
var path: String {
switch self {
case .fetchStoreContributorHistories(let storeId, _):
return "/v1/screen/store/\(storeId)/contributors/section/histories"
case .saveStore(let storeId, _):
return "/api/v2/store/\(storeId)/favorite"
}
}
}
@MainActor
private func fetchData() async {
guard !state.isLoading else { return }
state.isLoading = true
let result = await dependency.repository.fetchData(
input: FetchDataInput(id: config.id)
)
state.isLoading = false
switch result {
case .success(let response):
output.items.send(response.items)
case .failure(let error):
output.error.send(error)
}
}
테스트 시 Mock Repository를 생성할 수 있습니다:
final class MockMyRepository: MyRepository {
var fetchDataResult: Result<MyDataResponse, Error>?
func fetchData(input: FetchDataInput) async -> Result<MyDataResponse, Error> {
guard let result = fetchDataResult else {
return .failure(NSError(domain: "Mock", code: -1))
}
return result
}
func saveData(id: String, isDelete: Bool) async -> Result<String, Error> {
return .success("OK")
}
}
Modules/Core/Network/Sources/
├── API/
│ ├── StoreApi.swift # API enum + RequestType 확장
│ ├── UserApi.swift
│ └── ...
└── Repository/
├── StoreRepository.swift # Protocol + Impl
├── UserRepository.swift
└── ...
규칙:
testing
iOS ViewModel 유저 플로우 기반 테스트 코드 자동 생성. Given-When-Then 패턴으로 Input/Output 검증 테스트를 생성합니다. XCTest와 Combine을 사용하며, 다른 iOS 프로젝트에서도 재사용 가능합니다.
data-ai
iOS MVVM 패턴의 ViewModel 구조 정의. Input/Output/Route/Config/Dependency/State 패턴으로 ViewModel을 생성합니다. Combine 기반으로 작동하며, 다른 iOS 프로젝트에서도 재사용 가능합니다.
tools
테크스펙 문서를 기반으로 새로운 피처를 구현하는 전체 파이프라인을 자동화합니다. 구현 → 테스트 → 수정 → 커밋까지 자동으로 수행합니다.
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.