# 공사현장 사진대지

> **작성일**: 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
