Files
SPipe/src/BatchPropertyEditor.tsx
T
bjkim1 21ebb16fb7 SPipe 데모 v2 (2026-10-30) 서버 배포본
- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음)
- README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리
- 패치내역을 docs/로 이동, _worklog.md 추가
- 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
2026-10-30 11:00:46 +09:00

735 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState } from 'react';
import {
Trash2,
Copy,
X,
Check,
Move,
Layers,
ArrowRight,
Sliders,
Maximize2,
} from 'lucide-react';
import type { NetworkModel, InpNode, InpPipe } from './inpModel';
import type { MultiSelectionState, SelectionTarget } from './types';
import {
m3DayToFlow,
metersToLength,
millimetersToDiameter,
type DisplayUnitMode,
} from './unitSystem';
export interface BatchPropertyEditorProps {
model: NetworkModel;
multiSelection: MultiSelectionState;
selectedTarget: SelectionTarget;
displayUnitMode?: DisplayUnitMode;
onUpdatePipes: (pipeIds: string[], updates: Partial<InpPipe>) => void;
onUpdateNodes: (nodeIds: string[], updater: (node: InpNode) => Partial<InpNode>) => void;
onMoveNodes: (nodeIds: string[], dx: number, dy: number) => void;
onDeleteSelected: () => void;
onClearSelection: () => void;
onSelectTarget: (target: SelectionTarget) => void;
onCopyClipboard?: () => void;
}
export const BatchPropertyEditor: React.FC<BatchPropertyEditorProps> = ({
model,
multiSelection,
selectedTarget,
displayUnitMode = 'korean',
onUpdatePipes,
onUpdateNodes,
onMoveNodes,
onDeleteSelected,
onClearSelection,
onSelectTarget,
onCopyClipboard,
}) => {
// Extract all currently selected node IDs and pipe IDs
const selectedNodeIds = Array.from(
new Set([
...multiSelection.nodes,
...(selectedTarget?.type === 'node' ? [selectedTarget.id] : []),
])
);
const selectedPipeIds = Array.from(
new Set([
...multiSelection.links,
...(selectedTarget?.type === 'link' ? [selectedTarget.id] : []),
])
);
// Pipe batch state
const [batchDiameter, setBatchDiameter] = useState<string>('');
const [batchRoughness, setBatchRoughness] = useState<string>('');
const [batchStatus, setBatchStatus] = useState<string>('OPEN');
const [batchMinorLoss, setBatchMinorLoss] = useState<string>('');
const [batchTag, setBatchTag] = useState<string>('');
const [batchDesc, setBatchDesc] = useState<string>('');
// Node batch state
const [batchElevMode, setBatchElevMode] = useState<'set' | 'offset'>('offset');
const [batchElevValue, setBatchElevValue] = useState<string>('');
const [batchDemandMode, setBatchDemandMode] = useState<'set' | 'mult'>('mult');
const [batchDemandValue, setBatchDemandValue] = useState<string>('1.0');
const [batchPattern, setBatchPattern] = useState<string>('');
// Move offset state
const [moveDx, setMoveDx] = useState<string>('0');
const [moveDy, setMoveDy] = useState<string>('0');
// Success flash messages
const [flashMsg, setFlashMsg] = useState<string | null>(null);
const notify = (msg: string) => {
setFlashMsg(msg);
setTimeout(() => setFlashMsg(null), 2500);
};
// 1. Pipe actions
// The batch editor's inputs are always entered in the fixed Korean/metric
// display units shown in its labels (mm / m / m³/day), regardless of the
// app-wide displayUnitMode toggle, so they always go through the display ->
// model conversion (never a raw pass-through).
const applyDiameter = (diamMm: number) => {
if (!Number.isFinite(diamMm) || diamMm <= 0) return;
const sourceUnits = model.options.units || 'LPS';
const internalDiam = millimetersToDiameter(diamMm, sourceUnits);
onUpdatePipes(selectedPipeIds, { diameter: internalDiam });
notify(`관경 D${diamMm} 일괄 적용 (${selectedPipeIds.length}개 관로)`);
};
const applyRoughness = (roughness: number) => {
if (!Number.isFinite(roughness) || roughness <= 0) return;
onUpdatePipes(selectedPipeIds, { roughness });
notify(`조도계수 C=${roughness} 일괄 적용 (${selectedPipeIds.length}개 관로)`);
};
const applyStatus = (status: 'OPEN' | 'CLOSED' | 'CV') => {
onUpdatePipes(selectedPipeIds, { status });
notify(`관로 상태 '${status}' 일괄 적용 (${selectedPipeIds.length}개 관로)`);
};
const applyMinorLoss = () => {
const val = parseFloat(batchMinorLoss);
if (!Number.isFinite(val) || val < 0) return;
onUpdatePipes(selectedPipeIds, { minorLoss: val });
notify(`미소손실계수 ${val} 일괄 적용 (${selectedPipeIds.length}개 관로)`);
};
const applyTagDesc = () => {
const updates: Partial<InpPipe> = {};
if (batchTag.trim()) updates.tag = batchTag.trim();
if (batchDesc.trim()) updates.description = batchDesc.trim();
if (Object.keys(updates).length === 0) return;
onUpdatePipes(selectedPipeIds, updates);
notify(`태그/설명 일괄 적용 (${selectedPipeIds.length}개 관로)`);
};
// 2. Node actions
const applyElevation = () => {
const val = parseFloat(batchElevValue);
if (!Number.isFinite(val)) return;
const sourceUnits = model.options.units || 'LPS';
// batchElevValue is always entered in meters (display); convert the
// set-value / offset-delta into the model's native length unit (m or ft).
const modelVal = metersToLength(val, sourceUnits);
onUpdateNodes(selectedNodeIds, (node) => {
if (batchElevMode === 'set') {
return { elevation: modelVal };
} else {
return { elevation: Math.round((node.elevation + modelVal) * 100) / 100 };
}
});
notify(
batchElevMode === 'set'
? `지반고 ${val}m 일괄 설정 (${selectedNodeIds.length}개 절점)`
: `지반고 ${val >= 0 ? `+${val}` : val}m 일괄 증감 (${selectedNodeIds.length}개 절점)`
);
};
const applyDemand = () => {
const val = parseFloat(batchDemandValue);
if (!Number.isFinite(val)) return;
const sourceUnits = model.options.units || 'LPS';
onUpdateNodes(selectedNodeIds, (node) => {
if (batchDemandMode === 'set') {
// batchDemandValue (set mode) is entered in m³/day (display);
// convert into the model's native flow unit before storing.
return { demand: m3DayToFlow(val, sourceUnits) };
} else {
// Multiplier is dimensionless; applies directly to the stored value.
const curDemand = node.demand ?? 0;
return { demand: Math.round(curDemand * val * 1000) / 1000 };
}
});
notify(
batchDemandMode === 'set'
? `기본수요량 ${val} m³/day 일괄 설정 (${selectedNodeIds.length}개 절점)`
: `기본수요량 ×${val}배 일괄 곱셈 (${selectedNodeIds.length}개 절점)`
);
};
const applyPattern = () => {
if (!batchPattern) return;
onUpdateNodes(selectedNodeIds, () => ({ pattern: batchPattern }));
notify(`수요 패턴 '${batchPattern}' 일괄 지정 (${selectedNodeIds.length}개 절점)`);
};
const applyMoveOffset = () => {
const dx = parseFloat(moveDx) || 0;
const dy = parseFloat(moveDy) || 0;
if (dx === 0 && dy === 0) return;
onMoveNodes(selectedNodeIds, dx, dy);
notify(`절점 ${selectedNodeIds.length}개 일괄 이동 완료 (ΔX: ${dx}, ΔY: ${dy})`);
};
const patterns = Object.keys(model.patterns || {});
return (
<div
style={{
background: 'var(--bg-panel)',
borderRadius: 'var(--r-sm)',
border: '1px solid var(--border-strong)',
overflow: 'hidden',
fontSize: '12px',
boxShadow: 'var(--shadow-sm)',
}}
>
{/* Header with Element Counts & Global Multi Actions */}
<div
style={{
padding: '10px 12px',
background: 'var(--bg-subtle)',
borderBottom: '1px solid var(--border)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
flexWrap: 'wrap',
gap: '6px',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<Layers size={15} style={{ color: 'var(--accent)' }} />
<strong style={{ color: 'var(--text)', fontSize: '13px' }}>
다중 선택 일괄 편집기
</strong>
<span
style={{
fontSize: '11px',
padding: '1px 6px',
borderRadius: '10px',
background: 'var(--accent-wash)',
color: 'var(--accent)',
fontWeight: 600,
}}
>
총 {selectedNodeIds.length + selectedPipeIds.length}개
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
{onCopyClipboard && (
<button
type="button"
className="btn btn-secondary"
onClick={onCopyClipboard}
title="선택된 요소 복사 (Ctrl+C)"
style={{ height: '24px', padding: '0 6px', fontSize: '11px', display: 'inline-flex', alignItems: 'center', gap: '3px' }}
>
<Copy size={11} />
복사
</button>
)}
<button
type="button"
onClick={onDeleteSelected}
className="btn btn-danger"
title="선택된 요소 일괄 삭제"
style={{
height: '24px',
padding: '0 6px',
fontSize: '11px',
display: 'inline-flex',
alignItems: 'center',
gap: '3px',
background: 'var(--crit-wash)',
color: 'var(--crit)',
border: '1px solid var(--crit)',
}}
>
<Trash2 size={11} />
삭제
</button>
<button
type="button"
className="btn btn-secondary"
onClick={onClearSelection}
title="전체 선택 해제 (ESC)"
style={{ height: '24px', padding: '0 6px', fontSize: '11px' }}
>
<X size={11} />
</button>
</div>
</div>
{/* Toast Flash Notice */}
{flashMsg && (
<div
style={{
padding: '6px 12px',
background: 'var(--good-wash)',
color: 'var(--good)',
borderBottom: '1px solid var(--good)',
fontSize: '11px',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: '4px',
}}
>
<Check size={13} />
{flashMsg}
</div>
)}
{/* Selected Items ID Chips Preview */}
<div
style={{
padding: '6px 12px',
background: 'var(--bg-app)',
borderBottom: '1px solid var(--border)',
display: 'flex',
flexWrap: 'wrap',
gap: '4px',
maxHeight: '75px',
overflowY: 'auto',
}}
>
{selectedNodeIds.map((id) => (
<span
key={`node-chip-${id}`}
onClick={() => onSelectTarget({ type: 'node', id })}
style={{
cursor: 'pointer',
fontSize: '10px',
padding: '1px 5px',
borderRadius: '3px',
background: selectedTarget?.type === 'node' && selectedTarget.id === id ? 'var(--accent)' : 'var(--accent-wash)',
color: selectedTarget?.type === 'node' && selectedTarget.id === id ? '#fff' : 'var(--accent)',
fontFamily: 'var(--font-mono)',
}}
title="클릭 시 대표 요소로 포커스"
>
절점 {id}
</span>
))}
{selectedPipeIds.map((id) => (
<span
key={`pipe-chip-${id}`}
onClick={() => onSelectTarget({ type: 'link', id })}
style={{
cursor: 'pointer',
fontSize: '10px',
padding: '1px 5px',
borderRadius: '3px',
background: selectedTarget?.type === 'link' && selectedTarget.id === id ? 'var(--good)' : 'var(--good-wash)',
color: selectedTarget?.type === 'link' && selectedTarget.id === id ? '#fff' : 'var(--good)',
fontFamily: 'var(--font-mono)',
}}
title="클릭 시 대표 요소로 포커스"
>
관로 {id}
</span>
))}
</div>
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
{/* ======================================================== */}
{/* 1. PIPE BATCH EDIT CARD */}
{/* ======================================================== */}
{selectedPipeIds.length > 0 && (
<div
style={{
background: 'var(--bg-subtle)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-sm)',
padding: '10px',
display: 'flex',
flexDirection: 'column',
gap: '8px',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontWeight: 600, color: 'var(--text)' }}>
관로 일괄 속성 변경 ({selectedPipeIds.length}개 관로)
</span>
</div>
{/* Pipe Diameter */}
<div>
<label style={{ display: 'block', fontSize: '11px', color: 'var(--text-secondary)', marginBottom: '3px' }}>
관경 (Diameter mm)
</label>
<div style={{ display: 'flex', gap: '4px', marginBottom: '4px' }}>
<input
type="number"
step="10"
value={batchDiameter}
onChange={(e) => setBatchDiameter(e.target.value)}
placeholder="예: 150"
style={{ width: '100%', padding: '3px 6px', fontSize: '11px', border: '1px solid #cbd5e1', borderRadius: '4px' }}
/>
<button
type="button"
className="btn btn-secondary"
disabled={!batchDiameter}
onClick={() => applyDiameter(parseFloat(batchDiameter))}
style={{ whiteSpace: 'nowrap', padding: '0 8px', fontSize: '11px' }}
>
적용
</button>
</div>
<div style={{ display: 'flex', gap: '3px', flexWrap: 'wrap' }}>
{[80, 100, 150, 200, 250, 300, 400, 500].map((d) => (
<button
key={`diam-${d}`}
type="button"
onClick={() => {
setBatchDiameter(String(d));
applyDiameter(d);
}}
style={{
padding: '2px 5px',
fontSize: '10px',
background: 'var(--bg-panel)',
border: '1px solid var(--border)',
borderRadius: '3px',
cursor: 'pointer',
fontFamily: 'var(--font-mono)',
}}
>
D{d}
</button>
))}
</div>
</div>
{/* Roughness Presets */}
<div style={{ borderTop: '1px solid var(--border)', paddingTop: '6px' }}>
<label style={{ display: 'block', fontSize: '11px', color: 'var(--text-secondary)', marginBottom: '3px' }}>
Hazen-Williams 조도계수 (Roughness C)
</label>
<div style={{ display: 'flex', gap: '4px', marginBottom: '4px' }}>
<input
type="number"
step="5"
value={batchRoughness}
onChange={(e) => setBatchRoughness(e.target.value)}
placeholder="예: 130"
style={{ width: '100%', padding: '3px 6px', fontSize: '11px', border: '1px solid #cbd5e1', borderRadius: '4px' }}
/>
<button
type="button"
className="btn btn-secondary"
disabled={!batchRoughness}
onClick={() => applyRoughness(parseFloat(batchRoughness))}
style={{ whiteSpace: 'nowrap', padding: '0 8px', fontSize: '11px' }}
>
적용
</button>
</div>
<div style={{ display: 'flex', gap: '3px', flexWrap: 'wrap' }}>
{[
{ name: '주철관 (100)', val: 100 },
{ name: '강관 (110)', val: 110 },
{ name: '닥타일주철 (130)', val: 130 },
{ name: 'PVC/PE (140)', val: 140 },
].map((preset) => (
<button
key={`rough-${preset.val}`}
type="button"
onClick={() => {
setBatchRoughness(String(preset.val));
applyRoughness(preset.val);
}}
style={{
padding: '2px 5px',
fontSize: '10px',
background: 'var(--bg-panel)',
border: '1px solid var(--border)',
borderRadius: '3px',
cursor: 'pointer',
}}
>
{preset.name}
</button>
))}
</div>
</div>
{/* Pipe Status */}
<div style={{ borderTop: '1px solid var(--border)', paddingTop: '6px' }}>
<label style={{ display: 'block', fontSize: '11px', color: 'var(--text-secondary)', marginBottom: '3px' }}>
관로 개폐 상태 (Status)
</label>
<div style={{ display: 'flex', gap: '4px' }}>
<button
type="button"
onClick={() => applyStatus('OPEN')}
className="btn btn-secondary"
style={{ flex: 1, padding: '4px', fontSize: '11px', color: 'var(--good)' }}
>
전체 열림 (OPEN)
</button>
<button
type="button"
onClick={() => applyStatus('CLOSED')}
className="btn btn-secondary"
style={{ flex: 1, padding: '4px', fontSize: '11px', color: 'var(--crit)' }}
>
전체 닫힘 (CLOSED)
</button>
<button
type="button"
onClick={() => applyStatus('CV')}
className="btn btn-secondary"
style={{ flex: 1, padding: '4px', fontSize: '11px', color: 'var(--accent)' }}
>
체크밸브 (CV)
</button>
</div>
</div>
{/* Minor Loss & Tag */}
<div style={{ borderTop: '1px solid var(--border)', paddingTop: '6px', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px' }}>
<div>
<label style={{ display: 'block', fontSize: '10px', color: 'var(--text-muted)' }}>미소손실 K</label>
<div style={{ display: 'flex', gap: '2px' }}>
<input
type="number"
step="0.1"
value={batchMinorLoss}
onChange={(e) => setBatchMinorLoss(e.target.value)}
placeholder="0.0"
style={{ width: '100%', padding: '2px 4px', fontSize: '11px', border: '1px solid #cbd5e1', borderRadius: '3px' }}
/>
<button type="button" className="btn btn-secondary" onClick={applyMinorLoss} style={{ padding: '0 4px', fontSize: '10px' }}>
적용
</button>
</div>
</div>
<div>
<label style={{ display: 'block', fontSize: '10px', color: 'var(--text-muted)' }}>설명/태그</label>
<div style={{ display: 'flex', gap: '2px' }}>
<input
type="text"
value={batchTag}
onChange={(e) => setBatchTag(e.target.value)}
placeholder="배수관로"
style={{ width: '100%', padding: '2px 4px', fontSize: '11px', border: '1px solid #cbd5e1', borderRadius: '3px' }}
/>
<button type="button" className="btn btn-secondary" onClick={applyTagDesc} style={{ padding: '0 4px', fontSize: '10px' }}>
적용
</button>
</div>
</div>
</div>
</div>
)}
{/* ======================================================== */}
{/* 2. NODE BATCH EDIT CARD */}
{/* ======================================================== */}
{selectedNodeIds.length > 0 && (
<div
style={{
background: 'var(--bg-subtle)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-sm)',
padding: '10px',
display: 'flex',
flexDirection: 'column',
gap: '8px',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontWeight: 600, color: 'var(--text)' }}>
절점 일괄 속성 변경 ({selectedNodeIds.length}개 절점)
</span>
</div>
{/* Node Elevation */}
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '3px' }}>
<label style={{ fontSize: '11px', color: 'var(--text-secondary)' }}>
지반고 (Elevation m)
</label>
<div style={{ display: 'flex', gap: '6px', fontSize: '10px' }}>
<label style={{ cursor: 'pointer' }}>
<input
type="radio"
name="batchElevMode"
checked={batchElevMode === 'offset'}
onChange={() => setBatchElevMode('offset')}
/>{' '}
증감 가감 (±)
</label>
<label style={{ cursor: 'pointer' }}>
<input
type="radio"
name="batchElevMode"
checked={batchElevMode === 'set'}
onChange={() => setBatchElevMode('set')}
/>{' '}
값 고정
</label>
</div>
</div>
<div style={{ display: 'flex', gap: '4px' }}>
<input
type="number"
step="0.5"
value={batchElevValue}
onChange={(e) => setBatchElevValue(e.target.value)}
placeholder={batchElevMode === 'offset' ? '+1.5 또는 -0.5' : '30.0'}
style={{ width: '100%', padding: '3px 6px', fontSize: '11px', border: '1px solid #cbd5e1', borderRadius: '4px' }}
/>
<button
type="button"
className="btn btn-secondary"
disabled={!batchElevValue}
onClick={applyElevation}
style={{ whiteSpace: 'nowrap', padding: '0 8px', fontSize: '11px' }}
>
적용
</button>
</div>
</div>
{/* Node Demand */}
<div style={{ borderTop: '1px solid var(--border)', paddingTop: '6px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '3px' }}>
<label style={{ fontSize: '11px', color: 'var(--text-secondary)' }}>
기본수요량 (Base Demand)
</label>
<div style={{ display: 'flex', gap: '6px', fontSize: '10px' }}>
<label style={{ cursor: 'pointer' }}>
<input
type="radio"
name="batchDemandMode"
checked={batchDemandMode === 'mult'}
onChange={() => setBatchDemandMode('mult')}
/>{' '}
배율 곱셈 (×)
</label>
<label style={{ cursor: 'pointer' }}>
<input
type="radio"
name="batchDemandMode"
checked={batchDemandMode === 'set'}
onChange={() => setBatchDemandMode('set')}
/>{' '}
값 고정
</label>
</div>
</div>
<div style={{ display: 'flex', gap: '4px' }}>
<input
type="number"
step="0.1"
value={batchDemandValue}
onChange={(e) => setBatchDemandValue(e.target.value)}
placeholder={batchDemandMode === 'mult' ? '1.2 (20% 증가)' : '100.0'}
style={{ width: '100%', padding: '3px 6px', fontSize: '11px', border: '1px solid #cbd5e1', borderRadius: '4px' }}
/>
<button
type="button"
className="btn btn-secondary"
disabled={!batchDemandValue}
onClick={applyDemand}
style={{ whiteSpace: 'nowrap', padding: '0 8px', fontSize: '11px' }}
>
적용
</button>
</div>
</div>
{/* Demand Pattern */}
{patterns.length > 0 && (
<div style={{ borderTop: '1px solid var(--border)', paddingTop: '6px' }}>
<label style={{ display: 'block', fontSize: '11px', color: 'var(--text-secondary)', marginBottom: '3px' }}>
수요 패턴 (Pattern ID)
</label>
<div style={{ display: 'flex', gap: '4px' }}>
<select
value={batchPattern}
onChange={(e) => setBatchPattern(e.target.value)}
style={{ width: '100%', padding: '3px 6px', fontSize: '11px', border: '1px solid #cbd5e1', borderRadius: '4px' }}
>
<option value="">패턴 선택...</option>
{patterns.map((patId) => (
<option key={patId} value={patId}>
{patId} {model.patterns[patId]?.description ? `(${model.patterns[patId].description})` : ''}
</option>
))}
</select>
<button
type="button"
className="btn btn-secondary"
disabled={!batchPattern}
onClick={applyPattern}
style={{ whiteSpace: 'nowrap', padding: '0 8px', fontSize: '11px' }}
>
적용
</button>
</div>
</div>
)}
{/* CAD Move Offset */}
<div style={{ borderTop: '1px solid var(--border)', paddingTop: '6px' }}>
<label style={{ display: 'block', fontSize: '11px', color: 'var(--text-secondary)', marginBottom: '3px' }}>
절점 위치 일괄 평행이동 (CAD Move Offset)
</label>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr auto', gap: '4px' }}>
<div>
<input
type="number"
step="1"
value={moveDx}
onChange={(e) => setMoveDx(e.target.value)}
placeholder="ΔX (m)"
style={{ width: '100%', padding: '3px 6px', fontSize: '11px', border: '1px solid #cbd5e1', borderRadius: '4px' }}
/>
</div>
<div>
<input
type="number"
step="1"
value={moveDy}
onChange={(e) => setMoveDy(e.target.value)}
placeholder="ΔY (m)"
style={{ width: '100%', padding: '3px 6px', fontSize: '11px', border: '1px solid #cbd5e1', borderRadius: '4px' }}
/>
</div>
<button
type="button"
className="btn btn-secondary"
onClick={applyMoveOffset}
style={{ whiteSpace: 'nowrap', padding: '0 8px', fontSize: '11px', display: 'inline-flex', alignItems: 'center', gap: '3px' }}
>
<Move size={11} />
이동
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
};