Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7eab19508a | ||
|
|
100a78b6e5 | ||
|
|
f662a389f7 | ||
|
|
8222ba667f | ||
| 047524c19f | |||
| bd500a87bd | |||
| c46b950fde | |||
|
|
7ff9206f93 | ||
| 28ae481a7d | |||
| b6d2b9942e | |||
| 4208ca3010 | |||
| 95371fd841 | |||
| 1df34b2fa9 | |||
| 3d12687a2d | |||
| 5e4cbc7742 | |||
| 4dd38ab14d | |||
| f9cd219f67 | |||
|
|
091719e81b | ||
|
|
b06438cc52 | ||
|
|
1e5cd70081 | ||
|
|
b21c9de6eb | ||
|
|
91567c54bd | ||
|
|
88eb507426 | ||
|
|
0bf56931fa | ||
|
|
c55a4a42e6 | ||
|
|
428b2e2a12 | ||
|
|
64877869e6 | ||
|
|
92efe2e83b | ||
|
|
78eb9363f4 | ||
|
|
c611f551a6 | ||
|
|
48889a7250 | ||
|
|
18f39433ae | ||
|
|
3785d87df4 | ||
|
|
d9075e5da5 | ||
| 1d71b588cb | |||
|
|
521229adcf | ||
|
|
5ce2d2fcbf | ||
|
|
5f5b5db59f | ||
|
|
814b965748 | ||
|
|
c55380f1d2 | ||
|
|
4870b7e6eb | ||
|
|
88ef6a8490 | ||
|
|
2fd122feba | ||
|
|
5a0deddb58 | ||
| d68fd56232 | |||
|
|
d8abc57271 | ||
|
|
d7dd6cdbc5 | ||
|
|
2bb3a2872a | ||
|
|
6df1da9e42 | ||
|
|
93e94901b7 | ||
|
|
7028e27517 | ||
|
|
1d2876d90c | ||
|
|
bfb821698a | ||
|
|
b80f4a0392 | ||
|
|
2ed90dc6db | ||
|
|
347d351d9d | ||
|
|
87a8930c00 | ||
|
|
bbcb0205fe | ||
| 9bf0cc8df2 | |||
|
|
255fad99e7 | ||
|
|
08b07c724a | ||
|
|
91cdfe9917 | ||
|
|
10c09b9fea | ||
|
|
04bb990045 | ||
| 7543054df3 |
@@ -54,4 +54,4 @@ ## 관련 파일
|
||||
|
||||
- `api/app/Services/ComprehensiveAnalysisService.php`
|
||||
- `api/database/seeders/ComprehensiveAnalysisSeeder.php`
|
||||
- `docs/dev/dev_plans/react-mock-remaining-tasks.md`
|
||||
- `docs/plans/react-mock-remaining-tasks.md`
|
||||
|
||||
@@ -15,7 +15,7 @@ ## Phase 구성
|
||||
- Phase 5: MNG 관리자 패널 (4항목) — mng/ /system/alerts
|
||||
|
||||
## 핵심 파일
|
||||
- 계획 문서: docs/dev/dev_plans/db-backup-system-plan.md
|
||||
- 계획 문서: docs/plans/db-backup-system-plan.md
|
||||
- 개발서버: 114.203.209.83 (SSH: hskwon)
|
||||
- DB: sam (메인) + sam_stat (통계)
|
||||
- Slack 웹훅: api/.env → LOG_SLACK_WEBHOOK_URL (이미 설정됨)
|
||||
|
||||
@@ -16,7 +16,7 @@ ### 생성된 파일
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `app/Http/Requests/Quote/QuoteBomBulkCalculateRequest.php` | 다건 BOM 산출 FormRequest |
|
||||
| `api/docs/dev/changes/20260102_1300_quote_bom_bulk_calculation.md` | 변경 내용 문서 |
|
||||
| `api/docs/changes/20260102_1300_quote_bom_bulk_calculation.md` | 변경 내용 문서 |
|
||||
|
||||
### 수정된 파일
|
||||
| 파일 | 설명 |
|
||||
@@ -93,9 +93,9 @@ ### QuoteCalculationService::calculateBomBulk()
|
||||
- 개별 품목 실패가 전체에 영향 없음 (예외 처리)
|
||||
|
||||
## 관련 문서
|
||||
- 계획 문서: `docs/dev/dev_plans/quote-calculation-api-plan.md`
|
||||
- Phase 1.1 문서: `docs/dev/changes/20260102_quote_bom_calculation_api.md`
|
||||
- Phase 1.2 문서: `docs/dev/changes/20260102_1300_quote_bom_bulk_calculation.md`
|
||||
- 계획 문서: `docs/plans/quote-calculation-api-plan.md`
|
||||
- Phase 1.1 문서: `docs/changes/20260102_quote_bom_calculation_api.md`
|
||||
- Phase 1.2 문서: `docs/changes/20260102_1300_quote_bom_bulk_calculation.md`
|
||||
|
||||
## 다음 단계
|
||||
- React 프론트엔드에서 `/calculate/bom/bulk` API 연동
|
||||
|
||||
@@ -107,10 +107,3 @@ fixed_tools: []
|
||||
# override of the corresponding setting in serena_config.yml, see the documentation there.
|
||||
# If null or missing, the value from the global config is used.
|
||||
symbol_info_budget:
|
||||
|
||||
# The language backend to use for this project.
|
||||
# If not set, the global setting from serena_config.yml is used.
|
||||
# Valid values: LSP, JetBrains
|
||||
# Note: the backend is fixed at startup. If a project with a different backend
|
||||
# is activated post-init, an error will be returned.
|
||||
language_backend:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 논리적 데이터베이스 관계 문서
|
||||
|
||||
> **자동 생성**: 2026-03-06 21:25:05
|
||||
> **자동 생성**: 2026-03-07 02:57:21
|
||||
> **소스**: Eloquent 모델 관계 분석
|
||||
|
||||
## 📊 모델별 관계 현황
|
||||
|
||||
@@ -40,8 +40,8 @@ public function handle(): int
|
||||
foreach ($files as $file) {
|
||||
try {
|
||||
// Delete physical file
|
||||
if (Storage::disk('r2')->exists($file->file_path)) {
|
||||
Storage::disk('r2')->delete($file->file_path);
|
||||
if (Storage::disk('tenant')->exists($file->file_path)) {
|
||||
Storage::disk('tenant')->delete($file->file_path);
|
||||
}
|
||||
|
||||
// Force delete from DB
|
||||
|
||||
@@ -60,8 +60,8 @@ private function permanentDelete(File $file): void
|
||||
{
|
||||
DB::transaction(function () use ($file) {
|
||||
// Delete physical file
|
||||
if (Storage::disk('r2')->exists($file->file_path)) {
|
||||
Storage::disk('r2')->delete($file->file_path);
|
||||
if (Storage::disk('tenant')->exists($file->file_path)) {
|
||||
Storage::disk('tenant')->delete($file->file_path);
|
||||
}
|
||||
|
||||
// Update tenant storage usage
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Commons\File;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\File as FileFacade;
|
||||
|
||||
class UploadLocalFilesToR2 extends Command
|
||||
{
|
||||
protected $signature = 'r2:upload-local
|
||||
{--count=3 : Number of files to upload}
|
||||
{--source=db : Source: "db" (latest DB records) or "disk" (latest local files)}
|
||||
{--dry-run : Show files without uploading}
|
||||
{--fix : Delete wrong-path files from R2 before re-uploading}';
|
||||
|
||||
protected $description = 'Upload local files to Cloudflare R2 (by DB records or local disk)';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$count = (int) $this->option('count');
|
||||
$source = $this->option('source');
|
||||
$dryRun = $this->option('dry-run');
|
||||
|
||||
$this->info("=== R2 Upload Tool ===");
|
||||
$this->info("Source: {$source} | Count: {$count}");
|
||||
|
||||
return $source === 'db'
|
||||
? $this->uploadFromDb($count, $dryRun)
|
||||
: $this->uploadFromDisk($count, $dryRun);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload files based on DB records (latest by ID desc)
|
||||
*/
|
||||
private function uploadFromDb(int $count, bool $dryRun): int
|
||||
{
|
||||
$files = File::orderByDesc('id')->limit($count)->get();
|
||||
|
||||
if ($files->isEmpty()) {
|
||||
$this->warn('No files in DB.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$headers = ['ID', 'Display Name', 'R2 Path', 'R2 Exists', 'Local Exists', 'Size'];
|
||||
$rows = [];
|
||||
|
||||
foreach ($files as $f) {
|
||||
$localPath = storage_path("app/tenants/{$f->file_path}");
|
||||
$r2Exists = Storage::disk('r2')->exists($f->file_path);
|
||||
$localExists = file_exists($localPath);
|
||||
|
||||
$rows[] = [
|
||||
$f->id,
|
||||
mb_strimwidth($f->display_name ?? '', 0, 25, '...'),
|
||||
$f->file_path,
|
||||
$r2Exists ? '✓ YES' : '✗ NO',
|
||||
$localExists ? '✓ YES' : '✗ NO',
|
||||
$f->file_size ? $this->formatSize($f->file_size) : '-',
|
||||
];
|
||||
}
|
||||
|
||||
$this->table($headers, $rows);
|
||||
|
||||
// Filter: local exists but R2 doesn't
|
||||
$toUpload = $files->filter(function ($f) {
|
||||
$localPath = storage_path("app/tenants/{$f->file_path}");
|
||||
return file_exists($localPath) && !Storage::disk('r2')->exists($f->file_path);
|
||||
});
|
||||
|
||||
$alreadyInR2 = $files->filter(function ($f) {
|
||||
return Storage::disk('r2')->exists($f->file_path);
|
||||
});
|
||||
|
||||
if ($alreadyInR2->isNotEmpty()) {
|
||||
$this->info("Already in R2: {$alreadyInR2->count()} files (skipped)");
|
||||
}
|
||||
|
||||
if ($toUpload->isEmpty()) {
|
||||
$missingBoth = $files->filter(function ($f) {
|
||||
$localPath = storage_path("app/tenants/{$f->file_path}");
|
||||
return !file_exists($localPath) && !Storage::disk('r2')->exists($f->file_path);
|
||||
});
|
||||
|
||||
if ($missingBoth->isNotEmpty()) {
|
||||
$this->warn("Missing both locally and in R2: {$missingBoth->count()} files");
|
||||
$this->warn("These files may exist on the dev server only.");
|
||||
}
|
||||
|
||||
$this->info('Nothing to upload.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->warn("[DRY RUN] Would upload {$toUpload->count()} files.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Test R2 connection
|
||||
$this->info('Testing R2 connection...');
|
||||
try {
|
||||
Storage::disk('r2')->directories('/');
|
||||
$this->info('✓ R2 connection OK');
|
||||
} catch (\Exception $e) {
|
||||
$this->error('✗ R2 connection failed: ' . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Upload
|
||||
$bar = $this->output->createProgressBar($toUpload->count());
|
||||
$bar->start();
|
||||
$success = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($toUpload as $f) {
|
||||
$localPath = storage_path("app/tenants/{$f->file_path}");
|
||||
try {
|
||||
$content = FileFacade::get($localPath);
|
||||
$mimeType = $f->mime_type ?: FileFacade::mimeType($localPath);
|
||||
|
||||
Storage::disk('r2')->put($f->file_path, $content, [
|
||||
'ContentType' => $mimeType,
|
||||
]);
|
||||
$success++;
|
||||
} catch (\Exception $e) {
|
||||
$failed++;
|
||||
$this->newLine();
|
||||
$this->error(" ✗ ID {$f->id}: {$e->getMessage()}");
|
||||
}
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine(2);
|
||||
$this->info("=== Upload Complete ===");
|
||||
$this->info("✓ Success: {$success}");
|
||||
if ($failed > 0) {
|
||||
$this->error("✗ Failed: {$failed}");
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload files based on local disk (newest files by mtime)
|
||||
*/
|
||||
private function uploadFromDisk(int $count, bool $dryRun): int
|
||||
{
|
||||
$storagePath = storage_path('app/tenants');
|
||||
|
||||
if (!is_dir($storagePath)) {
|
||||
$this->error("Path not found: {$storagePath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
$allFiles = $this->collectFiles($storagePath);
|
||||
if (empty($allFiles)) {
|
||||
$this->warn('No files found.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
usort($allFiles, fn($a, $b) => filemtime($b) - filemtime($a));
|
||||
$filesToUpload = array_slice($allFiles, 0, $count);
|
||||
|
||||
$this->info("Found " . count($allFiles) . " total files, uploading {$count} most recent:");
|
||||
$this->newLine();
|
||||
|
||||
$headers = ['#', 'File', 'Size', 'Modified', 'R2 Path'];
|
||||
$rows = [];
|
||||
|
||||
foreach ($filesToUpload as $i => $filePath) {
|
||||
$r2Path = $this->toR2Path($filePath);
|
||||
$rows[] = [$i + 1, basename($filePath), $this->formatSize(filesize($filePath)), date('Y-m-d H:i:s', filemtime($filePath)), $r2Path];
|
||||
}
|
||||
|
||||
$this->table($headers, $rows);
|
||||
|
||||
if ($dryRun) {
|
||||
$this->warn('[DRY RUN] No files uploaded.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->info('Testing R2 connection...');
|
||||
try {
|
||||
Storage::disk('r2')->directories('/');
|
||||
$this->info('✓ R2 connection OK');
|
||||
} catch (\Exception $e) {
|
||||
$this->error('✗ R2 connection failed: ' . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
|
||||
$bar = $this->output->createProgressBar(count($filesToUpload));
|
||||
$bar->start();
|
||||
$success = 0;
|
||||
$failed = 0;
|
||||
$fix = $this->option('fix');
|
||||
|
||||
foreach ($filesToUpload as $filePath) {
|
||||
$r2Path = $this->toR2Path($filePath);
|
||||
try {
|
||||
if ($fix) {
|
||||
$wrongPath = $this->toRelativePath($filePath);
|
||||
if ($wrongPath !== $r2Path && Storage::disk('r2')->exists($wrongPath)) {
|
||||
Storage::disk('r2')->delete($wrongPath);
|
||||
}
|
||||
}
|
||||
|
||||
$content = FileFacade::get($filePath);
|
||||
$mimeType = FileFacade::mimeType($filePath);
|
||||
|
||||
Storage::disk('r2')->put($r2Path, $content, ['ContentType' => $mimeType]);
|
||||
$success++;
|
||||
} catch (\Exception $e) {
|
||||
$failed++;
|
||||
$this->newLine();
|
||||
$this->error(" ✗ Failed: {$r2Path} - {$e->getMessage()}");
|
||||
}
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine(2);
|
||||
$this->info("=== Upload Complete ===");
|
||||
$this->info("✓ Success: {$success}");
|
||||
if ($failed > 0) {
|
||||
$this->error("✗ Failed: {$failed}");
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function toR2Path(string $filePath): string
|
||||
{
|
||||
$relative = $this->toRelativePath($filePath);
|
||||
return str_starts_with($relative, 'tenants/') ? substr($relative, strlen('tenants/')) : $relative;
|
||||
}
|
||||
|
||||
private function toRelativePath(string $filePath): string
|
||||
{
|
||||
return str_replace(str_replace('\\', '/', storage_path('app/')), '', str_replace('\\', '/', $filePath));
|
||||
}
|
||||
|
||||
private function collectFiles(string $dir): array
|
||||
{
|
||||
$files = [];
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS)
|
||||
);
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile() && $file->getFilename() !== '.gitignore') {
|
||||
$files[] = $file->getPathname();
|
||||
}
|
||||
}
|
||||
return $files;
|
||||
}
|
||||
|
||||
private function formatSize(int $bytes): string
|
||||
{
|
||||
if ($bytes >= 1048576) return round($bytes / 1048576, 1) . ' MB';
|
||||
return round($bytes / 1024, 1) . ' KB';
|
||||
}
|
||||
}
|
||||
@@ -83,25 +83,14 @@ public function trash()
|
||||
}
|
||||
|
||||
/**
|
||||
* Download file (attachment)
|
||||
* Download file
|
||||
*/
|
||||
public function download(int $id)
|
||||
{
|
||||
$service = new FileStorageService;
|
||||
$file = $service->getFile($id);
|
||||
|
||||
return $file->download(inline: false);
|
||||
}
|
||||
|
||||
/**
|
||||
* View file inline (이미지/PDF 브라우저에서 바로 표시)
|
||||
*/
|
||||
public function view(int $id)
|
||||
{
|
||||
$service = new FileStorageService;
|
||||
$file = $service->getFile($id);
|
||||
|
||||
return $file->download(inline: true);
|
||||
return $file->download();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -109,7 +109,7 @@ public function upload(int $id, ItemFileUploadRequest $request)
|
||||
$filePath = $directory.'/'.$storedName;
|
||||
|
||||
// 파일 저장 (tenant 디스크)
|
||||
Storage::disk('r2')->putFileAs($directory, $uploadedFile, $storedName);
|
||||
Storage::disk('tenant')->putFileAs($directory, $uploadedFile, $storedName);
|
||||
|
||||
// file_type 자동 분류 (MIME 타입 기반)
|
||||
$mimeType = $uploadedFile->getMimeType();
|
||||
|
||||
@@ -31,7 +31,6 @@ public function rules(): array
|
||||
'remark' => ['nullable', 'string', 'max:1000'],
|
||||
'manufacturer' => ['nullable', 'string', 'max:100'],
|
||||
'material_no' => ['nullable', 'string', 'max:50'],
|
||||
'certificate_file_id' => ['nullable', 'integer', 'exists:files,id'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ public function rules(): array
|
||||
'inspection_result' => ['nullable', 'string', 'max:20'],
|
||||
'manufacturer' => ['nullable', 'string', 'max:100'],
|
||||
'material_no' => ['nullable', 'string', 'max:50'],
|
||||
'certificate_file_id' => ['nullable', 'integer', 'exists:files,id'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ public function fileable()
|
||||
*/
|
||||
public function getStoragePath(): string
|
||||
{
|
||||
return $this->file_path;
|
||||
return Storage::disk('tenant')->path($this->file_path);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,38 +111,22 @@ public function getStoragePath(): string
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return Storage::disk('r2')->exists($this->file_path);
|
||||
return Storage::disk('tenant')->exists($this->file_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get download response (streaming from R2)
|
||||
*
|
||||
* @param bool $inline true = 브라우저에서 바로 표시 (이미지/PDF), false = 다운로드
|
||||
* Get download response
|
||||
*/
|
||||
public function download(bool $inline = false)
|
||||
public function download()
|
||||
{
|
||||
if (! $this->exists()) {
|
||||
abort(404, 'File not found in storage');
|
||||
}
|
||||
|
||||
$fileName = $this->display_name ?? $this->original_name;
|
||||
$mimeType = $this->mime_type ?? 'application/octet-stream';
|
||||
$disposition = $inline ? 'inline' : 'attachment';
|
||||
|
||||
// Stream from R2 (메모리에 전체 로드하지 않음)
|
||||
$stream = Storage::disk('r2')->readStream($this->file_path);
|
||||
|
||||
return response()->stream(function () use ($stream) {
|
||||
fpassthru($stream);
|
||||
if (is_resource($stream)) {
|
||||
fclose($stream);
|
||||
}
|
||||
}, 200, [
|
||||
'Content-Type' => $mimeType,
|
||||
'Content-Disposition' => $disposition . '; filename="' . $fileName . '"',
|
||||
'Content-Length' => $this->file_size,
|
||||
'Cache-Control' => 'private, max-age=3600',
|
||||
]);
|
||||
return response()->download(
|
||||
$this->getStoragePath(),
|
||||
$this->display_name ?? $this->original_name
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,9 +149,9 @@ public function moveToFolder(Folder $folder): bool
|
||||
$this->stored_name ?? $this->file_name
|
||||
);
|
||||
|
||||
// Move physical file in R2
|
||||
if (Storage::disk('r2')->exists($this->file_path)) {
|
||||
Storage::disk('r2')->move($this->file_path, $newPath);
|
||||
// Move physical file
|
||||
if (Storage::disk('tenant')->exists($this->file_path)) {
|
||||
Storage::disk('tenant')->move($this->file_path, $newPath);
|
||||
}
|
||||
|
||||
// Update DB
|
||||
@@ -198,9 +182,9 @@ public function softDeleteFile(int $userId): void
|
||||
*/
|
||||
public function permanentDelete(): void
|
||||
{
|
||||
// Delete physical file from R2
|
||||
// Delete physical file
|
||||
if ($this->exists()) {
|
||||
Storage::disk('r2')->delete($this->file_path);
|
||||
Storage::disk('tenant')->delete($this->file_path);
|
||||
}
|
||||
|
||||
// Decrement tenant storage
|
||||
|
||||
@@ -34,7 +34,6 @@ class Receiving extends Model
|
||||
'status',
|
||||
'remark',
|
||||
'options',
|
||||
'certificate_file_id',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
@@ -48,7 +47,6 @@ class Receiving extends Model
|
||||
'receiving_qty' => 'decimal:2',
|
||||
'item_id' => 'integer',
|
||||
'options' => 'array',
|
||||
'certificate_file_id' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -94,14 +92,6 @@ public function item(): BelongsTo
|
||||
return $this->belongsTo(\App\Models\Items\Item::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 업체 제공 성적서 파일 관계
|
||||
*/
|
||||
public function certificateFile(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Commons\File::class, 'certificate_file_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 생성자 관계
|
||||
*/
|
||||
|
||||
@@ -54,16 +54,18 @@ public function upload(UploadedFile $file, ?string $description = null): File
|
||||
$storedName
|
||||
);
|
||||
|
||||
// Store file to R2 (Cloudflare R2, S3 compatible)
|
||||
Storage::disk('r2')->put($tempPath, file_get_contents($file->getRealPath()), [
|
||||
'ContentType' => $file->getMimeType(),
|
||||
]);
|
||||
// Store file
|
||||
Storage::disk('tenant')->putFileAs(
|
||||
dirname($tempPath),
|
||||
$file,
|
||||
basename($tempPath)
|
||||
);
|
||||
|
||||
// Determine file type
|
||||
$mimeType = $file->getMimeType();
|
||||
$fileType = $this->determineFileType($mimeType);
|
||||
|
||||
// Create DB record (file_path = R2 key path)
|
||||
// Create DB record
|
||||
$fileRecord = File::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'display_name' => $file->getClientOriginalName(),
|
||||
|
||||
@@ -16,7 +16,7 @@ public function index(array $params): LengthAwarePaginator
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = Receiving::query()
|
||||
->with(['creator:id,name', 'item:id,item_type,code,name', 'certificateFile:id,display_name,file_path'])
|
||||
->with(['creator:id,name', 'item:id,item_type,code,name'])
|
||||
->where('tenant_id', $tenantId);
|
||||
|
||||
// 검색어 필터
|
||||
@@ -162,7 +162,7 @@ public function show(int $id): Receiving
|
||||
|
||||
return Receiving::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with(['creator:id,name', 'item:id,item_type,code,name', 'certificateFile:id,display_name,file_path'])
|
||||
->with(['creator:id,name', 'item:id,item_type,code,name'])
|
||||
->findOrFail($id);
|
||||
}
|
||||
|
||||
@@ -203,11 +203,6 @@ public function store(array $data): Receiving
|
||||
$receiving->status = $data['status'] ?? 'receiving_pending';
|
||||
$receiving->remark = $data['remark'] ?? null;
|
||||
|
||||
// 성적서 파일 ID
|
||||
if (isset($data['certificate_file_id'])) {
|
||||
$receiving->certificate_file_id = $data['certificate_file_id'];
|
||||
}
|
||||
|
||||
// options 필드 처리 (제조사, 수입검사 등 확장 필드)
|
||||
$receiving->options = $this->buildOptions($data);
|
||||
|
||||
@@ -304,11 +299,6 @@ public function update(int $id, array $data): Receiving
|
||||
}
|
||||
}
|
||||
|
||||
// 성적서 파일 ID
|
||||
if (array_key_exists('certificate_file_id', $data)) {
|
||||
$receiving->certificate_file_id = $data['certificate_file_id'];
|
||||
}
|
||||
|
||||
// options 필드 업데이트 (제조사, 수입검사 등 확장 필드)
|
||||
$receiving->options = $this->mergeOptions($receiving->options, $data);
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"laravel/mcp": "^0.1.1",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"league/flysystem-aws-s3-v3": "^3.32",
|
||||
"livewire/livewire": "^3.0",
|
||||
"maatwebsite/excel": "^3.1",
|
||||
"spatie/laravel-permission": "^6.21"
|
||||
|
||||
346
composer.lock
generated
346
composer.lock
generated
@@ -4,159 +4,8 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "f39a7807cc0a6aa991e31a6acffc9508",
|
||||
"content-hash": "340d586f1c4e3f7bd0728229300967da",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
"version": "v1.2.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/awslabs/aws-crt-php.git",
|
||||
"reference": "d71d9906c7bb63a28295447ba12e74723bd3730e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/d71d9906c7bb63a28295447ba12e74723bd3730e",
|
||||
"reference": "d71d9906c7bb63a28295447ba12e74723bd3730e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8.35||^5.6.3||^9.5",
|
||||
"yoast/phpunit-polyfills": "^1.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality."
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "AWS SDK Common Runtime Team",
|
||||
"email": "aws-sdk-common-runtime@amazon.com"
|
||||
}
|
||||
],
|
||||
"description": "AWS Common Runtime for PHP",
|
||||
"homepage": "https://github.com/awslabs/aws-crt-php",
|
||||
"keywords": [
|
||||
"amazon",
|
||||
"aws",
|
||||
"crt",
|
||||
"sdk"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/awslabs/aws-crt-php/issues",
|
||||
"source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7"
|
||||
},
|
||||
"time": "2024-10-18T22:15:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "aws/aws-sdk-php",
|
||||
"version": "3.372.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aws/aws-sdk-php.git",
|
||||
"reference": "d207d2ca972c9b10674e535dacd4a5d956a80bad"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/d207d2ca972c9b10674e535dacd4a5d956a80bad",
|
||||
"reference": "d207d2ca972c9b10674e535dacd4a5d956a80bad",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"aws/aws-crt-php": "^1.2.3",
|
||||
"ext-json": "*",
|
||||
"ext-pcre": "*",
|
||||
"ext-simplexml": "*",
|
||||
"guzzlehttp/guzzle": "^7.4.5",
|
||||
"guzzlehttp/promises": "^2.0",
|
||||
"guzzlehttp/psr7": "^2.4.5",
|
||||
"mtdowling/jmespath.php": "^2.8.0",
|
||||
"php": ">=8.1",
|
||||
"psr/http-message": "^1.0 || ^2.0",
|
||||
"symfony/filesystem": "^v5.4.45 || ^v6.4.3 || ^v7.1.0 || ^v8.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"andrewsville/php-token-reflection": "^1.4",
|
||||
"aws/aws-php-sns-message-validator": "~1.0",
|
||||
"behat/behat": "~3.0",
|
||||
"composer/composer": "^2.7.8",
|
||||
"dms/phpunit-arraysubset-asserts": "^0.4.0",
|
||||
"doctrine/cache": "~1.4",
|
||||
"ext-dom": "*",
|
||||
"ext-openssl": "*",
|
||||
"ext-sockets": "*",
|
||||
"phpunit/phpunit": "^9.6",
|
||||
"psr/cache": "^2.0 || ^3.0",
|
||||
"psr/simple-cache": "^2.0 || ^3.0",
|
||||
"sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0",
|
||||
"yoast/phpunit-polyfills": "^2.0"
|
||||
},
|
||||
"suggest": {
|
||||
"aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
|
||||
"doctrine/cache": "To use the DoctrineCacheAdapter",
|
||||
"ext-curl": "To send requests using cURL",
|
||||
"ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages",
|
||||
"ext-pcntl": "To use client-side monitoring",
|
||||
"ext-sockets": "To use client-side monitoring"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Aws\\": "src/"
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"src/data/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Amazon Web Services",
|
||||
"homepage": "https://aws.amazon.com"
|
||||
}
|
||||
],
|
||||
"description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project",
|
||||
"homepage": "https://aws.amazon.com/sdk-for-php",
|
||||
"keywords": [
|
||||
"amazon",
|
||||
"aws",
|
||||
"cloud",
|
||||
"dynamodb",
|
||||
"ec2",
|
||||
"glacier",
|
||||
"s3",
|
||||
"sdk"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://github.com/aws/aws-sdk-php/discussions",
|
||||
"issues": "https://github.com/aws/aws-sdk-php/issues",
|
||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.372.3"
|
||||
},
|
||||
"time": "2026-03-10T18:07:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brick/math",
|
||||
"version": "0.13.1",
|
||||
@@ -2663,61 +2512,6 @@
|
||||
},
|
||||
"time": "2025-06-25T13:29:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem-aws-s3-v3",
|
||||
"version": "3.32.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
|
||||
"reference": "a1979df7c9784d334ea6df356aed3d18ac6673d0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/a1979df7c9784d334ea6df356aed3d18ac6673d0",
|
||||
"reference": "a1979df7c9784d334ea6df356aed3d18ac6673d0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"aws/aws-sdk-php": "^3.295.10",
|
||||
"league/flysystem": "^3.10.0",
|
||||
"league/mime-type-detection": "^1.0.0",
|
||||
"php": "^8.0.2"
|
||||
},
|
||||
"conflict": {
|
||||
"guzzlehttp/guzzle": "<7.0",
|
||||
"guzzlehttp/ringphp": "<1.1.1"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\Flysystem\\AwsS3V3\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Frank de Jonge",
|
||||
"email": "info@frankdejonge.nl"
|
||||
}
|
||||
],
|
||||
"description": "AWS S3 filesystem adapter for Flysystem.",
|
||||
"keywords": [
|
||||
"Flysystem",
|
||||
"aws",
|
||||
"file",
|
||||
"files",
|
||||
"filesystem",
|
||||
"s3",
|
||||
"storage"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.32.0"
|
||||
},
|
||||
"time": "2026-02-25T16:46:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem-local",
|
||||
"version": "3.30.0",
|
||||
@@ -3442,72 +3236,6 @@
|
||||
],
|
||||
"time": "2025-03-24T10:02:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mtdowling/jmespath.php",
|
||||
"version": "2.8.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jmespath/jmespath.php.git",
|
||||
"reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a2a865e05d5f420b50cc2f85bb78d565db12a6bc",
|
||||
"reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2.5 || ^8.0",
|
||||
"symfony/polyfill-mbstring": "^1.17"
|
||||
},
|
||||
"require-dev": {
|
||||
"composer/xdebug-handler": "^3.0.3",
|
||||
"phpunit/phpunit": "^8.5.33"
|
||||
},
|
||||
"bin": [
|
||||
"bin/jp.php"
|
||||
],
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.8-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/JmesPath.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"JmesPath\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Graham Campbell",
|
||||
"email": "hello@gjcampbell.co.uk",
|
||||
"homepage": "https://github.com/GrahamCampbell"
|
||||
},
|
||||
{
|
||||
"name": "Michael Dowling",
|
||||
"email": "mtdowling@gmail.com",
|
||||
"homepage": "https://github.com/mtdowling"
|
||||
}
|
||||
],
|
||||
"description": "Declaratively specify how to extract elements from a JSON document",
|
||||
"keywords": [
|
||||
"json",
|
||||
"jsonpath"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/jmespath/jmespath.php/issues",
|
||||
"source": "https://github.com/jmespath/jmespath.php/tree/2.8.0"
|
||||
},
|
||||
"time": "2024-09-04T18:46:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nesbot/carbon",
|
||||
"version": "3.10.1",
|
||||
@@ -5502,76 +5230,6 @@
|
||||
],
|
||||
"time": "2024-09-25T14:21:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "v8.0.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "7bf9162d7a0dff98d079b72948508fa48018a770"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/7bf9162d7a0dff98d079b72948508fa48018a770",
|
||||
"reference": "7bf9162d7a0dff98d079b72948508fa48018a770",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"symfony/polyfill-ctype": "~1.8",
|
||||
"symfony/polyfill-mbstring": "~1.8"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/process": "^7.4|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Filesystem\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides basic utilities for the filesystem",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/filesystem/tree/v8.0.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-25T16:59:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v7.3.0",
|
||||
@@ -11115,5 +10773,5 @@
|
||||
"php": "^8.2"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -76,18 +76,6 @@
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
'r2' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('R2_ACCESS_KEY_ID'),
|
||||
'secret' => env('R2_SECRET_ACCESS_KEY'),
|
||||
'region' => env('R2_REGION', 'auto'),
|
||||
'bucket' => env('R2_BUCKET'),
|
||||
'endpoint' => env('R2_ENDPOINT'),
|
||||
'use_path_style_endpoint' => true,
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('receivings', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('certificate_file_id')
|
||||
->nullable()
|
||||
->after('options')
|
||||
->comment('업체 제공 성적서 파일 ID');
|
||||
|
||||
$table->foreign('certificate_file_id')
|
||||
->references('id')
|
||||
->on('files')
|
||||
->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('receivings', function (Blueprint $table) {
|
||||
$table->dropForeign(['certificate_file_id']);
|
||||
$table->dropColumn('certificate_file_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -21,7 +21,6 @@
|
||||
Route::get('/trash', [FileStorageController::class, 'trash'])->name('v1.files.trash'); // 휴지통 목록
|
||||
Route::get('/{id}', [FileStorageController::class, 'show'])->name('v1.files.show'); // 파일 상세
|
||||
Route::get('/{id}/download', [FileStorageController::class, 'download'])->name('v1.files.download'); // 파일 다운로드
|
||||
Route::get('/{id}/view', [FileStorageController::class, 'view'])->name('v1.files.view'); // 파일 인라인 보기 (이미지/PDF)
|
||||
Route::delete('/{id}', [FileStorageController::class, 'destroy'])->name('v1.files.destroy'); // 파일 삭제 (soft)
|
||||
Route::post('/{id}/restore', [FileStorageController::class, 'restore'])->name('v1.files.restore'); // 파일 복구
|
||||
Route::delete('/{id}/permanent', [FileStorageController::class, 'permanentDelete'])->name('v1.files.permanent'); // 파일 영구 삭제
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user