Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 0 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,6 @@ Config/Secrets.Release.xcconfig

# AI Agentic Coding
.claude/
CLAUDE.md
AGENTS.md
repo-map.md
feature-template.md
DESIGN.md
docs/
scripts/
phases/**/phase*-output.json
phases/**/step*-output.json
73 changes: 73 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# 프로젝트: 새록 (Saerok)

탐조(새 관찰) 기록·도감·커뮤니티·지도를 통합한 iOS 앱.

## 기술 스택
- **UI**: SwiftUI + `@Observable` (iOS 17+)
- **아키텍처**: MVVM + Clean Architecture (5계층)
- **로컬 저장소**: SwiftData
- **네트워킹**: Alamofire 5 (`SRNetworkService` 프로토콜 추상화)
- **전역 상태**: Combine 기반 `Store<AppState>`
- **의존성 주입**: `DIContainer` + `@Environment`
- **주요 SDK**: Kakao(소셜 로그인), Firebase(푸시·원격설정), Naver Maps, Lottie, Amplitude

## 디렉토리 지도

| 작업 목적 | 찾아갈 경로 |
|----------|------------|
| 앱 진입·전역 상태 | `saerok/Sources/App/` |
| 새 화면 추가 | `saerok/Sources/Feature/{기능}/View/` + `ViewModel/` — **반드시 [VIEW-GUIDE](docs/VIEW-GUIDE.md)·[VIEWMODEL-GUIDE](docs/VIEWMODEL-GUIDE.md) 참고** |
| 비즈니스 로직 수정 | `saerok/Sources/Interactors/` |
| 데이터 모델·API 응답 변환 | `saerok/Sources/Repositories/Models/` |
| API 엔드포인트 추가 | `saerok/Sources/Network/EndPoint/` |
| 공통 컴포넌트·디자인 토큰 | `saerok/Sources/Common/SRDesignSystem/` |
| 공통 뷰 재사용 | `saerok/Sources/Common/Views/` |
| 유틸·익스텐션 | `saerok/Sources/Common/Utils/` |
| 화면 전환 | `saerok/Sources/App/AppCoordinator.swift` |
| 의존성 부트스트랩 | `saerok/Sources/App/Dependency/` |

## 개발 가이드 문서
- [VIEW-GUIDE](docs/VIEW-GUIDE.md) — View 작성 규칙 (라우팅, 생명주기, 컴포넌트 사용, 금지 패턴)
- [VIEWMODEL-GUIDE](docs/VIEWMODEL-GUIDE.md) — ViewModel 작성 규칙 (LoadState, Output, CancelBag, 비동기 패턴)

## 아키텍처 규칙
- CRITICAL: 레이어 흐름은 **View → ViewModel → Interactor → Repository → Network/SwiftData** 방향만 허용. 역방향 참조 금지.
- CRITICAL: 비즈니스 로직은 반드시 **Interactor**에 작성. ViewModel은 상태 관리·UI 이벤트만 처리.
- CRITICAL: 화면 전환은 반드시 **AppCoordinator**를 통해서만. View에서 직접 NavigationLink로 다음 화면을 참조하지 않는다.
- 새 기능 추가 시 파일 생성 순서: Model → Repository → Interactor(프로토콜+구현체) → ViewModel → View
- Interactor는 프로토콜 기반으로 작성해 Mock 구현체와 분리 (프리뷰·테스트용)
- 전역 상태 변경은 `appState.bulkUpdate()`로 묶어 불필요한 렌더링 방지

## 상태 관리 패턴

```swift
// ViewModel: 비동기 상태
@Observable class SomeViewModel {
var items: LoadState<[Item]> = .notRequested
}

// 전역 상태 읽기/쓰기
container.appState[\.currentUser]
container.appState.bulkUpdate { $0.routing.xxx = ... }
```

