feat: Point Builder 실시간 추가/제거(선택분만) + 미리보기 닫기
- 전체 태그 목록 체크박스 + 버튼을 realtime_table 기준으로 전환 · + 실시간 추가: 선택 태그만 is_active+realtime_enabled 켜고 realtime_table 직접 등록(즉시 라이브) · - 실시간 제거: realtime_enabled 끄고 realtime_table 행 직접 삭제 · full-Sync 미사용(전체 추가 사고 방지) → 선택 id만 insert/delete · 신규 API: POST /api/pointbuilder/realtime-add, realtime-remove · 레거시 pbBulkActivate(is_active만 토글) 제거 - 미리보기 ✕ 닫기 버튼 추가(설정 화면 복귀) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -332,6 +332,36 @@ public class PointBuilderController : ControllerBase
|
|||||||
return Ok(new { success = true, count, message = $"{count}개 포인트 추가 완료" });
|
return Ok(new { success = true, count, message = $"{count}개 포인트 추가 완료" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed record RealtimeBulkDto(List<int>? Ids, string? ControllerId);
|
||||||
|
|
||||||
|
// 전체 태그 목록에서 선택(id)한 태그를 realtime_table에 즉시 라이브로 편입
|
||||||
|
[HttpPost("realtime-add")]
|
||||||
|
public async Task<IActionResult> RealtimeAdd([FromBody] RealtimeBulkDto dto)
|
||||||
|
{
|
||||||
|
if (dto.Ids == null || dto.Ids.Count == 0)
|
||||||
|
return BadRequest(new { success = false, error = "ids는 최소 1개 이상 필요합니다" });
|
||||||
|
if (string.IsNullOrEmpty(dto.ControllerId))
|
||||||
|
return BadRequest(new { success = false, error = "controllerId는 필수입니다" });
|
||||||
|
|
||||||
|
var count = await _db.Hc900RealtimeAddByIdsAsync(dto.ControllerId, dto.Ids);
|
||||||
|
_log.LogInformation("[PointBuilder] 실시간 추가: {Count}개 (controller={Ctrl})", count, dto.ControllerId);
|
||||||
|
return Ok(new { success = true, count, message = $"{count}개 실시간 편입 완료" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택(id)한 태그를 realtime_table에서 제거
|
||||||
|
[HttpPost("realtime-remove")]
|
||||||
|
public async Task<IActionResult> RealtimeRemove([FromBody] RealtimeBulkDto dto)
|
||||||
|
{
|
||||||
|
if (dto.Ids == null || dto.Ids.Count == 0)
|
||||||
|
return BadRequest(new { success = false, error = "ids는 최소 1개 이상 필요합니다" });
|
||||||
|
if (string.IsNullOrEmpty(dto.ControllerId))
|
||||||
|
return BadRequest(new { success = false, error = "controllerId는 필수입니다" });
|
||||||
|
|
||||||
|
var count = await _db.Hc900RealtimeRemoveByIdsAsync(dto.ControllerId, dto.Ids);
|
||||||
|
_log.LogInformation("[PointBuilder] 실시간 제거: {Count}개 (controller={Ctrl})", count, dto.ControllerId);
|
||||||
|
return Ok(new { success = true, count, message = $"{count}개 실시간 제거 완료" });
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("points")]
|
[HttpGet("points")]
|
||||||
public async Task<IActionResult> GetPoints(
|
public async Task<IActionResult> GetPoints(
|
||||||
[FromQuery] string? rt = null, // null/"" = 전체, "only" = 실시간(realtime_table만), "exclude" = 실시간 제외
|
[FromQuery] string? rt = null, // null/"" = 전체, "only" = 실시간(realtime_table만), "exclude" = 실시간 제외
|
||||||
|
|||||||
@@ -70,6 +70,13 @@ async function pbPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pbClosePreview() {
|
||||||
|
const area = document.getElementById('pb-preview-area');
|
||||||
|
if (area) area.style.display = 'none';
|
||||||
|
const chk = document.getElementById('pb-chk-all-preview');
|
||||||
|
if (chk) chk.checked = false;
|
||||||
|
}
|
||||||
|
|
||||||
async function pbBuild() {
|
async function pbBuild() {
|
||||||
if (!confirm('모든 활성 태그를 해제하고 조건에 맞는 태그만 활성화합니다. 계속하시겠습니까?')) return;
|
if (!confirm('모든 활성 태그를 해제하고 조건에 맞는 태그만 활성화합니다. 계속하시겠습니까?')) return;
|
||||||
const payload = pbCollectAllGroups();
|
const payload = pbCollectAllGroups();
|
||||||
@@ -322,6 +329,39 @@ function pbUpdateBulkBar() {
|
|||||||
const checked = document.querySelectorAll('#pb-points-tbody .pb-point-chk:checked');
|
const checked = document.querySelectorAll('#pb-points-tbody .pb-point-chk:checked');
|
||||||
const bar = document.getElementById('pb-points-bulk');
|
const bar = document.getElementById('pb-points-bulk');
|
||||||
bar.hidden = checked.length === 0;
|
bar.hidden = checked.length === 0;
|
||||||
|
const cnt = document.getElementById('pb-points-selcount');
|
||||||
|
if (cnt) cnt.textContent = checked.length ? `${checked.length}개 선택` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function pbSelectedPointIds() {
|
||||||
|
return Array.from(document.querySelectorAll('#pb-points-tbody .pb-point-chk:checked'))
|
||||||
|
.map(cb => parseInt(cb.value)).filter(n => !isNaN(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택 태그를 realtime_table에 즉시 라이브로 편입
|
||||||
|
async function pbRealtimeAdd() {
|
||||||
|
const ids = pbSelectedPointIds();
|
||||||
|
const ctrl = pbGetControllerId();
|
||||||
|
if (ids.length === 0) return;
|
||||||
|
if (!ctrl) { alert('컨트롤러를 선택해주세요.'); return; }
|
||||||
|
if (!confirm(`[${ctrl}] 선택한 ${ids.length}개를 실시간(realtime_table)에 추가합니다. 계속하시겠습니까?`)) return;
|
||||||
|
const res = await pbApi('/api/pointbuilder/realtime-add', 'POST', { ids, controllerId: ctrl });
|
||||||
|
alert(res.message || `${res.count}개 실시간 편입`);
|
||||||
|
pbRefresh();
|
||||||
|
pbLoadSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택 태그를 realtime_table에서 제거
|
||||||
|
async function pbRealtimeRemove() {
|
||||||
|
const ids = pbSelectedPointIds();
|
||||||
|
const ctrl = pbGetControllerId();
|
||||||
|
if (ids.length === 0) return;
|
||||||
|
if (!ctrl) { alert('컨트롤러를 선택해주세요.'); return; }
|
||||||
|
if (!confirm(`[${ctrl}] 선택한 ${ids.length}개를 실시간(realtime_table)에서 제거합니다. 계속하시겠습니까?`)) return;
|
||||||
|
const res = await pbApi('/api/pointbuilder/realtime-remove', 'POST', { ids, controllerId: ctrl });
|
||||||
|
alert(res.message || `${res.count}개 실시간 제거`);
|
||||||
|
pbRefresh();
|
||||||
|
pbLoadSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pbDeleteOne(id) {
|
async function pbDeleteOne(id) {
|
||||||
@@ -333,19 +373,6 @@ async function pbDeleteOne(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pbBulkActivate(active) {
|
|
||||||
const ids = Array.from(document.querySelectorAll('#pb-points-tbody .pb-point-chk:checked'))
|
|
||||||
.map(cb => parseInt(cb.value));
|
|
||||||
const ctrl = pbGetControllerId();
|
|
||||||
if (ids.length === 0) return;
|
|
||||||
if (!confirm(`[${ctrl || '전체'}] 선택한 ${ids.length}개를 ${active ? '활성화' : '비활성화'}합니다. 계속하시겠습니까?`)) return;
|
|
||||||
const res = await pbApi('/api/hc900/tags/bulk-active', 'POST', { ids, active, controllerId: ctrl || undefined });
|
|
||||||
if (res) {
|
|
||||||
pbRefresh();
|
|
||||||
pbLoadSummary();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Sinam xlsx 파싱 ────────────────────────────────────────────────────────────
|
// ── Sinam xlsx 파싱 ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
let _pbSinamBusy = false;
|
let _pbSinamBusy = false;
|
||||||
|
|||||||
@@ -189,10 +189,11 @@
|
|||||||
<div id="pb-preview-area" class="pb-preview" style="display:none">
|
<div id="pb-preview-area" class="pb-preview" style="display:none">
|
||||||
<div class="pb-preview-header">
|
<div class="pb-preview-header">
|
||||||
<span>미리보기 <span id="pb-preview-count" style="color:var(--a)"></span></span>
|
<span>미리보기 <span id="pb-preview-count" style="color:var(--a)"></span></span>
|
||||||
<div class="pb-preview-actions">
|
<div class="pb-preview-actions" style="display:flex;align-items:center;gap:10px">
|
||||||
<label style="font-size:12px;display:flex;align-items:center;gap:4px;cursor:pointer">
|
<label style="font-size:12px;display:flex;align-items:center;gap:4px;cursor:pointer">
|
||||||
<input type="checkbox" id="pb-chk-all-preview" onchange="pbToggleAllPreview(this)"> 전체 선택
|
<input type="checkbox" id="pb-chk-all-preview" onchange="pbToggleAllPreview(this)"> 전체 선택
|
||||||
</label>
|
</label>
|
||||||
|
<button class="btn-c" style="padding:3px 10px;font-size:11px" onclick="pbClosePreview()">✕ 닫기</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="overflow-x:auto">
|
<div style="overflow-x:auto">
|
||||||
@@ -297,9 +298,10 @@
|
|||||||
<option value="exclude">실시간 제외</option>
|
<option value="exclude">실시간 제외</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="btn-c" onclick="pbReload()" style="padding:3px 10px;font-size:11px">조회</button>
|
<button class="btn-c" onclick="pbReload()" style="padding:3px 10px;font-size:11px">조회</button>
|
||||||
<div style="display:flex;gap:6px" id="pb-points-bulk" hidden>
|
<div style="display:flex;gap:6px;align-items:center" id="pb-points-bulk" hidden>
|
||||||
<button class="btn-a" style="padding:3px 10px;font-size:11px" onclick="pbBulkActivate(true)">✓ 선택 활성화</button>
|
<span id="pb-points-selcount" style="font-size:11px;color:var(--t2)"></span>
|
||||||
<button class="btn-b" style="padding:3px 10px;font-size:11px;background:#5a1a1a;color:#faa" onclick="pbBulkActivate(false)">✗ 선택 비활성화</button>
|
<button class="btn-a" style="padding:3px 10px;font-size:11px" onclick="pbRealtimeAdd()">+ 실시간 추가</button>
|
||||||
|
<button class="btn-b" style="padding:3px 10px;font-size:11px;background:#5a1a1a;color:#faa" onclick="pbRealtimeRemove()">- 실시간 제거</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -381,6 +381,72 @@ public class Hc900DbContext : DbContext
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 선택 태그(id)만 realtime_table에 편입(즉시 라이브). full-Sync 금지(전체 추가됨) → 선택분만 직접 insert.
|
||||||
|
public async Task<int> Hc900RealtimeAddByIdsAsync(string controllerId, IEnumerable<int> ids)
|
||||||
|
{
|
||||||
|
var idList = ids.Distinct().ToList();
|
||||||
|
if (idList.Count == 0) return 0;
|
||||||
|
using var tx = await Database.BeginTransactionAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entries = await Hc900MapEntries
|
||||||
|
.Where(x => idList.Contains(x.Id) && x.ControllerId == controllerId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// 폴링 대상화: is_active + realtime_enabled
|
||||||
|
foreach (var e in entries) { e.IsActive = true; e.RealtimeEnabled = true; }
|
||||||
|
|
||||||
|
// realtime_table에 없는 것만 직접 등록
|
||||||
|
var tagNames = entries.Select(e => e.TagName).ToList();
|
||||||
|
var existing = await RealtimePoints
|
||||||
|
.Where(r => r.ControllerId == controllerId && tagNames.Contains(r.TagName))
|
||||||
|
.Select(r => r.TagName).ToListAsync();
|
||||||
|
var toAdd = entries.Where(e => !existing.Contains(e.TagName))
|
||||||
|
.Select(e => new RealtimePoint
|
||||||
|
{
|
||||||
|
ControllerId = controllerId,
|
||||||
|
TagName = e.TagName,
|
||||||
|
NodeId = e.Hc900Tag,
|
||||||
|
LiveValue = null,
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
});
|
||||||
|
RealtimePoints.AddRange(toAdd);
|
||||||
|
|
||||||
|
await SaveChangesAsync();
|
||||||
|
await tx.CommitAsync();
|
||||||
|
return entries.Count;
|
||||||
|
}
|
||||||
|
catch { await tx.RollbackAsync(); throw; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택 태그(id)만 realtime_table에서 제거. realtime_enabled=FALSE로 폴링 재추가 차단 + 행 직접 삭제.
|
||||||
|
public async Task<int> Hc900RealtimeRemoveByIdsAsync(string controllerId, IEnumerable<int> ids)
|
||||||
|
{
|
||||||
|
var idList = ids.Distinct().ToList();
|
||||||
|
if (idList.Count == 0) return 0;
|
||||||
|
using var tx = await Database.BeginTransactionAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entries = await Hc900MapEntries
|
||||||
|
.Where(x => idList.Contains(x.Id) && x.ControllerId == controllerId)
|
||||||
|
.ToListAsync();
|
||||||
|
var tagNames = entries.Select(e => e.TagName).ToList();
|
||||||
|
|
||||||
|
// 폴링이 다시 upsert하지 않도록 realtime_enabled 끄기
|
||||||
|
foreach (var e in entries) e.RealtimeEnabled = false;
|
||||||
|
|
||||||
|
var rows = await RealtimePoints
|
||||||
|
.Where(r => r.ControllerId == controllerId && tagNames.Contains(r.TagName))
|
||||||
|
.ToListAsync();
|
||||||
|
RealtimePoints.RemoveRange(rows);
|
||||||
|
|
||||||
|
await SaveChangesAsync();
|
||||||
|
await tx.CommitAsync();
|
||||||
|
return rows.Count;
|
||||||
|
}
|
||||||
|
catch { await tx.RollbackAsync(); throw; }
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<RealtimePoint> Hc900AddRealtimePointAsync(
|
public async Task<RealtimePoint> Hc900AddRealtimePointAsync(
|
||||||
string tagName, string hc900Tag, int modbusAddr,
|
string tagName, string hc900Tag, int modbusAddr,
|
||||||
string dataType, string? paramType, string access, string controllerId)
|
string dataType, string? paramType, string access, string controllerId)
|
||||||
|
|||||||
Reference in New Issue
Block a user