Files
Gnuboard7/app/Http/Controllers/Api/Admin/LayoutController.php
T
HeuJung 19789433df perf(core): 레이아웃 목록 응답에서 편집 대상 아닌 본문 제외
레이아웃 코드 편집 화면이 목록을 부를 때 모든 레이아웃의 본문(content)까지 함께
내려받고 있었다. 레이아웃이 많은 템플릿에서는 목록을 여는 것만으로 수십 MB 가
브라우저에 쌓여 화면이 크게 느려졌고, 디버그 모드에서는 탭이 메모리 부족으로
종료됐다. 목록 화면은 본문을 쓰지 않고 선택한 파일 하나만 편집한다.

목록 전용 Resource 를 두어 이름·설명·크기·수정일만 싣고, 본문은 상세 조회에서만
읽는다. 크기는 목록에서 표시하므로 저장소가 길이만 계산해 돌려준다.
2026-08-04 09:22:00 +09:00

268 lines
8.9 KiB
PHP

<?php
namespace App\Http\Controllers\Api\Admin;
use App\Exceptions\ConcurrentModificationException;
use App\Http\Controllers\Api\Base\AdminBaseController;
use App\Http\Requests\Layout\StoreLayoutPreviewRequest;
use App\Http\Requests\Layout\UpdateLayoutContentRequest;
use App\Http\Resources\LayoutListResource;
use App\Http\Resources\LayoutResource;
use App\Http\Resources\LayoutVersionResource;
use App\Services\LayoutPreviewService;
use App\Services\LayoutService;
use App\Services\TemplateService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
class LayoutController extends AdminBaseController
{
public function __construct(
private LayoutService $layoutService,
private TemplateService $templateService,
private LayoutPreviewService $layoutPreviewService
) {
parent::__construct();
}
/**
* 특정 템플릿의 모든 레이아웃 목록 조회
*
* @param string $templateName 템플릿 identifier
* @return JsonResponse 레이아웃 목록 응답
*/
public function index(string $templateName): JsonResponse
{
$template = $this->templateService->findByIdentifier($templateName);
if (! $template) {
return $this->notFound('common.not_found');
}
// 목록은 본문(`content`)을 담지 않는 경량 조회를 쓴다 — 파일 목록 화면은 이름·설명·
// 크기·수정일만 표시하고, 편집 대상 본문은 상세 엔드포인트가 따로 제공한다.
// 종전에는 목록·상세가 같은 Resource 를 공용해 102개 레이아웃의 본문이 전부 실렸고
// (응답 18.30MB 중 content 17.41MB), 디버그 모드에서 렌더러가 메모리 한계를 넘었다.
$layouts = $this->layoutService->getLayoutListByTemplateId($template->id);
// 레이아웃 이름 → 라우트 path 매핑 — 코드 편집기가 파일 선택 시 ?route= URL
// 동기화 / 위지윅에서 넘어온 ?route= 로 해당 파일 복원에 사용한다.
$routePathMap = $this->templateService->getLayoutRoutePathMap($templateName);
$collection = LayoutListResource::collection($layouts);
$collection->collection->transform(
fn (LayoutListResource $resource) => $resource->withRoutePathMap($routePathMap)
);
return $this->success('common.success', $collection);
}
/**
* 특정 레이아웃 상세 조회
*
* @param string $templateName 템플릿 identifier
* @param string $name 레이아웃 이름
* @return JsonResponse 레이아웃 상세 응답
*/
public function show(string $templateName, string $name): JsonResponse
{
$template = $this->templateService->findByIdentifier($templateName);
if (! $template) {
return $this->notFound('common.not_found');
}
$layout = $this->layoutService->getLayoutByName($template->id, $name);
if (! $layout) {
return $this->notFound('common.not_found');
}
return $this->success(
'common.success',
new LayoutResource($layout)
);
}
/**
* 레이아웃 수정
*
* @param UpdateLayoutContentRequest $request 레이아웃 수정 요청
* @param string $templateName 템플릿 identifier
* @param string $name 레이아웃 이름
* @return JsonResponse 수정된 레이아웃 응답
*/
public function update(UpdateLayoutContentRequest $request, string $templateName, string $name): JsonResponse
{
$template = $this->templateService->findByIdentifier($templateName);
if (! $template) {
return $this->notFound('common.not_found');
}
try {
DB::beginTransaction();
$layout = $this->layoutService->updateLayout($template->id, $name, $request->validated());
DB::commit();
return $this->success(
'common.success',
new LayoutResource($layout)
);
} catch (ConcurrentModificationException $e) {
DB::rollBack();
return $this->error(
'exceptions.concurrent_modification',
409,
[
'error' => 'concurrent_modification',
'current_version' => $e->currentVersion,
'your_version' => $e->expectedVersion,
'resource' => $e->resource,
],
['resource' => $e->resource, 'current' => $e->currentVersion, 'expected' => $e->expectedVersion],
);
} catch (\Exception $e) {
DB::rollBack();
return $this->error(
'common.failed',
500,
['error' => $e->getMessage()]
);
}
}
/**
* 레이아웃의 모든 버전 목록 조회
*
* @param string $templateName 템플릿 identifier
* @param string $name 레이아웃 이름
* @return JsonResponse 버전 목록 응답
*/
public function versions(string $templateName, string $name): JsonResponse
{
$template = $this->templateService->findByIdentifier($templateName);
if (! $template) {
return $this->notFound('common.not_found');
}
$layout = $this->layoutService->getLayoutByName($template->id, $name);
if (! $layout) {
return $this->notFound('common.not_found');
}
$versions = $this->layoutService->getLayoutVersions($template->id, $name);
return $this->success(
'common.success',
LayoutVersionResource::collection($versions)
);
}
/**
* 특정 버전의 레이아웃 content 조회
*
* @param string $templateName 템플릿 identifier
* @param string $name 레이아웃 이름
* @param int $version 버전 번호
* @return JsonResponse 버전 content 응답
*/
public function showVersion(string $templateName, string $name, int $version): JsonResponse
{
$template = $this->templateService->findByIdentifier($templateName);
if (! $template) {
return $this->notFound('common.not_found');
}
$layoutVersion = $this->layoutService->getLayoutVersion($template->id, $name, $version);
if (! $layoutVersion) {
return $this->notFound('common.not_found');
}
return $this->success(
'common.success',
// 버전 비교 diff 용 — content 원본 전체(slots/extends 등 포함) 노출.
(new LayoutVersionResource($layoutVersion))->withFullContent()
);
}
/**
* 버전 복원
*
* @param string $templateName 템플릿 identifier
* @param string $name 레이아웃 이름
* @param int $versionId 버전 ID
* @return JsonResponse 복원된 버전 응답
*/
public function restoreVersion(string $templateName, string $name, int $versionId): JsonResponse
{
$template = $this->templateService->findByIdentifier($templateName);
if (! $template) {
return $this->notFound('common.not_found');
}
$newVersion = $this->layoutService->restoreVersion($template->id, $name, $versionId);
if (! $newVersion) {
return $this->notFound('common.not_found');
}
return $this->success(
'common.success',
new LayoutVersionResource($newVersion)
);
}
/**
* 레이아웃 미리보기 생성
*
* 편집 중인 레이아웃 content를 임시 저장하고 미리보기 URL을 반환합니다.
*
* @param StoreLayoutPreviewRequest $request 미리보기 생성 요청
* @param string $templateName 템플릿 identifier
* @param string $name 레이아웃 이름
* @return JsonResponse 미리보기 토큰/URL 응답
*/
public function storePreview(StoreLayoutPreviewRequest $request, string $templateName, string $name): JsonResponse
{
$template = $this->templateService->findByIdentifier($templateName);
if (! $template) {
return $this->notFound('common.not_found');
}
try {
$preview = $this->layoutPreviewService->createPreview(
$template->id,
$name,
$request->validated('content'),
$request->user()->id
);
return $this->success(
'common.success',
[
'token' => $preview->token,
'preview_url' => '/preview/'.$preview->token,
'expires_at' => $preview->expires_at->toIso8601String(),
]
);
} catch (\Exception $e) {
return $this->error(
'common.failed',
500,
['error' => $e->getMessage()]
);
}
}
}