## UI 규칙
- CRITICAL: 색상은 반드시 **Asset Catalog 이름**으로 참조 (`Color.main`, `.srWhite`, `.srGray` 등). hex 하드코딩 금지.
- CRITICAL: 폰트는 반드시 **`SRFontSet`** 을 통해 적용 (`.font(.SRFontSet.body1)`). 시스템 폰트 직접 사용 금지.
- 레이아웃 상수는 가능하면 `SRDesignConstant` 사용 (cornerRadius: 24, cardCornerRadius: 10, defaultPadding: 24)
- 버튼 스타일: `PrimaryButtonStyle` / `SecondaryButtonStyle` / `FilterButtonStyle` / `SRIconButtonStyle` 재사용
- 바텀시트·팝업·토스트는 `.srBottomSheet()` / `.srPopup()` / `.srToast()` modifier 사용

**UI 안티패턴 (절대 금지)**
- `blur` / `ultraThinMaterial` 남용
- 그라데이션 텍스트, 네온·글로우 shadow
- 보라/인디고 계열 임의 색상 추가
- 배경 장식 도형·그라데이션 orb
- 화면 전체 shake·반복 pulse 애니메이션
- `.main` 컬러를 CTA·활성 상태 외에 남용

## 이미지 업로드 흐름
S3 Presigned URL 방식: 백엔드에서 URL 발급 → S3 직접 업로드 → 백엔드에 메타데이터 등록 → 실패 시 채집 기록 전체 롤백

## 커밋 메시지
`feat:` / `fix:` / `refactor:` / `docs:` / `chore:` conventional commits 형식 사용
60 changes: 60 additions & 0 deletions docs/ADR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Architecture Decision Records

## 철학
기능 단위 독립성과 테스트 가능성을 유지하면서 빠른 개발 속도를 확보한다. Apple 네이티브 스택을 최우선으로 선택하고, 외부 의존성은 명확한 이유가 있을 때만 도입한다. 복잡한 추상화보다 작동하는 최소 구현을 선택하되, 레이어 간 경계는 명확히 유지한다.

---

### ADR-001: MVVM + Clean Architecture (레이어드 아키텍처) 채택
**결정**: View → ViewModel → Interactor → Repository → Network/Storage 5계층 구조 채택
**이유**: 기능이 증가해도 비즈니스 로직(Interactor)과 데이터 접근(Repository)이 UI와 분리되어 있어 독립적으로 테스트 가능하다. Interactor는 프로토콜 기반이므로 Mock 구현체로 교체가 쉬워 프리뷰·단위 테스트에서 네트워크 없이 동작 검증이 가능하다.
**트레이드오프**: 단순 CRUD 화면에서도 4~5개 파일(View, ViewModel, Interactor, Repository, Model)을 만들어야 해 초기 코드량이 증가한다. 레이어 경계를 잘못 이해하면 로직이 View나 Repository에 흘러들기 쉽다.

---

### ADR-002: SwiftUI + @Observable 선택 (UIKit 대신)
**결정**: 모든 UI를 SwiftUI로 구현하고, ViewModel 상태 관찰에 iOS 17+ `@Observable` 매크로 사용
**이유**: 선언형 UI로 상태-화면 동기화 코드를 제거하고 개발 속도를 높인다. `@Observable`은 ObservableObject보다 불필요한 렌더링이 적고, Xcode 프리뷰와의 연동이 자연스럽다.
**트레이드오프**: iOS 17 미만 지원 불가. UIKit 컴포넌트가 필요한 엣지 케이스(예: 복잡한 지도 인터랙션, 커스텀 텍스트 에디터)에서 UIViewRepresentable 브릿징이 필요하다.

---

### ADR-003: DIContainer + @Environment 기반 의존성 주입 (3rd-party DI 프레임워크 대신)
**결정**: `AppEnvironment.bootstrap()`에서 모든 의존성을 조립하고, `DIContainer`를 SwiftUI `@Environment`로 뷰 트리에 전파
**이유**: Swinject 등 외부 DI 프레임워크 없이 Apple 기본 기능만으로 동일한 효과를 얻는다. 루트에서 한 번만 주입하면 하위 뷰 어디서든 꺼내 쓸 수 있어 객체를 수동으로 전달(prop drilling)할 필요가 없다. 테스트·프리뷰에서는 stub 구현체를 주입해 네트워크 없이 UI 확인이 가능하다.
**트레이드오프**: `@Environment` 접근은 런타임에 타입을 조회하므로 누락 시 컴파일 에러 대신 런타임 에러가 발생한다. 의존성 그래프가 복잡해질수록 `bootstrap()` 함수가 비대해진다.

