Files
sam-manage/resources/views/document-templates/partials/block-properties.blade.php
김보곤 86cc18020a fix: [document] 블록 빌더 Blade 이스케이프 오류 수정
- {{today}} → @{{today}} (Blade가 PHP 상수로 해석하는 문제)
2026-02-28 19:54:34 +09:00

415 lines
26 KiB
PHP

{{-- Block Properties Panel (우측 사이드바) --}}
<div class="block-properties bg-white border-l border-gray-200 overflow-y-auto shrink-0">
{{-- 미선택 상태 --}}
<div x-show="!getSelectedBlock()" class="flex flex-col items-center justify-center h-full text-gray-400 p-6">
<svg class="w-10 h-10 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
<p class="text-sm">블록을 선택하면</p>
<p class="text-sm">속성을 편집할 있습니다</p>
</div>
{{-- 선택된 블록 속성 편집 --}}
<div x-show="getSelectedBlock()" class="p-3">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-semibold text-gray-700">
<span x-text="blockTypes[getSelectedBlock()?.type]?.label || ''"></span> 속성
</h3>
<button @click="selectedBlockId = null" class="text-gray-400 hover:text-gray-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
{{-- ===== Heading 속성 ===== --}}
<template x-if="getSelectedBlock()?.type === 'heading'">
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">텍스트</label>
<input type="text" :value="getSelectedBlock()?.props.text"
@input="updateBlockProp('text', $event.target.value); pushHistory()"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">레벨</label>
<select :value="getSelectedBlock()?.props.level"
@change="updateBlockProp('level', parseInt($event.target.value)); pushHistory()"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500">
<option value="1">H1 가장 제목</option>
<option value="2">H2 제목</option>
<option value="3">H3 중간 제목</option>
<option value="4">H4 작은 제목</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">정렬</label>
<div class="flex border border-gray-200 rounded-md overflow-hidden">
<button @click="updateBlockProp('align', 'left'); pushHistory()"
:class="getSelectedBlock()?.props.align === 'left' ? 'bg-blue-50 text-blue-700' : 'text-gray-500 hover:bg-gray-50'"
class="flex-1 py-1.5 text-xs font-medium">왼쪽</button>
<button @click="updateBlockProp('align', 'center'); pushHistory()"
:class="getSelectedBlock()?.props.align === 'center' ? 'bg-blue-50 text-blue-700' : 'text-gray-500 hover:bg-gray-50'"
class="flex-1 py-1.5 text-xs font-medium border-l border-gray-200">가운데</button>
<button @click="updateBlockProp('align', 'right'); pushHistory()"
:class="getSelectedBlock()?.props.align === 'right' ? 'bg-blue-50 text-blue-700' : 'text-gray-500 hover:bg-gray-50'"
class="flex-1 py-1.5 text-xs font-medium border-l border-gray-200">오른쪽</button>
</div>
</div>
</div>
</template>
{{-- ===== Paragraph 속성 ===== --}}
<template x-if="getSelectedBlock()?.type === 'paragraph'">
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">텍스트</label>
<textarea :value="getSelectedBlock()?.props.text"
@input="updateBlockProp('text', $event.target.value); pushHistory()"
rows="4"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500 focus:border-blue-500"></textarea>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">정렬</label>
<div class="flex border border-gray-200 rounded-md overflow-hidden">
<button @click="updateBlockProp('align', 'left'); pushHistory()"
:class="getSelectedBlock()?.props.align === 'left' ? 'bg-blue-50 text-blue-700' : 'text-gray-500 hover:bg-gray-50'"
class="flex-1 py-1.5 text-xs font-medium">왼쪽</button>
<button @click="updateBlockProp('align', 'center'); pushHistory()"
:class="getSelectedBlock()?.props.align === 'center' ? 'bg-blue-50 text-blue-700' : 'text-gray-500 hover:bg-gray-50'"
class="flex-1 py-1.5 text-xs font-medium border-l border-gray-200">가운데</button>
<button @click="updateBlockProp('align', 'right'); pushHistory()"
:class="getSelectedBlock()?.props.align === 'right' ? 'bg-blue-50 text-blue-700' : 'text-gray-500 hover:bg-gray-50'"
class="flex-1 py-1.5 text-xs font-medium border-l border-gray-200">오른쪽</button>
</div>
</div>
</div>
</template>
{{-- ===== Table 속성 ===== --}}
<template x-if="getSelectedBlock()?.type === 'table'">
<div class="space-y-3">
<div>
<label class="flex items-center gap-2 text-xs font-medium text-gray-500">
<input type="checkbox" :checked="getSelectedBlock()?.props.showHeader !== false"
@change="updateBlockProp('showHeader', $event.target.checked); pushHistory()"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
헤더 표시
</label>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">헤더</label>
<template x-for="(header, hIdx) in getSelectedBlock()?.props.headers" :key="'ph_' + hIdx">
<div class="flex items-center gap-1 mb-1">
<input type="text" :value="header"
@input="let b = getSelectedBlock(); b.props.headers[hIdx] = $event.target.value; markDirty()"
@change="pushHistory()"
class="prop-input flex-1 border border-gray-200 rounded px-2 py-1 focus:ring-1 focus:ring-blue-500">
<button @click="removeTableColumn(getSelectedBlock(), hIdx)"
class="text-gray-400 hover:text-red-500 shrink-0" title="열 삭제">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</template>
<button @click="addTableColumn(getSelectedBlock())"
class="text-xs text-blue-600 hover:text-blue-800 mt-1">+ 추가</button>
</div>
<div>
<div class="flex items-center justify-between mb-1">
<label class="text-xs font-medium text-gray-500">데이터 </label>
<span class="text-xs text-gray-400" x-text="getSelectedBlock()?.props.rows?.length + '행'"></span>
</div>
<template x-for="(row, rIdx) in getSelectedBlock()?.props.rows" :key="'pr_' + rIdx">
<div class="flex items-center gap-1 mb-1">
<span class="text-[10px] text-gray-400 shrink-0 w-4" x-text="rIdx + 1"></span>
<template x-for="(cell, cIdx) in row" :key="'pc_' + rIdx + '_' + cIdx">
<input type="text" :value="cell"
@input="let b = getSelectedBlock(); b.props.rows[rIdx][cIdx] = $event.target.value; markDirty()"
@change="pushHistory()"
class="prop-input flex-1 border border-gray-200 rounded px-1.5 py-0.5 focus:ring-1 focus:ring-blue-500"
style="min-width: 0;">
</template>
<button @click="removeTableRow(getSelectedBlock(), rIdx)"
class="text-gray-400 hover:text-red-500 shrink-0">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</template>
<button @click="addTableRow(getSelectedBlock())"
class="text-xs text-blue-600 hover:text-blue-800 mt-1">+ 추가</button>
</div>
</div>
</template>
{{-- ===== Columns 속성 ===== --}}
<template x-if="getSelectedBlock()?.type === 'columns'">
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1"> </label>
<select :value="getSelectedBlock()?.props.count"
@change="
let b = getSelectedBlock();
let newCount = parseInt($event.target.value);
let old = b.props.children || [];
while (old.length < newCount) old.push([]);
b.props.children = old.slice(0, newCount);
b.props.count = newCount;
pushHistory();
markDirty();
"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500">
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
</div>
<p class="text-xs text-gray-400">
Phase 2에서 내부에 블록을 추가하는 기능이 구현됩니다.
</p>
</div>
</template>
{{-- ===== Divider 속성 ===== --}}
<template x-if="getSelectedBlock()?.type === 'divider'">
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">스타일</label>
<select :value="getSelectedBlock()?.props.style"
@change="updateBlockProp('style', $event.target.value); pushHistory()"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500">
<option value="solid">실선</option>
<option value="dashed">점선</option>
</select>
</div>
</div>
</template>
{{-- ===== Spacer 속성 ===== --}}
<template x-if="getSelectedBlock()?.type === 'spacer'">
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">높이 (px)</label>
<input type="number" :value="getSelectedBlock()?.props.height"
@input="updateBlockProp('height', parseInt($event.target.value) || 20); pushHistory()"
min="5" max="200" step="5"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500">
</div>
</div>
</template>
{{-- ===== Text Field 속성 ===== --}}
<template x-if="getSelectedBlock()?.type === 'text_field'">
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">라벨</label>
<input type="text" :value="getSelectedBlock()?.props.label"
@input="updateBlockProp('label', $event.target.value); pushHistory()"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500">
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">플레이스홀더</label>
<input type="text" :value="getSelectedBlock()?.props.placeholder"
@input="updateBlockProp('placeholder', $event.target.value); pushHistory()"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500">
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">데이터 바인딩</label>
<input type="text" :value="getSelectedBlock()?.props.binding"
@input="updateBlockProp('binding', $event.target.value); pushHistory()"
placeholder="예: item.name, lot.number"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500">
</div>
<div>
<label class="flex items-center gap-2 text-xs font-medium text-gray-500">
<input type="checkbox" :checked="getSelectedBlock()?.props.required"
@change="updateBlockProp('required', $event.target.checked); pushHistory()"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
필수 입력
</label>
</div>
</div>
</template>
{{-- ===== Number Field 속성 ===== --}}
<template x-if="getSelectedBlock()?.type === 'number_field'">
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">라벨</label>
<input type="text" :value="getSelectedBlock()?.props.label"
@input="updateBlockProp('label', $event.target.value); pushHistory()"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500">
</div>
<div class="grid grid-cols-2 gap-2">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">최소값</label>
<input type="number" :value="getSelectedBlock()?.props.min"
@input="updateBlockProp('min', $event.target.value ? Number($event.target.value) : null); pushHistory()"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500">
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">최대값</label>
<input type="number" :value="getSelectedBlock()?.props.max"
@input="updateBlockProp('max', $event.target.value ? Number($event.target.value) : null); pushHistory()"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500">
</div>
</div>
<div class="grid grid-cols-2 gap-2">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">단위</label>
<input type="text" :value="getSelectedBlock()?.props.unit"
@input="updateBlockProp('unit', $event.target.value); pushHistory()"
placeholder="mm, kg"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500">
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">소수점</label>
<input type="number" :value="getSelectedBlock()?.props.decimal"
@input="updateBlockProp('decimal', parseInt($event.target.value) || 0); pushHistory()"
min="0" max="6"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500">
</div>
</div>
</div>
</template>
{{-- ===== Date Field 속성 ===== --}}
<template x-if="getSelectedBlock()?.type === 'date_field'">
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">라벨</label>
<input type="text" :value="getSelectedBlock()?.props.label"
@input="updateBlockProp('label', $event.target.value); pushHistory()"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500">
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">바인딩</label>
<input type="text" :value="getSelectedBlock()?.props.binding"
@input="updateBlockProp('binding', $event.target.value); pushHistory()"
placeholder="예: @{{today}}"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500">
</div>
</div>
</template>
{{-- ===== Select Field 속성 ===== --}}
<template x-if="getSelectedBlock()?.type === 'select_field'">
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">라벨</label>
<input type="text" :value="getSelectedBlock()?.props.label"
@input="updateBlockProp('label', $event.target.value); pushHistory()"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500">
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">옵션</label>
<template x-for="(opt, oi) in getSelectedBlock()?.props.options" :key="'so_' + oi">
<div class="flex items-center gap-1 mb-1">
<input type="text" :value="opt"
@input="let b = getSelectedBlock(); b.props.options[oi] = $event.target.value; markDirty()"
@change="pushHistory()"
class="prop-input flex-1 border border-gray-200 rounded px-2 py-1 focus:ring-1 focus:ring-blue-500">
<button @click="let b = getSelectedBlock(); b.props.options.splice(oi, 1); pushHistory(); markDirty()"
class="text-gray-400 hover:text-red-500 shrink-0">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</template>
<button @click="let b = getSelectedBlock(); b.props.options.push('새 옵션'); pushHistory(); markDirty()"
class="text-xs text-blue-600 hover:text-blue-800 mt-1">+ 옵션 추가</button>
</div>
<div>
<label class="flex items-center gap-2 text-xs font-medium text-gray-500">
<input type="checkbox" :checked="getSelectedBlock()?.props.multiple"
@change="updateBlockProp('multiple', $event.target.checked); pushHistory()"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
다중 선택
</label>
</div>
</div>
</template>
{{-- ===== Checkbox Field 속성 ===== --}}
<template x-if="getSelectedBlock()?.type === 'checkbox_field'">
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">라벨</label>
<input type="text" :value="getSelectedBlock()?.props.label"
@input="updateBlockProp('label', $event.target.value); pushHistory()"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500">
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">항목</label>
<template x-for="(opt, oi) in getSelectedBlock()?.props.options" :key="'co_' + oi">
<div class="flex items-center gap-1 mb-1">
<input type="text" :value="opt"
@input="let b = getSelectedBlock(); b.props.options[oi] = $event.target.value; markDirty()"
@change="pushHistory()"
class="prop-input flex-1 border border-gray-200 rounded px-2 py-1 focus:ring-1 focus:ring-blue-500">
<button @click="let b = getSelectedBlock(); b.props.options.splice(oi, 1); pushHistory(); markDirty()"
class="text-gray-400 hover:text-red-500 shrink-0">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</template>
<button @click="let b = getSelectedBlock(); b.props.options.push('새 항목'); pushHistory(); markDirty()"
class="text-xs text-blue-600 hover:text-blue-800 mt-1">+ 항목 추가</button>
</div>
</div>
</template>
{{-- ===== Textarea Field 속성 ===== --}}
<template x-if="getSelectedBlock()?.type === 'textarea_field'">
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">라벨</label>
<input type="text" :value="getSelectedBlock()?.props.label"
@input="updateBlockProp('label', $event.target.value); pushHistory()"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500">
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1"> </label>
<input type="number" :value="getSelectedBlock()?.props.rows"
@input="updateBlockProp('rows', parseInt($event.target.value) || 3); pushHistory()"
min="1" max="20"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500">
</div>
</div>
</template>
{{-- ===== Signature Field 속성 ===== --}}
<template x-if="getSelectedBlock()?.type === 'signature_field'">
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">라벨</label>
<input type="text" :value="getSelectedBlock()?.props.label"
@input="updateBlockProp('label', $event.target.value); pushHistory()"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500">
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">서명자</label>
<input type="text" :value="getSelectedBlock()?.props.signer"
@input="updateBlockProp('signer', $event.target.value); pushHistory()"
placeholder="예: inspector, manager"
class="prop-input w-full border border-gray-200 rounded-md px-2 py-1.5 focus:ring-1 focus:ring-blue-500">
</div>
</div>
</template>
{{-- ===== 공통: 블록 ID (디버그) ===== --}}
<div class="mt-6 pt-3 border-t border-gray-100">
<div class="text-[10px] text-gray-300">
ID: <span x-text="getSelectedBlock()?.id"></span>
</div>
</div>
</div>
</div>