refactor: robot-app 부품 리스트를 표에서 펼침형 카드로 변경
행마다 항상 다 펼쳐져 있던 표 대신, 이름/상태뱃지/소계만 보이는 헤더를 클릭하면 수량·단가·상태·메모 편집 필드가 펼쳐지는 카드로 교체. 펼침 상태는 화면 전용(서버 저장 안 함)이라 다시 그려도 유지됨. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+78
-39
@@ -76,16 +76,32 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
|
||||
.rb-chat{width:auto;height:38vh;border-left:none;border-top:1px solid var(--line);}
|
||||
}
|
||||
|
||||
/* ── 부품 표 ── */
|
||||
.rb-parts-table{width:100%;border-collapse:collapse;font-size:12px;}
|
||||
.rb-parts-table th{text-align:left;font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;padding:4px 6px;border-bottom:1px solid var(--line);}
|
||||
.rb-parts-table td{padding:4px 6px;border-bottom:1px solid var(--line);vertical-align:middle;}
|
||||
.rb-parts-table input[type=text],.rb-parts-table input[type=number]{width:100%;box-sizing:border-box;background:var(--panel-2);border:1px solid var(--line);border-radius:5px;padding:4px 6px;font-size:12px;color:var(--text);font-family:inherit;outline:none;}
|
||||
.rb-parts-table input:focus,.rb-parts-table select:focus{border-color:#f59e0b;}
|
||||
.rb-parts-table select{background:var(--panel-2);border:1px solid var(--line);border-radius:5px;padding:4px 6px;font-size:11px;color:var(--text);font-family:inherit;}
|
||||
.rb-col-qty{width:52px;}.rb-col-price{width:100px;}.rb-col-status{width:90px;}.rb-col-sub{width:90px;text-align:right;color:var(--muted);}.rb-col-del{width:28px;}
|
||||
.rb-del-btn{background:none;border:none;cursor:pointer;font-size:13px;color:var(--muted);padding:2px 4px;}
|
||||
.rb-del-btn:hover{color:#ef4444;}
|
||||
/* ── 부품 카드 ── */
|
||||
.rb-part-card{border:1px solid var(--line);border-radius:10px;margin-bottom:8px;overflow:hidden;background:var(--panel);}
|
||||
.rb-part-hdr{display:flex;align-items:center;gap:8px;padding:9px 12px;cursor:pointer;user-select:none;}
|
||||
.rb-part-hdr:hover{background:rgba(245,158,11,.05);}
|
||||
.rb-part-chevron{font-size:10px;color:var(--muted);transition:transform .15s;flex-shrink:0;}
|
||||
.rb-part-card.expanded .rb-part-chevron{transform:rotate(90deg);}
|
||||
.rb-part-name{flex:1;font-size:13px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
||||
.rb-part-badge{font-size:10px;padding:2px 8px;border-radius:10px;flex-shrink:0;font-weight:700;}
|
||||
.rb-part-badge.st-검토중{background:rgba(107,114,128,.15);color:#9ca3af;}
|
||||
.rb-part-badge.st-주문완료{background:rgba(59,130,246,.15);color:#60a5fa;}
|
||||
.rb-part-badge.st-보유{background:rgba(34,197,94,.15);color:#4ade80;}
|
||||
.rb-part-sub{font-size:12px;color:var(--muted);flex-shrink:0;min-width:80px;text-align:right;}
|
||||
.rb-part-body{display:none;padding:4px 12px 12px;border-top:1px solid var(--line);}
|
||||
.rb-part-card.expanded .rb-part-body{display:block;}
|
||||
.rb-part-row{display:flex;gap:8px;margin-top:8px;flex-wrap:wrap;}
|
||||
.rb-part-field{display:flex;flex-direction:column;gap:2px;}
|
||||
.rb-part-field label{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;}
|
||||
.rb-part-field input[type=text],.rb-part-field input[type=number],.rb-part-field select{background:var(--panel-2);border:1px solid var(--line);border-radius:6px;padding:5px 8px;font-size:12px;color:var(--text);font-family:inherit;outline:none;}
|
||||
.rb-part-field input:focus,.rb-part-field select:focus{border-color:#f59e0b;}
|
||||
.rb-part-field.grow{flex:1;min-width:120px;}
|
||||
.rb-part-field.qty{width:64px;}
|
||||
.rb-part-field.price{width:110px;}
|
||||
.rb-part-field.status{width:100px;}
|
||||
.rb-part-actions{display:flex;justify-content:flex-end;margin-top:8px;}
|
||||
.rb-del-btn{background:none;border:1px solid var(--line);border-radius:6px;cursor:pointer;font-size:11px;color:var(--muted);padding:4px 10px;}
|
||||
.rb-del-btn:hover{color:#ef4444;border-color:#ef4444;}
|
||||
.rb-add-btn{margin-top:10px;background:rgba(245,158,11,.1);border:1px solid rgba(245,158,11,.3);border-radius:6px;padding:5px 12px;font-size:11px;color:#f59e0b;cursor:pointer;font-family:inherit;font-weight:700;}
|
||||
.rb-add-btn:hover{background:rgba(245,158,11,.25);}
|
||||
|
||||
@@ -149,13 +165,7 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
|
||||
|
||||
<div class="rb-body">
|
||||
<div class="rb-pane active" id="pane-parts">
|
||||
<table class="rb-parts-table">
|
||||
<thead><tr>
|
||||
<th>이름</th><th class="rb-col-qty">수량</th><th class="rb-col-price">단가</th>
|
||||
<th class="rb-col-status">상태</th><th>메모</th><th class="rb-col-sub">소계</th><th class="rb-col-del"></th>
|
||||
</tr></thead>
|
||||
<tbody id="parts-tbody"></tbody>
|
||||
</table>
|
||||
<div id="parts-container"></div>
|
||||
<button class="rb-add-btn" onclick="addPart()">+ 부품 추가</button>
|
||||
</div>
|
||||
|
||||
@@ -407,25 +417,45 @@ async function refreshK2Widget(){
|
||||
// ── 부품 ──────────────────────────────────────────────────────────────────────
|
||||
function fmtWon(n){return (n||0).toLocaleString('ko-KR')+'원';}
|
||||
|
||||
// 펼침 상태는 화면 전용(서버에 저장 안 함) — 카드 다시 그려도 유지되게 모듈 전역에 둔다.
|
||||
const expandedParts=new Set();
|
||||
function togglePartExpand(id){
|
||||
if(expandedParts.has(id))expandedParts.delete(id);else expandedParts.add(id);
|
||||
renderParts();
|
||||
}
|
||||
|
||||
function renderParts(){
|
||||
const tbody=document.getElementById('parts-tbody');
|
||||
tbody.innerHTML=parts.map(p=>`
|
||||
<tr data-id="${escAttr(p.id)}">
|
||||
<td><input type="text" value="${escAttr(p.name)}" oninput="updatePart('${p.id}','name',this.value)"></td>
|
||||
<td class="rb-col-qty"><input type="number" min="0" value="${p.qty}" oninput="updatePart('${p.id}','qty',this.value)"></td>
|
||||
<td class="rb-col-price"><input type="number" min="0" value="${p.unitPrice}" oninput="updatePart('${p.id}','unitPrice',this.value)"></td>
|
||||
<td class="rb-col-status">
|
||||
<select onchange="updatePart('${p.id}','status',this.value)">
|
||||
<option value="검토중" ${p.status==='검토중'?'selected':''}>검토중</option>
|
||||
<option value="주문완료" ${p.status==='주문완료'?'selected':''}>주문완료</option>
|
||||
<option value="보유" ${p.status==='보유'?'selected':''}>보유</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" value="${escAttr(p.memo)}" oninput="updatePart('${p.id}','memo',this.value)"></td>
|
||||
<td class="rb-col-sub">${fmtWon(p.qty*p.unitPrice)}</td>
|
||||
<td class="rb-col-del"><button class="rb-del-btn" onclick="deletePart('${p.id}')">✕</button></td>
|
||||
</tr>
|
||||
`).join('');
|
||||
const container=document.getElementById('parts-container');
|
||||
container.innerHTML=parts.map(p=>{
|
||||
const expanded=expandedParts.has(p.id);
|
||||
return `
|
||||
<div class="rb-part-card${expanded?' expanded':''}" data-id="${escAttr(p.id)}">
|
||||
<div class="rb-part-hdr" onclick="togglePartExpand('${p.id}')">
|
||||
<span class="rb-part-chevron">▶</span>
|
||||
<span class="rb-part-name">${escAttr(p.name)||'(이름 없음)'}</span>
|
||||
<span class="rb-part-badge st-${p.status}">${p.status}</span>
|
||||
<span class="rb-part-sub">${fmtWon(p.qty*p.unitPrice)}</span>
|
||||
</div>
|
||||
<div class="rb-part-body" onclick="event.stopPropagation()">
|
||||
<div class="rb-part-row">
|
||||
<div class="rb-part-field grow"><label>이름</label><input type="text" value="${escAttr(p.name)}" oninput="updatePart('${p.id}','name',this.value)"></div>
|
||||
<div class="rb-part-field qty"><label>수량</label><input type="number" min="0" value="${p.qty}" oninput="updatePart('${p.id}','qty',this.value)"></div>
|
||||
<div class="rb-part-field price"><label>단가</label><input type="number" min="0" value="${p.unitPrice}" oninput="updatePart('${p.id}','unitPrice',this.value)"></div>
|
||||
<div class="rb-part-field status"><label>상태</label>
|
||||
<select onchange="updatePart('${p.id}','status',this.value)">
|
||||
<option value="검토중" ${p.status==='검토중'?'selected':''}>검토중</option>
|
||||
<option value="주문완료" ${p.status==='주문완료'?'selected':''}>주문완료</option>
|
||||
<option value="보유" ${p.status==='보유'?'selected':''}>보유</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rb-part-row">
|
||||
<div class="rb-part-field grow"><label>메모</label><input type="text" value="${escAttr(p.memo)}" oninput="updatePart('${p.id}','memo',this.value)"></div>
|
||||
</div>
|
||||
<div class="rb-part-actions"><button class="rb-del-btn" onclick="deletePart('${p.id}')">✕ 삭제</button></div>
|
||||
</div>
|
||||
</div>
|
||||
`;}).join('');
|
||||
}
|
||||
|
||||
function updatePart(id,field,value){
|
||||
@@ -433,22 +463,31 @@ function updatePart(id,field,value){
|
||||
if(!p)return;
|
||||
if(field==='qty'||field==='unitPrice')p[field]=Math.max(0,Number(value)||0);
|
||||
else p[field]=value;
|
||||
if(field==='qty'||field==='unitPrice'){
|
||||
const row=document.querySelector(`tr[data-id="${id}"] .rb-col-sub`);
|
||||
if(row)row.textContent=fmtWon(p.qty*p.unitPrice);
|
||||
const card=document.querySelector(`.rb-part-card[data-id="${id}"]`);
|
||||
if(card){
|
||||
if(field==='qty'||field==='unitPrice')card.querySelector('.rb-part-sub').textContent=fmtWon(p.qty*p.unitPrice);
|
||||
if(field==='name')card.querySelector('.rb-part-name').textContent=p.name||'(이름 없음)';
|
||||
if(field==='status'){
|
||||
const badge=card.querySelector('.rb-part-badge');
|
||||
badge.className='rb-part-badge st-'+p.status;
|
||||
badge.textContent=p.status;
|
||||
}
|
||||
}
|
||||
updateBudget();
|
||||
scheduleSave();
|
||||
}
|
||||
|
||||
function addPart(){
|
||||
parts.push({id:genId('p'),name:'',qty:1,unitPrice:0,status:'검토중',memo:''});
|
||||
const id=genId('p');
|
||||
parts.push({id,name:'',qty:1,unitPrice:0,status:'검토중',memo:''});
|
||||
expandedParts.add(id);
|
||||
renderParts();
|
||||
scheduleSave();
|
||||
}
|
||||
|
||||
function deletePart(id){
|
||||
parts=parts.filter(x=>x.id!==id);
|
||||
expandedParts.delete(id);
|
||||
renderParts();
|
||||
updateBudget();
|
||||
scheduleSave();
|
||||
|
||||
Reference in New Issue
Block a user