From 300dfe65a485aec48243fb1f11e1082159920a1a Mon Sep 17 00:00:00 2001 From: windpacer Date: Wed, 27 May 2026 15:47:09 +0900 Subject: [PATCH] =?UTF-8?q?feat(pid-ui):=20PREFIX=20=EB=B6=84=EB=A5=98=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=20=ED=8C=A8=EB=84=90=20=E2=80=94=20DCS=20?= =?UTF-8?q?=ED=83=9C=EA=B7=B8/=ED=98=84=EC=9E=A5=20=EA=B3=84=EA=B8=B0=20?= =?UTF-8?q?=EC=84=B9=EC=85=98=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 변경 내용 ### 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 --- src/Web/wwwroot/js/pid.js | 124 +++++++++++++++++++++++--------------- 1 file changed, 77 insertions(+), 47 deletions(-) diff --git a/src/Web/wwwroot/js/pid.js b/src/Web/wwwroot/js/pid.js index 0277e0d..1064b66 100644 --- a/src/Web/wwwroot/js/pid.js +++ b/src/Web/wwwroot/js/pid.js @@ -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 += `
+ // 그룹 헤더: 규칙이 0건이어도 추가 입력행은 항상 표시 + const count = rules ? rules.length : 0; + const isInstr = vcat === 'instrument_dcs' || vcat === 'instrument_field'; + + html += `
${meta.label} - ${rules.length} + ${count} - + - - +
`; - for (const r of rules) { - html += `
- - - - - - - - -
`; + if (rules) { + for (const r of rules) { + // instrument 그룹은 DCS 이동용 토글 표시 (체크 해제 시 반대 그룹으로 이동) + const dcsToggle = isInstr + ? `` + : ''; + html += `
+ + + + ${dcsToggle} + + + + +
`; + } } html += `
`; @@ -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 }));