Skip to content

✨ Feat: 사진 업로드 파이프라인 구현 - #58

Merged
kimyounghee425 merged 7 commits into
feat/onboardingfrom
feat/upload-image
Aug 10, 2026
Merged

✨ Feat: 사진 업로드 파이프라인 구현#58
kimyounghee425 merged 7 commits into
feat/onboardingfrom
feat/upload-image

Conversation

@kimyounghee425

@kimyounghee425 kimyounghee425 commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

🔍 PR 요약

사진 선택 화면에서 고른 그룹을 압축해 GCS에 업로드하고, 서버 분석을 시작한 뒤 완료될 때까지 기다리는 모바일 업로드 파이프라인을 연결했습니다.

업로드 도중 앱이 종료돼도 저장된 단계부터 이어갈 수 있도록 작업 스냅샷과 이벤트 로그를 영속화하고, presigned URL 만료·일시적 PUT 실패·분석 시작 경계를 복구하도록 구성했습니다.

apps/mobile/src/features/photo-upload/model 위주로만 보면 됩니다.

  • 내부 제어 방식: reducer 기반 영속 상태머신
  • upload-job.ts: 상태와 전이 규칙
  • upload-runner.ts: 파이프라인 실행기
  • upload-storage.ts: 상태 영속화
  • photo-upload-service.ts: 화면에서 사용하는 진입점
stateDiagram-v2
      [*] --> PREPARING: CTA / 작업 저장

      PREPARING --> PUTTING: 분석 ID와 사진 ID 매핑 기록

      PUTTING --> PUTTING: 사진 업로드 완료 기록
      PUTTING --> STARTING: 모든 사진 업로드 완료
      PUTTING --> CANCELING: 사진 업로드 최종 실패

      STARTING --> [*]: 분석 시작 확인 / 로컬 작업 삭제

      CANCELING --> [*]: 서버 취소 확인 / 로컬 작업 삭제
      CANCELING --> CANCELING: 취소 확인 실패 / 다음 실행에서 재시도

      note right of PREPARING
          분석 생성 응답이 유실되면
          서버 active 상태와 대조
      end note

      note right of PUTTING
          앱 재실행 시 완료된 사진은 건너뛰고
          미완료 사진만 다시 업로드
      end note

      note right of STARTING
          start 응답이 유실되면
          서버 상태 확인 후 재호출 여부 결정
      end note
Loading

🧾 관련 이슈

  • 관련 이슈 없음

🧠 의도 및 배경

한 번에 90~100개 그룹, 실제로는 그룹 내부 사진까지 100장 이상을 처리하기 때문에 CTA를 누른 뒤 압축부터 시작하면 대기 시간이 길어집니다. 사진을 고르는 동안 후보 사진을 미리 압축하고, CTA에서는 선택 상태만 고정한 뒤 BoardScreen으로 바로 이동하도록 했습니다.

또한 모바일 업로드는 네트워크 전환이나 앱 종료로 중단될 수 있으므로 메모리 Promise만으로 관리하지 않고, 로컬 파일과 서버 photoId 매핑 및 업로드 성공 기록을 디스크에 남겨 재진입 시 복구하도록 했습니다.

🛠️ 주요 변경 사항

  • 웹에서 현재 boardId를 브릿지 payload로 전달하고 PhotoSelectScreen에서 업로드 대상 보드를 확정
  • 갤러리 후보 사진을 긴 변 1280px, JPEG quality 0.8로 최대 5장씩 선제 압축
    • 사진별 1회 재시도 후 원본 폴백
    • ImageManipulator 임시 결과를 Documents staging 경로로 즉시 복사
    • 새 갤러리 목록이 로드되면 이전 압축 실행과 staging 파일 정리
  • CTA 클릭 시 현재 선택 그룹과 압축 결과를 스냅샷으로 고정하고 BoardScreen으로 즉시 이동
    • 제외된 사진과 빈 그룹 제거
    • 그룹별 첫 번째 남은 사진을 대표 사진으로 지정
    • 그룹 구조와 촬영 시각, MIME을 POST /analysis 요청 형태로 변환
  • 업로드 작업을 job.json + events.log + 작업 이미지로 영속화
    • PREPARING → PUTTING → STARTING → CANCELING 상태를 순수 reducer로 복구
    • 로컬 사진 ID와 서버 photoId 매핑, 사진별 PUT 완료 여부 기록
    • 동시 PUT에서 이벤트 append를 직렬화하고 마지막 로그가 일부만 기록된 경우 복구
    • 기존 작업 덮어쓰기와 이전 작업 잔재 혼입 방지
  • POST /analysis 응답의 presigned URL로 GCS PUT 수행
    • 최대 5장 병렬 업로드
    • 403은 /reissue 결과로 재시도하며, 동시 403은 재발급 요청 한 번을 공유
    • 네트워크 및 기타 HTTP 오류는 동일 URL로 1회 재시도
    • 사진 한 장이라도 최종 실패하면 /analysis/{id}를 DELETE하고 전체 작업 취소
  • 모든 사진 PUT 성공 후 POST /analysis/{id}/start 호출 및 2초 간격 상태 폴링
    • COMPLETED이면 보드 웹뷰 표시
    • FAILED이면 재선택 또는 보드 이동이 가능한 실패 모달 표시
  • 앱 재진입 시 로컬 작업 또는 /analysis/active를 확인해 진행 중 업로드 복구
    • PUT 완료 사진은 건너뛰고 미완료 사진만 /reissue URL로 재업로드
    • STARTING 상태는 서버 상태를 먼저 조회해 /start 중복 호출 방지
    • 진행 중 작업이 있으면 닫을 수 없는 확인 모달을 띄우고 확인 후 로딩 화면으로 이동
  • 업로드 중 화면 이탈 방지, 정상 보드 진입 시에는 LoadingOverlay 없이 웹뷰 즉시 렌더
  • API 응답 유실 복구
    • POST /analysis 응답이 유실되면 /analysis/active로 서버 상태 확인
    • 서버가 ANALYZING이면 기존 분석을 이어서 대기
    • 서버에 고아 UPLOADING 분석이 있으면 취소한 뒤 분석을 다시 생성
    • /start 응답이 유실되면 분석 상태를 조회해 이미 시작된 요청은 중복 호출하지 않음
  • 분석 취소 복구
    • DELETE 404는 이미 취소된 것으로 처리
    • 409는 서버 상태를 확인하고, 취소가 확정되지 않으면 CANCELING 작업을 보존
  • 분석 상태 조회가 일시적으로 실패하면 한 번 재시도
    • 재차 실패하면 서버 분석은 취소하지 않고 보드로 이동한 뒤 토스트 표시

