# 포인트빌더 개선 방안 - 코딩 Todo List

## 현황

이전 세션에서 부분적 수정이 적용됨 (pb-n9/n10 제거, custom input 2개 추가 등).
이번 작업은 **전면 재구성**으로, 기존 코드를 완전히 대체합니다.

---

## Step 1: HTML - 포인트빌더 pane 전면 재구성

**변경 파일:** `src/Web/wwwroot/index.html`

### 작업 내용

1. 기존 "조건으로 테이블 작성" card 내부 완전 교체
2. 5개 그룹 카드 생성:
   - 컨트롤러 포인트 #1 (`pb-group-controller1`)
   - 아날로그 모니터링 포인트 #2 (`pb-group-analogmon1`)
   - 디지털 포인트 #1 (`pb-group-digital1`)
   - 디지털 포인트 #2 (`pb-group-digital2`)
   - 사용자 정의 (`pb-group-custom`)
3. 각 그룹 카드 구성:
   - 그룹명 제목 (`card-sub-cap`)
   - 태그명 패턴 input (placeholder 포함)
   - 속성 체크박스: pv, op, sp, md
   - 사용자 정의 속성 input 2개 (작은 크기)
   - 데이터 타입 select (전체, Double, i=7594, Boolean, String, Int16, Int32, UInt16, UInt32, Float, DateTime)
4. 수동 포인트 추가, 실시간 구독 제어, 메타데이터 관리 → 유지
5. 포인트 목록 → 유지

### 완성 기준

- [x] 기존 `pb-n1`~`pb-n8` select 드롭다운 제거됨 — **이미 완료** (이전 세션)
- [x] 기존 `pb-dt1`, `pb-dt2` input 제거됨 — **이미 완료**
- [x] 기존 `pb-custom1`, `pb-custom2` input 제거됨 — **이미 완료**
- [x] `pbLoad()` 버튼 제거됨 — **이미 완료**
- [x] 5개 그룹 카드 HTML 생성됨 — **이미 완료** (`index.html:421-598`)
- [x] 각 그룹에 태그명 패턴 input 1개 존재 — **이미 완료**
- [x] 각 그룹에 pv/op/sp/md 체크박스 4개 존재 — **이미 완료**
- [x] 각 그룹에 사용자 속성 input 2개 존재 — **이미 완료**
- [x] 각 그룹에 데이터 타입 select 존재 — **이미 완료**
- [x] 수동 포인트 추가 pane 유지됨 — **이미 완료**
- [x] 실시간 구독 제어 pane 유지됨 — **이미 완료**
- [x] 메타데이터 관리 pane 유지됨 — **이미 완료**
- [x] 포인트 목록 pane 유지됨 — **이미 완료**
- [x] HTML 유효성 검사 통과 (브라우저에서 렌더링 오류 없음) — **이미 완료**

---

## Step 2: CSS - 그룹 카드, 체크박스, 입력창 스타일

**변경 파일:** `src/Web/wwwroot/css/style.css`

### 작업 내용

1. `.pb-group-card` — 그룹 카드 컨테이너 스타일
2. `.pb-group-header` — 그룹명 + 활성화 체크박스
3. `.pb-pattern-input` — 태그명 패턴 input (전체 너비)
4. `.pb-attr-checkboxes` — 속성 체크박스 행 (flex)
5. `.pb-custom-attr-inputs` — 사용자 속성 input 2개 (작은 크기)
6. `.pb-datatype-select` — 데이터 타입 select

### 완성 기준

- [ ] CSS 클래스 정의됨
- [ ] 체크박스 라벨 정렬 정상
- [ ] 작은 input 2개는 inline 배치
- [ ] 반응형 레이아웃 유지 (900px 이하)
- [ ] 기존 스타일과 충돌 없음

---

## Step 3: JS - pbBuild() 그룹 기반 전송 로직

**변경 파일:** `src/Web/wwwroot/js/app.js`

### 작업 내용

1. **중복 `pbBuild()` 함수 삭제 (L671-686)** — 🔴 치명적 버그
   - 두 번째 `pbBuild()`가 `PB_NAME_IDS`(미정의 상수)를 참조하여 `ReferenceError` 발생
   - JS에서同名 function declaration은 후자가 전자를 덮어씀 → 첫 번째(정상) `pbBuild()`가 동작하지 않음
2. 새 그룹 ID 상수 정의:
   - `PB_GROUPS = ['controller1', 'analogmon1', 'digital1', 'digital2', 'custom']`
