Skip to content

refactor(api,mobile): Result 패턴 도입 및 레이어 아키텍처 재구성 (#116)#117

Merged
dydals3440 merged 16 commits intomainfrom
refactor/result-pattern-and-layer-restructure
Feb 6, 2026
Merged

refactor(api,mobile): Result 패턴 도입 및 레이어 아키텍처 재구성 (#116)#117
dydals3440 merged 16 commits intomainfrom
refactor/result-pattern-and-layer-restructure

Conversation

@dydals3440
Copy link
Contributor

📋 개요

모바일 앱에 Result 패턴을 도입하여 에러 처리를 타입 안전하게 개선하고, 각 feature의 레이어별 역할을 명확히 재구성합니다.
API 서버에는 타임존 지원, Overlapping Intervals 날짜 필터링, 알림 중복 방지 로직을 추가합니다.

🏷️ 변경 유형

  • ♻️ refactor - 코드 리팩토링
  • feat - 새로운 기능 추가
  • test - 테스트 추가/수정
  • 📝 docs - 문서 수정

📦 영향 범위

  • apps/api - NestJS 백엔드
  • apps/mobile - Expo 모바일 앱

📝 변경 내용

1. Result 패턴 도입 (Mobile)

기존에는 에러 처리가 throw 기반으로 혼재되어 있어, **예상된 에러(4xx)**와 **예상치 못한 에러(5xx, 네트워크)**의 구분이 불명확했습니다.

Before

Service → throw Error → try/catch → 어떤 에러인지 불명확

After

Service → Result<T, E> → unwrap() → onError에서 타입별 분기 처리

Result 타입 정의

type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };

// 헬퍼 함수
ok(value)    // 성공 Result 생성
err(error)   // 실패 Result 생성
unwrap(r)    // 성공이면 값 반환, 실패면 throw
isOk(r)      // 타입 가드
isErr(r)     // 타입 가드

에러 분류 전략

에러 유형 분류 전달 방식 처리 위치 예시
InfraError 예상치 못한 에러 throw ErrorBoundary (자동) 5xx, 네트워크 에러, 타임아웃, 파싱 실패
ApiError (4xx) 예상된 에러 Result.err() Mutation onError 서버 유효성 검사, 인증 에러
{Feature}Error 예상된 에러 Result.err() Mutation onError 클라이언트 사전 검증 (Policy)

2. 레이어별 역할 재구성 (Mobile)

각 레이어의 책임을 명확히 분리하고, Mapper의 위치를 services/repositories/로 이동했습니다.

레이어 구조 및 역할

┌─────────────────────────────────────────────────┐
│  Presentation (Query/Mutation Options)           │
│  → unwrap(result)로 Result 해제                  │
│  → onError에서 에러 타입별 UI 분기               │
├─────────────────────────────────────────────────┤
│  Service                                         │
│  → Policy 검증 (서버 호출 전 클라이언트 사전 검증)│
│  → FeatureError 생성 (검증 실패 시)               │
│  → Repository 호출 및 Result pass-through        │
├─────────────────────────────────────────────────┤
│  Repository Interface                            │
│  → 도메인 타입만 사용하는 계약 정의               │
│  → HTTP, DTO에 대한 지식 없음                    │
├─────────────────────────────────────────────────┤
│  Repository Impl                                 │
│  → HttpClient.get/post → Result<raw, ApiError>   │
│  → Zod safeParse → 파싱 실패 시 throw ParseError │
│  → Mapper → ok(domainModel) 반환                 │
├─────────────────────────────────────────────────┤
│  Mapper (repositories/ 디렉토리)                  │
│  → DTO → 도메인 모델 변환 순수 함수               │
│  → Date 파싱, 필드 매핑                          │
├─────────────────────────────────────────────────┤
│  HttpClient Port                                 │
│  → 4xx → Result.err(ApiError)                    │
│  → 5xx/네트워크 → throw InfraError               │
└─────────────────────────────────────────────────┘

Mapper 위치 변경 이유

항목 Before (services/) After (repositories/)
의존 방향 Service가 DTO를 알아야 함 Service는 도메인 타입만 사용
변환 시점 Service에서 변환 후 반환 Repository에서 변환 후 반환
테스트 용이성 Service 테스트에 DTO mock 필요 순수 함수 단위 테스트 가능

각 레이어가 아는 것 / 모르는 것

레이어 아는 것 모르는 것
Model Zod, Policy 다른 모든 것
Mapper DTO 타입, 도메인 타입, Policy HttpClient, Service, UI
Repository Impl HttpClient, Zod, Mapper, DTO Service, UI, Policy
Service Repository(interface), Policy, FeatureError HttpClient, DTO, Mapper
Presentation Service, ErrorCode, FeatureError, ApiError HttpClient, DTO, Mapper

적용된 Feature 모듈

Feature Mapper 이동 Error 클래스 Policy
Auth auth.mapper.ts AuthError AuthPolicy (구독 활성화 등)
Friend friend.mapper.ts FriendError (EMPTY_TAG, INVALID_TAG) FriendPolicy (태그 형식 검증)
Notification notification.mapper.ts NotificationError -
Todo todo.mapper.ts TodoError TodoPolicy (색상, 공개범위)

3. 타임존 지원 (API)

구성 요소 변경 내용
@Timezone() 데코레이터 X-Timezone 헤더에서 IANA 타임존 추출 (기본값: UTC)
TIMESTAMPTZ 마이그레이션 scheduledTime 필드 타입 변경 (DateTime → Timestamptz)
toDateOnly() 날짜 문자열 → UTC 자정 Date (PostgreSQL @db.Date 호환)
getUserToday() 사용자 타임존 기준 오늘 날짜 반환
toScheduledTime() 사용자 로컬 시간 → UTC Date 변환
startOfDayInTimezone() 타임존 기준 하루 시작 시각 계산

적용 모듈: Todo, Cheer, Nudge - 일일 제한/쿨다운/완료 판정이 사용자 타임존 기준으로 동작

4. Overlapping Intervals 날짜 필터링 (API)

다중일 투두를 캘린더에서 올바르게 표시하기 위한 범위 필터링:

시나리오 필터 조건
다중일 투두 todo.startDate <= filterEnd AND todo.endDate >= filterStart
단일일 투두 filterStart <= todo.startDate <= filterEnd
파라미터 1개만 exact match (오픈 레인지 방지, Google Calendar 패턴)

5. 알림 중복 발송 방지 (API)

알림 타입 방지 전략
DAILY_COMPLETE existsNotification(): 같은 날 이미 발송 여부 체크
FRIEND_COMPLETED findAlreadyNotifiedUserIds(): Set 기반 배치 조회로 N+1 방지

🧪 테스트

테스트 방법

pnpm lint       # Biome 린트 통과
pnpm test       # 단위 테스트 (47 suites, 930 tests)
pnpm test:e2e   # E2E 테스트 (11 suites, 265 tests)
pnpm build      # 전체 빌드 통과

테스트 결과

  • 단위 테스트 통과
  • E2E 테스트 통과
  • 빌드 통과
  • 린트 통과

추가된 테스트

  • date.util.spec.ts: 타임존 변환 유틸리티 13건
  • todo.service.spec.ts: 날짜 범위 검증 + 타임존 연동 4건
  • todo.e2e-spec.ts: 다중일 투두 범위 필터, 특정 하루/기간 조회 3건
  • todo.listener.spec.ts: 알림 중복 방지 리스너 6건 (GWT 패턴)
  • notification.repository.spec.ts: 중복 체크 레포지토리 4건

✅ 체크리스트

작성자 확인

  • 코드가 프로젝트의 코딩 컨벤션을 따릅니다
  • pnpm lint (Biome) 검사를 통과했습니다
  • 변경사항에 대한 테스트를 작성/수정했습니다
  • 모든 테스트가 통과합니다 (pnpm test)
  • 빌드가 성공합니다 (pnpm build)
  • 필요한 경우 문서를 업데이트했습니다
  • 커밋 메시지가 Conventional Commits 규칙을 따릅니다

🔗 관련 이슈

Closes #116

- scheduledTime 필드를 DateTime?에서 DateTime? @db.Timestamptz(3)로 변경
- 타임존 인식 시간 저장으로 전환하여 다양한 타임존의 사용자 지원
- Google Calendar 패턴: 시간 이벤트는 TIMESTAMPTZ(UTC)로 저장
- toDateOnly: 날짜 문자열을 UTC 자정 Date로 변환
- getUserToday: 사용자 타임존 기준 오늘 날짜 반환
- toScheduledTime: 사용자 로컬 시간을 UTC Date로 변환
- startOfDayInTimezone: 타임존 기준 하루 시작 시각 반환
- @Timezone() 데코레이터: X-Timezone 헤더에서 타임존 추출
- date.util.spec.ts: 13개 테스트 케이스 추가
- @Timezone() 데코레이터로 사용자 타임존 수신
- toDateOnly()로 DATE 필드 변환 일관성 확보
- buildDateRangeFilter(): 다중일/단일일 투두 날짜 필터링 구현
- 다중일: todo.startDate <= filterEnd AND todo.endDate >= filterStart
- 단일일: filterStart <= todo.startDate <= filterEnd
- validateDateRange(): startDate > endDate 검증 추가
- getTodayTodoStats(): 타임존 기반 오늘 날짜 계산
- 단위 테스트: startDate만/endDate만/잘못된 범위 검증 (3건)
- 단위 테스트: toggleComplete 타임존 연동 검증
- E2E 테스트: 다중일 투두 범위 필터, 특정 하루 조회, 기간 조회 (3건)
- 통합 테스트: Overlapping Intervals 쿼리 조건 검증
- existsNotification: 같은 날 DAILY_COMPLETE 중복 체크
- findAlreadyNotifiedUserIds: Set 기반 배치 조회로 N+1 방지
- handleTodoAllCompleted: 중복 발송 시 스킵
- handleFriendCompleted: 이미 알림 받은 유저 필터링
- 리스너 테스트 6건 (GWT 패턴)
- 레포지토리 테스트 4건
- @Timezone() 데코레이터로 사용자 타임존 수신
- startOfDayInTimezone()으로 일일 제한 카운트의 날짜 경계 계산
- 일일 제한 및 쿨다운 계산이 사용자 타임존 기준으로 동작
Result 타입 시스템:
- Result<T, E> = { ok: true; value: T } | { ok: false; error: E }
- ok(), err(), unwrap(), isOk(), isErr() 헬퍼 함수
- 예상된 에러(4xx)는 Result.err()로 전달
- 예상치 못한 에러(5xx, 네트워크)는 throw InfraError

에러 체계 정비:
- InfraError: 네트워크/서버 에러 (ErrorBoundary 처리)
- ApiError: 4xx 비즈니스 에러 (Mutation onError 처리)
- ClientError 제거 → 도메인별 FeatureError로 대체

HTTP 클라이언트:
- HttpClient 포트를 Result<T, ApiError> 기반으로 재정의
- KyHttpClient: Result 기반 구현체
- 4xx → Result.err(ApiError), 5xx → throw InfraError
모델 정리:
- auth.model.ts 신규: Zod 스키마 기반 도메인 타입 통합
- auth-tokens.model.ts, user.model.ts, auth.policy.ts 삭제

Mapper 이동:
- services/auth.mapper.ts → repositories/auth.mapper.ts
- DTO→도메인 변환 책임을 Repository 레이어로 이동

Repository:
- Result<T, ApiError> 기반 계약으로 변경
- Zod safeParse 검증 + Mapper 변환 파이프라인 구축

Service:
- Repository 인터페이스만 의존 (DTO 무관)
- Policy 검증 → FeatureError 반환

Presentation:
- unwrap(result)로 Result 해제
- onError에서 에러 타입별 UI 분기
Mapper 이동:
- services/friend.mapper.ts → repositories/friend.mapper.ts
- DTO→도메인 변환을 Repository에서 수행

Repository:
- Result<T, ApiError> 기반 계약으로 변경
- Page<FriendUser>, Page<FriendRequest> 등 통합 페이지네이션 타입 사용

Service:
- FriendPolicy.isValidTag() 사전 검증
- FriendError(EMPTY_TAG, INVALID_TAG) 반환

Presentation:
- unwrap(result) 적용
- FriendList, FriendRequestRow 등 컴포넌트 에러 처리 개선
Mapper 이동:
- services/notification.mapper.ts → repositories/notification.mapper.ts

Repository:
- Result<T, ApiError> 기반 계약으로 변경
- toNotification(), toNotificationListResult() 매퍼 적용

Service:
- Repository 인터페이스만 의존
- push-token.service에서 InfraError 처리 개선

Presentation:
- unwrap(result) 적용
- 불필요한 use-notification-handler 로직 정리
Mapper 이동:
- services/todo.mapper.ts → repositories/todo.mapper.ts
- toTodoItem(), toTodoItems(), toParsedTodoResult() 등 매퍼 함수

Repository:
- Result<T, ApiError> 기반 계약으로 변경
- Zod safeParse 검증 후 매퍼 적용

Service:
- TodoPolicy 기반 사전 검증
- get-todos-by-date → get-todos-by-range로 교체

Presentation:
- unwrap(result) 적용
- Zod 스키마 기반 폼 검증 추가
- AddTodoBottomSheet, UserAvatarList 에러 처리 개선
DI 컨테이너:
- KyHttpClient 기반 의존성 주입 체인 구성
- Repository → Service 주입 패턴 적용

쿼리 설정:
- retry 로직에서 BusinessError/ApiError 구분
- 인증 에러(401, 403)는 재시도하지 않음

공유 유틸리티:
- date.ts: 날짜 포맷팅 및 캘린더 유틸리티 확장
- timezone.ts: 디바이스 타임존 감지 (Intl.DateTimeFormat)
- CLAUDE.md: Result 패턴, 레이어 역할, Mapper 위치 문서화
- testing-guide.md: Result 기반 테스트 패턴 가이드 추가
- error-handling.md: 에러 분류 전략 및 처리 흐름 재작성
@dydals3440 dydals3440 added type: refactor 🧹 코드 개선 - 기능 변경 없이 가독성과 유지보수성을 높여 기술 부채를 해결하는 작업 scope: api 🌐 API - NestJS 서버 로직, DB 스키마 및 백엔드 인프라 관련 scope: mobile 📱 모바일 - Expo 기반 모바일 앱 UI/UX 및 클라이언트 로직 관련 status: needs review 🔍 리뷰 대기 - 작업 완료 후 병목 제거를 위해 팀원의 빠른 피드백이 필요한 상태 labels Feb 6, 2026
@dydals3440 dydals3440 self-assigned this Feb 6, 2026
@gemini-code-assist
Copy link

Summary of Changes

Hello @dydals3440, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 모바일 앱의 에러 처리 및 아키텍처를 현대화하고, API 서버의 핵심 기능을 강화하여 사용자 경험과 시스템 안정성을 향상시키는 데 중점을 둡니다. 특히, 모바일 앱은 Result 패턴과 재구성된 레이어를 통해 예측 가능한 에러 흐름을 구축하며, API는 타임존, 고급 날짜 필터링, 알림 중복 방지 기능을 도입하여 데이터 처리의 정확성과 효율성을 높였습니다.

Highlights

  • Result 패턴 도입 (모바일): 모바일 앱에 Result 패턴을 도입하여 예상된 에러(4xx, ApiError, FeatureError)와 예상치 못한 에러(5xx, 네트워크, InfraError)를 명확히 구분하고 타입 안전하게 처리하도록 개선했습니다. InfraError는 ErrorBoundary에서 자동으로 처리되며, ApiError와 FeatureError는 Mutation의 onError 콜백에서 처리됩니다.
  • 레이어 아키텍처 재구성 (모바일): 각 레이어의 책임을 명확히 분리하고, DTO를 도메인 모델로 변환하는 Mapper를 services/ 디렉토리에서 repositories/ 디렉토리로 이동했습니다. 이를 통해 Service 레이어는 비즈니스 로직과 Policy 검증에 집중하고, Repository는 API 통신 및 데이터 변환을 담당하도록 재구성되었습니다.
  • 타임존 지원 (API): API 서버에 IANA 타임존을 지원하는 로직을 추가했습니다. X-Timezone 헤더에서 타임존을 추출하는 @Timezone() 데코레이터와 함께, scheduledTime 필드를 TIMESTAMPTZ(3) 타입으로 마이그레이션하고, 사용자 타임존 기준으로 날짜를 처리하는 유틸리티 함수들을 도입했습니다. 이는 Todo, Cheer, Nudge 모듈의 일일 제한 및 쿨다운 판정에 적용됩니다.
  • Overlapping Intervals 날짜 필터링 (API): 캘린더에서 다중일 투두를 올바르게 표시하기 위해 Overlapping Intervals 패턴을 적용한 날짜 필터링 로직을 구현했습니다. startDate와 endDate 파라미터를 사용하여 특정 날짜 또는 기간에 걸쳐 있는 투두를 효율적으로 조회할 수 있습니다.
  • 알림 중복 발송 방지 (API): DAILY_COMPLETE 및 FRIEND_COMPLETED 알림의 중복 발송을 방지하는 로직을 추가했습니다. DAILY_COMPLETE는 하루에 한 번만 발송되도록 체크하며, FRIEND_COMPLETED는 Set 기반 배치 조회를 통해 N+1 문제를 방지하고 이미 알림을 받은 친구에게는 다시 발송하지 않도록 처리합니다.
Changelog
  • apps/api/prisma/migrations/20260205233052_add_timestamptz_to_scheduled_time/migration.sql
    • Todo 테이블의 scheduledTime 컬럼 타입을 TIMESTAMPTZ(3)으로 변경하는 마이그레이션 추가
  • apps/api/prisma/schema.prisma
    • Todo 모델의 scheduledTime 필드에 @db.Timestamptz(3) 속성 추가
  • apps/api/src/common/date/utils/date.util.spec.ts
    • 날짜 유틸리티 함수 (getUserToday, startOfDayInTimezone, toScheduledTime)에 대한 단위 테스트 추가
  • apps/api/src/common/date/utils/date.util.ts
    • dayjs에 timezone 플러그인 확장 및 DEFAULT_TIMEZONE 상수 추가
    • PostgreSQL @db.Date 호환을 위한 toDateOnly() 함수 추가
    • 타임존을 고려한 getUserToday(), toScheduledTime(), startOfDayInTimezone() 함수 추가
  • apps/api/src/common/decorators/index.ts
    • Timezone 데코레이터 export 추가
  • apps/api/src/common/decorators/timezone.decorator.ts
    • X-Timezone 헤더에서 IANA 타임존을 추출하는 @Timezone() 커스텀 데코레이터 구현
  • apps/api/src/modules/cheer/cheer.controller.ts
    • sendCheer 및 getLimitInfo 메서드에 @Timezone() 데코레이터를 사용하여 타임존 파라미터 추가
  • apps/api/src/modules/cheer/cheer.service.ts
    • addMilliseconds, now, startOfDayInTimezone 유틸리티 함수 임포트
    • sendCheer 및 getLimitInfo 메서드에 타임존(tz) 파라미터 추가 및 로직에 반영
  • apps/api/src/modules/notification/listeners/todo.listener.spec.ts
    • TodoListener의 알림 중복 방지 로직 (DAILY_COMPLETE, FRIEND_COMPLETED)에 대한 단위 테스트 추가
  • apps/api/src/modules/notification/listeners/todo.listener.ts
    • NotificationRepository 의존성 주입 및 알림 중복 방지 로직 (existsNotification, findAlreadyNotifiedUserIds) 적용
  • apps/api/src/modules/notification/notification.repository.spec.ts
    • existsNotification 및 findAlreadyNotifiedUserIds 메서드에 대한 단위 테스트 추가
  • apps/api/src/modules/notification/notification.repository.ts
    • 특정 타입의 알림 존재 여부를 확인하는 existsNotification 메서드 추가
    • 이미 알림을 받은 사용자 ID 목록을 조회하는 findAlreadyNotifiedUserIds 메서드 추가
  • apps/api/src/modules/nudge/nudge.controller.ts
    • sendNudge 및 getLimitInfo 메서드에 @Timezone() 데코레이터를 사용하여 타임존 파라미터 추가
  • apps/api/src/modules/nudge/nudge.service.ts
    • addMilliseconds, now, startOfDayInTimezone 유틸리티 함수 임포트
    • sendNudge 및 getLimitInfo 메서드에 타임존(tz) 파라미터 추가 및 로직에 반영
  • apps/api/src/modules/todo/todo.controller.ts
    • toDateOnly, toScheduledTime, Timezone 유틸리티 및 데코레이터 임포트
    • Todo 생성, 수정, 완료 상태 변경, 일정 변경 메서드에 @Timezone() 데코레이터를 사용하여 타임존 파라미터 추가
    • 날짜 및 시간 관련 필드에 toDateOnly 및 toScheduledTime 함수 적용
    • Swagger 문서에 scheduledTime 및 날짜 필터링(Overlapping Intervals)에 대한 상세 설명 추가
  • apps/api/src/modules/todo/todo.repository.ts
    • 날짜 범위 필터링(Overlapping Intervals) 로직을 위한 buildDateRangeFilter 함수 추가
    • findManyByUserId, findPublicTodosByUserId, getTodayTodoStats 메서드에 buildDateRangeFilter 적용
  • apps/api/src/modules/todo/todo.service.spec.ts
    • 날짜 필터링 및 타임존 연동 관련 단위 테스트 추가
  • apps/api/src/modules/todo/todo.service.ts
    • getUserToday, now, toDateOnly, toScheduledTime 유틸리티 함수 임포트
    • 날짜 범위 유효성 검증을 위한 validateDateRange 메서드 추가 및 findMany, findFriendTodos에 적용
    • toggleComplete 및 checkAndEmitAllCompletedEvent 메서드에 타임존(tz) 파라미터 추가 및 getUserToday 적용
    • updateSchedule 메서드에 타임존(tz) 파라미터 추가 및 toDateOnly, toScheduledTime 적용
  • apps/api/test/e2e/todo.e2e-spec.ts
    • 다중일 투두 범위 필터링 및 특정 하루/기간 조회에 대한 E2E 테스트 추가
  • apps/api/test/integration/todo.integration-spec.ts
    • 날짜 범위 필터링 로직 변경에 따른 통합 테스트 업데이트
  • apps/mobile/.claude/testing-guide.md
    • 테스팅 가이드를 Stub 기반에서 Mock 기반으로 전면 개편
    • Mapper, Policy, Repository, Service, ErrorBoundary 각 계층별 테스트 패턴 및 Mock 객체 생성 방법 상세화
    • Result 타입 검증 패턴 및 Mock 설정 패턴 추가
  • apps/mobile/CLAUDE.md
    • 모바일 앱 아키텍처 문서 업데이트: 레이어별 책임 요약, Mapper 위치 변경, Result 시스템 및 에러 처리 흐름 상세화
    • 파일 네이밍 규칙 및 의존성 방향 다이어그램 업데이트
  • apps/mobile/app/(app)/(tabs)/feed/index.tsx
    • QueryErrorBoundary 컴포넌트에 key prop 추가하여 selectedDate 변경 시 에러 상태 초기화
  • apps/mobile/app/(app)/settings/terms.tsx
    • formatDate 함수가 Date 객체를 직접 받도록 변경
  • apps/mobile/docs/error-handling.md
    • 에러 처리 가이드를 Result 패턴 및 InfraError/ApiError/BusinessError 분류에 맞춰 전면 개편
    • 레이어별 데이터 흐름, 에러 코드 매핑, Policy 에러, SDK 에러 변환, QueryErrorBoundary 배치 전략 등 상세 설명 추가
  • apps/mobile/src/bootstrap/providers/di-provider.tsx
    • KyHttpClient 임포트 경로 변경
    • AuthRepositoryImpl 생성자에서 Storage 의존성 제거
    • AuthService 생성자에 Storage 의존성 추가
  • apps/mobile/src/bootstrap/providers/query-provider.tsx
    • QueryClient의 retry 로직에서 isClientError 대신 isBusinessError 사용
  • apps/mobile/src/core/ports/http.ts
    • HttpClient 인터페이스의 모든 메서드 반환 타입을 Promise<Result<T, ApiError>>로 변경
    • HttpClientConfig 및 HttpClientResponse 인터페이스 제거
  • apps/mobile/src/features/auth/models/auth-tokens.model.ts
    • AuthTokens 인터페이스를 auth.model.ts로 이동하여 제거
  • apps/mobile/src/features/auth/models/auth.error.ts
    • AuthError 클래스를 BusinessError 인터페이스 구현 및 AuthErrorCode enum 사용으로 리팩토링
    • AuthErrors 팩토리 객체 도입 및 isCancelledError, isValidationError 타입 가드 로직 변경
  • apps/mobile/src/features/auth/models/auth.model.ts
    • AuthTokens, User, Preference, Consent 등 인증 관련 도메인 모델 및 Zod 스키마 정의
    • AuthPolicy를 이 파일로 이동
  • apps/mobile/src/features/auth/models/auth.policy.ts
    • AuthPolicy를 auth.model.ts로 이동하여 제거
  • apps/mobile/src/features/auth/models/user.model.ts
    • User 및 SubscriptionStatus 모델을 auth.model.ts로 이동하여 제거
  • apps/mobile/src/features/auth/presentations/components/SignUpVerificationForm.tsx
    • VERIFY_0753 에러 발생 시 ApiError.details에서 remainingSeconds를 추출하여 쿨다운 처리
  • apps/mobile/src/features/auth/presentations/queries/email-login-mutation-options.ts
    • mutationFn에서 authService.emailLogin 결과에 unwrap() 적용
  • apps/mobile/src/features/auth/presentations/queries/exchange-code-mutation-options.ts
    • mutationFn에서 authService.exchangeCode 결과에 unwrap() 적용
    • notificationService.setupPushNotifications 결과에 대한 Result 처리 추가
  • apps/mobile/src/features/auth/presentations/queries/get-consent-query-options.ts
    • queryFn에서 authService.getConsent 결과에 unwrap() 적용
  • apps/mobile/src/features/auth/presentations/queries/get-me-query-options.ts
    • queryFn에서 authService.getCurrentUser 결과에 unwrap() 적용
  • apps/mobile/src/features/auth/presentations/queries/get-preference-query-options.ts
    • queryFn에서 authService.getPreference 결과에 unwrap() 적용
  • apps/mobile/src/features/auth/presentations/queries/logout-mutation-options.ts
    • mutationFn에서 notificationService.unregisterPushToken 결과에 대한 Result 처리 추가
    • mutationFn에서 authService.logout 결과에 unwrap() 적용
  • apps/mobile/src/features/auth/presentations/queries/open-apple-login-mutation-options.ts
    • mutationFn에서 authService.openAppleLogin 결과에 unwrap() 적용
  • apps/mobile/src/features/auth/presentations/queries/open-google-login-mutation-options.ts
    • mutationFn에서 authService.openGoogleLogin 결과에 unwrap() 적용
  • apps/mobile/src/features/auth/presentations/queries/open-kakao-login-mutation-options.ts
    • mutationFn에서 authService.openKakaoLogin 결과에 unwrap() 적용
  • apps/mobile/src/features/auth/presentations/queries/open-naver-login-mutation-options.ts
    • mutationFn에서 authService.openNaverLogin 결과에 unwrap() 적용
  • apps/mobile/src/features/auth/presentations/queries/register-mutation-options.ts
    • mutationFn에서 authService.register 결과에 unwrap() 적용
  • apps/mobile/src/features/auth/presentations/queries/resend-verification-mutation-options.ts
    • mutationFn에서 authService.resendVerification 결과에 unwrap() 적용
  • apps/mobile/src/features/auth/presentations/queries/update-marketing-consent-mutation-options.ts
    • mutationFn에서 authService.updateMarketingConsent 결과에 unwrap() 적용
    • onMutate 및 onSuccess 콜백에서 ConsentResponse 대신 Consent 타입 사용 및 marketingAgreedAt 필드 타입 변경 (string -> Date)
  • apps/mobile/src/features/auth/presentations/queries/update-preference-mutation-options.ts
    • mutationFn에서 authService.updatePreference 결과에 unwrap() 적용
    • onMutate 및 onSuccess 콜백에서 PreferenceResponse 대신 Preference 타입 사용
  • apps/mobile/src/features/auth/presentations/queries/verify-email-mutation-options.ts
    • mutationFn에서 authService.verifyEmail 결과에 unwrap() 적용
  • apps/mobile/src/features/auth/repositories/auth.mapper.ts
    • AuthTokens, User, Preference, Consent, RegisterResult, ResendVerificationResult, UpdateMarketingConsentResult DTO를 도메인 모델로 변환하는 매퍼 함수들 추가
  • apps/mobile/src/features/auth/repositories/auth.repository.impl.ts
    • Storage 의존성 제거
    • 모든 메서드 반환 타입을 Result<T, ApiError>로 변경
    • Zod 검증 실패 시 ParseError throw 로직 추가
    • 새로운 매퍼 함수들을 사용하여 DTO를 도메인 모델로 변환
  • apps/mobile/src/features/auth/repositories/auth.repository.ts
    • 모든 메서드 시그니처 반환 타입을 Promise<Result<T, ApiError>>로 변경
  • apps/mobile/src/features/auth/services/auth.mapper.ts
    • 매퍼 함수들을 auth.mapper.ts로 이동하여 제거
  • apps/mobile/src/features/auth/services/auth.service.ts
    • Storage 의존성 추가 및 saveTokens, clearTokens private 메서드 구현
    • 모든 메서드 반환 타입을 Result<T, AuthServiceError> 또는 Result<T, ApiError>로 변경
    • openOAuthLogin 및 openAppleLogin의 에러 처리 로직을 AuthErrors 팩토리 사용 및 err(AuthError) 반환으로 변경
  • apps/mobile/src/features/friend/models/friend.error.ts
    • FriendError 클래스를 BusinessError 인터페이스 구현 및 FriendErrorCode enum 사용으로 리팩토링
    • FriendErrors 팩토리 객체 도입 및 InvalidTagError, FriendValidationError 클래스 제거
  • apps/mobile/src/features/friend/models/friend.model.ts
    • FriendUser, FriendRequest 스키마 간소화 및 SendRequestResult 인터페이스로 변경
    • 기존의 여러 Result 스키마 제거
  • apps/mobile/src/features/friend/presentations/components/FriendList.tsx
    • 친구 목록 데이터 접근 시 page.friends 대신 page.items 사용
  • apps/mobile/src/features/friend/presentations/components/FriendRequestRow.tsx
    • user prop 타입 FriendRequestUser에서 FriendRequest로 변경
  • apps/mobile/src/features/friend/presentations/components/ReceivedRequestList.tsx
    • 받은 친구 요청 목록 데이터 접근 시 page.requests 대신 page.items 사용 및 renderItem prop 타입 변경
  • apps/mobile/src/features/friend/presentations/components/SentRequestList.tsx
    • 보낸 친구 요청 목록 데이터 접근 시 page.requests 대신 page.items 사용 및 renderItem prop 타입 변경
  • apps/mobile/src/features/friend/presentations/queries/accept-request-mutation-options.ts
    • mutationFn에서 friendService.acceptRequest 결과에 unwrap() 적용
    • onSuccess에서 received 및 sent 쿼리 무효화
  • apps/mobile/src/features/friend/presentations/queries/cancel-request-mutation-options.ts
    • mutationFn에서 friendService.cancelRequest 결과에 unwrap() 적용
  • apps/mobile/src/features/friend/presentations/queries/get-friends-query-options.ts
    • queryFn에서 friendService.getFriends 결과에 unwrap() 적용
    • getNextPageParam에서 lastPage.friends 대신 lastPage.items 사용
  • apps/mobile/src/features/friend/presentations/queries/get-received-requests-query-options.ts
    • queryFn에서 friendService.getReceivedRequests 결과에 unwrap() 적용
    • getNextPageParam에서 lastPage.requests 대신 lastPage.items 사용
  • apps/mobile/src/features/friend/presentations/queries/get-sent-requests-query-options.ts
    • queryFn에서 friendService.getSentRequests 결과에 unwrap() 적용
    • getNextPageParam에서 lastPage.requests 대신 lastPage.items 사용
  • apps/mobile/src/features/friend/presentations/queries/reject-request-mutation-options.ts
    • mutationFn에서 friendService.rejectRequest 결과에 unwrap() 적용
  • apps/mobile/src/features/friend/presentations/queries/remove-friend-mutation-options.ts
    • mutationFn에서 friendService.removeFriend 결과에 unwrap() 적용
  • apps/mobile/src/features/friend/presentations/queries/send-request-by-tag-mutation-options.ts
    • mutationFn에서 friendService.sendRequestByTag 결과에 unwrap() 적용
  • apps/mobile/src/features/friend/repositories/friend.mapper.ts
    • FriendUser, FriendRequest, SendRequestResult DTO를 도메인 모델로 변환하는 매퍼 함수들 추가
  • apps/mobile/src/features/friend/repositories/friend.repository.impl.ts
    • FriendValidationError 임포트 제거
    • 모든 메서드 반환 타입을 Result<T, ApiError>로 변경
    • Zod 검증 실패 시 ParseError throw 로직 추가
    • 새로운 매퍼 함수들을 사용하여 DTO를 도메인 모델로 변환
    • URL 인코딩 적용 (userTag, userId)
  • apps/mobile/src/features/friend/repositories/friend.repository.ts
    • 모든 메서드 시그니처 반환 타입을 Promise<Result<T, ApiError>>로 변경
  • apps/mobile/src/features/friend/services/friend.mapper.ts
    • 매퍼 함수들을 friend.mapper.ts로 이동하여 제거
  • apps/mobile/src/features/friend/services/friend.service.ts
    • sendRequestByTag 메서드에서 FriendErrors 팩토리 사용 및 err(FriendError) 반환으로 변경
    • 모든 메서드 반환 타입을 Result<T, FriendServiceError> 또는 Result<T, ApiError>로 변경
  • apps/mobile/src/features/notification/models/notification.error.ts
    • NotificationError 클래스를 BusinessError 인터페이스 구현 및 NotificationErrorCode enum 사용으로 리팩토링
    • NotificationErrors 팩토리 객체 도입 및 isPermissionDeniedError, isNotPhysicalDeviceError 타입 가드 로직 변경
  • apps/mobile/src/features/notification/presentations/hooks/use-notification-handler.ts
    • 클라이언트 중심 라우팅 결정 및 알림 처리 관련 주석 제거
  • apps/mobile/src/features/notification/presentations/queries/get-notifications-infinite-query-options.ts
    • queryFn에서 notificationService.getNotifications 결과에 unwrap() 적용
  • apps/mobile/src/features/notification/presentations/queries/get-unread-count-query-options.ts
    • queryFn에서 notificationService.getUnreadCount 결과에 unwrap() 적용
  • apps/mobile/src/features/notification/presentations/queries/mark-all-as-read-mutation-options.ts
    • mutationFn에서 notificationService.markAllAsRead 결과에 unwrap() 적용
  • apps/mobile/src/features/notification/presentations/queries/mark-as-read-mutation-options.ts
    • mutationFn에서 notificationService.markAsRead 결과에 unwrap() 적용
  • apps/mobile/src/features/notification/presentations/queries/register-push-token-mutation-options.ts
    • mutationFn에서 notificationService.setupPushNotifications 결과에 unwrap() 적용
    • retry 로직에서 isNotificationError, isNotPhysicalDeviceError, isPermissionDeniedError 타입 가드 사용
  • apps/mobile/src/features/notification/presentations/queries/unregister-push-token-mutation-options.ts
    • mutationFn에서 notificationService.unregisterPushToken 결과에 unwrap() 적용
  • apps/mobile/src/features/notification/repositories/device-id.repository.ts
    • JSDoc 주석 제거
  • apps/mobile/src/features/notification/repositories/notification.mapper.ts
    • ServerNotification을 Notification으로, NotificationListResponse를 NotificationListResult로 변환하는 매퍼 함수들 추가
  • apps/mobile/src/features/notification/repositories/notification.repository.impl.ts
    • NotificationValidationError 임포트 제거
    • 모든 메서드 반환 타입을 Result<T, ApiError>로 변경
    • Zod 검증 실패 시 ParseError throw 로직 추가
    • 새로운 매퍼 함수들을 사용하여 DTO를 도메인 모델로 변환
  • apps/mobile/src/features/notification/repositories/notification.repository.ts
    • 모든 메서드 시그니처 반환 타입을 Promise<Result<T, ApiError>>로 변경
  • apps/mobile/src/features/notification/services/notification.mapper.ts
    • 매퍼 함수들을 notification.mapper.ts로 이동하여 제거
  • apps/mobile/src/features/notification/services/notification.service.ts
    • setupPushNotifications 메서드에서 getExpoPushToken 결과에 대한 Result 처리 추가
    • 모든 메서드 반환 타입을 Result<T, NotificationServiceError> 또는 Result<T, ApiError>로 변경
    • getUnreadCount 및 syncBadgeCount 메서드에서 Result 처리 로직 반영
  • apps/mobile/src/features/notification/services/push-token.service.ts
    • getExpoPushToken 메서드 반환 타입을 Result<string, NotificationError>로 변경 및 NotificationErrors 팩토리 사용
  • apps/mobile/src/features/todo/models/todo.error.ts
    • TodoError 클래스를 BusinessError 인터페이스 구현 및 TodoErrorCode enum 사용으로 리팩토링
    • TodoErrors 팩토리 객체 도입 및 TodoValidationError 클래스 제거
  • apps/mobile/src/features/todo/models/todo.model.ts
    • Todo 관련 Zod 스키마 및 타입 간소화 (form 스키마 제거)
    • TodoPolicy 정의 유지
  • apps/mobile/src/features/todo/presentations/components/AddTodoBottomSheet.tsx
    • addTodoFormSchema 임포트 경로를 ../schemas/add-todo-form.schema로 변경
  • apps/mobile/src/features/todo/presentations/components/UserAvatarList.tsx
    • 친구 목록 데이터 접근 시 page.friends 대신 page.items 사용
  • apps/mobile/src/features/todo/presentations/constants/todo-query-keys.constant.ts
    • 날짜 범위 및 무한 스크롤 리스트를 위한 새로운 쿼리 키 (ranges, lists) 추가
  • apps/mobile/src/features/todo/presentations/queries/create-todo-mutation-options.ts
    • mutationFn에서 todoService.createTodo 결과에 unwrap() 적용
  • apps/mobile/src/features/todo/presentations/queries/get-todos-by-date-query-options.ts
    • 파일 제거 (get-todos-by-range-query-options.ts 및 get-todos-infinite-query-options.ts로 대체)
  • apps/mobile/src/features/todo/presentations/queries/get-todos-by-range-query-options.ts
    • 날짜 범위 내 투두 조회를 위한 새로운 쿼리 옵션 추가 (unwrap() 및 toViewModel 적용)
  • apps/mobile/src/features/todo/presentations/queries/get-todos-infinite-query-options.ts
    • queryKey를 listByDate로 변경, queryFn에서 todoService.getTodos 결과에 unwrap() 적용, placeholderData 제거
  • apps/mobile/src/features/todo/presentations/queries/parse-todo-mutation-options.ts
    • mutationFn에서 todoService.parseTodo 결과에 unwrap() 적용
  • apps/mobile/src/features/todo/presentations/queries/toggle-todo-mutation-options.ts
    • mutationFn에서 todoService.toggleTodoComplete 결과에 unwrap() 적용
  • apps/mobile/src/features/todo/presentations/schemas/add-todo-form.schema.ts
    • Todo 추가 폼 스키마 및 타입 정의 파일 추가
  • apps/mobile/src/features/todo/repositories/todo.mapper.ts
    • AiUsage, ParsedTodoResult, TodoItem, TodosResult DTO를 도메인 모델로 변환하는 매퍼 함수들 추가
  • apps/mobile/src/features/todo/repositories/todo.repository.impl.ts
    • TodoValidationError 임포트 제거
    • 모든 메서드 반환 타입을 Result<T, ApiError>로 변경
    • Zod 검증 실패 시 ParseError throw 로직 추가
    • 새로운 매퍼 함수들을 사용하여 DTO를 도메인 모델로 변환
  • apps/mobile/src/features/todo/repositories/todo.repository.ts
    • 모든 메서드 시그니처 반환 타입을 Promise<Result<T, ApiError>>로 변경
  • apps/mobile/src/features/todo/services/todo.mapper.ts
    • 매퍼 함수들을 todo.mapper.ts로 이동하여 제거
  • apps/mobile/src/features/todo/services/todo.service.ts
    • 매퍼 함수 임포트 제거
    • 모든 메서드 반환 타입을 Result<T, ApiError>로 변경 및 Repository의 Result를 직접 통과
  • apps/mobile/src/shared/errors/api-error.ts
    • ErrorCodeType 임포트 및 BusinessError 인터페이스 구현
    • ApiError에 details 속성 추가 및 hasCode 메서드를 타입 안전하게 변경
  • apps/mobile/src/shared/errors/client-error.ts
    • 파일 제거 (BusinessError 인터페이스 및 특정 FeatureError 클래스로 대체)
  • apps/mobile/src/shared/errors/index.ts
    • ClientError 제거 및 InfraError, Result 관련 타입 및 함수 export 추가
  • apps/mobile/src/shared/errors/infra-error.ts
    • InfraError 추상 클래스 및 NetworkError, TimeoutError, ServerError, ParseError 서브클래스 정의
  • apps/mobile/src/shared/errors/result.ts
    • Result<T, E> 타입, BusinessError 인터페이스, ok, err, isOk, isErr, unwrap 헬퍼 함수 정의
  • apps/mobile/src/shared/infra/http/auth-client.ts
    • getDeviceTimezone 임포트 및 X-Timezone 헤더 추가
  • apps/mobile/src/shared/infra/http/ky-client.ts
    • Ky 기반 Result HttpClient 구현 (4xx는 Result.err, 5xx/네트워크/타임아웃은 InfraError throw)
  • apps/mobile/src/shared/infra/http/ky-http-client.ts
    • 파일 제거 (ky-client.ts로 대체)
  • apps/mobile/src/shared/infra/http/public-client.ts
    • getDeviceTimezone 임포트 및 X-Timezone 헤더 추가
  • apps/mobile/src/shared/types/page.type.ts
    • 페이지네이션 응답을 위한 Page 인터페이스 정의
  • apps/mobile/src/shared/utils/date.ts
    • todoOverlapsDate, getCalendarRange, getWeekRange 날짜 유틸리티 함수 추가
  • apps/mobile/src/shared/utils/timezone.ts
    • 디바이스의 IANA 타임존을 반환하는 getDeviceTimezone 유틸리티 함수 추가
Activity
  • 모바일 앱의 Result 패턴 도입 및 레이어 아키텍처 재구성에 대한 상세한 문서화가 이루어졌습니다.
  • API 서버에 타임존 지원, Overlapping Intervals 날짜 필터링, 알림 중복 방지 로직이 추가되었습니다.
  • 총 47개의 테스트 스위트와 930개의 단위 테스트가 성공적으로 실행되었습니다.
  • 총 11개의 E2E 테스트 스위트와 265개의 E2E 테스트가 성공적으로 실행되었습니다.
  • date.util.spec.ts, todo.service.spec.ts, todo.e2e-spec.ts, todo.listener.spec.ts, notification.repository.spec.ts 파일에 새로운 테스트 케이스가 추가되었습니다.
  • 모든 빌드 및 린트 검사가 성공적으로 완료되었습니다.
  • PR 설명에 명시된 모든 체크리스트 항목이 완료된 것으로 표시되었습니다.
  • 이 PR은 이슈 #116을 해결합니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

훌륭하고 광범위한 리팩토링입니다. API와 모바일 앱 모두의 안정성과 유지보수성을 크게 향상시켰습니다. 에러 처리를 위한 Result 패턴 도입, 모바일 아키텍처의 명확한 레이어 분리, 그리고 API 측의 타임존 지원 및 날짜 필터링 개선과 같은 핵심 기능 추가 모두 매우 잘 구현되었습니다. 함께 업데이트된 문서도 상세하여 향후 개발에 큰 도움이 될 것입니다. 중복된 주석에 대한 사소한 제안이 하나 있습니다. 전반적으로 훌륭한 작업입니다!

@dydals3440 dydals3440 requested a review from hijjoy February 6, 2026 02:48
- README: 깨진 링크 수정, ORM 표기에 adapter-pg 추가, 구조도에 date/decorators 디렉토리 반영
- architecture.md: AI 일일 제한 FREE 10회→5회 수정, 타임스탬프 예시 현재 연도로 수정
- api-conventions.md: 타임스탬프 예시 현재 연도로 수정, AI 제한 5회 반영, 타임존 처리 규칙 섹션 추가
- testing-guide.md: 문서 버전 1.0.1 및 최종 수정일 갱신
- PUSH_NOTIFICATION_GUIDE: 모바일 구현 상태 3개 항목 "완료"로 수정
- cheer/nudge/todo controller: @Timezone() 사용 메서드에 @ApiHeader X-Timezone 추가
- Feature 구조도: ClientError 클래스 → {Feature}Error 클래스 (BusinessError 구현)
- 새 Feature 체크리스트: 동일 용어 수정
- 아키텍처 레이어 다이어그램: shared/types/ 디렉토리 추가 (Page<T> 등)
@dydals3440 dydals3440 merged commit d8c9da8 into main Feb 6, 2026
3 checks passed
@dydals3440 dydals3440 deleted the refactor/result-pattern-and-layer-restructure branch February 6, 2026 05:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: api 🌐 API - NestJS 서버 로직, DB 스키마 및 백엔드 인프라 관련 scope: mobile 📱 모바일 - Expo 기반 모바일 앱 UI/UX 및 클라이언트 로직 관련 status: needs review 🔍 리뷰 대기 - 작업 완료 후 병목 제거를 위해 팀원의 빠른 피드백이 필요한 상태 type: refactor 🧹 코드 개선 - 기능 변경 없이 가독성과 유지보수성을 높여 기술 부채를 해결하는 작업

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Result 패턴 도입 및 레이어 아키텍처 재구성

2 participants