feat:공사현장 사진대지 멀티행(N행) 사진 지원

- ConstructionSitePhotoRow 모델 추가
- 부모 모델에서 사진 컬럼 제거, rows() 관계 추가
- 서비스/컨트롤러에 행 추가/삭제 기능 추가
- 라우트를 행 기반 URL 구조로 변경
- 프론트엔드 멀티행 UI 전면 개편

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-11 18:04:09 +09:00
parent b0c4f18c2e
commit 9d314a62fa
6 changed files with 340 additions and 83 deletions

View File

@@ -5,6 +5,7 @@
use App\Helpers\AiTokenHelper;
use App\Http\Controllers\Controller;
use App\Models\Juil\ConstructionSitePhoto;
use App\Models\Juil\ConstructionSitePhotoRow;
use App\Services\ConstructionSitePhotoService;
use App\Services\GoogleCloudService;
use Illuminate\Http\JsonResponse;
@@ -40,7 +41,7 @@ public function list(Request $request): JsonResponse
public function show(int $id): JsonResponse
{
$photo = ConstructionSitePhoto::with('user')->find($id);
$photo = ConstructionSitePhoto::with(['user', 'rows'])->find($id);
if (!$photo) {
return response()->json([
@@ -72,7 +73,7 @@ public function store(Request $request): JsonResponse
], 201);
}
public function uploadPhoto(Request $request, int $id): JsonResponse
public function uploadPhoto(Request $request, int $id, int $rowId): JsonResponse
{
$photo = ConstructionSitePhoto::find($id);
@@ -83,12 +84,23 @@ public function uploadPhoto(Request $request, int $id): JsonResponse
], 404);
}
$row = ConstructionSitePhotoRow::where('id', $rowId)
->where('construction_site_photo_id', $id)
->first();
if (!$row) {
return response()->json([
'success' => false,
'message' => '사진 행을 찾을 수 없습니다.',
], 404);
}
$validated = $request->validate([
'type' => 'required|in:before,during,after',
'photo' => 'required|image|mimes:jpeg,jpg,png,webp|max:10240',
]);
$result = $this->service->uploadPhoto($photo, $request->file('photo'), $validated['type']);
$result = $this->service->uploadPhoto($row, $request->file('photo'), $validated['type']);
if (!$result) {
return response()->json([
@@ -100,7 +112,7 @@ public function uploadPhoto(Request $request, int $id): JsonResponse
return response()->json([
'success' => true,
'message' => '사진이 업로드되었습니다.',
'data' => $photo->fresh(),
'data' => $photo->fresh()->load('rows'),
]);
}
@@ -132,7 +144,7 @@ public function update(Request $request, int $id): JsonResponse
public function destroy(int $id): JsonResponse
{
$photo = ConstructionSitePhoto::find($id);
$photo = ConstructionSitePhoto::with('rows')->find($id);
if (!$photo) {
return response()->json([
@@ -149,7 +161,7 @@ public function destroy(int $id): JsonResponse
]);
}
public function deletePhoto(int $id, string $type): JsonResponse
public function deletePhoto(int $id, int $rowId, string $type): JsonResponse
{
$photo = ConstructionSitePhoto::find($id);
@@ -160,6 +172,17 @@ public function deletePhoto(int $id, string $type): JsonResponse
], 404);
}
$row = ConstructionSitePhotoRow::where('id', $rowId)
->where('construction_site_photo_id', $id)
->first();
if (!$row) {
return response()->json([
'success' => false,
'message' => '사진 행을 찾을 수 없습니다.',
], 404);
}
if (!in_array($type, ['before', 'during', 'after'])) {
return response()->json([
'success' => false,
@@ -167,16 +190,16 @@ public function deletePhoto(int $id, string $type): JsonResponse
], 422);
}
$this->service->deletePhotoByType($photo, $type);
$this->service->deletePhotoByType($row, $type);
return response()->json([
'success' => true,
'message' => '사진이 삭제되었습니다.',
'data' => $photo->fresh(),
'data' => $photo->fresh()->load('rows'),
]);
}
public function downloadPhoto(Request $request, int $id, string $type): Response|JsonResponse
public function downloadPhoto(Request $request, int $id, int $rowId, string $type): Response|JsonResponse
{
$photo = ConstructionSitePhoto::find($id);
@@ -187,6 +210,17 @@ public function downloadPhoto(Request $request, int $id, string $type): Response
], 404);
}
$row = ConstructionSitePhotoRow::where('id', $rowId)
->where('construction_site_photo_id', $id)
->first();
if (!$row) {
return response()->json([
'success' => false,
'message' => '사진 행을 찾을 수 없습니다.',
], 404);
}
if (!in_array($type, ['before', 'during', 'after'])) {
return response()->json([
'success' => false,
@@ -194,7 +228,7 @@ public function downloadPhoto(Request $request, int $id, string $type): Response
], 422);
}
$path = $photo->{$type . '_photo_path'};
$path = $row->{$type . '_photo_path'};
if (!$path) {
return response()->json([
@@ -235,6 +269,62 @@ public function downloadPhoto(Request $request, int $id, string $type): Response
->header('Cache-Control', 'private, max-age=3600');
}
public function addRow(int $id): JsonResponse
{
$photo = ConstructionSitePhoto::find($id);
if (!$photo) {
return response()->json([
'success' => false,
'message' => '사진대지를 찾을 수 없습니다.',
], 404);
}
$this->service->addRow($photo);
return response()->json([
'success' => true,
'message' => '사진 행이 추가되었습니다.',
'data' => $photo->fresh()->load('rows'),
]);
}
public function deleteRow(int $id, int $rowId): JsonResponse
{
$photo = ConstructionSitePhoto::with('rows')->find($id);
if (!$photo) {
return response()->json([
'success' => false,
'message' => '사진대지를 찾을 수 없습니다.',
], 404);
}
if ($photo->rows->count() <= 1) {
return response()->json([
'success' => false,
'message' => '최소 1개의 사진 행은 유지해야 합니다.',
], 422);
}
$row = $photo->rows->firstWhere('id', $rowId);
if (!$row) {
return response()->json([
'success' => false,
'message' => '사진 행을 찾을 수 없습니다.',
], 404);
}
$this->service->deleteRow($row);
return response()->json([
'success' => true,
'message' => '사진 행이 삭제되었습니다.',
'data' => $photo->fresh()->load('rows'),
]);
}
public function logSttUsage(Request $request): JsonResponse
{
$validated = $request->validate([