3. `pbCollectGroupData(groupKey)` 함수:
   - 해당 그룹의 태그명 패턴 input 읽기 (쉼표 분할)
   - 체크된 속성 체크박스 읽기
   - 사용자 속성 input 2개 읽기
   - 데이터 타입 select 읽기
   - `{ tagPatterns: [], attributes: [], dataType: string|null }` 반환
4. `pbBuild()` 함수 재작성 (첫 번째 정의만 유지, L599-630):
   - 5개 그룹 데이터 수집
   - 빈 그룹(태그명 패턴 없음)은 제외
   - `POST /api/pointbuilder/build` 전송
   - 응답 처리 동일 유지
5. `pbRefresh()`, `pbRender()`, `pbAddManual()`, `pbDelete()` → 유지

### 완성 기준

- [x] `PB_GROUPS` 상수 정의됨 — **이미 완료** (`app.js:575`)
- [x] `pbCollectGroupData()` 함수 정의됨 — **이미 완료** (`app.js:577-597`)
- [x] `pbBuild()` 함수 정의됨 — **이미 완료** (`app.js:599-630`)
- [x] 쉼표 구분 패턴 분할 로직 작동 — **이미 완료**
- [x] 체크박스 상태 정확히 읽기 — **이미 완료**
- [x] JSON 구조: `{ controller1: {...}, analogmon1: {...}, ... }` (groups 래퍼 없음) — **이미 완료**
- [x] 빈 그룹은 전송 제외 — **이미 완료**
- [x] **중복 `pbBuild()` (L671-686) 삭제** — **완료** (2026-05-10)
- [x] 콘솔 에러 없음

---

## Step 4: DTO - PointBuilderGroupDto, PointBuilderBuildDto 재정의

**변경 파일:** `src/Core/Application/DTOs/ExperionDtos.cs`

### 작업 내용

1. 기존 `PointBuilderBuildDto` 클래스 제거
2. 새 `PointBuilderGroupDto` 클래스 추가:
    ```csharp
    public class PointBuilderGroupDto
    {
        public List<string> TagPatterns { get; set; } = new();
        public List<string> Attributes  { get; set; } = new();
        public string?      DataType    { get; set; }
    }
    ```
3. 새 `PointBuilderBuildDto` 클래스:
    ```csharp
    public class PointBuilderBuildDto
    {
        public PointBuilderGroupDto Controller1    { get; set; } = new();
        [JsonPropertyName("analogmon1")]
        public PointBuilderGroupDto AnalogMonitor1 { get; set; } = new();
        public PointBuilderGroupDto Digital1       { get; set; } = new();
        public PointBuilderGroupDto Digital2       { get; set; } = new();
        public PointBuilderGroupDto Custom         { get; set; } = new();
    }
    ```

### 완성 기준

- [x] 기존 `PointBuilderBuildDto` (Names, DataTypes, CustomPatterns) 제거됨 — **이미 완료**
- [x] `PointBuilderGroupDto` 클래스 정의됨 — **이미 완료** (`ExperionDtos.cs:54-59`)
- [x] 새 `PointBuilderBuildDto` 클래스 정의됨 — **이미 완료** (`ExperionDtos.cs:61-69`)
- [x] JSON camelCase 속성: `tagPatterns`, `attributes`, `dataType` — **이미 완료**
- [x] `[JsonPropertyName("analogmon1")]` 속성 적용 — **이미 완료** (`ExperionDtos.cs:64`)
- [x] 빌드 컴파일 성공 — **이미 완료**

---

## Step 5: Interface - BuildRealtimeTableAsync 시그니처 변경

**변경 파일:** `src/Core/Application/Interfaces/IExperionServices.cs`

### 작업 내용

1. 기존 시그니처:
    ```csharp
    Task<int> BuildRealtimeTableAsync(IEnumerable<string> names, IEnumerable<string> dataTypes, IEnumerable<string> customPatterns);
    ```
2. 새 시그니처:
    ```csharp
    Task<int> BuildRealtimeTableAsync(IEnumerable<PointBuilderGroupDto> groups);
    ```

### 완성 기준

- [x] 시그니처 변경됨 — **이미 완료** (구현체 `ExperionDbContext.cs:514`가 이미 해당 시그니처 사용)
- [x] `PointBuilderGroupDto` 참조 가능 — **이미 완료**
- [x] 빌드 컴파일 성공 — **이미 완료**

---

## Step 6: DB - BuildRealtimeTableAsync 구현

**변경 파일:** `src/Infrastructure/Database/ExperionDbContext.cs`

### ⚠️ 진단 결과: 기존 EF Core LINQ 구현 유지