---

### ADR-004: AppCoordinator 패턴으로 네비게이션 중앙 관리
**결정**: 화면 전환 로직을 `AppCoordinator`에 집중하고, ViewModel에서는 Coordinator를 통해서만 라우팅 요청
**이유**: 뷰가 다음 화면을 직접 알지 않아도 되므로 화면 간 결합도가 낮아진다. 딥링크·알림 등 외부 진입점에서도 Coordinator 한 곳을 수정해 모든 네비게이션 흐름을 제어할 수 있다. ViewModel 팩토리 역할도 겸하여 의존성 생성 책임을 한 곳으로 모은다.
**트레이드오프**: 화면 전환마다 Coordinator를 거쳐야 하므로 간단한 팝업·시트도 라우팅 케이스로 등록해야 해 초기 설계 비용이 있다.

---

### ADR-005: Combine 기반 `Store<T>`로 전역 상태 관리
**결정**: `AppState`를 `Store<AppState>` (`CurrentValueSubject` 래퍼)로 관리하고, `KeyPath` 서브스크립트로 타입 안전하게 접근
**이유**: TCA·Redux 등 외부 상태관리 프레임워크 없이 Combine만으로 단방향 데이터 흐름을 구현한다. `bulkUpdate()`로 여러 상태를 한 트랜잭션에 변경해 불필요한 중간 렌더링을 방지한다.
**트레이드오프**: Combine에 대한 이해가 부족하면 메모리 누수(AnyCancellable 미해제)가 발생하기 쉽다. 전역 상태가 커질수록 `AppState` 구조체가 비대해진다.

---

### ADR-006: SwiftData 로컬 영속성 (CoreData/Realm 대신)
**결정**: 로컬 캐싱 및 영속성 저장에 iOS 17+ SwiftData 사용
**이유**: CoreData보다 Swift-native API로 보일러플레이트가 적고, `@Model` 매크로로 모델 정의가 간결하다. 앱이 iOS 17을 최소 지원 버전으로 타겟하므로 호환성 문제가 없다. Realm은 추가 외부 의존성을 도입하므로 제외했다.
**트레이드오프**: SwiftData는 CoreData 대비 성숙도가 낮아 복잡한 마이그레이션·쿼리 최적화에서 제약이 있다. CloudKit 연동이 필요할 때 CoreData만큼 검증된 사례가 부족하다.

---

### ADR-007: S3 Presigned URL 방식으로 이미지 업로드
**결정**: 이미지 업로드 시 백엔드에서 S3 Presigned URL을 발급받아 클라이언트가 S3에 직접 업로드
**이유**: 이미지 바이너리를 백엔드 서버를 거치지 않아 서버 트래픽·부하가 줄어든다. 업로드 실패 시 채집 기록 자체를 롤백하는 흐름으로 데이터 정합성을 보장한다.
**트레이드오프**: 클라이언트가 업로드 성공 여부를 직접 처리해야 하므로 에러 핸들링 흐름이 복잡해진다(Presigned URL 발급 → S3 업로드 → 메타데이터 등록 → 실패 시 롤백). 네트워크 단절 중간 단계에서 부분 실패 시나리오를 별도로 고려해야 한다.

---

### ADR-008: Alamofire 기반 HTTP 네트워킹 (URLSession 직접 사용 대신)
**결정**: 네트워크 레이어를 `SRNetworkService` 프로토콜로 추상화하고 내부 구현은 Alamofire 5 사용
**이유**: 인터셉터·리트라이·멀티파트 업로드 등 반복적인 네트워킹 보일러플레이트를 Alamofire가 대신 처리한다. 프로토콜 추상화 덕분에 테스트 시 URLSession 레벨 Mocking 없이 서비스 구현체만 교체 가능하다.
**트레이드오프**: Swift Concurrency 시대에 URLSession async/await만으로도 충분한 요구사항에서 불필요한 외부 의존성이 된다. Alamofire 버전 업그레이드 시 인터페이스 변화에 대응해야 하는 유지보수 비용이 있다.
162 changes: 162 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# 아키텍처