✅ 검증

  • 모바일 Jest: 16 suites, 88 tests 통과
  • 모바일·웹·브릿지 TypeScript typecheck 통과
  • 모바일·웹 ESLint 통과
  • Prettier 통과
  • iOS 시뮬레이터에서 100그룹/176장 GCS PUT 완료 및 /analysis/{id}/start 202 응답 확인
  • 스티커 생성 테스트 (단순히 기다리기 / 이미지 업로드 중 앱 완전종료 후 재접속 / 분석 생성 중 완전 종료 후 재접속 등)
image

🚨 트러블슈팅 (선택)

기타

  • 디버깅용 보드 스티커 전체 삭제 버튼 추가

@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ppotto-client-web Skipped Skipped Aug 10, 2026 5:22pm

@kimyounghee425
kimyounghee425 marked this pull request as ready for review August 9, 2026 17:48
@kimyounghee425 kimyounghee425 linked an issue Aug 9, 2026 that may be closed by this pull request

@euoonw euoonw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

고생하셨습니다! 실패 시나리오들 꼼꼼하게 잘 커버된것같아요! 복잡한 작업인데 정말 고생하셨습니다...

Comment on lines +23 to +26
const selectedGroups = selection.groups
.map((group) => group.photos.slice(selection.excludedCounts[group.id] ?? 0))
.filter((photos) => photos.length > 0)
.reverse();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

업로드는 오래된 순으로 하는게 더 효율적인가요? 상관은없는데 그냥 단순 궁금증

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

아뇨 이유없습니다
ㅋㅋㅋㅋㅋㅋ

Comment on lines +72 to +77

try {
await dependencies.clearJob();
} catch {
// 실패 화면을 빠져나가는 동작은 남은 임시 파일 정리에 막히지 않는다.
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

canceling은 서버 취소 확인 후에 정리하는건데, 나머지 경우에는 서버에는 따로 취소요청 안하고 로컬만 지우는건가요? 서버에 고아 분석이 남는 경우가 생길 수도 있을 것 같아요.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

로컬 잡 삭제 시 서버 조회 한번 하는 식으로 반영해 놓았습니다

Comment on lines +18 to +31
<View className="items-center justify-center flex-1 px-6 bg-black/70">
<View accessibilityViewIsModal className="w-full gap-6 p-6 bg-gray-900 rounded-3xl">
<Text className="text-center text-white text-body-01">
분석 중인 사진들이 있어요.{`\n`}분석을 계속 진행할까요?
</Text>

<Pressable
accessibilityRole="button"
className="items-center justify-center py-3 bg-white rounded-full"
onPress={onConfirm}
>
<Text className="text-black text-body-03">확인</Text>
</Pressable>
</View>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

취소 옵션은 없어도 괜찮을까요? 지금은 재진입시에 무조건 이어가기 밖에 선택지가 없는건데, 분석을 그만하고 싶어할 경우도 생각해봐도 좋을 것 같아요
무조건 분석 계속하게끔 하는게 의도된 정책이라면 상관없습니다

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

상태마다 동작이 달라서 그냥 한번 끝을 보게 하는 식으로 하는게 단순해서 이렇게 두었습니다

@cchaeyoung cchaeyoung left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

업로드 파이프라인이 생각보다 복잡하네요.. 고생 많으셨어요!

Comment on lines +209 to +214
try {
await dependencies.startAnalysis(analysisId);
} catch (error) {
if (error instanceof NetworkError) return resolveStartOutcome(analysisId, dependencies);
throw error;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

PREPARING과 CANCELING 단계에서는 실패 원인(NetworkError/HttpError)과 관계없이 서버 상태를 확인한 뒤 로컬 정리를 하는데, STARTING은 NetworkError일 때만 서버 상태를 확인하는 것 같아요.
STARTING만 다르게 처리하신 이유가 있을까요?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

HttpError 일 때는 모두 서버를 확인하지 않습니다
예외적으로 CANCELING 일 때 409일때만 한번 확인합니다!

@kimyounghee425
kimyounghee425 merged commit 788dc0f into feat/onboarding Aug 10, 2026
2 checks passed
This was referenced Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ Feat: 이미지 업로드 연동

3 participants