# 공사현장 사진대지 > **작성일**: 2026-03-06 > **상태**: 운영 중 > **라우트**: `/juil/construction-photos` > **관련**: [README.md](README.md) | [회의록](meeting-minutes.md) | [뷰 화면](planning-views.md) --- ## 1. 개요 건설/시공 현장의 작업 과정을 **작업전/작업중/작업후** 3단계 사진으로 기록하고 관리하는 기능. Google Cloud Storage에 사진을 저장하며, 음성 입력(Web Speech API)으로 현장명과 설명을 입력할 수 있다. --- ## 2. 라우트 ``` /juil/construction-photos ├── GET / → index (목록 페이지) ├── GET /list → list (JSON 목록) ├── POST / → store (새 사진대지 등록) ├── POST /log-stt-usage → logSttUsage (STT 시간 기록) ├── GET /{id} → show (상세 조회) ├── PUT /{id} → update (메타데이터 수정) ├── DELETE /{id} → destroy (삭제) ├── POST /{id}/rows → addRow (행 추가) ├── DELETE /{id}/rows/{rowId} → deleteRow (행 삭제) ├── POST /{id}/rows/{rowId}/upload → uploadPhoto (사진 업로드) ├── DELETE /{id}/rows/{rowId}/photo/{type} → deletePhoto (사진 삭제) └── GET /{id}/rows/{rowId}/download/{type} → downloadPhoto (다운로드) ``` --- ## 3. 데이터베이스 ### 3.1 construction_site_photos (사진대지) | 컬럼 | 타입 | 설명 | |------|------|------| | `id` | BIGINT PK | | | `tenant_id` | BIGINT FK | 테넌트 격리 | | `user_id` | BIGINT FK | 등록자 | | `site_name` | VARCHAR(200) | 현장명 (필수) | | `work_date` | DATE | 작업일자 (필수) | | `description` | TEXT NULL | 설명 | | `deleted_at` | TIMESTAMP NULL | 소프트 삭제 | **인덱스**: `tenant_id`, `user_id`, `(tenant_id, work_date)` ### 3.2 construction_site_photo_rows (사진 행) 각 사진대지는 1개 이상의 행을 가지며, 각 행에 3개 타입(before/during/after) 사진 저장. | 컬럼 | 타입 | 설명 | |------|------|------| | `id` | BIGINT PK | | | `construction_site_photo_id` | BIGINT FK | 부모 (cascade delete) | | `sort_order` | INT | 정렬 순서 (0부터) | | `before_photo_path` | VARCHAR(500) NULL | 작업전 GCS 경로 | | `before_photo_gcs_uri` | VARCHAR(500) NULL | 작업전 GCS URI | | `before_photo_size` | INT UNSIGNED NULL | 작업전 파일크기 (bytes) | | `during_photo_path` | VARCHAR(500) NULL | 작업중 GCS 경로 | | `during_photo_gcs_uri` | VARCHAR(500) NULL | 작업중 GCS URI | | `during_photo_size` | INT UNSIGNED NULL | 작업중 파일크기 (bytes) | | `after_photo_path` | VARCHAR(500) NULL | 작업후 GCS 경로 | | `after_photo_gcs_uri` | VARCHAR(500) NULL | 작업후 GCS URI | | `after_photo_size` | INT UNSIGNED NULL | 작업후 파일크기 (bytes) | ### 3.3 테이블 관계 ``` construction_site_photos │ 1:N ▼ construction_site_photo_rows (sort_order ASC) ├── before_photo_* (작업전) ├── during_photo_* (작업중) └── after_photo_* (작업후) ``` --- ## 4. API 명세 ### 4.1 목록 조회 ``` GET /juil/construction-photos/list ``` | 파라미터 | 타입 | 설명 | |---------|------|------| | `search` | string | 현장명 검색 | | `date_from` | date | 시작일 | | `date_to` | date | 종료일 | | `per_page` | int | 페이지당 건수 | ### 4.2 생성 ``` POST /juil/construction-photos ``` | 필드 | 규칙 | 설명 | |------|------|------| | `site_name` | required, max:200 | 현장명 | | `work_date` | required, date | 작업일자 | | `description` | nullable, max:2000 | 설명 | > 생성 시 빈 행 1개 자동 추가 ### 4.3 사진 업로드 ``` POST /juil/construction-photos/{id}/rows/{rowId}/upload ``` | 필드 | 규칙 | 설명 | |------|------|------| | `type` | required, in:before,during,after | 사진 타입 | | `photo` | required, image, mimes:jpeg,jpg,png,webp, max:10240 | 최대 10MB | ### 4.4 사진 다운로드 ``` GET /juil/construction-photos/{id}/rows/{rowId}/download/{type}?inline=1 ``` | 파라미터 | 설명 | |---------|------| | `inline=1` | 브라우저 표시 (미지정 시 다운로드) | --- ## 5. GCS 저장 구조 ### 5.1 경로 패턴 ``` construction-site-photos/{tenant_id}/{photo_id}/{row_id}_{timestamp}_{type}.{ext} ``` **예시:** ``` construction-site-photos/1/42/15_1709723456_before.jpg construction-site-photos/1/42/15_1709723456_during.jpg construction-site-photos/1/42/15_1709723456_after.png ``` ### 5.2 업로드 흐름 ``` 클라이언트 (Canvas 이미지 압축: 1920px, quality 80%) ↓ FormData (multipart) 전송 ↓ 컨트롤러: uploadPhoto() ↓ 서비스: uploadPhoto() ├── 기존 사진 있으면 GCS에서 삭제 ├── GCS에 업로드 ├── DB에 path + uri + size 저장 └── AiTokenHelper::saveGcsStorageUsage() 호출 ↓ 응답: { success, data: Photo with rows } ``` ### 5.3 삭제 흐름 ``` 사진 삭제: GCS 파일 삭제 → DB 필드 null 행 삭제: 행 내 모든 사진 GCS 삭제 → 행 삭제 → sort_order 재정렬 사진대지 삭제: 모든 행의 모든 사진 GCS 삭제 → soft delete ``` --- ## 6. 음성 입력 (Web Speech API) ### 6.1 VoiceInputButton 컴포넌트 현장명, 설명 필드에 음성으로 텍스트 입력 가능. ```javascript // Web Speech Recognition 설정 recognition.lang = 'ko-KR'; recognition.continuous = true; recognition.interimResults = true; recognition.maxAlternatives = 1; ``` ### 6.2 인식 상태 | 상태 | 표시 | 설명 | |------|------|------| | interim (미확정) | 이탤릭 + 회색 | 인식 중간 결과, 2초 후 소실 | | final (확정) | 일반체 + 진한색 | 확정 텍스트, 영구 저장 | ### 6.3 사용량 추적 ``` STT 사용 종료 시: duration = Math.max(1, (Date.now() - startTime) / 1000) ↓ POST /juil/construction-photos/log-stt-usage body: { duration_seconds } ↓ AiTokenHelper::saveSttUsage('공사현장사진대지-음성입력', seconds) ``` --- ## 7. UI 구성 (React) ### 7.1 사진 타입별 색상 | 타입 | 라벨 | 배경색 | 뱃지색 | |------|------|--------|--------| | `before` | 작업전 | `bg-blue-50` | `bg-blue-100 text-blue-800` | | `during` | 작업중 | `bg-yellow-50` | `bg-yellow-100 text-yellow-800` | | `after` | 작업후 | `bg-green-50` | `bg-green-100 text-green-800` | ### 7.2 행 관리 - **행 추가**: sort_order 자동 계산 (마지막 + 1) - **행 삭제**: 최소 1개 행 유지 필수 - **행별 사진**: 각 행에 3개 타입 사진 독립 업로드/삭제 --- ## 8. 모델 메서드 ### 8.1 ConstructionSitePhoto ```php user() # BelongsTo User (등록자) rows() # HasMany Row (sort_order ASC) getPhotoCount(): int # 전체 사진 개수 (모든 행의 사진 합계) ``` ### 8.2 ConstructionSitePhotoRow ```php constructionSitePhoto() # BelongsTo 부모 hasPhoto(string $type): bool # 특정 타입 사진 존재 여부 getPhotoCount(): int # 이 행의 사진 개수 (0~3) ``` ### 8.3 ConstructionSitePhotoService ```php getList(array $filters) # 검색/필터 목록 (페이지네이션) create(array $data) # 생성 + 빈 행 1개 자동 추가 update(ConstructionSitePhoto, array $data) # 메타데이터만 수정 delete(ConstructionSitePhoto) # GCS 전체 삭제 → soft delete uploadPhoto(Row, UploadedFile, string $type) # GCS 업로드 + DB 기록 deletePhotoByType(Row, string $type) # 특정 타입 GCS 삭제 addRow(ConstructionSitePhoto) # 행 추가 (sort_order 자동) deleteRow(Row) # 행 내 GCS 삭제 → 행 삭제 → 재정렬 ``` --- ## 관련 문서 - [README.md](README.md) — 기획 메뉴 전체 개요 - [회의록 작성](meeting-minutes.md) — STT/AI 통합 회의 기록 - [견적/프로젝트/워크플로우](planning-views.md) — 화면 명세 --- **최종 업데이트**: 2026-03-06