## 디렉토리 구조

```
saerok/Sources/
├── App/ # 앱 진입점, 상태, 의존성 주입
│ ├── AppState.swift # 전역 상태 구조체
│ ├── AppCoordinator.swift # 네비게이션 코디네이터 + ViewModel 팩토리
│ └── Dependency/ # DIContainer, AppEnvironment 부트스트랩
├── Common/ # 공통 유틸리티 + 디자인 시스템
│ ├── DesignSystem/ # 색상, 폰트, 컴포넌트
│ ├── Utils/ # Extensions, Helper, Store, Loadable
│ └── Views/ # 공통 SwiftUI 컴포넌트
├── Feature/ # 기능별 모듈
│ ├── Collection/ # 채집 기록 기능
│ ├── Community/ # 커뮤니티 기능
│ ├── FieldGuide/ # 도감 기능
│ ├── Login/ # 로그인/인증
│ ├── Map/ # 지도 기능
│ ├── MyPage/ # 마이페이지
│ ├── Onboarding/ # 온보딩
│ └── Root/ # 루트 탭 뷰
├── Interactors/ # 비즈니스 로직 (Use Case)
├── Network/ # HTTP 네트워킹 레이어
│ ├── API/ # APIClient (URLSession 래퍼)
│ ├── EndPoint/ # SREndpoint, KakaoEndpoint
│ └── SRNetworkService.swift # 네트워크 서비스 프로토콜
└── Repositories/ # 데이터 접근 추상화
└── Models/ # DTO ↔ Local 도메인 모델
```

각 Feature 내부 구조:
```
Feature/{FeatureName}/
├── View/ # SwiftUI 뷰
└── ViewModel/ # @Observable ViewModel
```

## 패턴

**MVVM + Clean Architecture (레이어드 아키텍처)**

- **View**: SwiftUI, `@Environment`로 주입된 ViewModel·DIContainer를 구독
- **ViewModel**: `@Observable` 매크로 사용, LoadState로 비동기 상태 관리
- **Interactor (Use Case)**: 비즈니스 로직 담당, 프로토콜 기반으로 Mock 구현체와 분리
- **Repository**: 네트워크·로컬 스토리지 추상화, DTO → Local 모델 변환 담당
- **Network**: 타입 안전한 Endpoint 프로토콜, Alamofire 기반 `DefaultAPIClient`

**디자인 패턴 요약**

| 패턴 | 사용처 |
|------|--------|
| Repository Pattern | 데이터 소스 추상화 |
| Interactor / Use Case | 비즈니스 로직 캡슐화 |
| Coordinator Pattern | 화면 전환 (`AppCoordinator`) |
| Dependency Injection | `DIContainer` + `@Environment` |
| Observer Pattern | Combine (`Store`), `@Observable` |
| Builder Pattern | `@resultBuilder` 기반 `CancelBag` |

## 데이터 흐름

```
사용자 입력
SwiftUI View (@Environment 주입, @Observable 구독)
ViewModel (LoadState 관리, async 메서드 호출)
Interactor (비즈니스 로직, 유효성 검사, 정렬·필터)
Repository (데이터 소스 조합: 네트워크 + 로컬)
SRNetworkService / SwiftData
↓ ↑
외부 API 응답 (DTO) → Local 모델 변환
응답 → ViewModel 상태 업데이트 → UI 자동 갱신
```

**이미지 업로드 특수 흐름:**
```
이미지 선택
→ 백엔드에서 S3 Presigned URL 발급
→ 이미지를 S3에 직접 업로드
→ 백엔드에 이미지 메타데이터 등록
→ 실패 시 채집 기록 롤백
```

**비동기 병렬 로딩:**
`withTaskGroup`으로 상세 데이터, 댓글, 연관 항목을 동시에 로드

## 상태 관리

### 전역 상태 — `AppState` + `Store<T>`

```swift
// Store = CurrentValueSubject<State, Never> (Combine)
let appState = Store<AppState>(AppState())
```

`AppState`에는 아래 하위 상태가 포함됨:
- `routing` — 화면별 네비게이션 경로
- `system` — 앱 버전, 네트워크 상태
- `authStatus` — 인증 여부
- `currentUser` — 로그인 유저 정보