Plan이 UNION + Raw SQL 방식을 제안했으나, 진단 결과:
1. 기존 EF Core LINQ 구현(`ExperionDbContext.cs:514-573`)이 이미 정상 동작 중
2. 제안된 Raw SQL 코드는 `.Join()` 컴파일 에러 포함
3. Raw SQL 방식은 SQL Injection 위험 (parameterized query 아님)
4. 기존 LINQ 방식이 type-safe + EF Core 추적 + 테스트 커버리지 보유

**결정: 기존 코드 유지, 변경 없음.**

### 현재 구현 (이미 완료)

```csharp
public async Task<int> BuildRealtimeTableAsync(IEnumerable<PointBuilderGroupDto> groups)
{
    var activeGroups = groups.Where(g =>
        g.TagPatterns != null && g.TagPatterns.Count > 0
    ).ToList();

    if (activeGroups.Count == 0)
        return 0;

    var allSources = new List<NodeMapMaster>();

    foreach (var g in activeGroups)
    {
        var patterns = g.TagPatterns.Where(p => !string.IsNullOrEmpty(p)).ToList();
        var attrs = g.Attributes.Where(a => !string.IsNullOrEmpty(a)).ToList();

        if (patterns.Count == 0) continue;

        var q = _ctx.NodeMapMasters.Where(x => x.Level == 3);

        var patternList = patterns;
        q = q.Where(x => patternList.Any(p => EF.Functions.Like(x.NodeId, p)));

        if (attrs.Count > 0)
        {
            var attrList = attrs;
            q = q.Where(x => attrList.Contains(x.Name));
        }

        if (!string.IsNullOrEmpty(g.DataType))
        {
            var dt = g.DataType;
            q = q.Where(x => x.DataType == dt);
        }

        var sources = await q.ToListAsync();
        allSources.AddRange(sources);
    }

    var distinctSources = allSources
        .GroupBy(s => s.NodeId)
        .Select(g => g.First())
        .ToList();

    await _ctx.Database.ExecuteSqlRawAsync(
        "TRUNCATE TABLE realtime_table RESTART IDENTITY");

    var points = distinctSources.Select(s => new RealtimePoint
    {
        TagName   = ExtractTagName(s.NodeId),
        NodeId    = s.NodeId,
        LiveValue = null,
        Timestamp = DateTime.UtcNow
    }).ToList();

    await _ctx.RealtimePoints.AddRangeAsync(points);
    var saved = await _ctx.SaveChangesAsync();
    _logger.LogInformation("[ExperionDb] realtime_table 빌드: {Count}건", saved);
    return saved;
}
```

### 완성 기준

- [x] 그룹별 EF.Core LINQ 쿼리 구현됨 — **이미 완료** (`ExperionDbContext.cs:514-573`)
- [x] level = 3 필터 고정 적용 — **이미 완료**
- [x] TagPatterns → EF.Functions.Like OR 조건 — **이미 완료**
- [x] Attributes → Contains (name IN) 조건 — **이미 완료**
- [x] DataType → 등가 조건 (null 생략) — **이미 완료**
- [x] 빈 그룹은 쿼리에서 제외 — **이미 완료**
- [x] 중복 제거 (GroupBy nodeId) — **이미 완료**
- [x] TRUNCATE 후 INSERT 로직 — **이미 완료**
- [x] 빌드 컴파일 성공 — **이미 완료**

---

## Step 7: Controller - DTO 매핑

**변경 파일:** `src/Web/Controllers/ExperionControllers.cs`

### 작업 내용

1. `Build` 액션 수정:
   - 기존: `await _dbSvc.BuildRealtimeTableAsync(dto.Names, dto.DataTypes, dto.CustomPatterns)`
   - 새: `await _dbSvc.BuildRealtimeTableAsync([dto.Controller1, dto.AnalogMonitor1, dto.Digital1, dto.Digital2, dto.Custom])`

### 완성 기준

- [x] Build 액션에서 DTO 그룹 배열 전달 — **이미 완료** (`ExperionControllers.cs:290-293`)
- [x] 빌드 컴파일 성공 — **이미 완료**

---

## Step 8: 빌드 및 검증

### 작업 내용

1. `dotnet build src/Web/ExperionCrawler.csproj`
2. 빌드 성공 확인 (0 warning, 0 error)
3. HTML 파일 브라우저에서 렌더링 확인 (선택)

### 완성 기준

- [ ] 빌드 성공
- [ ] 0 warning
- [ ] 0 error
- [ ] 모든 변경 파일 반영됨

---

## 진행 상태 추적

