MCP-서버 리팩토링 후 P&ID 추출 테스트전 다른 기능 확인 후 커밋
This commit is contained in:
122
CODING_CONVENTIONS.md
Normal file
122
CODING_CONVENTIONS.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# ExperionCrawler 코딩 컨벤션
|
||||
|
||||
## 1. ASP.NET Core 컨트롤러 JSON 직렬화
|
||||
|
||||
### 핵심 설정
|
||||
|
||||
`src/Web/Program.cs`:
|
||||
```csharp
|
||||
builder.Services.AddControllers().AddJsonOptions(opt => {
|
||||
opt.JsonSerializerOptions.PropertyNamingPolicy = null; // PascalCase 직렬화
|
||||
});
|
||||
```
|
||||
|
||||
`PropertyNamingPolicy = null`이므로 C# 속성명이 **그대로** JSON 키가 된다.
|
||||
프론트엔드(`wwwroot/js/app.js`)는 모든 JSON 필드를 **camelCase**로 접근한다.
|
||||
|
||||
### 규칙: 컨트롤러 응답은 반드시 명시적 camelCase 익명 객체 사용
|
||||
|
||||
#### 금지 패턴
|
||||
|
||||
```csharp
|
||||
// ❌ shorthand — "Id", "TagName"(PascalCase)이 JSON 키가 됨 → JS에서 undefined
|
||||
return Ok(new { x.Id, x.TagName, x.NodeId });
|
||||
|
||||
// ❌ typed 객체 직접 반환 — PascalCase 키 → JS에서 undefined
|
||||
return Ok(myDto);
|
||||
return Ok(new MyRecord { Id = 1, TagName = "abc" });
|
||||
```
|
||||
|
||||
#### 올바른 패턴
|
||||
|
||||
```csharp
|
||||
// ✅ 명시적 소문자 키 — JS에서 r.id, r.tagName으로 정상 접근
|
||||
return Ok(new
|
||||
{
|
||||
id = x.Id,
|
||||
tagName = x.TagName,
|
||||
nodeId = x.NodeId,
|
||||
liveValue = x.LiveValue,
|
||||
timestamp = x.Timestamp
|
||||
});
|
||||
|
||||
// ✅ 컬렉션
|
||||
return Ok(new
|
||||
{
|
||||
total = r.Total,
|
||||
items = r.Items.Select(x => new
|
||||
{
|
||||
id = x.Id,
|
||||
tagName = x.TagName,
|
||||
nodeId = x.NodeId,
|
||||
dataType = x.DataType
|
||||
})
|
||||
});
|
||||
|
||||
// ✅ C# 예약어 처리: @class → JSON "class"
|
||||
return Ok(new
|
||||
{
|
||||
id = x.Id,
|
||||
@class = x.Class, // JSON key: "class"
|
||||
name = x.Name
|
||||
});
|
||||
```
|
||||
|
||||
### 프론트엔드 접근 방식 (참고)
|
||||
|
||||
```javascript
|
||||
// app.js에서 모든 응답 필드를 camelCase로 접근
|
||||
items.forEach(x => {
|
||||
row.cells[0].textContent = x.id; // ← "id" (소문자)
|
||||
row.cells[1].textContent = x.tagName; // ← "tagName" (camelCase)
|
||||
row.cells[2].textContent = x.nodeId; // ← "nodeId"
|
||||
});
|
||||
```
|
||||
|
||||
### 발생 이력
|
||||
|
||||
이 규칙을 어기면 다음 증상이 나타난다:
|
||||
- 테이블에 모든 셀이 빈칸 또는 `[undefined]`
|
||||
- 브라우저 콘솔에 오류 없음 (값이 `undefined`이므로 조용히 실패)
|
||||
- 서버 응답 자체는 정상 (Network 탭에서 데이터 확인 가능)
|
||||
|
||||
**실제 발생 사례** (2026-04-26):
|
||||
- Browse 노드 목록: `n.NodeId`, `n.DisplayName` → `undefined` / "이름 없음"
|
||||
- NodeMap 대시보드: `x.Id`, `x.Level`, `x.Class` → `undefined`
|
||||
- PointBuilder 포인트 목록: `p.Id`, `p.TagName`, `p.LiveValue` → `undefined`
|
||||
|
||||
---
|
||||
|
||||
## 2. OPC UA SDK 버전 호환성 (v1.5.378.134)
|
||||
|
||||
### Session 생성
|
||||
|
||||
```csharp
|
||||
// ❌ 구버전 — Session.Create()가 Task<ISession>을 반환하므로 cast 실패
|
||||
var session = (ISession)Session.Create(config, endpoint, false, name, 60000, identity, null);
|
||||
|
||||
// ✅ 신버전
|
||||
var session = await new DefaultSessionFactory(null).CreateAsync(
|
||||
config, endpoint, false, name, 60_000, identity, null, CancellationToken.None);
|
||||
```
|
||||
|
||||
### 인증서 검증 이벤트
|
||||
|
||||
`OpcUaConfigProvider.GetConfigAsync`에서 config를 빌드한 후 이벤트 핸들러를 등록해야 한다.
|
||||
`ExperionOpcClient.BuildConfigAsync`는 실제로 호출되지 않는 dead code이므로 거기에 등록해도 무효.
|
||||
|
||||
```csharp
|
||||
await config.ValidateAsync(ApplicationType.Client);
|
||||
config.CertificateValidator.CertificateValidation += (_, e) => { e.Accept = true; };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 컨트롤러 응답 구조 체크리스트
|
||||
|
||||
컨트롤러에서 `Ok(...)` 사용 시 반드시 확인:
|
||||
|
||||
- [ ] 익명 객체의 **모든 키**가 camelCase인가? (`id`, `tagName`, `nodeId` ...)
|
||||
- [ ] `new { x.SomeProp }` shorthand가 **전혀** 없는가?
|
||||
- [ ] typed record/class를 `Ok()`에 **직접 전달**하지 않는가?
|
||||
- [ ] C# 예약어(`class`)에 `@` 접두사를 붙였는가?
|
||||
Reference in New Issue
Block a user