`KeyPath` 서브스크립트로 타입 안전하게 접근하고, `bulkUpdate()`로 여러 상태를 배치 업데이트한다.

### 로컬 컴포넌트 상태 — `LoadState<T>` + `@Observable`

```swift
enum LoadState<T> {
case notRequested
case loading
case success(T)
case failure(Error)
}
```

ViewModel이 `@Observable`로 선언되어 SwiftUI가 자동으로 변경 감지.

### 구독 관리 — `CancelBag`

Combine 구독을 `@resultBuilder` 기반의 `CancelBag`으로 한 곳에서 관리해 메모리 누수를 방지한다.

## 의존성 주입

앱 시작 시 `AppEnvironment.bootstrap()`에서 모든 의존성을 구성한다:

```
AppEnvironment.bootstrap()
→ SRNetworkServiceImpl
→ SwiftData ModelContainer
→ Repositories (MainRepository 등)
→ Interactors (각 feature별)
→ DIContainer (AppState + Interactors + NetworkService)
```

뷰에 주입:
```swift
// 루트에서 한 번만 inject
ContentView().inject(diContainer)

// 하위 뷰에서 꺼내 쓰기
@Environment(\.injected) var container
```

테스트·프리뷰에서는 `DIContainer.Interactors.stub`의 Mock 구현체가 자동으로 사용된다.

## 주요 외부 의존성

| 라이브러리 | 용도 |
|-----------|------|
| Alamofire 5 | HTTP 네트워킹 |
| SwiftData | 로컬 영속성 (Apple 네이티브) |
| Kakao iOS SDK | 소셜 로그인 |
| Firebase 12 | 푸시 알림, 원격 설정 |
| Naver Maps SDK | 지도 |
| Lottie 4 | 애니메이션 |
| Amplitude | 사용자 이벤트 분석 |
| AdFit | 광고 |
31 changes: 31 additions & 0 deletions docs/PRD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# PRD: 새록

## 목표
탐조(새 관찰) 기록을 모아두고, 조류 도감을 탐색하며, 같은 취미를 가진 사람들과 관찰 기록을 공유하는 iOS 앱

## 사용자
- 탐조를 즐기는 아마추어·취미 탐조인
- 관찰한 새를 기록하고 나중에 다시 찾아보고 싶은 사람
- 새 이름을 모를 때 커뮤니티에 물어보고 싶은 사람

## 핵심 기능

1. **새록 (관찰 기록)** — 새 이름, 사진, 위치, 날짜, 메모를 담은 관찰 기록 CRUD. 다른 사용자가 새 이름을 제안하거나 투표할 수 있는 조류 식별 기능 포함
2. **도감 (조류 도감)** — 전체 조류 목록을 앱 내 캐싱하여 오프라인에서도 탐색·검색 가능. 즐겨찾기 북마크 지원
3. **둥지 (커뮤니티)** — 다른 사용자의 관찰 기록 피드. 새 이름 미정 게시물, 인기순·최신순 탐색 및 사용자 검색
4. **지도** — 관찰 기록 위치를 Naver Maps로 시각화. 마커 클러스터링, 내 위치 기반 주변 기록 조회
5. **마이페이지** — 프로필 관리, 푸시 알림 설정, 차단 사용자 관리, 공지사항

## MVP 제외 사항
- Android 버전
- 웹 서비스
- 탐조 일정 예약·그룹 탐조 조율 기능
- 새 울음소리 인식·오디오 기록
- 지역 탐조 핫스팟 큐레이션 (운영자 편집 콘텐츠)

## 디자인
- 라이트 모드 기반 (다크모드 미지원)
- 메인 컬러 + 포인트 컬러 1가지 조합의 미니멀 팔레트; 무채색 계열(SRWhite, SRGray, SRLightGray) 중심
- 서체: 본문·UI는 Pretendard(가변체), 헤드라인은 잘풀리는하루, 숫자·액센트는 Moneygraphy Rounded
- 모서리 반경 24pt, 기본 패딩 24pt로 여유 있는 레이아웃
- 탭별 온보딩 오버레이로 신규 사용자 진입 장벽 최소화
Loading