OpenTripPlanner에서 사용자가 업로드한 사진의 EXIF 메타데이터(GPS 위도/경도, 촬영시간)를 서버에서 추출해 DB에 저장하고, 클라이언트에 자동 인식 결과를 보여주도록 연동했다. 핵심 흐름은 "파일 저장 → EXIF 파싱 → DB 저장 → 클라이언트에 결과 반환" !
1. 백엔드: EXIF 추출 구현 핵심
- exifread로 태그를 파싱하고, GPS DMS(도·분·초)를 십진법으로 변환.
- 촬영시간은 DateTimeOriginal(우선) 또는 Image DateTime에서 가져온다.
- 예외가 발생하면 안전하게 None을 반환.
좌표 변환 함수(convert_to_degrees)
- GPS DMS → 십진변환 함수
def _convert_to_degrees(value) -> Optional[float]:
# EXIF GPS 좌표를 도(degree) 단위로 변환
if not value:
return None
try:
# exifread는 GPS 좌표를 \[degrees, minutes, seconds\] 형태의 리스트로 반환
d = float(value.values\[0\].num) / float(value.values\[0\].den)
m = float(value.values\[1\].num) / float(value.values\[1\].den)
s = float(value.values\[2\].num) / float(value.values\[2\].den)
return d + (m / 60.0) + (s / 3600.0)
except (AttributeError, IndexError, ZeroDivisionError, ValueError):
return None
메인 추출 함수 (파일 열고 태그 파싱 → lat/lng/taken_at 반환)
- 실제 EXIF 추출(파일 열고 exifread로 처리 후 lat/lng/taken_at 반환)
def extract_exif_lat_lng_taken_at(file_path: str) -> tuple[Optional[float], Optional[float], Optional[datetime]]:
...
# 이미지 파일에서 EXIF 위치 정보(GPS 위도/경도)와 촬영 시간을 추출
if not os.path.exists(file\_path):
return None, None, None
try:
with open(file\_path, 'rb') as f:
tags = exifread.process\_file(f, details=False)
# GPS 위도 추출
lat = None
lat\_ref = tags.get('GPS GPSLatitudeRef')
lat\_val = tags.get('GPS GPSLatitude')
if lat\_val and lat\_ref:
lat = \_convert\_to\_degrees(lat\_val)
if lat is not None and str(lat\_ref) == 'S':
lat = -lat
...
2. 백엔드: 업로드 저장·응답 흐름
- 엔드포인트
/uploads/photos에 multipart/form-data(files)로 업로드. - 서버는 LocalStorageService로 파일을 저장한 뒤, 저장 경로로 EXIF 추출 함수를 호출하고 결과를 Photo 모델에 저장.
- 응답은 각 photo에
exif(있으면{lat,lng,taken\_at})와thumbnail\_url을 포함.
업로드 엔드포인트 호출부
@router.post("/uploads/photos")
async def upload_photos(
files: list\[UploadFile\] = File(...),
exif\_required: bool = Form(False),
db: AsyncSession = Depends(get\_db),
user=Depends(get\_current\_user),
)
svc = UploadService(db)
storage = LocalStorageService()
try:
upload, photos = await svc.create\_upload\_with\_photos(user.user\_id, files, exif\_required=exif\_required)
3. 프론트엔드: 이미지 선택 및 업로드
expo-image-picker로 갤러리 다중 선택(권한 요청 포함).- 선택한 이미지를
FormData에files로 append(각 파일에uri,name,type)하여api.postFormData('/uploads/photos', formData, { requiresAuth: true })호출. - 서버 응답의 각 photo에서 exif와 place를 받아 화면에 표시.
FormData 생성 및 API 호출 부분
const formData = new FormData();
// FormData에 이미지 추가
assets.forEach((asset) => {
const uri = asset.uri;
const filename = uri.split('/').pop() || 'image.jpg';
const match = /\\.(\\w+)$/.exec(filename);
const type = match ? \`image/${match\[1\]}\` : 'image/jpeg';
formData.append('files', {
uri: Platform.OS === 'ios' ? uri.replace('file://', '') : uri,
name: filename,
type: type,
} as any);
});
// 백엔드 API 호출
const response = await api.postFormData<{
upload\_id: string;
limits: { max\_photos: number };
photos: Array<{
photo\_id: string;
file\_name: string;
status: 'recognized' | 'needs\_manual';
exif: ExifData | null;
place: PlaceData | null;
thumbnail\_url: string | null;
}>;
}>('/uploads/photos', formData, {
requiresAuth: true,
});
프론트엔드 UX 요약
- EXIF가 있으면 "위치 자동 인식" 배지 표시, 위도/경도와 촬영시간 노출
- EXIF가 없으면 "위치 정보 없음" 처리 후 사용자에게 수동 입력 모달 제공
- 업로드 중 전역 로더(FullScreenLoader)로 상태 표시
4. 테스트 방법
- 백엔드 실행: storage 디렉터리 준비 및 FastAPI 서버 실행(예:
uvicorn app.main:app --reload) - 클라이언트 실행: Expo에서 앱 실행
- 사진 업로드:
- GPS EXIF가 포함된 실제 사진(휴대폰 촬영) 선택 → 업로드 → 응답의 exif 확인
- EXIF 없는 사진 업로드 → 수동 입력 흐름 확인
- DB 확인: Photo 레코드의
exif\_lat,exif\_lng,taken\_at값 확인
[ 주의사항 및 향후 개선 ]
- EXIF 시간대: EXIF의 시간은 타임존 정보가 명확하지 않을 수 있으므로 필요 시 보정 필요
- 개인정보·프라이버시: 위치 정보는 민감 정보이므로 사용자 동의/노출 정책 설계 필요
- 대량 업로드: 동시 업로드/대용량 이미지에 대한 큐잉·비동기 처리 고려
- 리버스 지오코딩: 추출한 좌표로 장소 이름을 자동으로 매핑해 UX 개선 가능
- 보안: 파일 형식 검증, 인증(업로드 권한), 악성 파일 검사 필요
더보기
[ 구현 코드·설치·권한 가이드 (실무 가이드) ]
- 백엔드: exifread 설치
- pip install exifread
- 프론트엔드: expo-image-picker 설치
- npm install expo-image-picker
- FormData 업로드(요점)
- formData.append('files', { uri, name, type }) → api.postFormData('/uploads/photos', formData, { requiresAuth: true })
- iOS 권한(Info.plist)
- NSPhotoLibraryUsageDescription / NSCameraUsageDescription / NSPhotoLibraryAddUsageDescription
- 테스트 체크
- 서버 실행(uvicorn) 및 STORAGE_DIR 확인
- 실제 사진(위치 EXIF 포함)으로 업로드 → 응답 exif 확인
- EXIF 없는 사진은 수동 입력 후 PATCH로 저장 확인
'개발 기록실 > 실험 & 구현' 카테고리의 다른 글
| [YOLOv8 + RNN] 편의점/매장 이상행동(전도·파손) 탐지 파이프라인 만들기 (0) | 2026.03.09 |
|---|---|
| [OpenCV + Machine Learning] Kaggle 주조 제품 불량 이미지를 이용한 Random Forest 분류기 만들기 (0) | 2026.03.09 |
| [FastAPI + PyTorch] 컴퓨터비전 모델 기반 이미지 분석 API 서버 구축하기 (0) | 2026.02.09 |
| [ React-Native ] 카카오 로그인 구현하기 (1) | 2026.01.27 |
| A Multi-label Hate Speech Detection Dataset 직접 모델 구현 코드 작성 해보기 (0) | 2026.01.22 |