Revert "fix: [bending] Docker 환경 API 호출 시 internal_url 미사용 500 에러 수정"
This reverts commit fc1a28e552.
This commit is contained in:
@@ -1,276 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class BendingBaseController extends Controller
|
||||
{
|
||||
private function api(): \Illuminate\Http\Client\PendingRequest
|
||||
{
|
||||
$baseUrl = config('services.api.base_url', 'https://api.sam.kr');
|
||||
$internalUrl = config('services.api.internal_url');
|
||||
$token = session('api_access_token', '');
|
||||
|
||||
$headers = [
|
||||
'X-API-KEY' => config('services.api.key') ?: '42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a',
|
||||
'X-TENANT-ID' => session('selected_tenant_id', 1),
|
||||
];
|
||||
|
||||
// Docker: internal_url(nginx 컨테이너) 경유, Host 헤더로 서버 블록 라우팅
|
||||
if ($internalUrl) {
|
||||
$headers['Host'] = parse_url($baseUrl, PHP_URL_HOST) ?: 'api.sam.kr';
|
||||
$baseUrl = $internalUrl;
|
||||
}
|
||||
|
||||
return Http::baseUrl($baseUrl)
|
||||
->withoutVerifying()
|
||||
->withHeaders($headers)
|
||||
->withToken($token)
|
||||
->timeout(10);
|
||||
}
|
||||
|
||||
public function index(Request $request): View|\Illuminate\Http\Response
|
||||
{
|
||||
$params = $request->only(['item_sep', 'item_bending', 'material', 'model_UA', 'item_name', 'search', 'page', 'size']);
|
||||
$params['size'] = $params['size'] ?? 30;
|
||||
|
||||
$response = $this->api()->get('/api/v1/bending-items', $params);
|
||||
$body = $response->successful() ? $response->json('data', []) : [];
|
||||
$data = [
|
||||
'data' => $body['data'] ?? [],
|
||||
'total' => $body['total'] ?? 0,
|
||||
'current_page' => $body['current_page'] ?? 1,
|
||||
'last_page' => $body['last_page'] ?? 1,
|
||||
];
|
||||
|
||||
$filterResponse = $this->api()->get('/api/v1/bending-items/filters');
|
||||
$filterOptions = $filterResponse->successful() ? $filterResponse->json('data', []) : [];
|
||||
|
||||
if ($request->header('HX-Request')) {
|
||||
if ($request->header('HX-Target') === 'items-table') {
|
||||
return view('bending.base.partials.table', ['items' => $data]);
|
||||
}
|
||||
|
||||
return response('', 200)->header('HX-Redirect', route('bending.base.index', $request->query()));
|
||||
}
|
||||
|
||||
return view('bending.base.index', [
|
||||
'items' => $data,
|
||||
'filterOptions' => $filterOptions,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(int $id): View
|
||||
{
|
||||
$response = $this->api()->get("/api/v1/bending-items/{$id}");
|
||||
$item = $response->successful() ? $response->json('data') : null;
|
||||
abort_unless($item, 404);
|
||||
|
||||
$imageFile = $this->getImageFile($id);
|
||||
|
||||
return view('bending.base.form', ['item' => $item, 'mode' => 'view', 'imageFile' => $imageFile]);
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
return view('bending.base.form', ['item' => null, 'mode' => 'create', 'imageFile' => null]);
|
||||
}
|
||||
|
||||
public function edit(int $id): View
|
||||
{
|
||||
$response = $this->api()->get("/api/v1/bending-items/{$id}");
|
||||
$item = $response->successful() ? $response->json('data') : null;
|
||||
abort_unless($item, 404);
|
||||
|
||||
$imageFile = $this->getImageFile($id);
|
||||
|
||||
return view('bending.base.form', ['item' => $item, 'mode' => 'edit', 'imageFile' => $imageFile]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'code' => 'required|string|max:100',
|
||||
'name' => 'required|string|max:200',
|
||||
'item_sep' => 'required|in:스크린,철재',
|
||||
'item_bending' => 'required|string|max:50',
|
||||
'item_name' => 'required|string|max:50',
|
||||
'material' => 'required|string|max:50',
|
||||
'model_UA' => 'nullable|in:인정,비인정',
|
||||
], [
|
||||
'code.required' => '코드를 입력하세요.',
|
||||
'name.required' => '이름을 입력하세요.',
|
||||
'item_sep.required' => '대분류를 선택하세요.',
|
||||
'item_sep.in' => '대분류는 스크린 또는 철재만 선택 가능합니다.',
|
||||
'item_bending.required' => '분류를 선택하세요.',
|
||||
'item_name.required' => '품명을 입력하세요.',
|
||||
'material.required' => '재질을 입력하세요.',
|
||||
'model_UA.in' => '인정여부는 인정 또는 비인정만 선택 가능합니다.',
|
||||
]);
|
||||
|
||||
$data = $this->prepareApiData($request);
|
||||
$response = $this->api()->post('/api/v1/bending-items', $data);
|
||||
|
||||
if (! $response->successful()) {
|
||||
$body = $response->json();
|
||||
\Log::error('BendingBase store API error', ['status' => $response->status(), 'body' => $body, 'sent_data' => $data]);
|
||||
$apiErrors = $body['errors'] ?? $body['error']['details'] ?? $body['data']['errors'] ?? [];
|
||||
$apiMessage = $body['message'] ?? 'API 오류';
|
||||
$errorBag = ['api' => "[{$response->status()}] {$apiMessage}"];
|
||||
foreach ($apiErrors as $field => $msgs) {
|
||||
$fieldLabel = match ($field) {
|
||||
'code' => '코드', 'name' => '이름', 'item_sep' => '대분류',
|
||||
'item_bending' => '분류', 'item_name' => '품명', 'material' => '재질',
|
||||
default => $field,
|
||||
};
|
||||
$errorBag["api_{$field}"] = "[{$fieldLabel}] ".(is_array($msgs) ? implode(', ', $msgs) : $msgs);
|
||||
}
|
||||
|
||||
return back()->withErrors($errorBag)->withInput();
|
||||
}
|
||||
|
||||
$itemId = $response->json('data.id');
|
||||
|
||||
if ($itemId) {
|
||||
$this->handleImageUpload($request, $itemId);
|
||||
}
|
||||
|
||||
return redirect()->route('bending.base.index')->with('success', '절곡품이 등록되었습니다.');
|
||||
}
|
||||
|
||||
public function update(Request $request, int $id)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'code' => 'required|string|max:100',
|
||||
'name' => 'required|string|max:200',
|
||||
'item_sep' => 'required|in:스크린,철재',
|
||||
'item_bending' => 'required|string|max:50',
|
||||
'item_name' => 'required|string|max:50',
|
||||
'material' => 'required|string|max:50',
|
||||
'model_UA' => 'nullable|in:인정,비인정',
|
||||
], [
|
||||
'code.required' => '코드를 입력하세요.',
|
||||
'name.required' => '이름을 입력하세요.',
|
||||
'item_sep.required' => '대분류를 선택하세요.',
|
||||
'item_sep.in' => '대분류는 스크린 또는 철재만 선택 가능합니다.',
|
||||
'item_bending.required' => '분류를 선택하세요.',
|
||||
'item_name.required' => '품명을 입력하세요.',
|
||||
'material.required' => '재질을 입력하세요.',
|
||||
'model_UA.in' => '인정여부는 인정 또는 비인정만 선택 가능합니다.',
|
||||
]);
|
||||
|
||||
$data = $this->prepareApiData($request);
|
||||
$response = $this->api()->put("/api/v1/bending-items/{$id}", $data);
|
||||
|
||||
if (! $response->successful()) {
|
||||
return back()->withErrors(['api' => $response->json('message', 'API 오류')])->withInput();
|
||||
}
|
||||
|
||||
$this->handleImageUpload($request, $id);
|
||||
|
||||
return redirect()->route('bending.base.show', $id)->with('success', '절곡품이 수정되었습니다.');
|
||||
}
|
||||
|
||||
public function destroy(int $id)
|
||||
{
|
||||
$this->api()->delete("/api/v1/bending-items/{$id}");
|
||||
|
||||
return redirect()->route('bending.base.index')->with('success', '절곡품이 삭제되었습니다.');
|
||||
}
|
||||
|
||||
private function getImageFile(int $itemId): ?array
|
||||
{
|
||||
$response = $this->api()->get("/api/v1/items/{$itemId}/files", ['field_key' => 'bending_diagram']);
|
||||
|
||||
if (! $response->successful()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$files = $response->json('data.bending_diagram', []);
|
||||
|
||||
return ! empty($files) ? $files[0] : null;
|
||||
}
|
||||
|
||||
private function uploadImage(int $itemId, \Illuminate\Http\UploadedFile $file): ?array
|
||||
{
|
||||
$existing = $this->getImageFile($itemId);
|
||||
|
||||
$postData = ['field_key' => 'bending_diagram'];
|
||||
if ($existing) {
|
||||
$postData['file_id'] = $existing['id'];
|
||||
}
|
||||
|
||||
$response = $this->api()
|
||||
->attach('file', $file->getContent(), $file->getClientOriginalName())
|
||||
->post("/api/v1/items/{$itemId}/files", $postData);
|
||||
|
||||
if (! $response->successful()) {
|
||||
\Log::error('Bending image upload failed', [
|
||||
'itemId' => $itemId,
|
||||
'status' => $response->status(),
|
||||
'body' => $response->json(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $response->successful() ? $response->json('data') : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 업로드 또는 Canvas Base64 이미지 처리
|
||||
*/
|
||||
private function handleImageUpload(Request $request, int $itemId): void
|
||||
{
|
||||
if ($request->hasFile('image')) {
|
||||
$this->uploadImage($itemId, $request->file('image'));
|
||||
} elseif ($request->filled('canvas_image')) {
|
||||
$this->uploadCanvasImage($itemId, $request->input('canvas_image'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Canvas Base64 DataURL → 임시 파일 → API 업로드
|
||||
*/
|
||||
private function uploadCanvasImage(int $itemId, string $dataURL): ?array
|
||||
{
|
||||
// data:image/png;base64,... 형식 파싱
|
||||
if (! preg_match('/^data:image\/(\w+);base64,/', $dataURL, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ext = $matches[1] === 'jpeg' ? 'jpg' : $matches[1];
|
||||
$base64 = substr($dataURL, strpos($dataURL, ',') + 1);
|
||||
$binary = base64_decode($base64);
|
||||
|
||||
if ($binary === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 임시 파일 생성
|
||||
$tmpPath = tempnam(sys_get_temp_dir(), 'canvas_').'.'.$ext;
|
||||
file_put_contents($tmpPath, $binary);
|
||||
|
||||
try {
|
||||
$file = new \Illuminate\Http\UploadedFile($tmpPath, "canvas.{$ext}", "image/{$ext}", null, true);
|
||||
|
||||
return $this->uploadImage($itemId, $file);
|
||||
} finally {
|
||||
@unlink($tmpPath);
|
||||
}
|
||||
}
|
||||
|
||||
private function prepareApiData(Request $request): array
|
||||
{
|
||||
$data = $request->except(['_token', '_method', 'image', 'canvas_image']);
|
||||
|
||||
if (isset($data['bendingData']) && is_string($data['bendingData'])) {
|
||||
$decoded = json_decode($data['bendingData'], true);
|
||||
$data['bendingData'] = is_array($decoded) ? $decoded : null;
|
||||
}
|
||||
|
||||
// 빈 문자열도 전송 (기존 값 삭제 가능하도록) — null만 제거
|
||||
return array_filter($data, fn ($v) => $v !== null);
|
||||
}
|
||||
}
|
||||
@@ -1,395 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class BendingProductController extends Controller
|
||||
{
|
||||
private const TYPE_CONFIG = [
|
||||
'GUIDERAIL_MODEL' => ['title' => '절곡품 (가이드레일)', 'prefix' => 'products', 'label' => '가이드레일'],
|
||||
'SHUTTERBOX_MODEL' => ['title' => '케이스 관리', 'prefix' => 'cases', 'label' => '케이스'],
|
||||
'BOTTOMBAR_MODEL' => ['title' => '하단마감재 관리', 'prefix' => 'bottombars', 'label' => '하단마감재'],
|
||||
];
|
||||
|
||||
private function api(): \Illuminate\Http\Client\PendingRequest
|
||||
{
|
||||
$baseUrl = config('services.api.base_url', 'https://api.sam.kr');
|
||||
$internalUrl = config('services.api.internal_url');
|
||||
$token = session('api_access_token', '');
|
||||
|
||||
$headers = [
|
||||
'X-API-KEY' => config('services.api.key') ?: '42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a',
|
||||
'X-TENANT-ID' => session('selected_tenant_id', 1),
|
||||
];
|
||||
|
||||
if ($internalUrl) {
|
||||
$headers['Host'] = parse_url($baseUrl, PHP_URL_HOST) ?: 'api.sam.kr';
|
||||
$baseUrl = $internalUrl;
|
||||
}
|
||||
|
||||
return Http::baseUrl($baseUrl)
|
||||
->withoutVerifying()
|
||||
->withHeaders($headers)
|
||||
->withToken($token)
|
||||
->timeout(10);
|
||||
}
|
||||
|
||||
private function getConfig(string $category): array
|
||||
{
|
||||
return self::TYPE_CONFIG[$category] ?? self::TYPE_CONFIG['GUIDERAIL_MODEL'];
|
||||
}
|
||||
|
||||
public function index(Request $request, string $category = 'GUIDERAIL_MODEL'): View|\Illuminate\Http\Response
|
||||
{
|
||||
$config = $this->getConfig($category);
|
||||
$params = $request->only(['item_sep', 'model_UA', 'check_type', 'model_name', 'exit_direction', 'search', 'page', 'size']);
|
||||
$params['size'] = $params['size'] ?? 30;
|
||||
$params['item_category'] = $category;
|
||||
|
||||
$response = $this->api()->get('/api/v1/guiderail-models', $params);
|
||||
$body = $response->successful() ? $response->json('data', []) : [];
|
||||
$data = [
|
||||
'data' => $body['data'] ?? [],
|
||||
'total' => $body['total'] ?? 0,
|
||||
'current_page' => $body['current_page'] ?? 1,
|
||||
'last_page' => $body['last_page'] ?? 1,
|
||||
];
|
||||
|
||||
$filterResponse = $this->api()->get('/api/v1/guiderail-models/filters');
|
||||
$filterOptions = $filterResponse->successful() ? $filterResponse->json('data', []) : [];
|
||||
|
||||
if ($request->header('HX-Request')) {
|
||||
// 필터/검색 HTMX (hx-target="#items-table") → 파셜 반환
|
||||
if ($request->header('HX-Target') === 'items-table') {
|
||||
return view('bending.products.partials.table', ['items' => $data, 'config' => $config, 'category' => $category]);
|
||||
}
|
||||
|
||||
// 사이드바 등 그 외 HTMX → 전체 페이지 리로드
|
||||
return response('', 200)->header('HX-Redirect', route("bending.{$config['prefix']}.index", $request->query()));
|
||||
}
|
||||
|
||||
return view('bending.products.index', [
|
||||
'items' => $data,
|
||||
'filterOptions' => $filterOptions,
|
||||
'config' => $config,
|
||||
'category' => $category,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(int $id): View
|
||||
{
|
||||
$response = $this->api()->get("/api/v1/guiderail-models/{$id}");
|
||||
$item = $response->successful() ? $response->json('data') : null;
|
||||
abort_unless($item, 404);
|
||||
|
||||
$item = $this->enrichComponentsWithSamIds($item);
|
||||
$config = $this->getConfig($item['item_category'] ?? 'GUIDERAIL_MODEL');
|
||||
$imageFile = $this->getImageFile($id, $item['image_file_id'] ?? null);
|
||||
|
||||
return view('bending.products.form', ['item' => $item, 'mode' => 'view', 'config' => $config, 'imageFile' => $imageFile]);
|
||||
}
|
||||
|
||||
public function create(string $category = 'GUIDERAIL_MODEL'): View
|
||||
{
|
||||
$config = $this->getConfig($category);
|
||||
|
||||
return view('bending.products.form', ['item' => null, 'mode' => 'create', 'config' => $config, 'category' => $category, 'imageFile' => null]);
|
||||
}
|
||||
|
||||
public function edit(int $id): View
|
||||
{
|
||||
$response = $this->api()->get("/api/v1/guiderail-models/{$id}");
|
||||
$item = $response->successful() ? $response->json('data') : null;
|
||||
abort_unless($item, 404);
|
||||
|
||||
$item = $this->enrichComponentsWithSamIds($item);
|
||||
$config = $this->getConfig($item['item_category'] ?? 'GUIDERAIL_MODEL');
|
||||
$imageFile = $this->getImageFile($id, $item['image_file_id'] ?? null);
|
||||
|
||||
return view('bending.products.form', ['item' => $item, 'mode' => 'edit', 'config' => $config, 'imageFile' => $imageFile]);
|
||||
}
|
||||
|
||||
public function store(Request $request, string $category = 'GUIDERAIL_MODEL')
|
||||
{
|
||||
$config = $this->getConfig($category);
|
||||
|
||||
if ($category === 'SHUTTERBOX_MODEL') {
|
||||
$rules = [
|
||||
'exit_direction' => 'required|string',
|
||||
'box_width' => 'required|numeric',
|
||||
'box_height' => 'required|numeric',
|
||||
];
|
||||
$messages = [
|
||||
'exit_direction.required' => '점검구 방향을 선택하세요.',
|
||||
'box_width.required' => '가로(폭)를 입력하세요.',
|
||||
'box_height.required' => '세로(높이)를 입력하세요.',
|
||||
];
|
||||
} else {
|
||||
$rules = [
|
||||
'item_sep' => 'required|string|max:20',
|
||||
'model_name' => 'required|string|max:50',
|
||||
];
|
||||
$messages = [
|
||||
'item_sep.required' => '대분류를 선택하세요.',
|
||||
'model_name.required' => '모델을 선택하세요.',
|
||||
];
|
||||
}
|
||||
|
||||
$request->validate($rules, $messages);
|
||||
|
||||
$data = $this->prepareApiData($request);
|
||||
$response = $this->api()->post('/api/v1/guiderail-models', $data);
|
||||
|
||||
if ($response->successful()) {
|
||||
$itemId = $response->json('data.id');
|
||||
|
||||
if ($itemId) {
|
||||
$this->handleImageUpload($request, $itemId);
|
||||
}
|
||||
|
||||
if ($itemId && $request->input('_redirect') === 'edit') {
|
||||
return redirect()->route("bending.{$config['prefix']}.edit", $itemId)->with('success', '등록 후 편집 모드로 전환되었습니다.');
|
||||
}
|
||||
|
||||
return redirect()->route("bending.{$config['prefix']}.index")->with('success', "{$config['label']} 모델이 등록되었습니다.");
|
||||
}
|
||||
|
||||
$body = $response->json();
|
||||
$apiErrors = $body['errors'] ?? $body['error']['details'] ?? $body['data']['errors'] ?? [];
|
||||
$apiMessage = $body['message'] ?? 'API 오류';
|
||||
$errorBag = ['api' => "[{$response->status()}] {$apiMessage}"];
|
||||
foreach ($apiErrors as $field => $msgs) {
|
||||
$errorBag["api_{$field}"] = "[{$field}] ".(is_array($msgs) ? implode(', ', $msgs) : $msgs);
|
||||
}
|
||||
|
||||
return back()->withErrors($errorBag)->withInput();
|
||||
}
|
||||
|
||||
public function update(Request $request, int $id)
|
||||
{
|
||||
$category = $request->input('item_category', 'GUIDERAIL_MODEL');
|
||||
|
||||
if ($category === 'SHUTTERBOX_MODEL') {
|
||||
$rules = [
|
||||
'exit_direction' => 'required|string',
|
||||
'box_width' => 'required|numeric',
|
||||
'box_height' => 'required|numeric',
|
||||
];
|
||||
$messages = [
|
||||
'exit_direction.required' => '점검구 방향을 선택하세요.',
|
||||
'box_width.required' => '가로(폭)를 입력하세요.',
|
||||
'box_height.required' => '세로(높이)를 입력하세요.',
|
||||
];
|
||||
} else {
|
||||
$rules = [
|
||||
'item_sep' => 'required|string|max:20',
|
||||
'model_name' => 'required|string|max:50',
|
||||
];
|
||||
$messages = [
|
||||
'item_sep.required' => '대분류를 선택하세요.',
|
||||
'model_name.required' => '모델을 선택하세요.',
|
||||
];
|
||||
}
|
||||
|
||||
$request->validate($rules, $messages);
|
||||
|
||||
$data = $this->prepareApiData($request);
|
||||
$response = $this->api()->put("/api/v1/guiderail-models/{$id}", $data);
|
||||
|
||||
if ($response->successful()) {
|
||||
$item = $response->json('data');
|
||||
$config = $this->getConfig($item['item_category'] ?? 'GUIDERAIL_MODEL');
|
||||
|
||||
$this->handleImageUpload($request, $id);
|
||||
|
||||
if ($request->input('_redirect') === 'edit') {
|
||||
return redirect()->route("bending.{$config['prefix']}.edit", $id)->with('success', '저장되었습니다.');
|
||||
}
|
||||
|
||||
return redirect()->route("bending.{$config['prefix']}.show", $id)->with('success', "{$config['label']} 모델이 수정되었습니다.");
|
||||
}
|
||||
|
||||
$body = $response->json();
|
||||
$apiErrors = $body['errors'] ?? $body['error']['details'] ?? $body['data']['errors'] ?? [];
|
||||
$apiMessage = $body['message'] ?? 'API 오류';
|
||||
$errorBag = ['api' => "[{$response->status()}] {$apiMessage}"];
|
||||
foreach ($apiErrors as $field => $msgs) {
|
||||
$errorBag["api_{$field}"] = "[{$field}] ".(is_array($msgs) ? implode(', ', $msgs) : $msgs);
|
||||
}
|
||||
|
||||
return back()->withErrors($errorBag)->withInput();
|
||||
}
|
||||
|
||||
public function destroy(Request $request, int $id)
|
||||
{
|
||||
$response = $this->api()->get("/api/v1/guiderail-models/{$id}");
|
||||
$item = $response->successful() ? $response->json('data') : null;
|
||||
$config = $this->getConfig($item['item_category'] ?? 'GUIDERAIL_MODEL');
|
||||
|
||||
$this->api()->delete("/api/v1/guiderail-models/{$id}");
|
||||
|
||||
return redirect()->route("bending.{$config['prefix']}.index")->with('success', "{$config['label']} 모델이 삭제되었습니다.");
|
||||
}
|
||||
|
||||
public function print(Request $request, int $id)
|
||||
{
|
||||
$response = $this->api()->get("/api/v1/guiderail-models/{$id}");
|
||||
$item = $response->successful() ? $response->json('data') : null;
|
||||
abort_unless($item, 404);
|
||||
|
||||
return view('bending.products.print', ['item' => $item]);
|
||||
}
|
||||
|
||||
public function searchBendingItems(Request $request)
|
||||
{
|
||||
$params = $request->only(['item_sep', 'item_bending', 'material', 'search', 'legacy_bending_num', 'size']);
|
||||
$params['size'] = $params['size'] ?? 100;
|
||||
|
||||
$response = $this->api()->get('/api/v1/bending-items', $params);
|
||||
$body = $response->successful() ? $response->json('data', []) : [];
|
||||
|
||||
return response()->json([
|
||||
'data' => $body['data'] ?? [],
|
||||
'total' => $body['total'] ?? 0,
|
||||
]);
|
||||
}
|
||||
|
||||
private function enrichComponentsWithSamIds(array $item): array
|
||||
{
|
||||
if (empty($item['components'])) {
|
||||
return $item;
|
||||
}
|
||||
|
||||
$legacyNums = array_filter(array_column($item['components'], 'legacy_bending_num'));
|
||||
if (empty($legacyNums)) {
|
||||
return $item;
|
||||
}
|
||||
|
||||
$response = $this->api()->get('/api/v1/bending-items', ['size' => 200]);
|
||||
$allItems = $response->successful() ? ($response->json('data.data') ?? []) : [];
|
||||
|
||||
$numToId = [];
|
||||
foreach ($allItems as $samItem) {
|
||||
$lbn = $samItem['legacy_bending_num'] ?? null;
|
||||
if ($lbn !== null) {
|
||||
$numToId[(string) $lbn] = $samItem['id'];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($item['components'] as &$comp) {
|
||||
$lbn = $comp['legacy_bending_num'] ?? null;
|
||||
$comp['sam_item_id'] = $lbn !== null ? ($numToId[(string) $lbn] ?? null) : null;
|
||||
}
|
||||
unset($comp);
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
private function getImageFile(int $itemId, ?int $imageFileId = null): ?array
|
||||
{
|
||||
// API Resource에서 image_file_id를 이미 반환 → 그대로 사용
|
||||
if ($imageFileId) {
|
||||
return ['id' => $imageFileId];
|
||||
}
|
||||
|
||||
// fallback: fileable 기반 조회
|
||||
$response = $this->api()->get("/api/v1/items/{$itemId}/files", ['field_key' => 'bending_diagram']);
|
||||
if ($response->successful()) {
|
||||
$files = $response->json('data.bending_diagram', []);
|
||||
if (! empty($files)) {
|
||||
return $files[0];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function uploadImage(int $itemId, \Illuminate\Http\UploadedFile $file): ?array
|
||||
{
|
||||
$existing = $this->getImageFile($itemId);
|
||||
|
||||
$postData = ['field_key' => 'bending_diagram'];
|
||||
if ($existing) {
|
||||
$postData['file_id'] = $existing['id'];
|
||||
}
|
||||
|
||||
$response = $this->api()
|
||||
->attach('file', $file->getContent(), $file->getClientOriginalName())
|
||||
->post("/api/v1/items/{$itemId}/files", $postData);
|
||||
|
||||
if (! $response->successful()) {
|
||||
\Log::error('Model image upload failed', [
|
||||
'itemId' => $itemId,
|
||||
'status' => $response->status(),
|
||||
'body' => $response->json(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $response->successful() ? $response->json('data') : null;
|
||||
}
|
||||
|
||||
private function handleImageUpload(Request $request, int $itemId): void
|
||||
{
|
||||
if ($request->hasFile('image')) {
|
||||
$this->uploadImage($itemId, $request->file('image'));
|
||||
} elseif ($request->filled('canvas_image')) {
|
||||
$this->uploadCanvasImage($itemId, $request->input('canvas_image'));
|
||||
}
|
||||
}
|
||||
|
||||
private function uploadCanvasImage(int $itemId, string $dataURL): ?array
|
||||
{
|
||||
if (! preg_match('/^data:image\/(\w+);base64,/', $dataURL, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ext = $matches[1] === 'jpeg' ? 'jpg' : $matches[1];
|
||||
$binary = base64_decode(substr($dataURL, strpos($dataURL, ',') + 1));
|
||||
|
||||
if ($binary === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tmpPath = tempnam(sys_get_temp_dir(), 'canvas_').'.'.$ext;
|
||||
file_put_contents($tmpPath, $binary);
|
||||
|
||||
try {
|
||||
$file = new \Illuminate\Http\UploadedFile($tmpPath, "canvas.{$ext}", "image/{$ext}", null, true);
|
||||
|
||||
return $this->uploadImage($itemId, $file);
|
||||
} finally {
|
||||
@unlink($tmpPath);
|
||||
}
|
||||
}
|
||||
|
||||
private function prepareApiData(Request $request): array
|
||||
{
|
||||
$data = $request->except(['_token', '_method', '_redirect', 'image', 'canvas_image']);
|
||||
|
||||
if (isset($data['components']) && is_string($data['components'])) {
|
||||
$decoded = json_decode($data['components'], true);
|
||||
$data['components'] = is_array($decoded) ? $decoded : null;
|
||||
}
|
||||
if (isset($data['material_summary']) && is_string($data['material_summary'])) {
|
||||
$decoded = json_decode($data['material_summary'], true);
|
||||
$data['material_summary'] = is_array($decoded) ? $decoded : null;
|
||||
}
|
||||
|
||||
// 빈 문자열도 전송 (기존 값 삭제 가능하도록) — null만 제거
|
||||
$data = array_filter($data, fn ($v) => $v !== null);
|
||||
|
||||
if (empty($data['code'])) {
|
||||
$modelName = $data['model_name'] ?? '';
|
||||
$itemSep = $data['item_sep'] ?? '';
|
||||
$data['code'] = trim("{$itemSep}_{$modelName}_".date('ymd_His'));
|
||||
}
|
||||
if (empty($data['name'])) {
|
||||
$data['name'] = $data['model_name'] ?? $data['code'];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -130,63 +130,6 @@ public function blockEdit(int $id): View
|
||||
/**
|
||||
* 현재 선택된 테넌트 조회
|
||||
*/
|
||||
/**
|
||||
* API에서 presigned URL 발급
|
||||
*/
|
||||
private function getPresignedUrlFromApi(int $fileId): ?string
|
||||
{
|
||||
$baseUrl = config('services.api.base_url', 'https://api.sam.kr');
|
||||
$internalUrl = config('services.api.internal_url');
|
||||
$apiKey = config('services.api.key');
|
||||
$token = session('api_access_token', '');
|
||||
|
||||
$headers = [
|
||||
'X-API-KEY' => $apiKey,
|
||||
'X-TENANT-ID' => session('selected_tenant_id', 1),
|
||||
];
|
||||
|
||||
if ($internalUrl) {
|
||||
$headers['Host'] = parse_url($baseUrl, PHP_URL_HOST) ?: 'api.sam.kr';
|
||||
$baseUrl = $internalUrl;
|
||||
}
|
||||
|
||||
$response = \Illuminate\Support\Facades\Http::baseUrl($baseUrl)
|
||||
->withoutVerifying()
|
||||
->withHeaders($headers)
|
||||
->withToken($token)
|
||||
->timeout(10)
|
||||
->get("/api/v1/files/{$fileId}/presigned-url");
|
||||
|
||||
return $response->successful() ? $response->json('data.url') : null;
|
||||
}
|
||||
|
||||
private function getPresignedUrlByPath(string $path): ?string
|
||||
{
|
||||
$baseUrl = config('services.api.base_url', 'https://api.sam.kr');
|
||||
$internalUrl = config('services.api.internal_url');
|
||||
$apiKey = config('services.api.key');
|
||||
$token = session('api_access_token', '');
|
||||
|
||||
$headers = [
|
||||
'X-API-KEY' => $apiKey,
|
||||
'X-TENANT-ID' => session('selected_tenant_id', 1),
|
||||
];
|
||||
|
||||
if ($internalUrl) {
|
||||
$headers['Host'] = parse_url($baseUrl, PHP_URL_HOST) ?: 'api.sam.kr';
|
||||
$baseUrl = $internalUrl;
|
||||
}
|
||||
|
||||
$response = \Illuminate\Support\Facades\Http::baseUrl($baseUrl)
|
||||
->withoutVerifying()
|
||||
->withHeaders($headers)
|
||||
->withToken($token)
|
||||
->timeout(10)
|
||||
->post('/api/v1/files/presigned-url-by-path', ['path' => $path]);
|
||||
|
||||
return $response->successful() ? $response->json('data.url') : null;
|
||||
}
|
||||
|
||||
private function getCurrentTenant(): ?Tenant
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* R2 파일 Presigned URL 리다이렉트
|
||||
*
|
||||
* API에서 R2 presigned URL을 발급받아 브라우저를 직접 R2로 리다이렉트.
|
||||
* presigned URL은 5분간 캐시하여 동일 이미지 반복 요청 시 API 호출 최소화.
|
||||
*/
|
||||
class FileViewController extends Controller
|
||||
{
|
||||
public function show(int $id)
|
||||
{
|
||||
$cacheKey = "file_presigned_url:{$id}";
|
||||
|
||||
$url = Cache::remember($cacheKey, now()->addMinutes(5), function () use ($id) {
|
||||
$baseUrl = config('services.api.base_url', 'https://api.sam.kr');
|
||||
$internalUrl = config('services.api.internal_url');
|
||||
$apiKey = config('services.api.key');
|
||||
$token = session('api_access_token', '');
|
||||
|
||||
$headers = [
|
||||
'X-API-KEY' => $apiKey,
|
||||
'X-TENANT-ID' => session('selected_tenant_id', 1),
|
||||
];
|
||||
|
||||
if ($internalUrl) {
|
||||
$headers['Host'] = parse_url($baseUrl, PHP_URL_HOST) ?: 'api.sam.kr';
|
||||
$baseUrl = $internalUrl;
|
||||
}
|
||||
|
||||
$response = Http::baseUrl($baseUrl)
|
||||
->withoutVerifying()
|
||||
->withHeaders($headers)
|
||||
->withToken($token)
|
||||
->timeout(10)
|
||||
->get("/api/v1/files/{$id}/presigned-url");
|
||||
|
||||
if (! $response->successful()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $response->json('data.url');
|
||||
});
|
||||
|
||||
if (! $url) {
|
||||
Cache::forget($cacheKey);
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return redirect($url);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user