{['before', 'during', 'after'].map(type => (
- {item[`${type}_photo_path`] ? (
+ {firstRow && firstRow[`${type}_photo_path`] ? (
![{TYPE_LABELS[type]}]({API.photoUrl(item.id,)
{item.site_name}
{item.work_date}
- {photoCount}/3
+
+ {totalPhotos}/{totalSlots}
+ {rows.length > 1 && ` (${rows.length}행)`}
+
{item.user && (
{item.user.name}
@@ -498,7 +510,7 @@ className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus
}
// --- DetailModal ---
-function DetailModal({ item, onClose, onUpload, onDeletePhoto, onUpdate, onDelete, onRefresh }) {
+function DetailModal({ item, onClose, onUpload, onDeletePhoto, onUpdate, onDelete, onRefresh, onAddRow, onDeleteRow }) {
const [editing, setEditing] = useState(false);
const [siteName, setSiteName] = useState('');
const [workDate, setWorkDate] = useState('');
@@ -517,6 +529,8 @@ function DetailModal({ item, onClose, onUpload, onDeletePhoto, onUpdate, onDelet
if (!item) return null;
+ const rows = item.rows || [];
+
const handleSave = async () => {
if (!siteName.trim()) return alert('현장명을 입력해주세요.');
setSaving(true);
@@ -541,6 +555,23 @@ function DetailModal({ item, onClose, onUpload, onDeletePhoto, onUpdate, onDelet
}
};
+ const handleAddRow = async () => {
+ try {
+ await onAddRow(item.id);
+ } catch (err) {
+ alert(err.message);
+ }
+ };
+
+ const handleDeleteRow = async (rowId) => {
+ if (!confirm('이 사진 행을 삭제하시겠습니까? 해당 행의 모든 사진이 삭제됩니다.')) return;
+ try {
+ await onDeleteRow(item.id, rowId);
+ } catch (err) {
+ alert(err.message);
+ }
+ };
+
return (
e.stopPropagation()}>
@@ -604,19 +635,49 @@ className="flex-1 px-3 py-1.5 border border-gray-300 rounded-lg text-sm" rows={2
)}
- {/* Photos */}
-
- {['before', 'during', 'after'].map(type => (
-
+ {/* Photo Rows */}
+
+ {rows.map((row, idx) => (
+
+
+ 행 {idx + 1}
+ {rows.length > 1 && (
+
+ )}
+
+
+ {['before', 'during', 'after'].map(type => (
+
+ ))}
+
+
))}
+
+ {/* 행 추가 버튼 */}
+
{/* Actions */}
@@ -712,26 +773,25 @@ function App() {
setSelectedItem(res.data);
};
- const handleUpload = async (id, type, file) => {
+ const handleUpload = async (id, rowId, type, file) => {
const formData = new FormData();
formData.append('type', type);
formData.append('photo', file);
- const res = await apiFetch(API.upload(id), {
+ const res = await apiFetch(API.upload(id, rowId), {
method: 'POST',
body: formData,
});
showToast('사진이 업로드되었습니다.');
modalDirtyRef.current = true;
- // 모달 데이터만 갱신 (배경 리스트는 모달 닫힐 때)
if (selectedItem?.id === id) {
setSelectedItem(res.data);
}
};
- const handleDeletePhoto = async (id, type) => {
+ const handleDeletePhoto = async (id, rowId, type) => {
if (!confirm(`${TYPE_LABELS[type]} 사진을 삭제하시겠습니까?`)) return;
- const res = await apiFetch(API.deletePhoto(id, type), { method: 'DELETE' });
+ const res = await apiFetch(API.deletePhoto(id, rowId, type), { method: 'DELETE' });
showToast('사진이 삭제되었습니다.');
modalDirtyRef.current = true;
if (selectedItem?.id === id) {
@@ -755,6 +815,24 @@ function App() {
modalDirtyRef.current = true;
};
+ const handleAddRow = async (id) => {
+ const res = await apiFetch(API.addRow(id), { method: 'POST' });
+ showToast('사진 행이 추가되었습니다.');
+ modalDirtyRef.current = true;
+ if (selectedItem?.id === id) {
+ setSelectedItem(res.data);
+ }
+ };
+
+ const handleDeleteRow = async (id, rowId) => {
+ const res = await apiFetch(API.deleteRow(id, rowId), { method: 'DELETE' });
+ showToast('사진 행이 삭제되었습니다.');
+ modalDirtyRef.current = true;
+ if (selectedItem?.id === id) {
+ setSelectedItem(res.data);
+ }
+ };
+
const handleSelectItem = async (item) => {
modalDirtyRef.current = false;
try {
@@ -861,8 +939,6 @@ className="px-3 py-2 text-sm text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-
key={item.id}
item={item}
onSelect={handleSelectItem}
- onUpload={handleUpload}
- onDeletePhoto={handleDeletePhoto}
/>
))}
@@ -904,6 +980,8 @@ className="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50
onUpdate={handleUpdate}
onDelete={handleDelete}
onRefresh={refreshSelected}
+ onAddRow={handleAddRow}
+ onDeleteRow={handleDeleteRow}
/>
);
diff --git a/routes/web.php b/routes/web.php
index 93e69079..359f039a 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -1357,14 +1357,18 @@
Route::prefix('construction-photos')->name('construction-photos.')->group(function () {
Route::get('/', [ConstructionSitePhotoController::class, 'index'])->name('index');
Route::get('/list', [ConstructionSitePhotoController::class, 'list'])->name('list');
- Route::get('/{id}', [ConstructionSitePhotoController::class, 'show'])->name('show');
Route::post('/', [ConstructionSitePhotoController::class, 'store'])->name('store');
- Route::post('/{id}/upload', [ConstructionSitePhotoController::class, 'uploadPhoto'])->name('upload');
+ Route::post('/log-stt-usage', [ConstructionSitePhotoController::class, 'logSttUsage'])->name('log-stt-usage');
+ Route::get('/{id}', [ConstructionSitePhotoController::class, 'show'])->name('show');
Route::put('/{id}', [ConstructionSitePhotoController::class, 'update'])->name('update');
Route::delete('/{id}', [ConstructionSitePhotoController::class, 'destroy'])->name('destroy');
- Route::delete('/{id}/photo/{type}', [ConstructionSitePhotoController::class, 'deletePhoto'])->name('delete-photo');
- Route::get('/{id}/download/{type}', [ConstructionSitePhotoController::class, 'downloadPhoto'])->name('download');
- Route::post('/log-stt-usage', [ConstructionSitePhotoController::class, 'logSttUsage'])->name('log-stt-usage');
+ // 행 관리
+ Route::post('/{id}/rows', [ConstructionSitePhotoController::class, 'addRow'])->name('add-row');
+ Route::delete('/{id}/rows/{rowId}', [ConstructionSitePhotoController::class, 'deleteRow'])->name('delete-row');
+ // 행별 사진 관리
+ Route::post('/{id}/rows/{rowId}/upload', [ConstructionSitePhotoController::class, 'uploadPhoto'])->name('upload');
+ Route::delete('/{id}/rows/{rowId}/photo/{type}', [ConstructionSitePhotoController::class, 'deletePhoto'])->name('delete-photo');
+ Route::get('/{id}/rows/{rowId}/download/{type}', [ConstructionSitePhotoController::class, 'downloadPhoto'])->name('download');
});
// 회의록 작성