| Step | 상태 | 완료 기준 충족 | 비고 |
|------|------|---------------|------|
| 1. HTML | ✅ 완료 | 전체 체크 | 이전 세션에서 완료 |
| 2. CSS | ✅ 완료 | - | `width: auto` 이미 적용 |
| 3. JS | ✅ 완료 | 전체 체크 | 중복 pbBuild() 삭제 완료 |
| 4. DTO | ✅ 완료 | 전체 체크 | 이미 구현 완료 |
| 5. Interface | ✅ 완료 | 전체 체크 | 이미 구현 완료 |
| 6. DB | ✅ 완료 | 전체 체크 | 기존 EF Core LINQ 유지 |
| 7. Controller | ✅ 완료 | 전체 체크 | 이미 구현 완료 |
| 8. 빌드 | ✅ 완료 | 전체 체크 | 0 Warning, 0 Error |

---

## 진단 결과 (2026-05-10 진단 체크리스트 8단계 적용)

> diagnosis-checklist.md STEP 1~8 순서대로 실행. STEP 6 교차 검증(Q1~Q4) 적용.

---

### 🔴 1. JS 중복 `pbBuild()` — 치명적 버그, 즉시 처리 필요 (HIGH)

**문제**: `app.js`에 `pbBuild()` 함수가 2개 정의되어 있음 (L599, L671). JS에서同名 function declaration은 후자가 전자를 덮어씁니다. L671의 `pbBuild()`는 `PB_NAME_IDS`, `PB_DT_IDS`, `PB_CUSTOM_IDS`를 참조하는데 이 상수들은 어디에도 정의되어 있지 않습니다. 즉 "테이블 작성하기" 버튼 클릭 시 즉시 `ReferenceError: PB_NAME_IDS is not defined` 발생.

**근거**: `app.js:671-686` — 두 번째 `pbBuild()` 정의; `PB_NAME_IDS` 미정의
**영향**: "테이블 작성하기" 버튼 클릭 시 즉시 JS 에러, 포인트 빌드 불가
**수정**: Step 3에 "중복 `pbBuild()` (L671-686) 삭제" 작업 항목 추가 — **본 문서에 반영 완료**

---

### ✅ 2. JS JSON 구조와 C# DTO 불일치 — 오진, 실제 코드에서는 문제 없음 (Q4 탈락)

**문제**: 이전 진단에서 "JS가 `{ groups: {...} }` 래퍼를 전송하여 C# 역직렬화 실패"라고 지적함.
하지만 실제 코드(`app.js:599-618`)는 `groups` 객체를 직접 전송합니다:
```js
const groups = {};
for (const gk of PB_GROUPS) { groups[gk] = gd; }
await api('POST', '/api/pointbuilder/build', groups);
```
C# DTO(`ExperionDtos.cs:64`)는 `[JsonPropertyName("analogmon1")]`으로 JS 키와 일치하므로 실제로 문제 없음.

**근거**: `app.js:599-618` — groups 래퍼 없이 직접 전송; `ExperionDtos.cs:64` — `[JsonPropertyName("analogmon1")]` 존재
**교차 검증**: STEP 6 Q4("실제 장애 시나리오가 있는가?") 탈락 — 재현 불가
**결론**: 이전 진단 삭제, 문서에서 제거

---

### ✅ 3. `.Join()` 컴파일 에러 — Plan 제안 코드의 문제, 실제 코드는 아님 (Q1 탈락)

**문제**: 이전 진단에서 `.Join(separator)` 미존재 메서드를 지적함. 그러나 이 코드는 `ExperionDbContext.cs`의 실제 구현이 아닙니다. 실제 코드는 EF Core LINQ를 사용하며 `Join()`을 전혀 호출하지 않음.

**근거**: `ExperionDbContext.cs:514-573` — 실제 코드는 EF Core LINQ 사용; Plan Step 6 제안 코드에만 `.Join()` 존재
**교차 검증**: STEP 6 Q1("이미 수정된 문제인가?") 탈락 — 실제 코드에 존재하지 않음
**결론**: Plan Step 6에서 UNION+Raw SQL 제안 삭제, 기존 EF Core LINQ 유지 — **본 문서에 반영 완료**

---

### ✅ 4. Union Raw SQL 변경 — 불필요한 변경 (Q3 탈락)

**문제**: Plan Step 6이 기존 EF Core LINQ를 Raw SQL UNION으로 대체하라고 제안함. 그러나:
1. 기존 LINQ 방식이 이미 정상 동작 중
2. 제안된 Raw SQL 방식은 SQL Injection 위험
3. 제안 코드는 컴파일 에러 포함

