feat(pid-ui): PREFIX 분류 정의 패널 — DCS 태그/현장 계기 섹션 분리

## 변경 내용

### CATEGORY_META
- instrument → instrument_dcs / instrument_field 가상 키로 분할
- 각 가상 키에 dbCat/tagDcs 메타 추가 (DB에는 여전히 category='instrument')
- instrument 키는 장비 목록 배지용으로 유지

### CATEGORY_ORDER
- 'instrument' 제거 → 'instrument_dcs', 'instrument_field' 두 섹션으로 분리

### pidRefreshPrefixRules
- 그룹핑 로직: r.category==='instrument' → tagDcs로 분기
- 그룹 div에 data-vcat 속성 저장 (인덱스 기반 취약한 방식 제거)
- 추가 입력행: 그룹이 tagDcs 결정 → 별도 DCS 체크박스 없음
  (placeholder도 그룹별 예시로 변경: FIC / FT / P-)
- 규칙 0건이어도 추가 입력행 항상 표시
- instrument 그룹 행: DCS 토글 표시 (체크 변경 후 수정 클릭 시 반대 그룹으로 이동)
- 비instrument 행: DCS 토글 없음

### pidResolveCat() 신규
- 가상 카테고리 키 → { category, tagDcs } 변환 헬퍼

### pidAddPrefixRule
- vcat 인자 사용, pidResolveCat()로 실제 DB 값 결정
- Add 행에 DCS 체크박스 없음 — 그룹이 결정

### pidUpdatePrefixRule
- data-vcat으로 그룹 인식 (기존 인덱스 기반 제거)
- instrument 행: DCS 토글 체크로 tagDcs 결정 (그룹 이동 가능)
- 비instrument 행: 그룹 기본값(false) 사용

## 결과
- DCS 태그 (FIC/TIC/PIC/LIC/FY/TY/PY/LY/FV/TV/PV/LV) 12건 별도 섹션
- 현장 계기 (FT/FCV/PSV/XV 등) 29건 별도 섹션
- 추가 시 자동으로 tagDcs 설정 — 혼동 없음

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
windpacer
2026-05-27 15:47:09 +09:00
parent c1d228d1f6
commit 300dfe65a4

View File

