Files
ExperionCrawler/엑스페리온-WRITE-기능추가-플랜.md
windpacer 302183c97e feat: P&ID 연결 분석, LLM 에이전트 모드, KB 확장, MCP 서버 리팩토링
- P&ID: 연결 분석 API, Prefix 규칙 관리, 카테고리 분류, DXF 그래프 빌드
- LLM: 대화 요약, tool card 영구 보존, 시계열 차트(uPlot), 에이전트 모드
- KB: 청크 미리보기, Field Instrument Inference, 인증/Qdrant 클라이언트
- MCP: 서버 기능 확장, 파이프라인 수정, timeout 개선
- Frontend: P&ID UI, LLM UI, KB UI, OPC UA Write 탭 추가
- 설정: AGENTS.md, plant_context, README, opencode.json 업데이트
- 정리: 진단 체크리스트 문서 삭제
2026-05-21 23:36:57 +09:00

11 KiB

ExperionCrawler — OPC UA Write 기능 추가 구현 계획

⚠️ 이 문서를 실행할 LLM에게

이 문서는 ExperionCrawler 프로젝트에 OPC UA Write 기능(SP/OP/MD/MODE 쓰기)을 추가하는 구현 계획이다. 아래 규칙을 반드시 준수하여 구현할 것:

  1. diagnosis-checklist.md 규칙 준수: STEP 1~8을 순서대로 실행. 코드를 읽지 않고 구현하지 말 것.
  2. JSON camelCase 규칙: 모든 Controller Ok(...) 응답은 명시적 camelCase 키 사용. 예: return Ok(new { success = r.Success, nodeId = r.NodeId, status = r.Status, error = r.Error });
  3. 네임스페이스: ExperionCrawler.Infrastructure.OpcUa (Infrastructure), ExperionCrawler.Core.Application.Interfaces (인터페이스), ExperionCrawler.Core.Application.DTOs (DTO)
  4. 단위별 구현: 각 TODO 항목을 하나씩 완료하고, 검증 단계를 실행한 후 다음으로 진행.
  5. 빌드 검증: 모든 코드 변경 후 dotnet build src/Web/ExperionCrawler.csproj로 빌드 확인.
  6. 기존 코드 수정 금지: ExperionOpcClient.cs(읽기 전용)는 수정하지 않음. 신규 파일로 추가.
  7. DI 등록: Program.cs에 신규 서비스를 Scoped로 등록.
  8. 완료 표시: 각 TODO 항목 완료 시 [ ][x]로 변경하고, 검증 결과 기록.
  9. 프론트엔드: wwwroot/js/app.js에 Write 탭 UI 추가. 기존 패턴(tab navigation, api() helper, log() helper) 따름.
  10. 보안: 쓰기 이력 로깅은 DB가 아닌 Application Log로만 기록 (단순화).

테스트 결과 요약 (TEST_RESULTS.md 기반)

태그 노드 ID DataType 방법 결과
SP ns=1;s=sinamserver:ficq-6101.sp float WriteAsync() 직접 성공
OP ns=1;s=sinamserver:ficq-6101.op Double WriteAsync() (MD=MAN 필요) 성공
MD ns=1;s=sinamserver:ficq-6101.md EnumValueType EnumValueType Write 성공
Mode ns=1;s=sinamserver:ficq-6101.mode EnumValueType EnumValueType Write 성공

핵심 발견

  • MD/Mode는 Method가 아님: EnumValueType(Value, DisplayName, Description)WriteAsync()로 직접 쓰기
  • OP 쓰기 조건: MD를 MAN(0)으로 변경해야 OP 쓰기 가능 (AUTO면 BadNoData)
  • SDK v1.5.378: WriteValue(WriteValueId 아님), DefaultSessionFactory.CreateAsync() 사용

구현 TODO 리스트

Phase 1: Backend Core (Infrastructure + Interface + DTO)

TODO 1-1: IExperionOpcWriteClient 인터페이스 정의

  • 파일: src/Core/Application/Interfaces/IExperionServices.cs
  • 작업: 기존 파일 하단에 인터페이스 추가
  • 내용:
    public interface IExperionOpcWriteClient
    {
        Task<ExperionWriteResultDto> WriteTagAsync(ExperionServerConfig cfg, string nodeId, double value, CancellationToken ct = default);
        Task<ExperionModeResultDto> SetModeAsync(ExperionServerConfig cfg, string nodeId, string mode, CancellationToken ct = default);
    }
    
  • 검증: dotnet build 성공