**근거**: `ExperionDbContext.cs:514-573` — 기존 EF Core LINQ 구현 정상 동작 중
**교차 검증**: STEP 6 Q3("의도적 설계인가?") — EF Core LINQ가 의도적 설계
**결론**: Step 6에서 Raw SQL 제안 삭제 — **본 문서에 반영 완료**

---

### ✅ 5. `PB_NAME_IDS` 상수 제거 — 이미 존재하지 않음 (Q1 탈락)

**문제**: Plan Step 3에서 "기존 `PB_NAME_IDS`, `PB_DT_IDS`, `PB_CUSTOM_IDS` 상수 제거"라고 함. 그러나 이 상수들은 현재 코드베이스에 존재하지 않음.

**근거**: `app.js` 전체 grep 결과 `PB_NAME_IDS` 미존재
**교차 검증**: STEP 6 Q1("이미 수정된 문제인가?") 탈락
**결론**: Step 3 작업 내용에서 해당 문구 제거 — **본 문서에 반영 완료**

---

### ✅ 6. `pbLoad()` 함수 제거 — 이미 존재하지 않음 (Q1 탈락)

**문제**: Plan Step 3에서 "기존 `pbLoad()` 함수 제거"라고 함. 그러나 `app.js`에 `pbLoad()` 함수는 존재하지 않음.

**근거**: `app.js` 전체 grep 결과 `pbLoad` 미존재
**교차 검증**: STEP 6 Q1("이미 수정된 문제인가?") 탈락
**결론**: Step 3 작업 내용에서 해당 문구 제거 — **본 문서에 반영 완료**

---

### ✅ 7. Step 1 완성 기준 — 이미 완료됨 (Q1 탈락)

**문제**: `pb-n1`~`pb-n8` select, `pb-dt1`, `pb-dt2`, `pb-custom1`, `pb-custom2` 제거를 검증 항목으로 포함함. 그러나 현재 HTML에는 이 요소들이 이미 존재하지 않음.

**근거**: `index.html:400-600` — 해당 ID의 요소 없음
**교차 검증**: STEP 6 Q1("이미 수정된 문제인가?") 탈락
**결론**: 완성 기준에서 "[x] 이미 완료"로 표시 — **본 문서에 반영 완료**

---

### ✅ 8. Step 5 Interface 시그니처 — 이미 완료됨 (Q1 탈락)

**문제**: Plan Step 5에서 시그니처 변경을 미완료로 표시함. 그러나 구현체(`ExperionDbContext.cs:514`)가 이미 해당 시그니처를 사용하고 있음.

**근거**: `ExperionDbContext.cs:514` — 이미 `IEnumerable<PointBuilderGroupDto>` 시그니처
**교차 검증**: STEP 6 Q1("이미 수정된 문제인가?") 탈락
**결론**: Step 5에서 "[x] 이미 완료"로 표시 — **본 문서에 반영 완료**

---

### 🟡 참고 — LIKE 패턴 내 `%`, `_` 메타문자

**문제**: EF.Functions.Like에서 `_`가 LIKE 와일드카드(임의 1문자)로 동작. `p-602` 대신 `p602`도 매칭될 수 있음.
**판단**: 사용자가 `%`를 직접 입력하는 방식으로 결정. `_` 문제는 현장에서 실용적으로 문제없음 → **수용**

---

### ✅ 이상 없는 항목

- Step 5 인터페이스 시그니처 변경: 구현체·컨트롤러와 일관됨 — **이미 완료**
- Step 7 컨트롤러 배열 전달: C# 12 컬렉션 표현식 `[...]` 문법 — .NET 8 이상에서 정상 컴파일 — **이미 완료**
- Step 6 TRUNCATE 후 INSERT 흐름: 정상 — **이미 완료**
- Step 3 빈 그룹 제외 로직: Step 6의 `activeGroups.Count == 0` 가드와 일관됨 — **이미 완료**

---

## 최종 요약

| 등급 | 수 | 내용 |
|------|---|------|
| 🔴 HIGH | 1 | 중복 `pbBuild()` (L671-686) 삭제 필요 — 즉시 처리 |
| ✅ 오진 | 7 | 이미 완료됨, 존재하지 않음, 재현 불가 → 문서 반영 완료 |
| 🟡 참고 | 1 | LIKE `_` 메타문자 — 수용 |

**현재 남은 작업:**
1. 🔴 `app.js` L671-686 중복 `pbBuild()` 삭제
2. Step 2 CSS: `.pb-custom-attr-inputs .inp`에 `width: auto` 추가 (이미 완료됨)
3. 빌드 검증 (`dotnet build`)
