삽질하다 도저히 문제 파악이 안돼서 opcUaManager로 분리 테스트 중
This commit is contained in:
83
opcUaManager/Controllers/CertController.cs
Normal file
83
opcUaManager/Controllers/CertController.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OpcUaManager.Models;
|
||||
using OpcUaManager.Services;
|
||||
|
||||
namespace OpcUaManager.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/cert")]
|
||||
[Produces("application/json")]
|
||||
public class CertController : ControllerBase
|
||||
{
|
||||
private readonly CertService _certService;
|
||||
private readonly ILogger<CertController> _logger;
|
||||
|
||||
public CertController(CertService certService, ILogger<CertController> logger)
|
||||
{
|
||||
_certService = certService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// X.509 클라이언트 인증서를 생성하고 pki/own/certs/ 에 PFX로 저장합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 원본 Program.cs 의 인증서 체계(pki/ 폴더, Exportable|MachineKeySet 플래그)를 유지합니다.
|
||||
///
|
||||
/// Sample request:
|
||||
///
|
||||
/// POST /api/cert/generate
|
||||
/// {
|
||||
/// "clientHostName": "dbsvr",
|
||||
/// "applicationName": "OpcTestClient",
|
||||
/// "serverHostName": "opc-server-01",
|
||||
/// "serverIp": "192.168.0.20",
|
||||
/// "pfxPassword": "",
|
||||
/// "validDays": 365
|
||||
/// }
|
||||
/// </remarks>
|
||||
[HttpPost("generate")]
|
||||
[ProducesResponseType(typeof(CertCreateResult), 200)]
|
||||
[ProducesResponseType(typeof(ApiError), 400)]
|
||||
public async Task<IActionResult> Generate([FromBody] CertCreateRequest req)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(req.ClientHostName) ||
|
||||
string.IsNullOrWhiteSpace(req.ApplicationName))
|
||||
return BadRequest(new ApiError
|
||||
{
|
||||
Error = "필수값 누락",
|
||||
Detail = "ClientHostName, ApplicationName 은 필수입니다."
|
||||
});
|
||||
|
||||
_logger.LogInformation("인증서 생성 요청: {Host}/{App}", req.ClientHostName, req.ApplicationName);
|
||||
|
||||
var result = await _certService.GenerateAsync(req);
|
||||
|
||||
if (!result.Success)
|
||||
return BadRequest(new ApiError { Error = "인증서 생성 실패", Detail = result.Message });
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>pki/own/certs/ 폴더의 PFX 파일 목록을 반환합니다.</summary>
|
||||
[HttpGet("list")]
|
||||
[ProducesResponseType(typeof(IEnumerable<string>), 200)]
|
||||
public IActionResult List()
|
||||
=> Ok(_certService.ListCertificates());
|
||||
|
||||
/// <summary>PFX 파일을 다운로드합니다.</summary>
|
||||
[HttpGet("download/{fileName}")]
|
||||
public IActionResult Download(string fileName)
|
||||
{
|
||||
// 경로 traversal 방지
|
||||
if (fileName.Contains("..") || fileName.Contains('/') || fileName.Contains('\\'))
|
||||
return BadRequest(new ApiError { Error = "잘못된 파일 이름" });
|
||||
|
||||
string path = Path.Combine("pki", "own", "certs", fileName);
|
||||
if (!System.IO.File.Exists(path))
|
||||
return NotFound(new ApiError { Error = "파일 없음", Detail = path });
|
||||
|
||||
byte[] bytes = System.IO.File.ReadAllBytes(path);
|
||||
return File(bytes, "application/x-pkcs12", fileName);
|
||||
}
|
||||
}
|
||||
75
opcUaManager/Controllers/CrawlerController.cs
Normal file
75
opcUaManager/Controllers/CrawlerController.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OpcUaManager.Models;
|
||||
using OpcUaManager.Services;
|
||||
|
||||
namespace OpcUaManager.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/crawler")]
|
||||
[Produces("application/json")]
|
||||
public class CrawlerController : ControllerBase
|
||||
{
|
||||
private readonly OpcCrawlerService _crawlerSvc;
|
||||
private readonly OpcSessionService _sessionSvc;
|
||||
private readonly ILogger<CrawlerController> _logger;
|
||||
|
||||
public CrawlerController(
|
||||
OpcCrawlerService crawlerSvc,
|
||||
OpcSessionService sessionSvc,
|
||||
ILogger<CrawlerController> logger)
|
||||
{
|
||||
_crawlerSvc = crawlerSvc;
|
||||
_sessionSvc = sessionSvc;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정한 시작 노드부터 OPC UA 노드 트리를 재귀 탐색합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Sample request:
|
||||
///
|
||||
/// POST /api/crawler/start
|
||||
/// {
|
||||
/// "startNodeId": "ns=1;s=$assetmodel",
|
||||
/// "maxDepth": 5
|
||||
/// }
|
||||
///
|
||||
/// 탐색 결과는 응답 JSON 과 함께 서버 로컬에 Honeywell_FullMap.csv 로 저장됩니다.
|
||||
/// </remarks>
|
||||
[HttpPost("start")]
|
||||
[ProducesResponseType(typeof(CrawlResult), 200)]
|
||||
[ProducesResponseType(typeof(ApiError), 400)]
|
||||
public async Task<IActionResult> Start([FromBody] CrawlRequest req)
|
||||
{
|
||||
if (!_sessionSvc.IsConnected)
|
||||
return BadRequest(new ApiError
|
||||
{
|
||||
Error = "세션 없음",
|
||||
Detail = "먼저 /api/session/connect 로 OPC 서버에 연결하세요."
|
||||
});
|
||||
|
||||
_logger.LogInformation("Crawler 시작: {NodeId} (depth={Depth})",
|
||||
req.StartNodeId, req.MaxDepth);
|
||||
|
||||
// 대규모 탐사는 시간이 오래 걸릴 수 있으므로 타임아웃을 늘려줍니다
|
||||
HttpContext.RequestAborted.ThrowIfCancellationRequested();
|
||||
|
||||
var result = await _crawlerSvc.CrawlAsync(req);
|
||||
|
||||
return result.Success ? Ok(result)
|
||||
: BadRequest(new ApiError { Error = "탐사 실패", Detail = result.Message });
|
||||
}
|
||||
|
||||
/// <summary>마지막 탐사로 생성된 CSV 파일을 다운로드합니다.</summary>
|
||||
[HttpGet("csv")]
|
||||
public IActionResult DownloadCsv()
|
||||
{
|
||||
string path = Path.GetFullPath("Honeywell_FullMap.csv");
|
||||
if (!System.IO.File.Exists(path))
|
||||
return NotFound(new ApiError { Error = "CSV 없음", Detail = "탐사를 먼저 실행하세요." });
|
||||
|
||||
byte[] bytes = System.IO.File.ReadAllBytes(path);
|
||||
return File(bytes, "text/csv", "Honeywell_FullMap.csv");
|
||||
}
|
||||
}
|
||||
98
opcUaManager/Controllers/DatabaseController.cs
Normal file
98
opcUaManager/Controllers/DatabaseController.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OpcUaManager.Models;
|
||||
using OpcUaManager.Services;
|
||||
|
||||
namespace OpcUaManager.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/database")]
|
||||
[Produces("application/json")]
|
||||
public class DatabaseController : ControllerBase
|
||||
{
|
||||
private readonly DatabaseService _dbSvc;
|
||||
private readonly ILogger<DatabaseController> _logger;
|
||||
|
||||
public DatabaseController(DatabaseService dbSvc, ILogger<DatabaseController> logger)
|
||||
{
|
||||
_dbSvc = dbSvc;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DB 연결을 테스트합니다.
|
||||
/// </summary>
|
||||
[HttpPost("test")]
|
||||
[ProducesResponseType(200)]
|
||||
[ProducesResponseType(typeof(ApiError), 400)]
|
||||
public async Task<IActionResult> Test([FromBody] DbWriteRequest req)
|
||||
{
|
||||
var (ok, msg) = await _dbSvc.TestConnectionAsync(req);
|
||||
return ok ? Ok(new { Message = msg })
|
||||
: BadRequest(new ApiError { Error = "연결 실패", Detail = msg });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// opc_history 테이블을 없으면 생성합니다.
|
||||
/// </summary>
|
||||
[HttpPost("init")]
|
||||
[ProducesResponseType(200)]
|
||||
[ProducesResponseType(typeof(ApiError), 400)]
|
||||
public async Task<IActionResult> Init([FromBody] DbWriteRequest req)
|
||||
{
|
||||
var (ok, msg) = await _dbSvc.EnsureTableAsync(req);
|
||||
return ok ? Ok(new { Message = msg })
|
||||
: BadRequest(new ApiError { Error = "테이블 초기화 실패", Detail = msg });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OPC 태그를 지정 횟수만큼 읽어 DB에 저장합니다 (원본 5회 루프).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Sample request:
|
||||
///
|
||||
/// POST /api/database/write
|
||||
/// {
|
||||
/// "tagNodeId": "ns=1;s=shinam:p-6102.hzset.fieldvalue",
|
||||
/// "tagName": "p-6102",
|
||||
/// "count": 5,
|
||||
/// "intervalMs": 2000,
|
||||
/// "dbHost": "localhost",
|
||||
/// "dbName": "opcdb",
|
||||
/// "dbUser": "postgres",
|
||||
/// "dbPassword": "postgres"
|
||||
/// }
|
||||
/// </remarks>
|
||||
[HttpPost("write")]
|
||||
[ProducesResponseType(typeof(DbWriteResult), 200)]
|
||||
[ProducesResponseType(typeof(ApiError), 400)]
|
||||
public async Task<IActionResult> Write([FromBody] DbWriteRequest req)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(req.TagNodeId) || string.IsNullOrWhiteSpace(req.TagName))
|
||||
return BadRequest(new ApiError
|
||||
{
|
||||
Error = "필수값 누락",
|
||||
Detail = "TagNodeId, TagName 은 필수입니다."
|
||||
});
|
||||
|
||||
req.Count = Math.Clamp(req.Count, 1, 100);
|
||||
|
||||
_logger.LogInformation("DB 저장 시작: {Tag} × {Count}회", req.TagName, req.Count);
|
||||
var result = await _dbSvc.WriteLoopAsync(req);
|
||||
|
||||
return result.Success ? Ok(result)
|
||||
: BadRequest(new ApiError { Error = "저장 실패", Detail = result.Message });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// opc_history 테이블의 최근 레코드를 조회합니다.
|
||||
/// </summary>
|
||||
[HttpPost("query")]
|
||||
[ProducesResponseType(typeof(DbQueryResult), 200)]
|
||||
[ProducesResponseType(typeof(ApiError), 400)]
|
||||
public async Task<IActionResult> Query([FromBody] DbWriteRequest req, [FromQuery] int limit = 100)
|
||||
{
|
||||
var result = await _dbSvc.QueryRecentAsync(req, limit);
|
||||
return result.Success ? Ok(result)
|
||||
: BadRequest(new ApiError { Error = "조회 실패", Detail = result.Message });
|
||||
}
|
||||
}
|
||||
74
opcUaManager/Controllers/SessionController.cs
Normal file
74
opcUaManager/Controllers/SessionController.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OpcUaManager.Models;
|
||||
using OpcUaManager.Services;
|
||||
|
||||
namespace OpcUaManager.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/session")]
|
||||
[Produces("application/json")]
|
||||
public class SessionController : ControllerBase
|
||||
{
|
||||
private readonly OpcSessionService _sessionSvc;
|
||||
private readonly ILogger<SessionController> _logger;
|
||||
|
||||
public SessionController(OpcSessionService sessionSvc, ILogger<SessionController> logger)
|
||||
{
|
||||
_sessionSvc = sessionSvc;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OPC UA 서버에 연결합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Sample request:
|
||||
///
|
||||
/// POST /api/session/connect
|
||||
/// {
|
||||
/// "serverIp": "192.168.0.20",
|
||||
/// "port": 4840,
|
||||
/// "userName": "mngr",
|
||||
/// "password": "mngr",
|
||||
/// "securityPolicy": "Basic256Sha256",
|
||||
/// "sessionTimeoutMs": 60000,
|
||||
/// "pfxPath": "pki/own/certs/OpcTestClient.pfx",
|
||||
/// "pfxPassword": "",
|
||||
/// "applicationName": "OpcTestClient",
|
||||
/// "applicationUri": "urn:dbsvr:OpcTestClient"
|
||||
/// }
|
||||
/// </remarks>
|
||||
[HttpPost("connect")]
|
||||
[ProducesResponseType(typeof(ConnectResult), 200)]
|
||||
[ProducesResponseType(typeof(ApiError), 400)]
|
||||
public async Task<IActionResult> Connect([FromBody] ConnectRequest req)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(req.ServerIp))
|
||||
return BadRequest(new ApiError { Error = "ServerIp 는 필수입니다." });
|
||||
|
||||
_logger.LogInformation("연결 요청: {Ip}:{Port}", req.ServerIp, req.Port);
|
||||
var result = await _sessionSvc.ConnectAsync(req);
|
||||
|
||||
return result.Success ? Ok(result)
|
||||
: BadRequest(new ApiError { Error = "연결 실패", Detail = result.Message });
|
||||
}
|
||||
|
||||
/// <summary>현재 OPC UA 세션을 종료합니다.</summary>
|
||||
[HttpPost("disconnect")]
|
||||
[ProducesResponseType(200)]
|
||||
public async Task<IActionResult> Disconnect()
|
||||
{
|
||||
await _sessionSvc.DisconnectAsync();
|
||||
return Ok(new { Message = "세션 종료 완료" });
|
||||
}
|
||||
|
||||
/// <summary>현재 세션 상태를 반환합니다.</summary>
|
||||
[HttpGet("status")]
|
||||
[ProducesResponseType(200)]
|
||||
public IActionResult Status()
|
||||
=> Ok(new
|
||||
{
|
||||
IsConnected = _sessionSvc.IsConnected,
|
||||
SessionId = _sessionSvc.SessionId
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user