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:
@@ -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 }));
|
||||||
|
|||||||
Reference in New Issue
Block a user