TODO 1-2: Write Result DTOs 정의

  • 파일: src/Core/Application/DTOs/ExperionDtos.cs
  • 작업: 파일 하단에 DTO 클래스 추가
  • 내용:
    public class ExperionWriteResultDto
    {
        public bool Success { get; set; }
        public string NodeId { get; set; } = "";
        public string Status { get; set; } = "";
        public string? Error { get; set; }
    }
    public class ExperionModeResultDto
    {
        public bool Success { get; set; }
        public string NodeId { get; set; } = "";
        public string Mode { get; set; } = "";
        public int EnumValue { get; set; }
        public string Status { get; set; } = "";
        public string? Error { get; set; }
    }
    
  • 검증: dotnet build 성공

TODO 1-3: ExperionOpcWriteClient 구현체 생성

  • 파일: src/Infrastructure/OpcUa/ExperionOpcWriteClient.cs (신규)
  • 작업: /tmp/opcua-test/ExperionOpcWriteClient.cs를 기반으로 구현. 변경 사항:
    • 네임스페이스: ExperionCrawler.Infrastructure.OpcUa
    • IExperionOpcWriteClient 구현
    • IOpcUaConfigProvider 의존성 주입 (기존 ExperionOpcClient와 동일 패턴)
    • BuildConfigAsync, SelectEndpointAsync, CreateSessionAsyncExperionOpcClient와 동일한 로직 사용
    • ExperionServerConfig 사용 (별도 파라미터 아님)
    • Result 타입: ExperionWriteResultDto, ExperionModeResultDto (Core DTO 사용)
  • 검증: dotnet build 성공

TODO 1-4: Program.cs에 DI 등록

  • 파일: src/Web/Program.cs
  • 작업: IExperionOpcClient 등록 바로 아래에 IExperionOpcWriteClient 등록 추가
    builder.Services.AddScoped<IExperionOpcWriteClient, ExperionOpcWriteClient>();
    
  • 검증: dotnet build 성공

Phase 2: API Controller

TODO 2-1: Write Controller 생성

  • 파일: src/Web/Controllers/ExperionControllers.cs
  • 작업: 파일 하단에 신규 Controller 추가
  • 내용:
    [ApiController]
    [Route("api/points")]
    public class ExperionWriteController : ControllerBase
    {
        private readonly IExperionOpcWriteClient _writeClient;
        private readonly IExperionOpcClient _readClient;
        private readonly IConfiguration _config;
        private readonly ILogger<ExperionWriteController> _logger;
        // ...
    }
    
  • 엔드포인트:
    • POST /api/points/write — SP/OP 쓰기
    • POST /api/points/mode — MD/MODE 변경
    • POST /api/points/control — 통합 제어 (MD→MAN → OP 쓰기 → AUTO 복귀)
    • POST /api/points/read — 태그 현재값 읽기 (확인용)
  • Request DTOs (Controller 내부에 정의):
    public class WriteTagRequest(string NodeId, double Value);
    public class SetModeRequest(string NodeId, string Mode);
    public class ControlOpRequest(string TagName, double OpValue, bool? RestoreAuto = true);
    
  • ServerConfig: appsettings.jsonExperion:Default 섹션에서 읽음 (기존 패턴 동일)
  • camelCase 응답: 모든 Ok(new { success = ..., nodeId = ..., status = ..., error = ... })
  • 검증: dotnet build 성공

Phase 3: Frontend UI

TODO 3-1: Write 탭 HTML 추가

  • 파일: src/Web/wwwroot/index.html
  • 작업:
    • nav-item 추가: <a class="nav-item" data-tab="write">Write</a>
    • pane-write 추가: SP/OP/Mode 입력 폼 + 로그 영역
  • UI 구성:
    • 태그명 입력 (예: ficq-6101)
    • 속성 선택: SP / OP / MD / Mode
    • 값 입력 (SP/OP: 숫자, MD/Mode: MAN/AUTO 선택)
    • 실행 버튼
    • 통합 제어: "OP 변경 (자동 MAN→AUTO)" 체크박스
    • 로그 출력 영역
  • 검증: 브라우저에서 탭 전환 확인

TODO 3-2: Write 탭 JS 구현

  • 파일: src/Web/wwwroot/js/app.js
  • 작업: 파일 하단에 Write 관련 함수 추가
  • 함수:
    • writeTag()/api/points/write 호출
    • setMode()/api/points/mode 호출
    • controlOp()/api/points/control 호출
    • readTagValue()/api/points/read 호출
  • 검증: 브라우저에서 Write 탭에서 SP 쓰기 테스트

Phase 4: 검증 및 테스트

TODO 4-1: 빌드 검증

  • dotnet build src/Web/ExperionCrawler.csproj — 오류 없음
  • dotnet test — 기존 테스트 통과

TODO 4-2: API 테스트 (Postman/curl)

  • POST /api/points/write — ficq-6101.sp = 36.0 → 성공
  • POST /api/points/mode — ficq-6101.md = MAN → 성공
  • POST /api/points/control — ficq-6101 OP = 35.5 → MAN 전환 → OP 쓰기 → AUTO 복귀 → 성공
  • POST /api/points/read — ficq-6101.sp → 현재값 반환

TODO 4-3: Frontend 테스트

  • Write 탭에서 SP 쓰기 UI 테스트
  • Mode 변경 UI 테스트
  • 통합 제어 UI 테스트

파일 변경 요약

파일 액션 내용
src/Core/Application/Interfaces/IExperionServices.cs 수정 IExperionOpcWriteClient 인터페이스 추가
src/Core/Application/DTOs/ExperionDtos.cs 수정 ExperionWriteResultDto, ExperionModeResultDto 추가
src/Infrastructure/OpcUa/ExperionOpcWriteClient.cs 신규 Write 클라이언트 구현체
src/Web/Program.cs 수정 DI 등록 1줄 추가
src/Web/Controllers/ExperionControllers.cs 수정 ExperionWriteController + Request DTOs 추가
src/Web/wwwroot/index.html 수정 Write 탭 HTML 추가
src/Web/wwwroot/js/app.js 수정 Write 탭 JS 함수 추가

구현 진행 상태

# TODO 상태 검증 비고
1-1 IExperionOpcWriteClient 인터페이스 완료 빌드 성공 IExperionServices.cs 하단 추가
1-2 Write Result DTOs 완료 빌드 성공 ExperionDtos.cs 하단 추가
1-3 ExperionOpcWriteClient 구현체 완료 빌드 성공 IOpcUaConfigProvider DI 사용
1-4 Program.cs DI 등록 완료 빌드 성공 Scoped 등록
2-1 Write Controller 완료 빌드 성공 4개 엔드포인트 + Request DTOs
3-1 Write 탭 HTML 완료 시각 확인 nav-item 15 + pane-write
3-2 Write 탭 JS 완료 시각 확인 wrWriteTag/wrSetMode/wrControlOp/wrReadTag
4-1 빌드 검증 완료 0 Warning, 0 Error dotnet build 성공
4-2 API 테스트 미완료 런타임 테스트 필요
4-3 Frontend 테스트 미완료 브라우저 테스트 필요

참고: 기존 아키텍처 패턴

Service 등록 패턴 (Program.cs)

builder.Services.AddScoped<IExperionOpcClient,  ExperionOpcClient>();
// ↑ 이 패턴을 따름

Controller에서 ServerConfig 읽기 패턴

var section = _config.GetSection("Experion:Default");
return new ExperionServerConfig
{
    ServerHostName = section["ServerHostName"] ?? "",
    Port = section.GetValue<int?>("Port") ?? 4840,
    ClientHostName = section["ClientHostName"] ?? "dbsvr",
    UserName = section["UserName"] ?? "",
    Password = section["Password"] ?? ""
};

JSON camelCase 응답 패턴

return Ok(new { success = r.Success, nodeId = r.NodeId, status = r.Status, error = r.Error });

Frontend api() helper

async function api(method, path, body) {
  const opt = { method, headers: { 'Content-Type': 'application/json' } };
  if (body) opt.body = JSON.stringify(body);
  const res = await fetch(path, opt);
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
  return res.json();
}