@@ -354,14 +354,22 @@ function pidTogglePrefixPanel() {
}
const CATEGORY_META = {
instrument: { label: 'Instrument', badge: 'ok' },
power_equipment: { label: 'Power Equipment', badge: 'warn' },
storage_equipment: { label: 'Storage Equipment', badge: 'inf' },
process_equipment: { label: 'Process Equipment', badge: '' },
utility_equipment: { label: 'Utility Equipment', badge: 'warn' },
pipings: { label: 'Pipings', badge: 'ok' }
// ── instrument 가상 분할 (DB category='instrument', tag_dcs로 구분) ──
instrument_dcs: { label: 'DCS 태그', badge: 'warn', dbCat: 'instrument', tagDcs: true },
instrument_field: { label: '현장 계기', badge: 'ok', dbCat: 'instrument', tagDcs: false },
// ── 그 외 equipment / pipings ──
power_equipment: { label: 'Power Equipment', badge: 'warn', dbCat: 'power_equipment', tagDcs: false },
storage_equipment:{ label: 'Storage Equipment', badge: 'inf', dbCat: 'storage_equipment', tagDcs: false },
process_equipment:{ label: 'Process Equipment', badge: '', dbCat: 'process_equipment', tagDcs: false },
utility_equipment:{ label: 'Utility Equipment', badge: 'warn', dbCat: 'utility_equipment', tagDcs: false },
pipings: { label: 'Pipings', badge: 'ok', dbCat: 'pipings', tagDcs: false },
// ── 장비 테이블 배지용 (equipment 목록의 r.category 값과 매핑) ──
instrument: { label: 'Instrument', badge: 'ok' },
};
const CATEGORY_ORDER = ['instrument', 'power_equipment', 'storage_equipment', 'process_equipment', 'utility_equipment', 'pipings'];
const CATEGORY_ORDER = [
'instrument_dcs', 'instrument_field',
'power_equipment', 'storage_equipment', 'process_equipment', 'utility_equipment', 'pipings'
];
function pidCategoryBadge(cat) {
return CATEGORY_META[cat] ? CATEGORY_META[cat].badge : '';
@@ -383,50 +391,60 @@ async function pidRefreshPrefixRules() {
return;
}
// instrument → tag_dcs 기준으로 가상 분할
const grouped = {};
for (const r of items) {
if (!grouped[r.category]) grouped[r.category] = [];
grouped[r.category].push(r);
let key = r.category;
if (r.category === 'instrument') {
key = r.tagDcs ? 'instrument_dcs' : 'instrument_field';
}
if (!grouped[key]) grouped[key] = [];
grouped[key].push(r);
}
let html = '';
for (const cat of CATEGORY_ORDER) {
const rules = grouped[cat];
if (!rules) continue;
const meta = CATEGORY_META[cat] || { label: cat, badge: '' };
rules.sort((a, b) => a.sortOrder - b.sortOrder);
for (const vcat of CATEGORY_ORDER) {
const rules = grouped[vcat];
const meta = CATEGORY_META[vcat] || { label: vcat, badge: '', dbCat: vcat, tagDcs: false };
rules?.sort((a, b) => a.sortOrder - b.sortOrder);
html += `<div class="pid-cat-group">
// 그룹 헤더: 규칙이 0건이어도 추가 입력행은 항상 표시
const count = rules ? rules.length : 0;
const isInstr = vcat === 'instrument_dcs' || vcat === 'instrument_field';
html += `<div class="pid-cat-group" data-vcat="${vcat}">
<div class="pid-cat-header">
<span class="badge ${meta.badge}">${meta.label}</span>
<span class="pid-cat-count">${rules.length}</span>
<span class="pid-cat-count">${count}</span>
<span class="pid-cat-add" style="margin-left:auto">
<input class="inp pid-cat-prefix-input" placeholder="예: FT" style="width:90px;font-family:var(--mono)" />
<input class="inp pid-cat-prefix-input" placeholder="예: ${vcat === 'instrument_dcs' ? 'FIC' : vcat === 'instrument_field' ? 'FT' : 'P-'}" style="width:90px;font-family:var(--mono)" />
<input class="inp pid-cat-desc-input" placeholder="설명 (선택)" style="width:160px" />
<input class="inp pid-cat-order-input" type="number" value="10" style="width:50px" />
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;white-space:nowrap">
<input type="checkbox" class="pid-cat-dcs-input" style="margin:0" />
<span style="font-size:11px;color:var(--t2)">DCS</span>
</label>
<button class="btn-a btn-sm" onclick="pidAddPrefixRule('${cat}')">추가</button>
<button class="btn-a btn-sm" onclick="pidAddPrefixRule('${vcat}')">추가</button>
</span>
</div>
<div class="pid-cat-body">`;
for (const r of rules) {
html += `<div class="pid-cat-row">
<input class="inp pid-cat-prefix-input" value="${esc(r.prefix)}" style="width:80px;font-family:var(--mono)" />
<input class="inp pid-cat-desc-input" value="${esc(r.description) || ''}" style="flex:1;min-width:0" />
<input class="inp pid-cat-order-input" type="number" value="${r.sortOrder}" style="width:44px" />
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;white-space:nowrap">
<input type="checkbox" class="pid-cat-dcs-input" ${r.tagDcs ? 'checked' : ''} style="margin:0" />
<span class="badge ${r.tagDcs ? 'warn' : 'ok'}" style="font-size:11px">${r.tagDcs ? 'DCS' : '현장'}</span>
</label>
<span class="pid-cat-actions">
<button class="btn-sm btn-a" onclick="pidUpdatePrefixRule(${r.id}, this)">수정</button>
<button class="btn-sm btn-b" onclick="pidDeletePrefixRule(${r.id}, '${esc(r.prefix)}')">삭제</button>
</span>
</div>`;
if (rules) {
for (const r of rules) {
// instrument 그룹은 DCS 이동용 토글 표시 (체크 해제 시 반대 그룹으로 이동)
const dcsToggle = isInstr
? `<label style="display:flex;align-items:center;gap:3px;cursor:pointer;white-space:nowrap" title="DCS 태그 여부 변경 후 수정 클릭">
<input type="checkbox" class="pid-cat-dcs-input" ${r.tagDcs ? 'checked' : ''} style="margin:0" />
<span style="font-size:10px;color:var(--t2)">DCS</span>
</label>`
: '';
html += `<div class="pid-cat-row">
<input class="inp pid-cat-prefix-input" value="${esc(r.prefix)}" style="width:80px;font-family:var(--mono)" />
<input class="inp pid-cat-desc-input" value="${esc(r.description) || ''}" style="flex:1;min-width:0" />
<input class="inp pid-cat-order-input" type="number" value="${r.sortOrder}" style="width:44px" />
${dcsToggle}
<span class="pid-cat-actions">
<button class="btn-sm btn-a" onclick="pidUpdatePrefixRule(${r.id}, this)">수정</button>
<button class="btn-sm btn-b" onclick="pidDeletePrefixRule(${r.id}, '${esc(r.prefix)}')">삭제</button>
</span>
</div>`;
}
}
html += `</div></div>`;
@@ -438,12 +456,21 @@ async function pidRefreshPrefixRules() {
}
}
async function pidAddPrefixRule(category) {
// 가상 카테고리 키 → { DB category, tagDcs } 변환
function pidResolveCat(vcat) {
const meta = CATEGORY_META[vcat];
return {
category: meta?.dbCat ?? vcat,
tagDcs: meta?.tagDcs ?? false
};
}
async function pidAddPrefixRule(vcat) {
const row = event.target.closest('.pid-cat-add');
const prefix = row.querySelector('.pid-cat-prefix-input').value.trim();
const desc = row.querySelector('.pid-cat-desc-input').value.trim();
const order = parseInt(row.querySelector('.pid-cat-order-input').value) || 10;
const tagDcs = row.querySelector('.pid-cat-dcs-input')?.checked ?? false;
const desc = row.querySelector('.pid-cat-desc-input').value.trim();
const order = parseInt(row.querySelector('.pid-cat-order-input').value) || 10;
const { category, tagDcs } = pidResolveCat(vcat); // 그룹이 tagDcs 결정
if (!prefix) { alert('Prefix를 입력하세요.'); return; }
@@ -466,14 +493,17 @@ async function pidAddPrefixRule(category) {
}
async function pidUpdatePrefixRule(id, btn) {
const row = btn.closest('.pid-cat-row');
const row = btn.closest('.pid-cat-row');
const group = btn.closest('.pid-cat-group');
const catIdx = [...group.parentElement.children].indexOf(group);
const cat = CATEGORY_ORDER[catIdx];
const vcat = group.dataset.vcat; // data-vcat 속성으로 가상 키 읽기
const { category } = pidResolveCat(vcat);
const prefix = row.querySelector('.pid-cat-prefix-input').value.trim();
const desc = row.querySelector('.pid-cat-desc-input').value.trim();
const order = parseInt(row.querySelector('.pid-cat-order-input').value) || 10;
const tagDcs = row.querySelector('.pid-cat-dcs-input')?.checked ?? false;
const desc = row.querySelector('.pid-cat-desc-input').value.trim();
const order = parseInt(row.querySelector('.pid-cat-order-input').value) || 10;
// instrument 행에는 DCS 토글이 있음 → 체크 상태로 tagDcs 결정 (그룹 이동 가능)
// 그 외 행에는 토글 없음 → 그룹 기본값 사용
const dcsInput = row.querySelector('.pid-cat-dcs-input');
const tagDcs = dcsInput ? dcsInput.checked : (CATEGORY_META[vcat]?.tagDcs ?? false);
if (!prefix) { alert('Prefix를 입력하세요.'); return; }
@@ -481,7 +511,7 @@ async function pidUpdatePrefixRule(id, btn) {
const res = await fetch(`/api/pid/prefix-rules/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prefix, category: cat, tagDcs, description: desc, sortOrder: order })
body: JSON.stringify({ prefix, category, tagDcs, description: desc, sortOrder: order })
});
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }));