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