192 lines
11 KiB
PHP
192 lines
11 KiB
PHP
@extends('admin.layouts.app')
|
|
|
|
@section('title', '이미지 라이브러리')
|
|
@section('page_title', '이미지 라이브러리')
|
|
@section('page_desc', '상품 등록 시 사용할 이미지를 폴더별로 관리하고 이름을 지정합니다.')
|
|
|
|
@push('head')
|
|
<style>
|
|
.media-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; margin-top: 20px; }
|
|
.media-item { background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.10); border-radius: 12px; overflow: hidden; position: relative; display: flex; flex-direction: column; transition: transform 0.2s, border-color 0.2s; }
|
|
.media-item:hover { transform: translateY(-2px); border-color: rgba(255,255,255,.3); }
|
|
|
|
.media-thumb { width: 100%; height: 150px; background-color: #000; display: flex; align-items: center; justify-content: center; overflow: hidden; border-bottom: 1px solid rgba(255,255,255,.05); }
|
|
.media-thumb img { width: 100%; height: 100%; object-fit: contain; }
|
|
|
|
.media-info { padding: 12px; font-size: 12px; flex: 1; display: flex; flex-direction: column; justify-content: space-between; }
|
|
.media-name { font-weight: bold; font-size: 13px; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; }
|
|
.media-name:hover { text-decoration: underline; color: #60a5fa; }
|
|
|
|
.media-meta { font-size: 11px; color: #9ca3af; margin-bottom: 10px; display: flex; gap: 6px; align-items: center; }
|
|
|
|
.action-row { display: flex; justify-content: space-between; align-items: center; border-top: 1px solid rgba(255,255,255,.1); padding-top: 8px; margin-top: auto; }
|
|
.action-btn { padding: 4px 8px; font-size: 11px; border-radius: 4px; border: 1px solid rgba(255,255,255,.2); background: transparent; color: #ccc; cursor: pointer; }
|
|
.action-btn:hover { background: rgba(255,255,255,.1); color: #fff; }
|
|
.action-btn.delete:hover { background: rgba(244,63,94,.2); border-color: rgba(244,63,94,.5); color: #f43f5e; }
|
|
</style>
|
|
@endpush
|
|
|
|
@section('content')
|
|
<div class="a-card" style="padding: 20px; border-top: 3px solid rgba(59,130,246,.8);">
|
|
<div style="font-weight:900; font-size:16px; margin-bottom:12px;">📤 새 이미지 업로드</div>
|
|
<form method="POST" action="{{ route('admin.media.store') }}" enctype="multipart/form-data">
|
|
@csrf
|
|
<div style="display:flex; gap:16px; align-items:flex-start; flex-wrap:wrap; margin-bottom: 16px;">
|
|
<div class="a-field" style="flex:1; min-width:150px; margin:0;">
|
|
<label class="a-label">저장 폴더 (영문/숫자)</label>
|
|
<input class="a-input" name="folder_name" placeholder="예: google, culture" value="default" required>
|
|
<div class="a-muted" style="font-size:11px; margin-top:4px;">※ 상품 종류별로 폴더를 나누면 관리가 편합니다.</div>
|
|
</div>
|
|
|
|
<div class="a-field" style="flex:1; min-width:200px; margin:0;">
|
|
<label class="a-label">이미지 출력 이름 (선택)</label>
|
|
<input class="a-input" name="media_name" placeholder="예: 도서문화상품권 5만원권">
|
|
<div class="a-muted" style="font-size:11px; margin-top:4px;">※ 입력 시 다중파일은 뒤에 _1, _2가 붙습니다. (비워두면 원본파일명)</div>
|
|
</div>
|
|
|
|
<div class="a-field" style="flex:2; min-width:300px; margin:0;">
|
|
<label class="a-label">이미지 파일 (다중 선택 가능)</label>
|
|
<input type="file" class="a-input" name="images[]" multiple accept="image/*" required style="padding: 6px;">
|
|
<div class="a-muted" style="font-size:11px; margin-top:4px;">※ 최대 20장 동시 업로드 가능 (장당 2MB 제한)</div>
|
|
</div>
|
|
|
|
<div style="margin-top: 24px;">
|
|
<button type="submit" class="lbtn lbtn--primary" style="height: 40px; padding: 0 24px;">업로드 실행</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="a-card" style="padding: 20px; margin-top: 20px;">
|
|
<div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; border-bottom:1px solid rgba(255,255,255,.1); padding-bottom:12px;">
|
|
<div style="font-weight:900; font-size:16px;">🖼️ 라이브러리 목록</div>
|
|
|
|
<form method="GET" action="{{ route('admin.media.index') }}" style="display:flex; gap:8px;">
|
|
<select class="a-input" name="folder" style="width:150px; height:50px; font-size:13px;" onchange="this.form.submit()">
|
|
<option value="">-- 전체 폴더 --</option>
|
|
@foreach($folders as $f)
|
|
<option value="{{ $f }}" {{ $currentFolder === $f ? 'selected' : '' }}>{{ $f }}</option>
|
|
@endforeach
|
|
</select>
|
|
@if($currentFolder)
|
|
<a href="{{ route('admin.media.index') }}" class="lbtn lbtn--ghost" style="height:50px; font-size:13px; padding:0 12px; line-height:34px;">초기화</a>
|
|
@endif
|
|
</form>
|
|
</div>
|
|
|
|
<div class="media-grid">
|
|
@forelse($page as $media)
|
|
<div class="media-item">
|
|
<div class="media-thumb">
|
|
<img src="{{ $media->file_path }}" alt="{{ $media->name ?? $media->original_name }}" loading="lazy">
|
|
</div>
|
|
<div class="media-info">
|
|
<div>
|
|
<div class="media-name"
|
|
id="media-name-{{ $media->id }}"
|
|
onclick="renameMedia({{ $media->id }}, '{{ addslashes($media->name ?? $media->original_name) }}')"
|
|
title="클릭하여 이름 변경">
|
|
{{ $media->name ?? $media->original_name }}
|
|
</div>
|
|
|
|
<div class="media-meta">
|
|
<span class="pill pill--muted" style="padding:2px 6px;">{{ $media->folder_name }}</span>
|
|
<span>{{ number_format($media->file_size / 1024, 1) }} KB</span>
|
|
</div>
|
|
<div class="a-muted" style="font-size:10px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">
|
|
{{ $media->original_name }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="action-row">
|
|
<button type="button" class="action-btn" onclick="copyToClipboard('{{ $media->file_path }}')">경로복사</button>
|
|
|
|
<button type="button" class="action-btn" onclick="renameMedia({{ $media->id }}, '{{ addslashes($media->name ?? $media->original_name) }}')">이름변경</button>
|
|
|
|
<form action="{{ route('admin.media.destroy', $media->id) }}" method="POST" onsubmit="return confirm('이미지를 완전히 삭제하시겠습니까?\n이미 상품에 연결된 경우 엑스박스가 뜰 수 있습니다.');" style="margin:0;">
|
|
@csrf @method('DELETE')
|
|
<button type="submit" class="action-btn delete">삭제</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@empty
|
|
<div style="grid-column: 1 / -1; text-align: center; padding: 60px 0;">
|
|
<div class="a-muted" style="font-size:16px; margin-bottom:10px;">등록된 이미지가 없습니다.</div>
|
|
<div class="a-muted" style="font-size:13px;">상단 업로드 영역에서 이미지를 등록해주세요.</div>
|
|
</div>
|
|
@endforelse
|
|
</div>
|
|
|
|
<div style="margin-top:12px;">
|
|
{{ $page->onEachSide(1)->links('vendor.pagination.admin') }}
|
|
</div>
|
|
</div>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
<script>
|
|
// 1. 이미지 경로 복사 기능
|
|
function copyToClipboard(text) {
|
|
if (!navigator.clipboard) {
|
|
// HTTPS가 아닌 환경(로컬 개발 등) 대비 fallback
|
|
const ta = document.createElement("textarea");
|
|
ta.value = text;
|
|
document.body.appendChild(ta);
|
|
ta.select();
|
|
document.execCommand("copy");
|
|
document.body.removeChild(ta);
|
|
alert('이미지 경로가 복사되었습니다 (Fallback).\n' + text);
|
|
return;
|
|
}
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
alert('이미지 경로가 클립보드에 복사되었습니다.\n' + text);
|
|
}).catch(err => {
|
|
console.error('복사 실패:', err);
|
|
alert('복사 실패! 브라우저 권한을 확인해주세요.');
|
|
});
|
|
}
|
|
|
|
// 2. 이미지 이름 변경 기능 (AJAX)
|
|
function renameMedia(id, currentName) {
|
|
const newName = prompt('이미지의 새로운 이름을 입력하세요:\n(상품 등록 시 검색에 사용됩니다)', currentName);
|
|
|
|
if (newName !== null && newName.trim() !== '' && newName !== currentName) {
|
|
// CSRF 토큰 가져오기
|
|
const token = document.querySelector('meta[name="csrf-token"]').content;
|
|
|
|
fetch(`{{ route('admin.media.index') }}/${id}/name`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': token,
|
|
'Accept': 'application/json'
|
|
},
|
|
body: JSON.stringify({ name: newName.trim() })
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if(data.ok) {
|
|
// 성공 시 DOM 즉시 업데이트 (새로고침 없이)
|
|
const nameEl = document.getElementById(`media-name-${id}`);
|
|
if(nameEl) {
|
|
nameEl.innerText = newName.trim();
|
|
// onclick 이벤트의 인자도 업데이트 (다음 번 변경을 위해)
|
|
// 주의: addslashes 처리가 복잡하므로 여기선 텍스트만 바꾸고,
|
|
// 연속 변경 시에는 data.ok 메시지만 띄워주는게 안전함.
|
|
}
|
|
// 간단한 토스트 알림 대용
|
|
// alert('이름이 변경되었습니다.');
|
|
} else {
|
|
alert('변경 실패: ' + (data.message || '오류가 발생했습니다.'));
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error('AJAX Error:', err);
|
|
alert('서버 통신 중 오류가 발생했습니다.');
|
|
});
|
|
}
|
|
}
|
|
</script>
|
|
@endpush
|