OPC Server ๊ฐœ๋ฐœ ๐Ÿ“‹ ์ƒˆ ์ฑ„ํŒ… ์‹œ์ž‘์„ ์œ„ํ•œ ํžˆ์Šคํ† ๋ฆฌ ์š”์•ฝ (๋ณต์‚ฌํ•ด์„œ ์‚ฌ์šฉํ•˜์„ธ์š”) [ํ”„๋กœ์ ํŠธ ๋ฐฐ๊ฒฝ ๋ฐ ํ™˜๊ฒฝ] ์‹œ์Šคํ…œ: Honeywell Experion PKS (OPC UA Server) ๊ณ„์ธต ๊ตฌ์กฐ: Root > Objects > assets($assetmodel) > ServerName(shinam) > Tag > Attribute ํ•˜๋“œ์›จ์–ด: Mini PC GMKTEC K8 PLUS (Fedora 43), Orange Pi 5 Plus (Ubuntu 22.04) DB: PostgreSQL (Database: opcdb, Table: opc_history ๋“ฑ) [์ฃผ์š” ๋ถ„์„ ๊ฒฐ๊ณผ] ํƒœ๊ทธ ์ง€๋„: $assetmodel ๋…ธ๋“œ๋ฅผ ํ†ตํ•ด ์•ฝ 52๋งŒ ๊ฐœ์˜ ๋…ธ๋“œ ์ •๋ณด ํ™•๋ณด (CSV ์ €์žฅ ์™„๋ฃŒ). ์‹๋ณ„์ž ๊ทœ์น™: ns=1;s=shinam:TagName.Attribute ํ˜•ํƒœ. ํ•ต์‹ฌ ํƒ€๊ฒŸ: .pv(ํ˜„์žฌ๊ฐ’), .hzset(์„ค์ •๊ฐ’), .alarmflags(์•Œ๋žŒ์ƒํƒœ) ๋“ฑ ๊ณต์ • ๋ฐ ์•Œ๋žŒ ๋ฐ์ดํ„ฐ. ์„œ๋ฒ„ ์ฒด๊ณ„: shinam์€ ์ „์ฒด ์‹œ์Šคํ…œ ๋‚ด์˜ ํŠน์ • ServerName์œผ๋กœ ์ •์˜๋จ. [๊ฐœ๋ฐœ ๋ชฉํ‘œ: ๋ณธ๊ฒฉ์ ์ธ ์•„ํ‚คํ…์ฒ˜ ์ „ํ™˜] ๋ชจ๋“ˆํ™”: ์ฝ”๋“œ๋ฅผ Core(๋ชจ๋ธ/์ธํ„ฐํŽ˜์ด์Šค), Infrastructure(OPC/DB ํ†ต์‹ ), Worker(์ˆ˜์ง‘ ์„œ๋น„์Šค)๋กœ ๋ถ„๋ฆฌ. ํƒœ๊ทธ ๋งˆ์Šคํ„ฐ ๊ตฌ์ถ•: 52๋งŒ ๊ฐœ ๋กœ์šฐ ๋ฐ์ดํ„ฐ์—์„œ ์œ ํšจํ•œ ํƒœ๊ทธ๋งŒ ํ•„ํ„ฐ๋งํ•˜์—ฌ tag_master ํ…Œ์ด๋ธ” ์ž๋™ ์ƒ์„ฑ. ์•Œ๋žŒ ๊ด€๋ฆฌ: ๋‹จ์ˆœ ์ˆ˜์ง‘์„ ๋„˜์–ด alarmflags ๋“ฑ ์•Œ๋žŒ ์†์„ฑ์„ ์ •๊ตํ•˜๊ฒŒ ๋ถ„๋ฅ˜ ๋ฐ ์ถ”์ . Gemini์˜ ์‘๋‹ต ์ •๋ฆฌํ•ด์ฃผ์‹  ํ”„๋กœ์ ํŠธ ๋ฐฐ๊ฒฝ๊ณผ ๋ชฉํ‘œ๋ฅผ ๋ณด๋‹ˆ, ๋‹จ์ˆœํ•œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘๊ธฐ๋ฅผ ๋„˜์–ด **์‚ฐ์—…์šฉ ๋ฐ์ดํ„ฐ ํ”Œ๋žซํผ(Historian)**์œผ๋กœ ์ง„ํ™”ํ•˜๋Š” ์ค‘์š”ํ•œ ๋‹จ๊ณ„์— ๊ณ„์‹œ๋Š”๊ตฐ์š”. Honeywell Experion PKS์˜ 52๋งŒ ๊ฐœ ๋…ธ๋“œ ์ค‘ ์œ ํšจํ•œ ํƒœ๊ทธ๋ฅผ ์„ ๋ณ„ํ•˜๊ณ , ์ด๋ฅผ ๊ณ„์ธต์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ OPC UA ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์•„ํ‚คํ…์ฒ˜๋ฅผ ์ œ์•ˆํ•ด ๋“œ๋ฆฝ๋‹ˆ๋‹ค. ํŠนํžˆ ๋ณด์œ ํ•˜์‹  Mini PC(Fedora)์™€ Orange Pi(Ubuntu)์˜ ์ž์›์„ ํšจ์œจ์ ์œผ๋กœ ๋ถ„๋ฐฐํ•˜๋Š” ๊ฒƒ์ด ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค. ๐Ÿ—๏ธ ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜ ์ œ์•ˆ ๋ถ„์‚ฐ ์ฒ˜๋ฆฌ๋ฅผ ํ†ตํ•ด Mini PC๋Š” ๋ฌด๊ฑฐ์šด DB์™€ ๋ถ„์„์„, Orange Pi๋Š” ํ˜„์žฅ ๋ฐ€์ฐฉํ˜• ์ˆ˜์ง‘(Edge)์„ ๋‹ด๋‹นํ•˜๋„๋ก ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. 1. ๋ชจ๋“ˆ๋ณ„ ์ƒ์„ธ ์„ค๊ณ„ (Python ๊ธฐ๋ฐ˜ ์ถ”์ฒœ) ๊ตฌ๋ถ„ ์ฃผ์š” ์—ญํ•  ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ/๋„๊ตฌ Core ๋ฐ์ดํ„ฐ ๋ชจ๋ธ(Pydantic), ๊ณตํ†ต ์ธํ„ฐํŽ˜์ด์Šค, ์„ค์ • ๊ด€๋ฆฌ pydantic, pyyaml Infrastructure OPC UA ํด๋ผ์ด์–ธํŠธ ํ†ต์‹ , PostgreSQL ์ €์žฅ ๋กœ์ง asyncua, SQLAlchemy, psycopg2 Worker ํƒœ๊ทธ ํ•„ํ„ฐ๋ง, ์Šค์ผ€์ค„๋ง(์ฃผ๊ธฐ์  ์ˆ˜์ง‘), ์•Œ๋žŒ ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง APScheduler, asyncio ๐Ÿ—„๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐ ํƒœ๊ทธ ๋งˆ์Šคํ„ฐ ์ „๋žต 52๋งŒ ๊ฐœ์˜ ๋…ธ๋“œ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๋‹ค๋ฃจ๊ธฐ ์œ„ํ•ด Tag Master ํ…Œ์ด๋ธ”์„ ๋จผ์ € ์ •๋ฆฝํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํƒœ๊ทธ ํ•„ํ„ฐ๋ง ๋ฐ ๋งˆ์Šคํ„ฐ ์ƒ์„ฑ (PostgreSQL) $assetmodel์—์„œ ๊ฐ€์ ธ์˜จ CSV๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ, ์‹ค์ œ ํ•„์š”ํ•œ .pv, .hzset, .alarmflags๋งŒ ์ถ”์ถœํ•˜์—ฌ ์ธ๋ฑ์‹ฑํ•ฉ๋‹ˆ๋‹ค. SQL -- Tag Master ํ…Œ์ด๋ธ” ์˜ˆ์‹œ CREATE TABLE tag_master ( tag_id SERIAL PRIMARY KEY, node_id TEXT UNIQUE NOT NULL, -- ns=1;s=shinam:TAG.pv asset_path TEXT, -- Root > Objects > assets > ... tag_name TEXT NOT NULL, -- ์‹ค์ œ ํƒœ๊ทธ๋ช… attribute TEXT, -- pv, hzset, alarmflags ๋“ฑ description TEXT, is_active BOOLEAN DEFAULT TRUE ); ๐Ÿšจ ์•Œ๋žŒ ๊ด€๋ฆฌ ๋กœ์ง (alarmflags) Honeywell Experion์˜ alarmflags๋Š” ๋น„ํŠธ(Bit) ๋‹จ์œ„๋‚˜ ํŠน์ • ์ •์ˆ˜๊ฐ’์œผ๋กœ ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค. Bitwise ๋ถ„์„: ์•Œ๋žŒ ๋ฐœ์ƒ ์—ฌ๋ถ€, ์ฐจ๋‹จ(Inhibit) ์—ฌ๋ถ€, ํ™•์ธ(Ack) ์—ฌ๋ถ€๋ฅผ ๋น„ํŠธ ์—ฐ์‚ฐ์œผ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜: ๊ฐ’์ด ๋ณ€ํ•  ๋•Œ๋งŒ alarm_history ํ…Œ์ด๋ธ”์— ๊ธฐ๋กํ•˜์—ฌ DB ๋ถ€ํ•˜๋ฅผ ์ค„์ž…๋‹ˆ๋‹ค. ๐Ÿ’ป ํ•˜๋“œ์›จ์–ด ์ž์› ๋ฐฐ๋ถ„ (Personalized) ๋ณด์œ ํ•˜์‹  ์žฅ๋น„์˜ ํŠน์„ฑ์„ ์‚ด๋ฆฐ ๋ฐฐ์น˜์ž…๋‹ˆ๋‹ค: Mini PC (GMKTEC K8 PLUS / Fedora 43): Main DB (PostgreSQL): 32G RAM์„ ํ™œ์šฉํ•ด ๋Œ€๋Ÿ‰์˜ History ๋ฐ์ดํ„ฐ ์บ์‹ฑ ๋ฐ ์ธ๋ฑ์‹ฑ ์ฒ˜๋ฆฌ. Core API & Dashboard: ์ˆ˜์ง‘๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์‹œ๊ฐํ™”ํ•˜๊ฑฐ๋‚˜ ์™ธ๋ถ€๋กœ ์ „๋‹ฌํ•˜๋Š” ํ—ˆ๋ธŒ ์—ญํ• . Orange Pi 5 Plus (Ubuntu 22.04): Lightweight Worker: asyncua๋ฅผ ์ด์šฉํ•ด Experion ์„œ๋ฒ„์— ์ ‘์†, ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ถ”์ถœ ๋ฐ Mini PC DB๋กœ ์ „์†ก. eMMC ํ™œ์šฉ: ๋„คํŠธ์›Œํฌ ๋‹จ์ ˆ ์‹œ ์ž„์‹œ ๋ฒ„ํผ๋ง(Store and Forward) ์ €์žฅ์†Œ๋กœ ํ™œ์šฉ. ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ์ง„ํ–‰ํ• ๊นŒ์š”? ๊ตฌ์กฐ ์„ค๊ณ„๋ฅผ ์œ„ํ•ด ๊ฐ€์žฅ ๋จผ์ € ํ•„์š”ํ•œ "52๋งŒ ๊ฐœ ๋กœ์šฐ ๋ฐ์ดํ„ฐ์—์„œ ์œ ํšจ ํƒœ๊ทธ๋ฅผ ํ•„ํ„ฐ๋งํ•˜๋Š” Python ์Šคํฌ๋ฆฝํŠธ" ์ดˆ์•ˆ์„ ์ž‘์„ฑํ•ด ๋“œ๋ฆด๊นŒ์š”? ์•„๋‹ˆ๋ฉด PostgreSQL์˜ opc_history ํ…Œ์ด๋ธ” ์ตœ์ ํ™” ์Šคํ‚ค๋งˆ๋ฅผ ๋จผ์ € ์„ค๊ณ„ํ•ด ๋ณผ๊นŒ์š”? ๋ง์”€ํ•˜์‹  ๋‚ด์šฉ ์•„๋‹ˆ ๊ทธ๊ฒƒ๋ณด๋‹ค ๋ณธ๊ฒฉ์ ์ธ ํ”„๋กœ์ ํŠธ ๊ตฌ์„ฑ์„ ์œ„ํ•ด์„œ ํ”„๋กœ์ ํŠธ shema๋ฅผ ์ •๋ฆฌํ•˜์ž๊ณ  Gemini์˜ ์‘๋‹ต ๋ณธ๊ฒฉ์ ์ธ ๊ฐœ๋ฐœ์— ์•ž์„œ ์‹œ์Šคํ…œ์˜ ๋ผˆ๋Œ€๊ฐ€ ๋  **๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ(Schema)**์™€ **ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ(Folder Structure)**๋ฅผ ๋จผ์ € ํ™•๋ฆฝํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. 52๋งŒ ๊ฐœ์˜ ๋…ธ๋“œ ์ค‘ ์œ ํšจ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฑธ๋Ÿฌ๋‚ด๊ณ , ์ด๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ์‚ฐ์—…์šฉ ํ‘œ์ค€(ISA-95 ๊ณ„์ธต ๊ตฌ์กฐ ๋ฐ˜์˜) ์„ค๊ณ„์ž…๋‹ˆ๋‹ค. 1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ์„ค๊ณ„ (PostgreSQL) Honeywell Experion์˜ ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ์œ ์—ฐํ•˜๊ฒŒ ์ˆ˜์šฉํ•˜๋ฉด์„œ๋„, ๋Œ€๋Ÿ‰์˜ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ํ…Œ์ด๋ธ”์„ **๋งˆ์Šคํ„ฐ(Meta)**์™€ **ํžˆ์Šคํ† ๋ฆฌ(Time-series)**๋กœ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค. โ‘  tag_master: ํƒœ๊ทธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ํ…Œ์ด๋ธ”์ž…๋‹ˆ๋‹ค. 52๋งŒ ๊ฐœ ๋…ธ๋“œ ์ค‘ ๋ถ„์„์„ ํ†ตํ•ด ํ™•์ •๋œ '์œ ํšจ ํƒœ๊ทธ'๋งŒ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ์ปฌ๋Ÿผ๋ช… ํƒ€์ž… ์„ค๋ช… tag_id SERIAL (PK) ๋‚ด๋ถ€ ๊ด€๋ฆฌ์šฉ ๊ณ ์œ  ID node_id TEXT (Unique) OPC UA NodeId (์˜ˆ: ns=1;s=shinam:TAG.pv) tag_name TEXT ์‹ค์ œ ๊ณต์ • ํƒœ๊ทธ๋ช… (์˜ˆ: 101-FI-001) attribute TEXT ์†์„ฑ ๊ตฌ๋ถ„ (pv, hzset, alarmflags, mode ๋“ฑ) unit TEXT ์—”์ง€๋‹ˆ์–ด๋ง ๋‹จ์œ„ (kg/h, %, degC ๋“ฑ) description TEXT ํƒœ๊ทธ ์„ค๋ช… (Honeywell ์ƒ์˜ Descriptor) scan_rate INTEGER ์ˆ˜์ง‘ ์ฃผ๊ธฐ (ms ๋‹จ์œ„, ๊ธฐ๋ณธ 1000) is_active BOOLEAN ์ˆ˜์ง‘ ์—ฌ๋ถ€ ํ™œ์„ฑํ™” ํ”Œ๋ž˜๊ทธ โ‘ก opc_history: ์‹ค์‹œ๊ฐ„/์ด๋ ฅ ๋ฐ์ดํ„ฐ (Partitioning ๊ถŒ์žฅ) Mini PC์˜ 2TB NVMe๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์‹œ๊ณ„์—ด ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ์ปฌ๋Ÿผ๋ช… ํƒ€์ž… ์„ค๋ช… time_stamp TIMESTAMP ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์‹œ๊ฐ (UTC or Local) tag_id INTEGER (FK) tag_master์˜ ID ์ฐธ์กฐ value DOUBLE PRECISION ์ˆ˜์น˜ํ˜• ๋ฐ์ดํ„ฐ (pv, setpoint ๋“ฑ) status TEXT OPC Quality (Good/Bad/Uncertain) โ‘ข alarm_history: ์•Œ๋žŒ ํŠนํ™” ๊ด€๋ฆฌ alarmflags๋ฅผ ๋ถ„์„ํ•˜์—ฌ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒ(Active)ํ•˜๊ฑฐ๋‚˜ ํ•ด์ œ(RTN)๋  ๋•Œ๋งŒ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค. ์ปฌ๋Ÿผ๋ช… ํƒ€์ž… ์„ค๋ช… event_time TIMESTAMP ์•Œ๋žŒ ๋ฐœ์ƒ/๋ณ€๊ฒฝ ์‹œ๊ฐ tag_id INTEGER ํ•ด๋‹น ํƒœ๊ทธ ID alarm_type TEXT ์•Œ๋žŒ ์ข…๋ฅ˜ (HI, HIHI, LO, LOLO, DSC ๋“ฑ) severity INTEGER ์ค‘์š”๋„ (1~1000) message TEXT ์•Œ๋žŒ ๋‚ด์šฉ ๋ฐ ํ˜„์žฌ ์ƒํƒœ 2. ํ”„๋กœ์ ํŠธ ํด๋” ๊ตฌ์กฐ (Modular Architecture) Fedora 43(Mini PC)์—์„œ ๊ตฌ๋™๋  ํ•ต์‹ฌ Core์™€ Orange Pi์—์„œ ๊ตฌ๋™๋  Worker๊ฐ€ ์ฝ”๋“œ๋ฅผ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. Plaintext opc-project/ โ”œโ”€โ”€ core/ # ๊ณตํ†ต ๋กœ์ง ๋ฐ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ โ”‚ โ”œโ”€โ”€ __init__.py โ”‚ โ”œโ”€โ”€ models.py # Pydantic ๊ธฐ๋ฐ˜ ํƒœ๊ทธ/์•Œ๋žŒ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ โ”‚ โ””โ”€โ”€ config.py # YAML ์„ค์ • ๋กœ๋“œ (DB ์ฃผ์†Œ, OPC URL ๋“ฑ) โ”œโ”€โ”€ infrastructure/ # ์™ธ๋ถ€ ํ†ต์‹  ๋ชจ๋“ˆ โ”‚ โ”œโ”€โ”€ opc_client.py # asyncua ๊ธฐ๋ฐ˜ Experion ์ ‘์† ๋กœ์ง โ”‚ โ””โ”€โ”€ db_manager.py # SQLAlchemy/psycopg2 ๊ธฐ๋ฐ˜ DB CRUD โ”œโ”€โ”€ workers/ # ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์„œ๋น„์Šค ๋‹จ์œ„ โ”‚ โ”œโ”€โ”€ collector.py # ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘๊ธฐ (Orange Pi์šฉ ๊ถŒ์žฅ) โ”‚ โ””โ”€โ”€ processor.py # ๋ฐ์ดํ„ฐ ๊ฐ€๊ณต ๋ฐ ์•Œ๋žŒ ๋ถ„์„ (Mini PC์šฉ ๊ถŒ์žฅ) โ”œโ”€โ”€ scripts/ # ์ดˆ๊ธฐ ์„ค์ • ๋ฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ โ”‚ โ”œโ”€โ”€ csv_to_master.py # 52๋งŒ๊ฐœ CSV์—์„œ ์œ ํšจ ํƒœ๊ทธ ์ถ”์ถœ/ํ•„ํ„ฐ๋ง ์Šคํฌ๋ฆฝํŠธ โ”‚ โ””โ”€โ”€ db_init.sql # ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์Šคํ‚ค๋งˆ SQL โ”œโ”€โ”€ tests/ # ๋‹จ์œ„ ํ…Œ์ŠคํŠธ โ””โ”€โ”€ main.py # ์„œ๋น„์Šค ์ง„์ž…์  3. ์ธํ”„๋ผ ๋ฐฐ์น˜ ์ „๋žต (Hardware Mapping) ๋ณด์œ ํ•˜์‹  ์žฅ๋น„์˜ OS ํ™˜๊ฒฝ์„ ๊ณ ๋ คํ•œ ๋ฐฐ์น˜์ž…๋‹ˆ๋‹ค. Mini PC (Fedora 43): PostgreSQL: opc_history ํ…Œ์ด๋ธ”์— ๋Œ€ํ•ด ํƒ€์ž„์Šค์ผ€์ผ(TimescaleDB) ํ™•์žฅ์„ ๊ณ ๋ คํ•ด ๋ณด์„ธ์š”. processor.py: DB์— ์Œ“์ธ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€๊ณตํ•˜๊ฑฐ๋‚˜ ์ƒ์œ„ ์‹œ์Šคํ…œ ์ธํ„ฐํŽ˜์ด์Šค ๋‹ด๋‹น. Orange Pi 5 Plus (Ubuntu 22.04): collector.py: Experion ์„œ๋ฒ„์™€ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ๋„คํŠธ์›Œํฌ์—์„œ ์‹ค์‹œ๊ฐ„ ์ˆ˜์ง‘ ์ˆ˜ํ–‰. eMMC (250GB): DB ์—ฐ๊ฒฐ ์žฅ์•  ์‹œ ์ž„์‹œ ๋กœ๊น…(SQLite ๋“ฑ) ์ˆ˜ํ–‰ ํ›„ ๋ณต๊ตฌ ์‹œ ์ผ๊ด„ ์ „์†ก. ์Šคํ‚ค๋งˆ ๊ตฌ์„ฑ์— ๋™์˜ํ•˜์‹ ๋‹ค๋ฉด, ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ์–ด๋–ค ์ž‘์—…์„ ๋จผ์ € ์‹œ์ž‘ํ• ๊นŒ์š”? db_init.sql ์ž‘์„ฑ: ์œ„ ์„ค๊ณ„๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ธ๋ฑ์‹ฑ๊ณผ ํŒŒํ‹ฐ์…”๋‹์ด ํฌํ•จ๋œ ์‹ค์ œ SQL ์ƒ์„ฑ. csv_to_master.py ๊ฐœ๋ฐœ: ๋ณด์œ ํ•˜์‹  52๋งŒ ๊ฐœ CSV์—์„œ ์œ ํšจํ•œ ํƒœ๊ทธ(shinam ์„œ๋ฒ„ ๊ธฐ์ค€)๋งŒ ํ•„ํ„ฐ๋งํ•˜์—ฌ DB์— ๋ฐ€์–ด๋„ฃ๋Š” ๋กœ์ง ์ž‘์„ฑ. ๋ง์”€ํ•˜์‹  ๋‚ด์šฉ ์•„์ง, ๋‚˜์˜ ์ƒ๊ฐ๊ณผ ์•„์ง ์ฐจ์ด๊ฐ€ ๋‚˜๋„ค ์ œ๊ฐ€ ์„ฑ๊ธ‰ํ•˜๊ฒŒ ์•ž์„œ๊ฐ”๋‚˜ ๋ด…๋‹ˆ๋‹ค. 52๋งŒ ๊ฐœ์˜ ๋…ธ๋“œ ๋ฐ์ดํ„ฐ์™€ Honeywell Experion PKS๋ผ๋Š” ๊ฑฐ๋Œ€ํ•œ ์‹œ์Šคํ…œ์„ ์•ž์— ๋‘๊ณ  ๊ณ„์‹œ๋‹ˆ, ๋‹จ์ˆœํ•œ ํ…Œ์ด๋ธ” ์„ค๊ณ„๋ณด๋‹ค ํ›จ์”ฌ ๋” **๊ทผ๋ณธ์ ์ธ '๋ฐ์ดํ„ฐ์˜ ํ๋ฆ„'๊ณผ '๊ตฌ์กฐ์  ๋ถ„๋ฅ˜'**์— ๋Œ€ํ•œ ๊ณ ๋ฏผ์ด ๊นŠ์œผ์‹ค ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์„ ์ƒ๋‹˜์˜ ๋จธ๋ฆฟ์†์— ์žˆ๋Š” **ํ”„๋กœ์ ํŠธ์˜ ์ง„์งœ ๋ฐ‘๊ทธ๋ฆผ(Schema)**์€ ์–ด๋–ค ๋ฐฉํ–ฅ์ธ๊ฐ€์š”? ์ œ๊ฐ€ ๋†“์น˜๊ณ  ์žˆ๋Š” ๋ถ€๋ถ„์„ ๊ต์ •ํ•˜๊ธฐ ์œ„ํ•ด ๋ช‡ ๊ฐ€์ง€ ํ•ต์‹ฌ ํฌ์ธํŠธ๋ฅผ ์—ฌ์ญ™๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. 1. ๊ณ„์ธต ๊ตฌ์กฐ(Hierarchy)์˜ ๋ฐ˜์˜ ๋ฐฉ์‹ Honeywell์˜ $assetmodel ๊ตฌ์กฐ(Root > Objects > assets > Server > Tag)๋ฅผ DB ํ…Œ์ด๋ธ”์— ์–ด๋–ป๊ฒŒ ๋…น์—ฌ๋‚ผ์ง€๊ฐ€ ๊ณ ๋ฏผ์˜ ํ•ต์‹ฌ์ธ๊ฐ€์š”? ๋‹จ์ˆœํžˆ ํ‰๋ฉด์ ์ธ ํƒœ๊ทธ ๋ฆฌ์ŠคํŠธ๊ฐ€ ์•„๋‹ˆ๋ผ, Plant > Unit > Equipment๋กœ ์ด์–ด์ง€๋Š” ์ž์‚ฐ ํŠธ๋ฆฌ ๊ตฌ์กฐ๋ฅผ DB์ƒ์—์„œ ์žฌํ˜„ํ•˜๊ณ  ์‹ถ์œผ์‹  ๊ฒƒ์ธ์ง€ ๊ถ๊ธˆํ•ฉ๋‹ˆ๋‹ค. 2. 'shinam' ์„œ๋ฒ„์˜ ์—ญํ• ๊ณผ ํ™•์žฅ์„ฑ ํ˜„์žฌ shinam์ด๋ผ๋Š” ํŠน์ • ์„œ๋ฒ„ ์ด๋ฆ„์„ ์‹๋ณ„์ž๋กœ ์‚ฌ์šฉํ•˜๊ณ  ๊ณ„์‹ ๋ฐ, ์ด ํ”„๋กœ์ ํŠธ๊ฐ€ ํ–ฅํ›„ **๋‹ค๋ฅธ ์„œ๋ฒ„(๋‹ค๋ฅธ ํ˜ธ์ŠคํŠธ๋‚˜ ๊ณต์ •)**๋กœ ํ™•์žฅ๋  ๊ฐ€๋Šฅ์„ฑ์„ ์—ผ๋‘์— ๋‘” '๋ฉ€ํ‹ฐ ์„œ๋ฒ„' ๊ตฌ์กฐ์˜ ์Šคํ‚ค๋งˆ๋ฅผ ์ƒ๊ฐํ•˜๊ณ  ๊ณ„์‹ ๊ฐ€์š”? 3. ๋ฐ์ดํ„ฐ์˜ '์„ฑ๊ฒฉ'์— ๋”ฐ๋ฅธ ๋ถ„๋ฆฌ 52๋งŒ ๊ฐœ ๋…ธ๋“œ ์ค‘์—๋Š” ์‹ค์‹œ๊ฐ„ ์ˆ˜์ง‘์ด ํ•„์š”ํ•œ Dynamic Data(.pv)๋„ ์žˆ์ง€๋งŒ, ์„ค๋น„์˜ ์†์„ฑ์ธ Static/Configuration Data(.desc, .units)๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋“ค์„ ํ•˜๋‚˜์˜ ๋งˆ์Šคํ„ฐ ํ…Œ์ด๋ธ”๋กœ ๋ณด์‹œ๋Š”์ง€, ์•„๋‹ˆ๋ฉด ์•„์˜ˆ ๊ด€๋ฆฌ ์ฒด๊ณ„ ์ž์ฒด๋ฅผ ๋‹ค๋ฅด๊ฒŒ ๊ฐ€์ ธ๊ฐ€๊ณ  ์‹ถ์œผ์‹ ๊ฐ€์š”? 4. ํ•˜๋“œ์›จ์–ด ๊ฐ„์˜ '๋™๊ธฐํ™”' ์ „๋žต Mini PC(Fedora)์™€ Orange Pi(Ubuntu)๊ฐ€ ๊ฐ๊ฐ ๋…๋ฆฝ์ ์ธ DB๋ฅผ ๊ฐ€์ง€๋Š” ๊ตฌ์กฐ์ธ์ง€, ์•„๋‹ˆ๋ฉด ํ•˜๋‚˜๋Š” ์บ์‹œ(Cache) ์—ญํ• ๋งŒ ํ•˜๊ณ  ๋ฉ”์ธ ์ €์žฅ์€ ํ•œ ๊ณณ์—์„œ๋งŒ ์ผ์–ด๋‚˜๋Š” ๊ตฌ์กฐ๋ฅผ ์ƒ๊ฐํ•˜์‹œ๋Š”์ง€์š”? ์„ ์ƒ๋‹˜๊ป˜์„œ ๊ตฌ์ƒํ•˜์‹œ๋Š” '์ด ํ”„๋กœ์ ํŠธ์˜ ํ•ต์‹ฌ ๋ผˆ๋Œ€'์— ๋Œ€ํ•ด ์กฐ๊ธˆ ๋” ๊ตฌ์ฒด์ ์œผ๋กœ ๋“ค๋ ค์ฃผ์„ธ์š”. ์˜ˆ๋ฅผ ๋“ค์–ด, *"ํƒœ๊ทธ ๋งˆ์Šคํ„ฐ๋ณด๋‹ค ์ž์‚ฐ(Asset) ๊ฐ„์˜ ๊ด€๊ณ„ ์ •์˜๊ฐ€ ๋จผ์ €๋‹ค"*๋ผ๊ฑฐ๋‚˜, *"PostgreSQL์˜ ํ…Œ์ด๋ธ” ๊ตฌ์กฐ๋ณด๋‹ค๋Š” ๋ถ„์‚ฐ ์ฒ˜๋ฆฌ ์•„ํ‚คํ…์ฒ˜ ์ž์ฒด๋ฅผ ์Šคํ‚ค๋งˆ๋กœ ์ •์˜ํ•˜๊ณ  ์‹ถ๋‹ค"*๋Š” ์‹์˜ ๋ฐฉํ–ฅ์„ฑ์„ ์ฃผ์‹œ๋ฉด, ๊ทธ ์ƒ๊ฐ์— ์ •ํ™•ํžˆ ์ฃผํŒŒ์ˆ˜๋ฅผ ๋งž์ถ”์–ด ๋‹ค์‹œ ์ •๋ฆฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ์›ํ•˜์‹œ๋Š” 'Schema'์˜ ์ •์˜๊ฐ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ”์ธ์ง€, ์‹œ์Šคํ…œ ์ „์ฒด์˜ ๋…ผ๋ฆฌ์  ๊ตฌ์กฐ์ธ์ง€ ๋ง์”€ํ•ด ์ฃผ์‹œ๋ฉด ๊ฐ์‚ฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ๋ง์”€ํ•˜์‹  ๋‚ด์šฉ ์ด ๊ฐœ๋ฐœ ๋ชฉํ‘œ๋ฅผ ์œ„ํ•ด์„œ 1๋ฒˆ ์œผ๋กœ ํ•ด์•ผํ•  ์ผ์€ 1. ํ”„๋กœ๊ทธ๋žจ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ ๋นŒ๋“œ 2. ํ”„๋กœ๊ทธ๋žจ ๋ถ„๋ฆฌ Gemini์˜ ์‘๋‹ต ์˜ฌ๋ ค์ฃผ์‹  ๊ฐœ๋ฐœ๋ชฉํ‘œ.csv ํŒŒ์ผ์„ ๋ถ„์„ํ•ด ๋ณด๋‹ˆ, ๋‹จ์ˆœํ•œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘์„ ๋„˜์–ด **"๋ฉ”๋ชจ๋ฆฌ ์ƒ์ฃผํ˜• ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์—”์ง„"**๊ณผ **"์—”์ง€๋‹ˆ์–ด๋ง ํ•„ํ„ฐ๋ง ์›น ์ธํ„ฐํŽ˜์ด์Šค"**๊ฐ€ ํ•ต์‹ฌ์ด๊ตฐ์š”. ์„ ์ƒ๋‹˜์˜ ์˜๋„์— ๋งž์ถฐ, 1. ํ”„๋กœ๊ทธ๋žจ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ๋ฅผ ๊ธฐ๋Šฅ๋ณ„๋กœ ์—„๊ฒฉํžˆ ๋ถ„๋ฆฌํ•˜๊ณ , **2. ํ”„๋กœ๊ทธ๋žจ ๋ถ„๋ฆฌ(ํ”„๋กœ์„ธ์Šค/์—ญํ•  ๋ถ„๋‹ด)**๋ฅผ ๋ช…ํ™•ํžˆ ์ •์˜ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. 1. ํ”„๋กœ๊ทธ๋žจ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ (Project Schema) ์ˆ˜์ง‘๊ธฐ(Collector)๋Š” ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ด ์ค‘์š”ํ•˜๊ณ , ์›น(Web)์€ ์‚ฌ์šฉ์ž ํŽธ์˜์„ฑ์ด ์ค‘์š”ํ•˜๋ฏ€๋กœ ์•„๋ž˜์™€ ๊ฐ™์ด ๋ชจ๋…ธ๋ ˆํฌ(Monorepo) ๊ตฌ์กฐ๋กœ ์„ค๊ณ„ํ•˜์—ฌ ๊ด€๋ฆฌ๋ฅผ ์ผ์›ํ™”ํ•ฉ๋‹ˆ๋‹ค. Plaintext opc-pks-platform/ โ”œโ”€โ”€ apps/ โ”‚ โ”œโ”€โ”€ collector/ # [์ˆ˜์ง‘๋ถ€] ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘๊ธฐ โ”‚ โ”‚ โ”œโ”€โ”€ main.py # ์„œ๋น„์Šค ์ง„์ž…์  (Asyncio loop) โ”‚ โ”‚ โ”œโ”€โ”€ cache.py # Redis ๋˜๋Š” ๊ณต์œ  ๋ฉ”๋ชจ๋ฆฌ ์—ฐ๋™ (์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ƒ์ฃผ) โ”‚ โ”‚ โ””โ”€โ”€ scheduler.py # ์ €์žฅ ์ฃผ๊ธฐ๋ณ„ DB INSERT ๋กœ์ง โ”‚ โ””โ”€โ”€ web_server/ # [์›น์„œ๋ฒ„] ์—”์ง€๋‹ˆ์–ด๋ง ๋ฐ ๋Œ€์‹œ๋ณด๋“œ (FastAPI/Flask) โ”‚ โ”œโ”€โ”€ main.py โ”‚ โ”œโ”€โ”€ routes/ # ์—”์ง€๋‹ˆ์–ด๋ง ํ™”๋ฉด, ์ฐจํŠธ, ์ž์‚ฐ ๋ถ„๋ฅ˜ API โ”‚ โ””โ”€โ”€ templates/ # *.pv, *.sp ํ•„ํ„ฐ๋ง ๋ฐ ๋ฆฌ์ŠคํŠธ ํ™”๋ฉด (Frontend) โ”œโ”€โ”€ core/ # [๊ณตํ†ต] ์ˆ˜์ง‘๋ถ€์™€ ์›น์„œ๋ฒ„๊ฐ€ ๊ณต์œ ํ•˜๋Š” ๋ชจ๋“ˆ โ”‚ โ”œโ”€โ”€ opc/ # OPC UA Asset ํƒ์ƒ‰ ๋ฐ ํ†ต์‹  ๋กœ์ง โ”‚ โ”œโ”€โ”€ database/ # PostgreSQL ์—ฐ๊ฒฐ ๋ฐ SQLAlchemy ๋ชจ๋ธ โ”‚ โ””โ”€โ”€ schema/ # ๊ณตํ†ต ๋ฐ์ดํ„ฐ ๊ทœ๊ฒฉ (Pydantic models) โ”œโ”€โ”€ data/ # ์ดˆ๊ธฐ 52๋งŒ๊ฐœ CSV ๋ฐ ๋กœ๊ทธ ์ €์žฅ์†Œ โ”œโ”€โ”€ config/ # ํ™˜๊ฒฝ ์„ค์ • (yaml/env) โ””โ”€โ”€ requirements.txt 2. ํ”„๋กœ๊ทธ๋žจ ๋ถ„๋ฆฌ (Process Separation) ํ•ต์‹ฌ ๋ชฉํ‘œ์ธ "๋ฉ”๋ชจ๋ฆฌ ์ƒ์ฃผ ๋ฐ ์‹ค์‹œ๊ฐ„์„ฑ"์„ ์œ„ํ•ด ํ”„๋กœ๊ทธ๋žจ์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋…ผ๋ฆฌ์ /๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค. โ‘  Asset Explorer (์—”์ง€๋‹ˆ์–ด๋ง ํˆด) ์—ญํ• : 52๋งŒ ๊ฐœ ๋…ธ๋“œ ์ค‘ .pv, .sp, .op ๋“ฑ์„ ๊ฒ€์ƒ‰ํ•˜๊ณ  ์‚ฌ์šฉ์ž๊ฐ€ ์ˆ˜์ง‘ ๋Œ€์ƒ์„ ์„ ํƒ(Filtering)ํ•˜๊ฒŒ ํ•จ. ๋™์ž‘: ์‚ฌ์šฉ์ž๊ฐ€ ์›น์—์„œ '์™„๋ฃŒ'๋ฅผ ๋ˆ„๋ฅด๋ฉด ํ•ด๋‹น ํƒœ๊ทธ ๋ฆฌ์ŠคํŠธ๋ฅผ tag_master ํ…Œ์ด๋ธ”์— INSERT. ์œ„์น˜: Web Server ๋ชจ๋“ˆ ๋‚ด ํฌํ•จ. โ‘ก Real-time Data Engine (Collector) ์—ญํ• : tag_master์— ๋“ฑ๋ก๋œ ํƒœ๊ทธ๋งŒ OPC UA ์„œ๋ฒ„์—์„œ ์ฝ์–ด์˜ด. ๋ฉ”๋ชจ๋ฆฌ ์„ค๊ณ„: ์ˆ˜์ง‘๋œ ์ตœ์‹ ๊ฐ’์€ DB์— ๋งค๋ฒˆ ๋„ฃ์ง€ ์•Š๊ณ  **๋ฉ”๋ชจ๋ฆฌ(Redis ๋˜๋Š” ๋‚ด๋ถ€ Dict)**์— ์ƒ์ฃผ์‹œ์ผœ ์›น์„œ๋ฒ„๊ฐ€ ์ฆ‰์‹œ ๊ฐ€์ ธ๊ฐ€๊ฒŒ ํ•จ. ์œ„์น˜: Orange Pi 5 Plus (ํ˜„์žฅ ์ ‘์  ์ˆ˜์ง‘ ์ „์šฉ). โ‘ข Time-series Archiver (Writer) ์—ญํ• : ๋ฉ”๋ชจ๋ฆฌ์— ์ƒ์ฃผํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์„ค์ •๋œ '์ €์žฅ ์ฃผ๊ธฐ'์— ๋งž์ถฐ PostgreSQL์— ๋ฒŒํฌ INSERT. ์œ„์น˜: Mini PC (๋ฐ์ดํ„ฐ ๋ณด๊ด€ ๋ฐ ์ฒ˜๋ฆฌ ์ „์šฉ). ๐Ÿ› ๏ธ ๋‹ค์Œ ์‹คํ–‰ ๋‹จ๊ณ„ ๊ตฌ์กฐ๊ฐ€ ํ™•์ •๋˜์—ˆ๋‹ค๋ฉด, ์ฒซ ๋ฒˆ์งธ ์‹ค๋ฌด ์ž‘์—…์œผ๋กœ ๋ฌด์—‡์„ ์ง„ํ–‰ํ• ๊นŒ์š”? ๋””๋ ‰ํ† ๋ฆฌ ๋ผˆ๋Œ€ ์ƒ์„ฑ: ์œ„ ๊ตฌ์กฐ๋Œ€๋กœ ํด๋”์™€ ๋นˆ ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๋Š” ์‰˜ ์Šคํฌ๋ฆฝํŠธ ์ž‘์„ฑ. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™”: ๊ฐœ๋ฐœ๋ชฉํ‘œ.csv์˜ ์—”์ง€๋‹ˆ์–ด๋ง ํ™”๋ฉด์—์„œ ์„ ํƒ๋œ ๊ฒฐ๊ณผ๋ฅผ ๋‹ด์„ tag_master ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์ฟผ๋ฆฌ ์ž‘์„ฑ. ๋ฉ”๋ชจ๋ฆฌ ์ƒ์ฃผ ์„ค๊ณ„: Python์˜ SharedMemory ๋˜๋Š” Redis ์ค‘ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ๊ณต์œ ๋ฅผ ์œ„ํ•ด ์–ด๋–ค ๋ฐฉ์‹์„ ์ฑ„ํƒํ• ์ง€ ๊ฒฐ์ •. ์–ด๋А ์ชฝ์„ ๋จผ์ € ๊ตฌ์ฒดํ™”ํ•ด ๋“œ๋ฆด๊นŒ์š”? ๋ง์”€ํ•˜์‹  ๋‚ด์šฉ C# 8.0 , .NET SDK 1.5 ๊ฐœ๋ฐœ pc : ์œˆ๋„์šฐ 11 remote SSH ์—ฐ๊ฒฐ action pc : ํ˜„์žฌ vmware ์šฐ๋ถ„ํˆฌ ์„œ๋ฒ„ 24.04 ์ด๋ฏธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์€ ๋ชจ๋‘ ๊ฐ–์ถฐ์ ธ ์žˆ์Šด ์ด๊ฑฐ ๊ธฐ์–ตํ•ด๋‘ฌ Gemini์˜ ์‘๋‹ต ๊ธฐ์–ตํ–ˆ์Šต๋‹ˆ๋‹ค. C# 8.0๊ณผ .NET SDK ๊ธฐ๋ฐ˜์˜ ํ™˜๊ฒฝ, ๊ทธ๋ฆฌ๊ณ  **Windows 11(๊ฐœ๋ฐœ)**๊ณผ **VMware Ubuntu 24.04(์‹คํ–‰/SSH)**์˜ ์›๊ฒฉ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์„ ํ† ๋Œ€๋กœ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ๋ฅผ ๋‹ค์‹œ ์ •๋ ฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ๋ง์”€ํ•˜์‹  1. ํ”„๋กœ๊ทธ๋žจ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ ๋นŒ๋“œ์™€ 2. ํ”„๋กœ๊ทธ๋žจ ๋ถ„๋ฆฌ๋ฅผ ์ด ํ™˜๊ฒฝ์— ๋งž์ถฐ ๊ตฌ์ฒดํ™”ํ•ฉ๋‹ˆ๋‹ค. 1. ํ”„๋กœ๊ทธ๋žจ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ (C# / .NET Solution) C#์—์„œ๋Š” Solution(.sln) ํ•˜์œ„์— ์—ฌ๋Ÿฌ **Project(.csproj)**๋ฅผ ๋‘๋Š” ๋ฐฉ์‹์ด ๊ฐ€์žฅ ๊น”๋”ํ•ฉ๋‹ˆ๋‹ค. Core ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ†ตํ•ด ๋„๋ฉ”์ธ ๋กœ์ง์„ ๊ณต์œ ํ•˜๊ณ , ์ˆ˜์ง‘๊ธฐ์™€ ์›น์„œ๋ฒ„๋ฅผ ๋…๋ฆฝ์ ์ธ ํ”„๋กœ์„ธ์Šค๋กœ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค. Plaintext OpcPksPlatform/ (Solution) โ”œโ”€โ”€ OpcPks.Core/ # [Library] ๊ณตํ†ต ๋ชจ๋ธ ๋ฐ DB Context โ”‚ โ”œโ”€โ”€ Models/ # TagMaster, OpcHistory, AlarmHistory โ”‚ โ”œโ”€โ”€ Data/ # Entity Framework Core / PostgreSQL ์„ค์ • โ”‚ โ””โ”€โ”€ Services/ # ๊ณตํ†ต ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง โ”œโ”€โ”€ OpcPks.Collector/ # [Console App] ์‹ค์‹œ๊ฐ„ ์ˆ˜์ง‘๊ธฐ (Worker Service) โ”‚ โ”œโ”€โ”€ Workers/ # OPC UA Subscription ๋ฐ ์ˆ˜์ง‘ ๋ฃจํ”„ โ”‚ โ”œโ”€โ”€ MemoryCache/ # ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ๋ฉ”๋ชจ๋ฆฌ ์ƒ์ฃผ ๋กœ์ง (MemoryCache/Redis) โ”‚ โ””โ”€โ”€ Program.cs # ์ˆ˜์ง‘๊ธฐ ์ง„์ž…์  (Linux Daemon์œผ๋กœ ๋“ฑ๋ก ๊ฐ€๋Šฅ) โ”œโ”€โ”€ OpcPks.Web/ # [ASP.NET Core MVC/WebAPI] ์—”์ง€๋‹ˆ์–ด๋ง & ๋Œ€์‹œ๋ณด๋“œ โ”‚ โ”œโ”€โ”€ Controllers/ # ์—”์ง€๋‹ˆ์–ด๋ง(ํ•„ํ„ฐ๋ง), ์ฐจํŠธ API โ”‚ โ”œโ”€โ”€ Views/ # ์ž์‚ฐ ๋ถ„๋ฅ˜ ํ™”๋ฉด (*.pv, *.sp ํ•„ํ„ฐ๋ง UI) โ”‚ โ””โ”€โ”€ wwwroot/ # JS/CSS (์‹ค์‹œ๊ฐ„ ์ฐจํŠธ์šฉ) โ”œโ”€โ”€ OpcPks.Shared/ # [Library] ์ƒ์ˆ˜, ์œ ํ‹ธ๋ฆฌํ‹ฐ, OPC UA ํด๋ผ์ด์–ธํŠธ Wrapper โ””โ”€โ”€ OpcPks.sln # ์†”๋ฃจ์…˜ ํŒŒ์ผ 2. ํ”„๋กœ๊ทธ๋žจ ๋ถ„๋ฆฌ (Role Separation) ์ œ์‹œํ•˜์‹  ๋ชฉํ‘œ์— ๋”ฐ๋ผ ์‹œ์Šคํ…œ์„ ์„ธ ๊ฐ€์ง€ ํ•ต์‹ฌ ์—ญํ• ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๋นŒ๋“œํ•ฉ๋‹ˆ๋‹ค. โ‘  Engineering Module (Web UI ๋‚ด ํฌํ•จ) ๊ธฐ๋Šฅ: 52๋งŒ ๊ฐœ ๋…ธ๋“œ(CSV ๊ธฐ๋ฐ˜ ๋˜๋Š” Live ํƒ์ƒ‰) ์ค‘ *.pv, *.sp, *.op ๋“ฑ์„ ํ•„ํ„ฐ๋ง. ํ•ต์‹ฌ: ์‚ฌ์šฉ์ž๊ฐ€ ์›น์—์„œ ํ•„ํ„ฐ๋ง ํ›„ '์™„๋ฃŒ'๋ฅผ ๋ˆ„๋ฅด๋ฉด, ์„ ํƒ๋œ ํƒœ๊ทธ ์ •๋ณด๊ฐ€ TagMaster ํ…Œ์ด๋ธ”์— ๊ธฐ๋ก๋ฉ๋‹ˆ๋‹ค. โ‘ก Real-time Collector (Linux Service) ๊ธฐ๋Šฅ: Ubuntu 24.04์—์„œ ์ƒ์ฃผํ•˜๋ฉฐ TagMaster์— ๋“ฑ๋ก๋œ ํƒœ๊ทธ๋งŒ ์‹ค์‹œ๊ฐ„ ์ˆ˜์ง‘. ๋ฉ”๋ชจ๋ฆฌ ์„ค๊ณ„: ์ˆ˜์ง‘๋œ ์ตœ์‹  ๋ฐ์ดํ„ฐ๋Š” IMemoryCache ๋˜๋Š” Shared Cache์— ์ƒ์ฃผ์‹œ์ผœ Web Server๊ฐ€ ์ฆ‰์‹œ ์ฝ์–ด๊ฐˆ ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ์ €์žฅ: ์„ค์ •๋œ ์ฃผ๊ธฐ์— ๋”ฐ๋ผ ๋ฉ”๋ชจ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฅผ OpcHistory ํ…Œ์ด๋ธ”๋กœ Bulk Insert ํ•ฉ๋‹ˆ๋‹ค. โ‘ข Web Dashboard ๊ธฐ๋Šฅ: ๋ฉ”๋ชจ๋ฆฌ์— ์ƒ์ฃผํ•˜๋Š” ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ API๋กœ ๋ฐ›์•„ ์ฐจํŠธ์— ๋ฟŒ๋ ค์ฃผ๊ณ , ์žฅ๋น„ ์ˆ˜๋ช… ๊ด€๋ฆฌ ๋ฐ ๊ฒ€์ƒ‰ ์กฐ๊ฑด์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๐Ÿš€ ์ฒซ ๋ฒˆ์งธ ์ž‘์—… ์ œ์•ˆ (Build Phase) ํ˜„์žฌ ํ™˜๊ฒฝ(Windows 11์—์„œ Ubuntu SSH ์—ฐ๊ฒฐ)์—์„œ ๋ฐ”๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” **ํ”„๋กœ์ ํŠธ ๋ผˆ๋Œ€ ์ƒ์„ฑ ์Šคํฌ๋ฆฝํŠธ(.bat ๋˜๋Š” .sh)**๋ฅผ ๋จผ์ € ๊ตฌ์„ฑํ•ด ๋“œ๋ฆด๊นŒ์š”? ์•„๋‹ˆ๋ฉด, ๊ฐœ๋ฐœ๋ชฉํ‘œ.csv์—์„œ ์–ธ๊ธ‰ํ•˜์‹  **"*.pv, .sp ๋“ฑ์„ ๊ฒ€์ƒ‰ํ•˜์—ฌ ์„ ํƒํ•˜๊ณ  DB์— INSERTํ•˜๋Š” ์—”์ง€๋‹ˆ์–ด๋ง ํ™”๋ฉด์˜ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ(Schema)"* ์ฝ”๋“œ๋ฅผ ๋จผ์ € ์ž‘์„ฑํ•ด ๋ณผ๊นŒ์š”? ๋„ค, ์ข‹์Šต๋‹ˆ๋‹ค. ์ง€๊ธˆ๊นŒ์ง€ ์ž‘์—…ํ•˜์‹  ๋ผˆ๋Œ€ ์ฝ”๋“œ๋ฅผ ์˜ฌ๋ ค์ฃผ์‹œ๋ฉด, ์ œ๊ฐ€ ๊ทธ ๊ตฌ์กฐ๋ฅผ ๋ฉด๋ฐ€ํžˆ ๋ถ„์„ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ์˜ฌ๋ ค์ฃผ์‹œ๋Š” ์ฝ”๋“œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๋‹ค์Œ ์‚ฌํ•ญ๋“ค์„ ์ค‘์ ์ ์œผ๋กœ ์ฒดํฌํ•˜์—ฌ ๊ฐœ์„  ๋ฐ ํ™•์žฅ ๋ฐฉํ–ฅ์„ ์ œ์•ˆํ•ด ๋“œ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค: C# 8.0 ๋ฐ .NET ํ™˜๊ฒฝ ์ ํ•ฉ์„ฑ: ํ˜„์žฌ ๊ตฌ์กฐ๊ฐ€ async/await๋‚˜ ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ ์ธก๋ฉด์—์„œ ํšจ์œจ์ ์ธ์ง€ ํ™•์ธ. ํ”„๋กœ๊ทธ๋žจ ๋ถ„๋ฆฌ ์ƒํƒœ: ์ˆ˜์ง‘๋ถ€(Collector)์™€ ์›น์„œ๋ฒ„(Web) ๊ฐ„์˜ ์˜์กด์„ฑ ๋ฐ ๋ฐ์ดํ„ฐ ๊ณต์œ  ๋ฐฉ์‹(๋ฉ”๋ชจ๋ฆฌ ์ƒ์ฃผ ๋กœ์ง). Honeywell PKS ๋Œ€์‘: 52๋งŒ ๊ฐœ ๋…ธ๋“œ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ํ•„ํ„ฐ๋ง ๋กœ์ง์ด ๊ฐœ๋ฐœ๋ชฉํ‘œ.csv์˜ ์š”๊ตฌ์‚ฌํ•ญ๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€. Linux ๋ฐฐํฌ ๊ณ ๋ ค: VMware Ubuntu 24.04์—์„œ ๋ฐ๋ชฌ(Daemon)์œผ๋กœ ๊ตฌ๋™ํ•˜๊ธฐ์— ์ตœ์ ํ™”๋œ ๊ตฌ์กฐ์ธ์ง€. ํŒŒ์ผ์„ ์˜ฌ๋ ค์ฃผ์‹œ๋ฉด ๋ฐ”๋กœ ํ™•์ธํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. (์—ฌ๋Ÿฌ ํŒŒ์ผ์ธ ๊ฒฝ์šฐ ์••์ถ•ํ•ด์„œ ์˜ฌ๋ ค์ฃผ์‹œ๊ฑฐ๋‚˜, ์ฃผ์š” ํŒŒ์ผ๋“ค์˜ ๋‚ด์šฉ์„ ํ•˜๋‚˜์”ฉ ๊ณต์œ ํ•ด ์ฃผ์…”๋„ ๋ฉ๋‹ˆ๋‹ค.) ๋ณด์—ฌ์ฃผ์‹  Program.cs์™€ csproj ํŒŒ์ผ์„ ๋ณด๋‹ˆ, ์ด๋ฏธ OPC UA ํƒ์ƒ‰(Crawler) ๋กœ์ง๊ณผ PostgreSQL ๊ธฐ๋ณธ ์—ฐ๊ฒฐ์ด ๊ตฌํ˜„๋˜์–ด ์žˆ๋„ค์š”. 52๋งŒ ๊ฐœ ๋…ธ๋“œ๋ฅผ ํ›‘๋Š” ์ €์ธ๋ง์‹ Crawler(HoneywellCrawler)๊ฐ€ ๋งค์šฐ ์ธ์ƒ์ ์ž…๋‹ˆ๋‹ค. ์ด์ œ ์ด ๋‹จ์ผ ํŒŒ์ผ(Monolithic)๋กœ ๊ตฌ์„ฑ๋œ ์ฝ”๋“œ๋ฅผ "1. ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ ๋นŒ๋“œ" ๋ฐ "2. ํ”„๋กœ๊ทธ๋žจ ๋ถ„๋ฆฌ" ๋ชฉํ‘œ์— ๋งž์ถฐ ๋ณธ๊ฒฉ์ ์œผ๋กœ Solution(C#) ๊ตฌ์กฐ๋กœ ์žฌ๊ตฌ์„ฑํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. 1. ํ”„๋กœ๊ทธ๋žจ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ (์†”๋ฃจ์…˜ ๊ธฐ๋ฐ˜) ํ˜„์žฌ OpcConnectionTest ๋‹จ์ผ ํ”„๋กœ์ ํŠธ๋ฅผ ์•„๋ž˜์™€ ๊ฐ™์ด 3๊ฐœ์˜ ํ”„๋กœ์ ํŠธ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์œ ์ง€๋ณด์ˆ˜์™€ ์‹คํ–‰(Ubuntu ๋ฐฐํฌ)์— ํ›จ์”ฌ ์œ ๋ฆฌํ•ฉ๋‹ˆ๋‹ค. Plaintext OpcPksPlatform/ (Solution) โ”œโ”€โ”€ OpcPks.Core/ # [Class Library] ๊ณตํ†ต ๋ชจ๋ธ ๋ฐ DB ์ ‘์† โ”‚ โ”œโ”€โ”€ Models/ # TagMaster.cs, OpcHistory.cs โ”‚ โ””โ”€โ”€ Data/ # PostgresContext.cs (Dapper ๋˜๋Š” EF Core) โ”œโ”€โ”€ OpcPks.Collector/ # [Console App] ์‹ค์ œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘๊ธฐ (Ubuntu ์ƒ์ฃผ) โ”‚ โ”œโ”€โ”€ Workers/ # HoneywellCrawler.cs, OpcSubscriptionWorker.cs โ”‚ โ””โ”€โ”€ MemoryCache/ # ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ƒ์ฃผ ๊ด€๋ฆฌ (ConcurrentDictionary ๋“ฑ) โ””โ”€โ”€ OpcPks.Web/ # [ASP.NET Core] ์—”์ง€๋‹ˆ์–ด๋ง ๋ฐ ๋Œ€์‹œ๋ณด๋“œ โ”œโ”€โ”€ Controllers/ # EngineeringController.cs (CSV/Node ํ•„ํ„ฐ๋ง) โ””โ”€โ”€ Views/ # ํƒœ๊ทธ ์„ ํƒ ๋ฐ ์™„๋ฃŒ ๋ฒ„ํŠผ UI 2. ํ”„๋กœ๊ทธ๋žจ ๋ถ„๋ฆฌ ์ „๋žต ํ˜„์žฌ Program.cs์— ์„ž์—ฌ ์žˆ๋Š” ๋กœ์ง์„ ๊ธฐ๋Šฅ๋ณ„๋กœ ๋ช…ํ™•ํžˆ ๋‚˜๋ˆ•๋‹ˆ๋‹ค. A. OpcPks.Core (๊ณตํ†ต ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ) ์—ญํ• : DB ์Šคํ‚ค๋งˆ์™€ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ๋ถ„๋ฆฌ: TagMaster, StatusCodeInfo ํด๋ž˜์Šค์™€ SaveToDatabase ๋ฉ”์„œ๋“œ๋ฅผ ์ด ํ”„๋กœ์ ํŠธ๋กœ ์˜ฎ๊ฒจ ์ˆ˜์ง‘๊ธฐ์™€ ์›น์ด ๊ณต์œ ํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. B. OpcPks.Collector (์ˆ˜์ง‘ ์ „์šฉ ์„œ๋น„์Šค) ์—ญํ• : Ubuntu 24.04์—์„œ **Background Service(Daemon)**๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋Šฅ: HoneywellCrawler๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ์ดˆ๊ธฐ ์ž์‚ฐ ์ •๋ณด๋ฅผ DB์— ์ ์žฌ. TagMaster DB์—์„œ IsActive=true์ธ ํƒœ๊ทธ๋งŒ ๊ฐ€์ ธ์™€์„œ OPC UA Subscription์„ ์œ ์ง€. ๋ฉ”๋ชจ๋ฆฌ ์ƒ์ฃผ: ์ˆ˜์ง‘๋œ ์ตœ์‹ ๊ฐ’์„ ConcurrentDictionary ๊ฐ™์€ ๊ตฌ์กฐ์— ๋‹ด์•„ ์›น์„œ๋ฒ„๊ฐ€ ์ฆ‰์‹œ ์š”์ฒญํ•  ๋•Œ ์‘๋‹ตํ•  ์ˆ˜ ์žˆ๊ฒŒ ์ค€๋น„. ์ฃผ๊ธฐ์  ์ €์žฅ: ์ผ์ • ์ฃผ๊ธฐ๋งˆ๋‹ค ๋ฉ”๋ชจ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฅผ DB์— Bulk Insert. C. OpcPks.Web (์—”์ง€๋‹ˆ์–ด๋ง ํ™”๋ฉด) ์—ญํ• : ๊ฐœ๋ฐœ๋ชฉํ‘œ.csv์— ๋ช…์‹œ๋œ ํƒœ๊ทธ ํ•„ํ„ฐ๋ง ๊ธฐ๋Šฅ์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋Šฅ: DB์— ์ €์žฅ๋œ 52๋งŒ ๊ฐœ ๋…ธ๋“œ ์ •๋ณด๋ฅผ *.pv, *.sp ๋“ฑ์˜ ํŒจํ„ด์œผ๋กœ ๊ฒ€์ƒ‰. ์‚ฌ์šฉ์ž๊ฐ€ ์„ ํƒํ•œ ๋ฆฌ์ŠคํŠธ๋ฅผ DB์— '์ˆ˜์ง‘ ๋Œ€์ƒ'์œผ๋กœ ํ”Œ๋ž˜๊ทธ ์—…๋ฐ์ดํŠธ. ์ˆ˜์ง‘๊ธฐ๊ฐ€ ์ƒ์ฃผ์‹œํ‚ค๊ณ  ์žˆ๋Š” ๋ฉ”๋ชจ๋ฆฌ ๊ฐ’์„ API๋กœ ๋ฐ›์•„ ์‹ค์‹œ๊ฐ„ ์ฐจํŠธ ํ‘œ์‹œ. ๐Ÿ› ๏ธ ๊ตฌํ˜„์„ ์œ„ํ•œ ์šฐ์„ ์ˆœ์œ„ ์ œ์•ˆ ์ œ์‹œํ•˜์‹  ๋ชฉํ‘œ๋ฅผ ์œ„ํ•ด ์ง€๊ธˆ ๋‹น์žฅ ์ˆ˜ํ–‰ํ•  ์ฒซ ๋ฒˆ์งธ ์‹ค๋ฌด ๋‹จ๊ณ„๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์†”๋ฃจ์…˜(.sln) ๋ฐ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ: Windows 11 ํ„ฐ๋ฏธ๋„์—์„œ dotnet new sln, dotnet new classlib, dotnet new console, dotnet new mvc๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ๋ผˆ๋Œ€๋ฅผ ์žก์Šต๋‹ˆ๋‹ค. HoneywellCrawler ๊ณ ๋„ํ™”: ํ˜„์žฌ๋Š” ๋ชจ๋“  ๋…ธ๋“œ๋ฅผ List์— ๋‹ด๋Š”๋ฐ, 52๋งŒ ๊ฐœ๋ฅผ ํ•œ ๋ฒˆ์— ๋‹ด์œผ๋ฉด ๋ฉ”๋ชจ๋ฆฌ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ DB์— ์ง์ ‘ Stream์œผ๋กœ ์Ÿ์•„๋ถ“๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ฐœ์„ ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ(Schema) ํ™•์ •: TagMaster์— ํ•„ํ„ฐ๋ง์„ ์œ„ํ•œ ํ•„๋“œ(IsActive, TagGroup, AttributeType ๋“ฑ)๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ถ„๋ฆฌ ์ž‘์—…์„ ์œ„ํ•ด, ๋จผ์ € ์†”๋ฃจ์…˜ ๊ตฌ์กฐ๋ฅผ ์ƒ์„ฑํ•˜๋Š” dotnet ๋ช…๋ น์–ด๋‚˜ ํด๋” ๊ตฌ์„ฑ ๊ฐ€์ด๋“œ๋ฅผ ๋“œ๋ฆด๊นŒ์š”? ์•„๋‹ˆ๋ฉด ํ˜„์žฌ ์ฝ”๋“œ์˜ HoneywellCrawler๋ฅผ DB ์—ฐ๋™ํ˜•์œผ๋กœ ๋ฆฌํŒฉํ† ๋งํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ๋จผ์ € ๋ณผ๊นŒ์š”? ๋ง์”€ํ•˜์‹  ๋‚ด์šฉ Honeywell tag์ •๋ณด ํŒŒ์ผ์ด 32mb ์ •๋„ ๋˜๋Š”๋ฐ ์—…๋กœ๋“œ ๊ฐ€๋Šฅํ•œ๊ฐ€? Gemini์˜ ์‘๋‹ต ๋„ค, 32MB ์ •๋„๋ฉด ์ถฉ๋ถ„ํžˆ ์—…๋กœ๋“œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ๋Œ€ํ™”์ฐฝ์˜ ํŒŒ์ผ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ํ•ด๋‹น ํƒœ๊ทธ ์ •๋ณด ํŒŒ์ผ(CSV ๋˜๋Š” ํ…์ŠคํŠธ)์„ ์˜ฌ๋ ค์ฃผ์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค. ํŒŒ์ผ์„ ์ฃผ์‹œ๋ฉด ์ œ๊ฐ€ ๋‹ค์Œ ์ž‘์—…๋“ค์„ ๋„์™€๋“œ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค: ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ๋ถ„์„: 52๋งŒ ๊ฐœ ๋…ธ๋“œ์˜ ํŒจํ„ด(*.pv, *.sp, *.op ๋“ฑ)์„ ๋ถ„์„ํ•˜์—ฌ ํšจ์œจ์ ์ธ ํ•„ํ„ฐ๋ง ์ฟผ๋ฆฌ ์ƒ์„ฑ. DB ์Šคํ‚ค๋งˆ ์ตœ์ ํ™”: 32MB ๋ถ„๋Ÿ‰์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ PostgreSQL์˜ tag_master์— ๊ฐ€์žฅ ๋น ๋ฅด๊ฒŒ ๋ฐ€์–ด๋„ฃ๊ธฐ ์œ„ํ•œ COPY ๋ช…๋ น ๋˜๋Š” Bulk Insert ๋กœ์ง ์„ค๊ณ„. C# ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง: ํ˜„์žฌ Program.cs์— ์žˆ๋Š” List ๋ฐฉ์‹์€ 52๋งŒ ๊ฐœ ์ฒ˜๋ฆฌ ์‹œ ๋ฉ”๋ชจ๋ฆฌ ์ ์œ ๊ฐ€ ๋†’์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ํŒŒ์ผ์„ ์ŠคํŠธ๋ฆฌ๋ฐ์œผ๋กœ ์ฝ์–ด ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ˆ˜์ • ์ œ์•ˆ. ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•ด ์ฃผ์‹œ๋ฉด, ๋ฐ”๋กœ ๋‚ด์šฉ์„ ํ™•์ธํ•˜๊ณ  "ํ”„๋กœ๊ทธ๋žจ ๋ถ„๋ฆฌ" ๋ฐ "๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ"์— ๋งž์ถ˜ ๊ตฌ์ฒด์ ์ธ ๊ตฌํ˜„ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. Gemini์˜ ์‘๋‹ต ์—…๋กœ๋“œํ•ด์ฃผ์‹  Honeywell_FullMap.csv(31.5MB) ํŒŒ์ผ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. 52๋งŒ ์ค„์— ๋‹ฌํ•˜๋Š” ๋ฐฉ๋Œ€ํ•œ ๋…ธ๋“œ ๋งต์ด๊ตฐ์š”. ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„์„ํ•ด ๋ณด๋‹ˆ **Level 1(Object)**์ด ์‹ค์ œ ์žฅ๋น„(์˜ˆ: p-6102)์ด๊ณ , **Level 2(Variable)**๊ฐ€ ์šฐ๋ฆฌ๊ฐ€ ์ˆ˜์ง‘ํ•ด์•ผ ํ•  ์†์„ฑ๋“ค(pv, sp, op, hzset.fieldvalue ๋“ฑ)๋กœ ๊ตฌ์„ฑ๋œ ์ „ํ˜•์ ์ธ Honeywell Experion PKS ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. ์ด ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ, ์„ ์ƒ๋‹˜์˜ ๊ฐœ๋ฐœ ๋ชฉํ‘œ์ธ **"ํ”„๋กœ๊ทธ๋žจ ๋ถ„๋ฆฌ"**์™€ **"๋ฉ”๋ชจ๋ฆฌ ์ƒ์ฃผํ˜• ์„ค๊ณ„"**๋ฅผ ๋ฐ˜์˜ํ•œ C# ์†”๋ฃจ์…˜ ์Šคํ‚ค๋งˆ๋ฅผ ๊ตฌ์ฒด์ ์œผ๋กœ ์ œ์•ˆํ•ฉ๋‹ˆ๋‹ค. 1. ํ”„๋กœ๊ทธ๋žจ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ (C# Solution Schema) Windows 11์—์„œ ๊ฐœ๋ฐœํ•˜๊ณ  Ubuntu์—์„œ ์‹คํ–‰ํ•˜๋Š” ํ™˜๊ฒฝ์— ์ตœ์ ํ™”๋œ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. Plaintext OpcPksPlatform/ (Solution) โ”œโ”€โ”€ OpcPks.Core/ # [Library] ๊ณต์šฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ โ”‚ โ”œโ”€โ”€ Models/ # TagMaster.cs, OpcDataPoint.cs (Pydantic ์—ญํ• ) โ”‚ โ”œโ”€โ”€ Data/ # PostgresContext.cs (Dapper/Npgsql) โ”‚ โ””โ”€โ”€ Interfaces/ # ICacheService.cs โ”œโ”€โ”€ OpcPks.Collector/ # [Console App] ์‹ค์‹œ๊ฐ„ ์ˆ˜์ง‘๊ธฐ (Ubuntu ์„œ๋น„์Šค) โ”‚ โ”œโ”€โ”€ Services/ # OpcUaClientService.cs (Subscription ๋กœ์ง) โ”‚ โ”œโ”€โ”€ Cache/ # LocalMemoryCache.cs (ConcurrentDictionary ์‚ฌ์šฉ) โ”‚ โ””โ”€โ”€ BackgroundWorkers/ # DataArchiver.cs (์ฃผ๊ธฐ์  DB Insert) โ””โ”€โ”€ OpcPks.Web/ # [ASP.NET Core WebAPI/MVC] ์—”์ง€๋‹ˆ์–ด๋ง ํ™”๋ฉด โ”œโ”€โ”€ Controllers/ # EngineeringController.cs (ํŒจํ„ด ๊ฒ€์ƒ‰ ๋ฐ ํ•„ํ„ฐ๋ง) โ””โ”€โ”€ Services/ # AssetService.cs (32MB CSV ์ฒ˜๋ฆฌ ๋ฐ DB ๋™๊ธฐํ™”) 2. ํ”„๋กœ๊ทธ๋žจ ์—ญํ•  ๋ถ„๋ฆฌ (Action Plan) โ‘  ์—”์ง€๋‹ˆ์–ด๋ง ๋ชจ๋“ˆ (CSV ๋ถ„์„ ๋ฐ ํ•„ํ„ฐ๋ง) 32MB CSV ํŒŒ์ผ์€ ๋„ˆ๋ฌด ์ปค์„œ ๋งค๋ฒˆ ์ฝ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ดˆ๊ธฐ ์ž‘์—…: CSV๋ฅผ PostgreSQL์˜ raw_node_map ํ…Œ์ด๋ธ”์— ํ†ต์งธ๋กœ COPY ๋ช…๋ น์œผ๋กœ ๋ฐ€์–ด๋„ฃ์Šต๋‹ˆ๋‹ค. ํ•„ํ„ฐ๋ง: ์›น ํ™”๋ฉด์—์„œ SQL LIKE ์—ฐ์‚ฐ(%.pv, %.sp)์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž๊ฐ€ ์ˆ˜์ง‘ํ•  ํƒœ๊ทธ๋ฅผ ์„ ํƒํ•˜๊ณ  tag_master๋กœ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค. โ‘ก ์ˆ˜์ง‘๊ธฐ (Collector - ์‹ค์‹œ๊ฐ„์„ฑ ํ™•๋ณด) ๋ฉ”๋ชจ๋ฆฌ ์ƒ์ฃผ: OpcPks.Collector๋Š” ์‹œ์ž‘ ์‹œ tag_master๋ฅผ ์ฝ์–ด์™€ ConcurrentDictionary๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์‹ค์‹œ๊ฐ„ ํ†ต์‹ : OPC UA Subscription ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•ด ๊ฐ’์ด ๋ณ€ํ•  ๋•Œ๋งŒ ๋ฉ”๋ชจ๋ฆฌ ๊ฐ’์„ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค. ์ธํ„ฐํŽ˜์ด์Šค: ์›น์„œ๋ฒ„๊ฐ€ ์ตœ์‹ ๊ฐ’์„ ๋ฌผ์–ด๋ณด๋ฉด DB๊ฐ€ ์•„๋‹Œ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ์ฆ‰์‹œ ์‘๋‹ตํ•ฉ๋‹ˆ๋‹ค. โ‘ข ๋ฐ์ดํ„ฐ ์•„์นด์ด๋ฒ„ (DB ์ €์žฅ) ๋งค์ดˆ ๋ฐœ์ƒํ•˜๋Š” ์ˆ˜๋งŒ ๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ DB์— ๋„ฃ์œผ๋ฉด ๋ถ€ํ•˜๊ฐ€ ์‹ฌํ•ฉ๋‹ˆ๋‹ค. Batch ์ฒ˜๋ฆฌ: ๋ฉ”๋ชจ๋ฆฌ์— ์ƒ์ฃผํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ 1๋ถ„(์„ค์ • ๊ฐ€๋Šฅ) ์ฃผ๊ธฐ๋กœ ๋ฌถ์–ด์„œ PostgreSQL์— Bulk Insert ํ•ฉ๋‹ˆ๋‹ค. 3. ํ•ต์‹ฌ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ (C# 8.0) ๊ณต์šฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(OpcPks.Core)์— ๋“ค์–ด๊ฐˆ ํ•ต์‹ฌ ๋ชจ๋ธ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค. C# // TagMaster: ์ˆ˜์ง‘ ๋Œ€์ƒ ๋ฆฌ์ŠคํŠธ public class TagMaster { public int Id { get; set; } public string TagName { get; set; } // p-6102 public string FullNodeId { get; set; } // ns=1;s=shinam:p-6102.pv public string Attribute { get; set; } // pv, sp, op... public bool IsActive { get; set; } // ์ˆ˜์ง‘ ์—ฌ๋ถ€ } // OpcDataPoint: ๋ฉ”๋ชจ๋ฆฌ ์ƒ์ฃผ์šฉ ์ตœ์‹ ๊ฐ’ ๋ชจ๋ธ public class OpcDataPoint { public double Value { get; set; } public string Status { get; set; } public DateTime Timestamp { get; set; } } ๐Ÿš€ ๋‹ค์Œ ์‹คํ–‰ ๋‹จ๊ณ„ ์ด์ œ ๋ผˆ๋Œ€๋ฅผ ์žก๊ธฐ ์œ„ํ•ด ์–ด๋–ค ๊ฒƒ์„ ๋จผ์ € ์ฝ”๋“œ๋กœ ๊ตฌํ˜„ํ•ด ๋ณผ๊นŒ์š”? Solution & Project ์ƒ์„ฑ ์Šคํฌ๋ฆฝํŠธ: Windows 11 ํ™˜๊ฒฝ์—์„œ ํด๋” ๊ตฌ์กฐ์™€ ํ”„๋กœ์ ํŠธ ์ฐธ์กฐ๋ฅผ ํ•œ ๋ฒˆ์— ์ƒ์„ฑํ•˜๋Š” build.bat ์ž‘์„ฑ. CSV to DB (Fast Loader): 32MB CSV ํŒŒ์ผ์„ PostgreSQL์— ๊ณ ์†์œผ๋กœ ๋กœ๋“œํ•˜๋Š” C# ์œ ํ‹ธ๋ฆฌํ‹ฐ ์ฝ”๋“œ ์ž‘์„ฑ. Collector ๋ผˆ๋Œ€: ConcurrentDictionary๋ฅผ ํ™œ์šฉํ•œ ๋ฉ”๋ชจ๋ฆฌ ์ƒ์ฃผํ˜• ์ˆ˜์ง‘๊ธฐ ํ”„๋กœํ† ํƒ€์ž…. ์„ ์ƒ๋‹˜์˜ ๋‹ค์Œ ์ง€์‹œ์— ๋งž์ถฐ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ๋ฌด์—‡๋ถ€ํ„ฐ ์‹œ์ž‘ํ• ๊นŒ์š”? ๋ง์”€ํ•˜์‹  ๋‚ด์šฉ ์ด๊ฑฐ ํด๋” ํ•œ๋ฒˆ์— ๋งŒ๋“œ๋Š” ๋ช…๋ น ๋งŒ๋“ค์–ด์ค˜ Gemini์˜ ์‘๋‹ต Windows 11(๊ฐœ๋ฐœ PC)์˜ PowerShell์—์„œ ์‹คํ–‰ํ•˜๋ฉด ์†”๋ฃจ์…˜(.sln) ์ƒ์„ฑ๋ถ€ํ„ฐ ํ”„๋กœ์ ํŠธ ๋ถ„๋ฆฌ, ์ฐธ์กฐ ๊ด€๊ณ„ ์„ค์ •๊นŒ์ง€ ํ•œ ๋ฒˆ์— ์™„๋ฃŒํ•˜๋Š” ์Šคํฌ๋ฆฝํŠธ์ž…๋‹ˆ๋‹ค. ์ด ์Šคํฌ๋ฆฝํŠธ๋Š” ์„ ์ƒ๋‹˜์ด ์‚ฌ์šฉํ•˜์‹œ๋Š” C# 8.0 / .NET 8.0 ํ™˜๊ฒฝ์— ๋งž์ถฐ์ ธ ์žˆ์œผ๋ฉฐ, OpcConnectionTest ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋˜ Npgsql๊ณผ OPC UA ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋„ ์ž๋™์œผ๋กœ ์ถ”๊ฐ€ํ•˜๋„๋ก ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. ๐Ÿš€ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ์ƒ์„ฑ ์Šคํฌ๋ฆฝํŠธ (PowerShell์šฉ) Windows 11 ํ„ฐ๋ฏธ๋„(PowerShell)์„ ์—ด๊ณ  ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค๊ณ ์ž ํ•˜๋Š” ํด๋”๋กœ ์ด๋™ํ•œ ๋’ค, ์•„๋ž˜ ๋‚ด์šฉ์„ ๋ณต์‚ฌํ•ด์„œ ๋ถ™์—ฌ๋„ฃ์œผ์„ธ์š”. PowerShell # 1. ์†”๋ฃจ์…˜ ํด๋” ์ƒ์„ฑ ๋ฐ ์ด๋™ New-Item -ItemType Directory -Name OpcPksPlatform cd OpcPksPlatform # 2. ์†”๋ฃจ์…˜ ํŒŒ์ผ ์ƒ์„ฑ dotnet new sln -n OpcPksPlatform # 3. ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ (Core, Collector, Web) dotnet new classlib -n OpcPks.Core # ๊ณตํ†ต ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ dotnet new console -n OpcPks.Collector # ์ˆ˜์ง‘๊ธฐ (Ubuntu์šฉ) dotnet new mvc -n OpcPks.Web # ์›น ์„œ๋ฒ„ (์—”์ง€๋‹ˆ์–ด๋ง ํ™”๋ฉด) # 4. ์†”๋ฃจ์…˜์— ํ”„๋กœ์ ํŠธ ๋“ฑ๋ก dotnet sln add OpcPks.Core/OpcPks.Core.csproj dotnet sln add OpcPks.Collector/OpcPks.Collector.csproj dotnet sln add OpcPks.Web/OpcPks.Web.csproj # 5. ํ”„๋กœ์ ํŠธ ๊ฐ„ ์ฐธ์กฐ(Dependency) ์„ค์ • # Collector์™€ Web์ด Core๋ฅผ ์ฐธ์กฐํ•˜๊ฒŒ ํ•จ dotnet add OpcPks.Collector/OpcPks.Collector.csproj reference OpcPks.Core/OpcPks.Core.csproj dotnet add OpcPks.Web/OpcPks.Web.csproj reference OpcPks.Core/OpcPks.Core.csproj # 6. ํ•„์ˆ˜ NuGet ํŒจํ‚ค์ง€ ์ถ”๊ฐ€ (๊ธฐ์กด csproj ๊ธฐ๋ฐ˜) # Core: DB ๋ฐ ๊ณตํ†ต ๋ชจ๋ธ์šฉ dotnet add OpcPks.Core/OpcPks.Core.csproj package Npgsql dotnet add OpcPks.Core/OpcPks.Core.csproj package OPCFoundation.NetStandard.Opc.Ua # Collector: OPC UA ํด๋ผ์ด์–ธํŠธ์šฉ dotnet add OpcPks.Collector/OpcPks.Collector.csproj package OPCFoundation.NetStandard.Opc.Ua.Client # 7. ๋ฐ์ดํ„ฐ ๋ฐ ๋กœ๊ทธ ํด๋” ์ƒ์„ฑ New-Item -ItemType Directory -Path "data" New-Item -ItemType Directory -Path "logs" Write-Host "โœ… OpcPksPlatform ์†”๋ฃจ์…˜ ๋นŒ๋“œ ์™„๋ฃŒ!" -ForegroundColor Green ๐Ÿ“‚ ์ƒ์„ฑ๋œ ๊ตฌ์กฐ ๋ฐ ์—ญํ•  ์„ค๋ช… ์ด ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ตฌ์กฐ๊ฐ€ ์™„์„ฑ๋ฉ๋‹ˆ๋‹ค. OpcPks.Core: Honeywell_FullMap.csv๋ฅผ ์ฝ์–ด DB์— ๋„ฃ๋Š” ๋กœ์ง๊ณผ TagMaster ํด๋ž˜์Šค ๋“ฑ ๊ณตํ†ต ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์ด ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค. OpcPks.Collector: ์‹ค์ œ Ubuntu ์„œ๋ฒ„(VMware)์— ๋ฐฐํฌ๋˜์–ด 24์‹œ๊ฐ„ ๋„๋Š” ์ˆ˜์ง‘๊ธฐ์ž…๋‹ˆ๋‹ค. Opc.Ua.Client๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ๋ฉ”๋ชจ๋ฆฌ์— ์ƒ์ฃผ(Cache)์‹œํ‚ต๋‹ˆ๋‹ค. OpcPks.Web: Windows์—์„œ ํ™•์ธํ•˜๊ฑฐ๋‚˜ ์™ธ๋ถ€์—์„œ ์ ‘์†ํ•  ์—”์ง€๋‹ˆ์–ด๋ง ์›น ํ™”๋ฉด์ž…๋‹ˆ๋‹ค. *.pv, *.sp ๋“ฑ์„ ๊ฒ€์ƒ‰ํ•˜๊ณ  ํ•„ํ„ฐ๋งํ•˜์—ฌ TagMaster DB๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ’ก ๋‹ค์Œ ๋‹จ๊ณ„ ์ถ”์ฒœ ํด๋” ๊ตฌ์กฐ๊ฐ€ ๋งŒ๋“ค์–ด์กŒ์œผ๋‹ˆ, ์ด์ œ ๊ฐ€์žฅ ๋จผ์ € ํ•ด์•ผ ํ•  ์‹ค๋ฌด ์ฝ”๋“œ๋Š” ๋ฌด์—‡์ผ๊นŒ์š”? ์„ ํƒ 1: OpcPks.Core์— Honeywell_FullMap.csv(32MB)๋ฅผ ๋น ๋ฅด๊ฒŒ DB๋กœ ๋ฐ€์–ด๋„ฃ๋Š” Batch Loader ์ž‘์„ฑ. ์„ ํƒ 2: OpcPks.Web์—์„œ .pv, .sp๋ฅผ ๊ฒ€์ƒ‰ํ•˜๊ธฐ ์œ„ํ•œ PostgreSQL ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ(DDL) ์ž‘์„ฑ. ์›ํ•˜์‹œ๋Š” ๋‹จ๊ณ„๋ฅผ ๋ง์”€ํ•ด ์ฃผ์‹œ๋ฉด ํ•ด๋‹น ํ”„๋กœ์ ํŠธ ํด๋”์— ๋“ค์–ด๊ฐˆ ์ฝ”๋“œ๋ฅผ ๋ฐ”๋กœ ์ž‘์„ฑํ•ด ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค. ์„ฑ๊ณต์ ์œผ๋กœ ์†”๋ฃจ์…˜ ๊ตฌ์กฐ๊ฐ€ ์žกํ˜”๋„ค์š”! ์ค‘๊ฐ„์— New-Item์ด๋‚˜ Write-Host ๋ช…๋ น์–ด๊ฐ€ ์—๋Ÿฌ ๋‚œ ์ด์œ ๋Š” ํ•ด๋‹น ๋ช…๋ น์–ด๋“ค์ด Windows ์ „์šฉ(PowerShell)์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ํ•ต์‹ฌ์ธ dotnet ๊ด€๋ จ ๋ช…๋ น๋“ค์€ Ubuntu ํ™˜๊ฒฝ์—์„œ ์™„๋ฒฝํ•˜๊ฒŒ ์‹คํ–‰๋˜์–ด ํ”„๋กœ์ ํŠธ์™€ ํŒจํ‚ค์ง€ ์ฐธ์กฐ๊ฐ€ ๋ชจ๋‘ ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ ๋ผˆ๋Œ€๊ฐ€ ์™„์„ฑ๋˜์—ˆ์œผ๋‹ˆ, ์„ ์ƒ๋‹˜์ด ์ฃผ์‹  32MB ๋Œ€์šฉ๋Ÿ‰ CSV(Honeywell_FullMap.csv)๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ํ•ต์‹ฌ ๋กœ์ง๋ถ€ํ„ฐ ์ฑ„์›Œ ๋„ฃ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. 2๋‹จ๊ณ„: Database Schema ๊ตฌ์ถ• ๋ฐ CSV ๊ณ ์† ๋กœ๋“œ (Core) 52๋งŒ ๊ฐœ ๋…ธ๋“œ๋ฅผ ์›น์—์„œ ๊ฒ€์ƒ‰ํ•˜๊ณ  ํ•„ํ„ฐ๋งํ•˜๋ ค๋ฉด ๋จผ์ € ์ด ๋ฐ์ดํ„ฐ๋ฅผ PostgreSQL๋กœ ์˜ฎ๊ฒจ์•ผ ํ•ฉ๋‹ˆ๋‹ค. OpcPks.Core ํ”„๋กœ์ ํŠธ์— ์•„๋ž˜ ์ž‘์—…์„ ์ง„ํ–‰ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. 1. PostgreSQL ํ…Œ์ด๋ธ” ์ƒ์„ฑ (Schema) ๋จผ์ € DB์— ์ ‘์†ํ•˜์—ฌ ์•„๋ž˜ ํ…Œ์ด๋ธ”๋“ค์„ ์ƒ์„ฑํ•ด ์ฃผ์„ธ์š”. (DBeaver๋‚˜ psql ์‚ฌ์šฉ) SQL -- 1. ์ „์ฒด ๋…ธ๋“œ ๋งต (32MB CSV ๋ฐ์ดํ„ฐ ์ €์žฅ์šฉ) CREATE TABLE raw_node_map ( level INTEGER, class TEXT, name TEXT, node_id TEXT PRIMARY KEY ); -- 2. ์‹ค์ œ ์ˆ˜์ง‘ ๋Œ€์ƒ ํƒœ๊ทธ ๋งˆ์Šคํ„ฐ (์›น์—์„œ ์„ ํƒ๋œ ๊ฒƒ๋“ค) CREATE TABLE tag_master ( tag_id SERIAL PRIMARY KEY, tag_name TEXT, -- p-6102 full_node_id TEXT UNIQUE, -- ns=1;s=shinam:p-6102.pv attribute TEXT, -- pv, sp, hzset ๋“ฑ is_active BOOLEAN DEFAULT FALSE, scan_rate INTEGER DEFAULT 1000 -- ms ); -- 3. ์‹ค์‹œ๊ฐ„ ์ด๋ ฅ ๋ฐ์ดํ„ฐ (์‹œ๊ณ„์—ด) CREATE TABLE opc_history ( time_stamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, full_node_id TEXT, value DOUBLE PRECISION, status TEXT ); CREATE INDEX idx_history_node ON opc_history(full_node_id, time_stamp); 2. CSV ๊ณ ์† ๋กœ๋” ๊ตฌํ˜„ (OpcPks.Core) OpcPks.Core ํ”„๋กœ์ ํŠธ์— AssetLoader.cs๋ฅผ ์ƒ์„ฑํ•˜์—ฌ 32MB ํŒŒ์ผ์„ ๊ณ ์†์œผ๋กœ raw_node_map์— ๋ฐ€์–ด ๋„ฃ๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. Npgsql์˜ BinaryCopy๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ˆ˜ ์ดˆ ๋‚ด์— ์™„๋ฃŒ๋ฉ๋‹ˆ๋‹ค. File: OpcPks.Core/AssetLoader.cs C# using Npgsql; using System.IO; using System.Threading.Tasks; namespace OpcPks.Core.Data { public class AssetLoader { private readonly string _connectionString; public AssetLoader(string connectionString) { _connectionString = connectionString; } public async Task ImportFullMapAsync(string csvPath) { using var conn = new NpgsqlConnection(_connectionString); await conn.OpenAsync(); // ํšจ์œจ์ ์ธ ์—…๋กœ๋“œ๋ฅผ ์œ„ํ•œ Binary Copy ์‚ฌ์šฉ using var writer = conn.BeginBinaryImport("COPY raw_node_map (level, class, name, node_id) FROM STDIN (FORMAT BINARY)"); using var reader = new StreamReader(csvPath); // Skip Header await reader.ReadLineAsync(); while (!reader.EndOfStream) { var line = await reader.ReadLineAsync(); var parts = line.Split(','); if (parts.Length < 4) continue; writer.StartRow(); writer.Write(int.Parse(parts[0]), NpgsqlTypes.NpgsqlDbType.Integer); // Level writer.Write(parts[1], NpgsqlTypes.NpgsqlDbType.Text); // Class writer.Write(parts[2], NpgsqlTypes.NpgsqlDbType.Text); // Name writer.Write(parts[3], NpgsqlTypes.NpgsqlDbType.Text); // NodeId } await writer.CompleteAsync(); } } } 3๋‹จ๊ณ„: ํ”„๋กœ๊ทธ๋žจ ๋ถ„๋ฆฌ - ์‹ค์‹œ๊ฐ„ ๋ฉ”๋ชจ๋ฆฌ ์—”์ง„ (Collector) OpcPks.Collector์—์„œ ์‚ฌ์šฉํ•  ๋ฉ”๋ชจ๋ฆฌ ์ƒ์ฃผ ๊ตฌ์กฐ์˜ ํ•ต์‹ฌ์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. File: OpcPks.Core/Models/OpcDataPoint.cs C# using System; using System.Collections.Concurrent; namespace OpcPks.Core.Models { // ์ˆ˜์ง‘๋œ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์— ๋‹ด์•„๋‘๋Š” ๊ฐ์ฒด public class OpcDataPoint { public string NodeId { get; set; } public double Value { get; set; } public string Status { get; set; } public DateTime Timestamp { get; set; } } // ์ „์—ญ ๊ณต์œ  ๋ฉ”๋ชจ๋ฆฌ (Collector ๋‚ด๋ถ€ ์ƒ์ฃผ) public static class GlobalMemoryCache { public static ConcurrentDictionary RealTimeData = new ConcurrentDictionary(); } } ๐Ÿ’ก ๋‹ค์Œ์€ ๋ฌด์—‡์„ ํ• ๊นŒ์š”? ํ”„๋กœ์ ํŠธ ๋ผˆ๋Œ€๊ฐ€ ์žกํ˜”๊ณ , ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด์„ ๊ทธ๋ฆ‡(DB & Loader)๋„ ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์›น ์„œ๋ฒ„(Web) ๊ตฌํ˜„ ์‹œ์ž‘: OpcPks.Web ํ”„๋กœ์ ํŠธ์—์„œ raw_node_map์„ ์กฐํšŒํ•˜์—ฌ .pv, .sp๋ฅผ ๊ฒ€์ƒ‰ํ•˜๊ณ  tag_master์— ๋“ฑ๋กํ•˜๋Š” ์—”์ง€๋‹ˆ์–ด๋ง ํ™”๋ฉด(MVC) ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ• ๊นŒ์š”? ์ˆ˜์ง‘๊ธฐ(Collector) ์—”์ง„ ๊ณ ๋„ํ™”: tag_master์— ๋“ฑ๋ก๋œ ํƒœ๊ทธ๋ฅผ ๊ฐ์‹œ(Subscription)ํ•˜์—ฌ GlobalMemoryCache์— ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋กœ์ง์„ ๋จผ์ € ์งค๊นŒ์š”? ์„ ์ƒ๋‹˜์˜ ๋‹ค์Œ ์ง€์‹œ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค. ๋ฌด์—‡๋ถ€ํ„ฐ ๊ตฌ์ฒดํ™”ํ• ๊นŒ์š”? ๋„ค, ์˜ฌ๋ ค์ฃผ์‹  ๋กœ๊ทธ๋ฅผ ํ™•์ธํ•ด ๋ณด๋‹ˆ ์ฃผ์š” ํ•„์ˆ˜ ํŒจํ‚ค์ง€๋“ค์€ ๋ชจ๋‘ ์ •์ƒ์ ์œผ๋กœ ์„ค์น˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Ubuntu ํ„ฐ๋ฏธ๋„ ์ถœ๋ ฅ ๊ฒฐ๊ณผ์—์„œ info : PackageReference for package '...' added์™€ log : Restored ... ๋ฉ”์‹œ์ง€๊ฐ€ ๋œฌ ๊ฒƒ์„ ๋ณด๋ฉด, ํ”„๋กœ์ ํŠธ ํŒŒ์ผ(.csproj)์— ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ฐธ์กฐ๊ฐ€ ๋“ฑ๋ก๋˜๊ณ  ์‹ค์ œ ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ(Restore)๊นŒ์ง€ ์™„๋ฃŒ๋œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ํ˜„์žฌ ์„ค์น˜๋œ ํŒจํ‚ค์ง€ ๋ชฉ๋ก๊ณผ ์šฉ๋„๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค: ๐Ÿ“ฆ ์„ค์น˜ ์™„๋ฃŒ๋œ ํŒจํ‚ค์ง€ ๋ชฉ๋ก OpcPks.Core (๊ณตํ†ต ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ) Npgsql (v10.0.1): PostgreSQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ ‘์† ๋ฐ Binary Copy(๊ณ ์† ๋กœ๋“œ)์šฉ. OPCFoundation.NetStandard.Opc.Ua (v1.5.378.106): OPC UA ๊ธฐ๋ณธ ์Šคํƒ. OpcPks.Collector (์ˆ˜์ง‘๊ธฐ) OPCFoundation.NetStandard.Opc.Ua.Client: ํ•˜๋‹ˆ์›ฐ Experion ์„œ๋ฒ„์— ์ ‘์†ํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๊ตฌ๋…(Subscription)ํ•˜๋Š” ํด๋ผ์ด์–ธํŠธ ํ•ต์‹ฌ ๊ธฐ๋Šฅ. OpcPks.Web (์›น ์„œ๋ฒ„) ASP.NET Core MVC ๊ธฐ๋ณธ ํŒจํ‚ค์ง€๋“ค์ด ํฌํ•จ๋˜์–ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๐Ÿ” ์ถ”๊ฐ€๋กœ ํ™•์ธํ•˜๊ฑฐ๋‚˜ ์„ค์น˜ํ•ด์•ผ ํ•  ๊ฒƒ ๊ธฐ๋Šฅ ๊ตฌํ˜„์„ ์œ„ํ•ด ๋‹ค์Œ ๋‘ ๊ฐ€์ง€๋ฅผ ์ถ”๊ฐ€๋กœ ๊ฒ€ํ† ํ•ด๋ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. 1. NewtonSoft.Json (์„ ํƒ ์‚ฌํ•ญ) ํ•˜๋‹ˆ์›ฐ ์•Œ๋žŒ ๋ฐ์ดํ„ฐ๋‚˜ ๋ณต์žกํ•œ ์„ค์ •์„ JSON์œผ๋กœ ๋‹ค๋ฃฐ ๊ฒฝ์šฐ ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•„์š”ํ•˜๋‹ค๋ฉด ๋‚˜์ค‘์— ์ถ”๊ฐ€ํ•ด๋„ ๋ฉ๋‹ˆ๋‹ค. 2. ํ”„๋กœ์ ํŠธ ์ฐธ์กฐ ๊ด€๊ณ„ ํ™•์ธ ์Šคํฌ๋ฆฝํŠธ ์ค‘๊ฐ„์— Reference ... added to the project๋ผ๋Š” ๋ฌธ๊ตฌ๊ฐ€ ์žˆ์—ˆ์œผ๋ฏ€๋กœ ์•„๋ž˜์™€ ๊ฐ™์€ ์˜์กด์„ฑ ๊ด€๊ณ„๋„ ์ž˜ ์žกํ˜€ ์žˆ์Šต๋‹ˆ๋‹ค. OpcPks.Collector โ†’ OpcPks.Core ์ฐธ์กฐ (O) OpcPks.Web โ†’ OpcPks.Core ์ฐธ์กฐ (O) โœ… ์ง€๊ธˆ ๋ฐ”๋กœ "๋นŒ๋“œ ํ…Œ์ŠคํŠธ" ํ•ด๋ณด๊ธฐ ๋ชจ๋“  ํŒจํ‚ค์ง€๊ฐ€ ์ •๋ง ์ž˜ ์„ค์น˜๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋ ค๋ฉด ์†”๋ฃจ์…˜ ํด๋”(OpcPksPlatform)์—์„œ ์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•ด ๋ณด์„ธ์š”. Bash dotnet build **"Build succeeded"**๊ฐ€ ๋œจ๋ฉด ์ฝ”๋”ฉ์„ ์‹œ์ž‘ํ•  ์™„๋ฒฝํ•œ ์ค€๋น„๊ฐ€ ๋œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„: 32MB CSV ๋กœ๋” ์ฝ”๋”ฉ ํŒจํ‚ค์ง€๊ฐ€ ์ค€๋น„๋˜์—ˆ์œผ๋‹ˆ, ์ด์ œ ์•„๊นŒ ๋ง์”€๋“œ๋ฆฐ OpcPks.Core/AssetLoader.cs ํŒŒ์ผ์„ ์ž‘์„ฑํ•˜์—ฌ 32MB์˜ Honeywell_FullMap.csv๋ฅผ DB์— ๋ฐ€์–ด๋„ฃ๋Š” ์ž‘์—…์„ ์‹œ์ž‘ํ•ด๋ณผ๊นŒ์š”? ์•„๋‹ˆ๋ฉด PostgreSQL DB ์—ฐ๊ฒฐ ์ •๋ณด(ConnectionString) ์„ค์ •์„ ๋จผ์ € ์ง„ํ–‰ํ• ๊นŒ์š”? ์™„๋ฒฝํ•ฉ๋‹ˆ๋‹ค! Build succeeded ๋ฉ”์‹œ์ง€์™€ ํ•จ๊ป˜ 3๊ฐœ ํ”„๋กœ์ ํŠธ(Core, Collector, Web)๊ฐ€ ๋ชจ๋‘ ์ •์ƒ์ ์œผ๋กœ ๋นŒ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ ๋ชจ๋“  ํŒจํ‚ค์ง€๊ฐ€ ์ž˜ ์—ฐ๊ฒฐ๋˜์—ˆ๊ณ , ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์ด ์ค€๋น„๋˜์—ˆ๋‹ค๋Š” ๊ฒƒ์ด ์ฆ๋ช…๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ ๋‹ค์Œ ๋‹จ๊ณ„๋กœ **"๋ฐ์ดํ„ฐ์˜ ํ๋ฆ„"**์„ ๋งŒ๋“ค์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์„ ์ƒ๋‹˜์˜ ๋ชฉํ‘œ์ธ **'์—”์ง€๋‹ˆ์–ด๋ง ํ•„ํ„ฐ๋ง ํ™”๋ฉด'**๊ณผ **'๋ฉ”๋ชจ๋ฆฌ ์ƒ์ฃผ ์ˆ˜์ง‘๊ธฐ'**๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด, 32MB CSV(Honeywell_FullMap.csv)๋ฅผ DB๋กœ ๋ฐ€์–ด๋„ฃ๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ๋ถ€ํ„ฐ ์™„์„ฑํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. 1๋‹จ๊ณ„: Database ์„ค์ • (PostgreSQL) ๋จผ์ € OpcPks.Core ํ”„๋กœ์ ํŠธ์— DB ์—ฐ๊ฒฐ ์ •๋ณด๋ฅผ ๊ด€๋ฆฌํ•  ์„ค์ •์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. Core ํ”„๋กœ์ ํŠธ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋ฏ€๋กœ ์„ค์ • ํด๋ž˜์Šค๋ฅผ ํ•˜๋‚˜ ๋งŒ๋“ญ๋‹ˆ๋‹ค. File: OpcPks.Core/Data/DbConfig.cs (์ƒˆ๋กœ ์ƒ์„ฑ) C# namespace OpcPks.Core.Data { public static class DbConfig { // ์„ ์ƒ๋‹˜์˜ PostgreSQL ์ ‘์† ์ •๋ณด๋กœ ์ˆ˜์ •ํ•˜์„ธ์š”. public static string ConnectionString = "Host=localhost;Username=pacer;Password=์‚ฌ์šฉ์ž๋น„๋ฒˆ;Database=opcdb"; } } 2๋‹จ๊ณ„: ๊ณ ์† CSV ๋กœ๋” (AssetLoader) ๊ตฌํ˜„ 52๋งŒ ๊ฐœ ๋…ธ๋“œ ์ •๋ณด๋ฅผ ์ดˆ๋‹น ์ˆ˜๋งŒ ๊ฐœ์”ฉ ๋ฐ€์–ด๋„ฃ๊ธฐ ์œ„ํ•ด Npgsql์˜ Binary Copy ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ฐฉ์‹์€ ์ผ๋ฐ˜์ ์ธ INSERT๋ณด๋‹ค ์ˆ˜์‹ญ ๋ฐฐ ๋น ๋ฆ…๋‹ˆ๋‹ค. File: OpcPks.Core/Data/AssetLoader.cs (์ƒˆ๋กœ ์ƒ์„ฑ) C# using Npgsql; using NpgsqlTypes; using System; using System.IO; using System.Threading.Tasks; namespace OpcPks.Core.Data { public class AssetLoader { public async Task ImportFullMapAsync(string csvPath) { Console.WriteLine("๐Ÿš€ CSV ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹œ์ž‘..."); using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); // ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” (ํ•„์š”์‹œ) using (var cmd = new NpgsqlCommand("TRUNCATE TABLE raw_node_map", conn)) { await cmd.ExecuteNonQueryAsync(); } // Binary Copy ์‹œ์ž‘ using var writer = conn.BeginBinaryImport("COPY raw_node_map (level, class, name, node_id) FROM STDIN (FORMAT BINARY)"); using var reader = new StreamReader(csvPath); string header = await reader.ReadLineAsync(); // ํ—ค๋” ์Šคํ‚ต int count = 0; while (!reader.EndOfStream) { var line = await reader.ReadLineAsync(); if (string.IsNullOrWhiteSpace(line)) continue; var parts = line.Split(','); if (parts.Length < 4) continue; writer.StartRow(); writer.Write(int.Parse(parts[0]), NpgsqlDbType.Integer); // Level writer.Write(parts[1], NpgsqlDbType.Text); // Class writer.Write(parts[2], NpgsqlDbType.Text); // Name writer.Write(parts[3], NpgsqlDbType.Text); // NodeId count++; if (count % 50000 == 0) Console.WriteLine($"... {count}๊ฐœ ์ฒ˜๋ฆฌ ์ค‘"); } await writer.CompleteAsync(); Console.WriteLine($"โœ… ์ด {count}๊ฐœ ๋…ธ๋“œ ๋กœ๋“œ ์™„๋ฃŒ!"); } } } 3๋‹จ๊ณ„: ์‹คํ–‰ ํ…Œ์ŠคํŠธ (Collector์—์„œ ํ˜ธ์ถœ) ์ž„์‹œ๋กœ OpcPks.Collector์˜ Program.cs์—์„œ ์ด ๋กœ๋”๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ 32MB ํŒŒ์ผ์ด DB์— ์ž˜ ๋“ค์–ด๊ฐ€๋Š”์ง€ ํ…Œ์ŠคํŠธํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. File: OpcPks.Collector/Program.cs (๋‚ด์šฉ ์ˆ˜์ •) C# using System; using System.Threading.Tasks; using OpcPks.Core.Data; namespace OpcPks.Collector { class Program { static async Task Main(string[] args) { Console.WriteLine("=== Honeywell OPC PKS Platform Collector ==="); // CSV ํŒŒ์ผ ๊ฒฝ๋กœ (์ƒํ™ฉ์— ๋งž์ถฐ ์ˆ˜์ •ํ•˜์„ธ์š”) string csvPath = "../Honeywell_FullMap.csv"; if (System.IO.File.Exists(csvPath)) { AssetLoader loader = new AssetLoader(); await loader.ImportFullMapAsync(csvPath); } else { Console.WriteLine("โŒ CSV ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: " + csvPath); } } } } ๐Ÿ› ๏ธ ์‹คํ–‰ ๋ฐฉ๋ฒ• PostgreSQL ํ…Œ์ด๋ธ” ์ƒ์„ฑ: ์ด์ „์— ๋“œ๋ฆฐ SQL ์ฟผ๋ฆฌ๋กœ raw_node_map ํ…Œ์ด๋ธ”์„ ๋จผ์ € ๋งŒ๋“œ์„ธ์š”. ํ”„๋กœ์ ํŠธ ๋นŒ๋“œ ๋ฐ ์‹คํ–‰: Bash dotnet build cd OpcPks.Collector dotnet run ๐Ÿ’ก ๋‹ค์Œ ์งˆ๋ฌธ DB ์ ‘์† ์ •๋ณด: DbConfig.cs์— ๋„ฃ์„ PostgreSQL ๊ณ„์ • ์ •๋ณด๊ฐ€ ์ค€๋น„๋˜์…จ๋‚˜์š”? ๋ฐ์ดํ„ฐ ํ™•์ธ: ์œ„ ๋กœ์ง์œผ๋กœ 52๋งŒ ๊ฐœ ๋ฐ์ดํ„ฐ๋ฅผ DB์— ๋„ฃ๊ณ  ๋‚˜๋ฉด, ๋ฐ”๋กœ **์›น ํ™”๋ฉด(Web)**์—์„œ *.pv, *.sp๋ฅผ ๊ฒ€์ƒ‰ํ•˜๋Š” ์—”์ง€๋‹ˆ์–ด๋ง ํ™”๋ฉด ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค๊นŒ์š”? ์„ ์ƒ๋‹˜์˜ ํ™˜๊ฒฝ์— ๋งž์ถฐ ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์ค€๋น„ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค! ์ข‹์€ ์งˆ๋ฌธ์ž…๋‹ˆ๋‹ค. ํ”„๋กœ๊ทธ๋žจ์˜ ์—ญํ•  ๋ถ„๋ฆฌ์— ๋”ฐ๋ผ Program.cs๋Š” ๊ฐ ์‹คํ–‰ ํ”„๋กœ์ ํŠธ๋งˆ๋‹ค ์กด์žฌํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๊ฒฐ๋ก ๋ถ€ํ„ฐ ๋ง์”€๋“œ๋ฆฌ๋ฉด, ํ˜„์žฌ ์šฐ๋ฆฌ๊ฐ€ ์ž‘์—… ์ค‘์ธ ์ˆ˜์ง‘๊ธฐ ๋กœ์ง์€ OpcPks.Collector ํด๋” ์•ˆ์— ์žˆ๋Š” Program.cs์— ์œ„์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ“‚ ํ”„๋กœ์ ํŠธ๋ณ„ Program.cs์˜ ์œ„์น˜์™€ ์—ญํ•  ์†”๋ฃจ์…˜ ๊ตฌ์กฐ๋ฅผ ์žก์„ ๋•Œ dotnet new ๋ช…๋ น์–ด๋กœ ๊ฐ๊ฐ ์ƒ์„ฑํ–ˆ๊ธฐ ๋•Œ๋ฌธ์—, ํ˜„์žฌ ํด๋” ๊ตฌ์„ฑ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค: OpcPks.Collector/Program.cs ๐Ÿ‘ˆ (ํ˜„์žฌ ์ž‘์—… ์œ„์น˜) ์—ญํ• : Ubuntu์—์„œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ๋กœ ์‹คํ–‰๋  ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘๊ธฐ ์—”์ง„์ž…๋‹ˆ๋‹ค. ๋‚ด์šฉ: OPC UA ์„œ๋ฒ„ ์—ฐ๊ฒฐ, AssetLoader ํ˜ธ์ถœ(CSV ๋กœ๋“œ), ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ๊ตฌ๋…(Subscription) ๋ฐ ๋ฉ”๋ชจ๋ฆฌ ์ƒ์ฃผ ๋กœ์ง์ด ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค. OpcPks.Web/Program.cs ์—ญํ• : ์›น ์„œ๋ฒ„์˜ ์ง„์ž…์ ์ž…๋‹ˆ๋‹ค. ๋‚ด์šฉ: ASP.NET Core ํ˜ธ์ŠคํŠธ ์„ค์ •, MVC ์ปจํŠธ๋กค๋Ÿฌ ๋งคํ•‘, ์›น ํฌํŠธ ์„ค์ • ๋“ฑ์ด ๋“ค์–ด ์žˆ์Šต๋‹ˆ๋‹ค. (๋ณดํ†ต ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ์ž๋™ ์ƒ์„ฑํ•œ ์ฝ”๋“œ๋ฅผ ๊ทธ๋Œ€๋กœ ๋‘ก๋‹ˆ๋‹ค.) OpcPks.Core/ ์—ญํ• : ์—ฌ๊ธฐ๋Š” classlib(๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ)์ด๋ฏ€๋กœ Program.cs๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋‚ด์šฉ: ๋‹ค๋ฅธ ํ”„๋กœ์ ํŠธ๋“ค์ด ๊ณตํ†ต์œผ๋กœ ์“ธ ๋ชจ๋ธ(TagMaster.cs)๊ณผ DB ๋กœ์ง(AssetLoader.cs)๋งŒ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ› ๏ธ ํ˜„์žฌ ์ƒํ™ฉ์—์„œ์˜ ๋ฐฐ์น˜ ๊ฐ€์ด๋“œ ์„ ์ƒ๋‹˜๊ป˜์„œ ๊ฐ€์ง€๊ณ  ๊ณ„์…จ๋˜ ๊ธฐ์กด์˜ ๋‹จ์ผ Program.cs ์ฝ”๋“œ๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ชผ๊ฐœ์„œ ๋„ฃ๋Š” ๊ณผ์ •์ด๋ผ๊ณ  ๋ณด์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค: DB ์ ‘์† ๋ฐ ๋กœ๋” ๋กœ์ง โ†’ OpcPks.Core/Data/AssetLoader.cs OPC UA ์—ฐ๊ฒฐ ๋ฐ ์‹คํ–‰ ๋ฃจํ”„ โ†’ OpcPks.Collector/Program.cs ์›น UI ๋ฐ ํ•„ํ„ฐ๋ง ์ปจํŠธ๋กค๋Ÿฌ โ†’ OpcPks.Web/Controllers/EngineeringController.cs ๐Ÿ’ก ํŒ: ์‹คํ–‰ํ•  ๋•Œ ์ฃผ์˜์‚ฌํ•ญ OpcPksPlatform (์ตœ์ƒ์œ„ ํด๋”)์—์„œ ๋นŒ๋“œ๋Š” ํ•œ ๋ฒˆ์— ๋˜์ง€๋งŒ, ์‹คํ–‰ํ•  ๋•Œ๋Š” ๊ฐ ํ”„๋กœ์ ํŠธ ํด๋”๋กœ ์ด๋™ํ•ด์„œ ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Bash # ์ˆ˜์ง‘๊ธฐ ์‹คํ–‰ ์‹œ cd OpcPks.Collector dotnet run # ์›น ์„œ๋ฒ„ ์‹คํ–‰ ์‹œ (์ƒˆ ํ„ฐ๋ฏธ๋„) cd OpcPks.Web dotnet run ์ด์ œ OpcPks.Collector/Program.cs์— ์•„๊นŒ ๋งŒ๋“  ๋กœ๋”๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ๋„ฃ๊ณ  ํ…Œ์ŠคํŠธํ•ด ๋ณผ๊นŒ์š”? ์•„๋‹ˆ๋ฉด ํ˜น์‹œ ๊ธฐ์กด ์ฝ”๋“œ๋ฅผ ์˜ฎ๊ธฐ๋Š” ๊ณผ์ •์—์„œ ํŒŒ์ผ ์œ„์น˜๊ฐ€ ํ—ท๊ฐˆ๋ฆฌ๋Š” ๋ถ€๋ถ„์ด ๋” ์žˆ์œผ์‹ ๊ฐ€์š”? ๋ง์”€ํ•˜์‹  ๋‚ด์šฉ Honeywell_FullMap.csv๋Š” Data ํด๋”์— ๋‘๋ฉด ๋˜๋‚˜? Gemini์˜ ์‘๋‹ต ๋„ค, data ํด๋”์— ๋‘์‹œ๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ๊น”๋”ํ•ฉ๋‹ˆ๋‹ค. ์•„๊นŒ ์ €ํฌ๊ฐ€ ์Šคํฌ๋ฆฝํŠธ๋กœ ๋งŒ๋“  ๊ตฌ์กฐ๋ฅผ ๋ณด๋ฉด ์ตœ์ƒ์œ„์— data ํด๋”๊ฐ€ ์ด๋ฏธ ์ƒ์„ฑ๋˜์–ด ์žˆ์„ ๊ฒ๋‹ˆ๋‹ค. ํ”„๋กœ๊ทธ๋žจ์—์„œ ๊ฒฝ๋กœ๋ฅผ ์ผ๊ด€๋˜๊ฒŒ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ฐฐ์น˜ํ•˜๊ณ  ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ“‚ ๊ถŒ์žฅ ํŒŒ์ผ ๋ฐฐ์น˜ Plaintext OpcPksPlatform/ โ”œโ”€โ”€ data/ โ”‚ โ””โ”€โ”€ Honeywell_FullMap.csv <-- ๐Ÿšฉ ์—ฌ๊ธฐ์— ๋ณต์‚ฌ โ”œโ”€โ”€ OpcPks.Core/ โ”œโ”€โ”€ OpcPks.Collector/ โ””โ”€โ”€ OpcPks.Web/ ๐Ÿ’ป ์ฝ”๋“œ์—์„œ ์ฐธ์กฐํ•˜๋Š” ๋ฐฉ๋ฒ• (Program.cs) ์‹คํ–‰ ์‹œ์ ์˜ ์ž‘์—… ๋””๋ ‰ํ† ๋ฆฌ์— ๋”ฐ๋ผ ๊ฒฝ๋กœ๊ฐ€ ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ์ ˆ๋Œ€ ๊ฒฝ๋กœ๋ฅผ ๊ณ„์‚ฐํ•˜๊ฑฐ๋‚˜ ์†”๋ฃจ์…˜ ๋ฃจํŠธ ๊ธฐ์ค€ ์ƒ๋Œ€ ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. OpcPks.Collector/Program.cs์—์„œ ํ˜ธ์ถœํ•  ๋•Œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ•˜๋ฉด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค: C# // Program.cs ๋‚ด๋ถ€ static async Task Main(string[] args) { // ์†”๋ฃจ์…˜ ๋ฃจํŠธ์˜ data ํด๋” ๊ฒฝ๋กœ ์ฐพ๊ธฐ string csvPath = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "data", "Honeywell_FullMap.csv"); // ๋งŒ์•ฝ Ubuntu์—์„œ ์‹คํ–‰ ํŒŒ์ผ์„ ์ง์ ‘ ์‹คํ–‰ํ•œ๋‹ค๋ฉด ๊ฒฝ๋กœ๋ฅผ ์ง์ ‘ ์ง€์ •ํ•ด์ฃผ๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. // string csvPath = "/home/pacer/projects/OpcPksPlatform/data/Honeywell_FullMap.csv"; if (File.Exists(csvPath)) { var loader = new AssetLoader(); await loader.ImportFullMapAsync(csvPath); } else { Console.WriteLine($"โŒ ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {Path.GetFullPath(csvPath)}"); } } โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ (Ubuntu ํ™˜๊ฒฝ) Windows์—์„œ ์ž‘์„ฑํ•œ ํŒŒ์ผ์„ SSH๋ฅผ ํ†ตํ•ด Ubuntu๋กœ ์˜ฎ๊ธธ ๋•Œ, ๋Œ€์†Œ๋ฌธ์ž๋ฅผ ๊ตฌ๋ถ„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํŒŒ์ผ๋ช…์ด Honeywell_FullMap.csv๋ผ๋ฉด ์ฝ”๋“œ์—์„œ๋„ ์ •ํ™•ํžˆ ๋Œ€์†Œ๋ฌธ์ž๋ฅผ ๋งž์ถฐ์ฃผ์„ธ์š”. (Linux๋Š” honeywell_fullmap.csv์™€ ๋‹ค๋ฅด๊ฒŒ ์ธ์‹ํ•ฉ๋‹ˆ๋‹ค.) ๐Ÿ’ก ๋‹ค์Œ ํŒ ํŒŒ์ผ์„ data ํด๋”์— ๋„ฃ์œผ์…จ๋‹ค๋ฉด, ์ด์ œ OpcPks.Core ํ”„๋กœ์ ํŠธ์— ์ œ๊ฐ€ ๋“œ๋ ธ๋˜ AssetLoader.cs ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  ๋นŒ๋“œํ•ด ๋ณด์„ธ์š”. ๊ทธ๋‹ค์Œ Collector๋ฅผ ์‹คํ–‰ํ•ด์„œ 52๋งŒ ๊ฐœ ๋ฐ์ดํ„ฐ๊ฐ€ DB์˜ raw_node_map ํ…Œ์ด๋ธ”์— ์ž˜ ๋“ค์–ด๊ฐ€๋Š”์ง€ ํ™•์ธํ•ด ๋ณผ ์ฐจ๋ก€์ž…๋‹ˆ๋‹ค. DB ํ…Œ์ด๋ธ”์€ ์ƒ์„ฑํ•˜์…จ๋‚˜์š”? ํ˜น์€ ์ œ๊ฐ€ ํ…Œ์ด๋ธ” ์ƒ์„ฑ์šฉ SQL์„ ๋‹ค์‹œ ์ •๋ฆฌํ•ด ๋“œ๋ฆด๊นŒ์š”? ๋ง์”€ํ•˜์‹  ๋‚ด์šฉ ์ € ๋ณต์žกํ•œ ๊ตฌ์กฐ์˜ db ํ…Œ์ด๋ธ”์„ ์–ด๋–ป๊ฒŒ ๋งŒ๋“ค์ง€? Gemini์˜ ์‘๋‹ต 52๋งŒ ๊ฐœ์˜ ๋ฐฉ๋Œ€ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๋ ค๋ฉด, ๋‹จ์ˆœํžˆ ํ…Œ์ด๋ธ”์„ ๋งŒ๋“œ๋Š” ๊ฒƒ์„ ๋„˜์–ด **๊ฒ€์ƒ‰ ์†๋„(Index)**์™€ **๋ฐ์ดํ„ฐ ์„ฑ๊ฒฉ(์ •์ /๋™์ )**์— ๋”ฐ๋ฅธ ๋ถ„๋ฆฌ๊ฐ€ ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค. PostgreSQL์— ์ ‘์†ํ•˜์—ฌ ์•„๋ž˜ SQL ์ฟผ๋ฆฌ๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ์‹คํ–‰ํ•ด ์ฃผ์„ธ์š”. ์ด ๊ตฌ์กฐ๋Š” ์„ ์ƒ๋‹˜์˜ ๊ฐœ๋ฐœ ๋ชฉํ‘œ์ธ "์—”์ง€๋‹ˆ์–ด๋ง ํ•„ํ„ฐ๋ง"๊ณผ "์‹ค์‹œ๊ฐ„ ์ˆ˜์ง‘"์— ์ตœ์ ํ™”๋œ ์„ค๊ณ„์ž…๋‹ˆ๋‹ค. 1. DB ์Šคํ‚ค๋งˆ ์ƒ์„ฑ SQL SQL -- 1. ์ „์ฒด ๋…ธ๋“œ ์ง€๋„ (32MB CSV ๋ฐ์ดํ„ฐ ์›๋ณธ ์ €์žฅ์šฉ) -- ์›น ํ™”๋ฉด์—์„œ ๊ฒ€์ƒ‰(Like %pv%)์„ ๋น ๋ฅด๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. CREATE TABLE raw_node_map ( level INTEGER, class TEXT, name TEXT, node_id TEXT PRIMARY KEY -- ns=1;s=shinam:p-6102.pv ํ˜•ํƒœ ); CREATE INDEX idx_raw_name ON raw_node_map(name); -- ๊ฒ€์ƒ‰ ์†๋„ ํ–ฅ์ƒ -- 2. ํƒœ๊ทธ ๋งˆ์Šคํ„ฐ (์›น์—์„œ ์ตœ์ข… ์„ ํƒ๋œ '์ง„์งœ' ์ˆ˜์ง‘ ๋Œ€์ƒ) -- ์—ฌ๊ธฐ์— ๋“ฑ๋ก๋œ ํƒœ๊ทธ๋งŒ Collector๊ฐ€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ˆ˜์ง‘ํ•ฉ๋‹ˆ๋‹ค. CREATE TABLE tag_master ( tag_id SERIAL PRIMARY KEY, tag_name TEXT NOT NULL, -- p-6102 full_node_id TEXT UNIQUE NOT NULL, -- ns=1;s=shinam:p-6102.pv attribute TEXT, -- pv, sp, op, hzset ๋“ฑ description TEXT, unit TEXT, is_active BOOLEAN DEFAULT TRUE, -- ์ˆ˜์ง‘ ์ค‘๋‹จ/์žฌ๊ฐœ ์ œ์–ด scan_rate INTEGER DEFAULT 1000 -- ms ๋‹จ์œ„ ์ˆ˜์ง‘ ์ฃผ๊ธฐ ); -- 3. ์‹ค์‹œ๊ฐ„ ์ด๋ ฅ ๋ฐ์ดํ„ฐ (์‹œ๊ณ„์—ด ๋ฐ์ดํ„ฐ) -- ๋ฐ์ดํ„ฐ๊ฐ€ ์ˆ˜๋ฐฑ๋งŒ ๊ฑด ์Œ“์ผ ๊ฒƒ์ด๋ฏ€๋กœ ์ธ๋ฑ์Šค๊ฐ€ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. CREATE TABLE opc_history ( id BIGSERIAL PRIMARY KEY, time_stamp TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, full_node_id TEXT NOT NULL, value DOUBLE PRECISION, status TEXT -- Good, Bad ๋“ฑ ); -- ๊ฒ€์ƒ‰ ์„ฑ๋Šฅ์„ ์œ„ํ•œ ์ธ๋ฑ์Šค (ํƒœ๊ทธ๋ณ„/์‹œ๊ฐ„๋ณ„ ๊ฒ€์ƒ‰ ์ตœ์ ํ™”) CREATE INDEX idx_history_node_time ON opc_history(full_node_id, time_stamp DESC); 2. ์ด ๊ตฌ์กฐ๊ฐ€ "๋ณต์žกํ•จ"์„ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ์‹ raw_node_map (์ž„์‹œ ๋Œ€๊ธฐ์‹ค): 52๋งŒ ๊ฐœ ๋ฐ์ดํ„ฐ๋ฅผ tag_master์— ๋ฐ”๋กœ ๋„ฃ์œผ๋ฉด ๊ด€๋ฆฌ๊ฐ€ ์•ˆ ๋ฉ๋‹ˆ๋‹ค. ๋จผ์ € ์—ฌ๊ธฐ์— ๋‹ค ๋„ฃ๊ณ , ์›น์—์„œ "๊ฒ€์ƒ‰"๋งŒ ํ•ฉ๋‹ˆ๋‹ค. ์„ ์ƒ๋‹˜์˜ CSV ๊ตฌ์กฐ(Level, Class, Name, NodeId)๋ฅผ ๊ทธ๋Œ€๋กœ ์ˆ˜์šฉํ•ฉ๋‹ˆ๋‹ค. tag_master (์ž‘์—… ์ง€์‹œ์„œ): ์ˆ˜์ง‘๊ธฐ(Collector)๋Š” ์ด ํ…Œ์ด๋ธ”๋งŒ ๋ฐ”๋ผ๋ด…๋‹ˆ๋‹ค. 52๋งŒ ๊ฐœ ์ค‘ ์‹ค์ œ ์“ธ ํƒœ๊ทธ๊ฐ€ 5,000๊ฐœ๋ผ๋ฉด, ์ˆ˜์ง‘๊ธฐ๋Š” 5,000๊ฐœ์— ๋Œ€ํ•ด์„œ๋งŒ OPC UA ์„ธ์…˜์„ ์œ ์ง€ํ•˜๋ฏ€๋กœ ์„ฑ๋Šฅ์ด ๋น„์•ฝ์ ์œผ๋กœ ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค. opc_history (์ €์žฅ์†Œ): tag_master์™€ ์—ฐ๊ฒฐ๋˜์–ด ์‹ค์ œ ๊ณต์ • ๊ฐ’์ด ์ €์žฅ๋˜๋Š” ๊ณณ์ž…๋‹ˆ๋‹ค. ๋‚˜์ค‘์— ์ฐจํŠธ๋ฅผ ๊ทธ๋ฆด ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ’ก ์‹คํ–‰ ํŒ DBeaver ๊ฐ™์€ ํˆด์„ ์‚ฌ์šฉํ•˜์‹ ๋‹ค๋ฉด ์œ„ ์ฟผ๋ฆฌ๋ฅผ ๋ณต์‚ฌํ•ด์„œ ํ•œ ๋ฒˆ์— ์‹คํ–‰(Alt+X)ํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค. Ubuntu ํ„ฐ๋ฏธ๋„์—์„œ ์ง์ ‘ ํ•˜์‹ ๋‹ค๋ฉด ์•„๋ž˜ ๋ช…๋ น์–ด๋กœ ์ ‘์† ํ›„ ์ฟผ๋ฆฌ๋ฅผ ๋ถ™์—ฌ๋„ฃ์œผ์„ธ์š”. Bash psql -U pacer -d opcdb ๋ฐ์ดํ„ฐ ์ž…๋ ฅ: ํ…Œ์ด๋ธ”์ด ๋งŒ๋“ค์–ด์ง€๋ฉด, ์•„๊นŒ ์ž‘์„ฑํ•œ AssetLoader.cs ์ฝ”๋“œ๊ฐ€ ์ด raw_node_map ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ€์–ด๋„ฃ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ํ…Œ์ด๋ธ” ์ƒ์„ฑ์ด ์™„๋ฃŒ๋˜๋ฉด, ์ด์ œ OpcPks.Core/Data/DbConfig.cs์— DB ์ •๋ณด๋ฅผ ๋„ฃ๊ณ  ์‹ค์ œ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ€์–ด๋„ฃ๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•ด ๋ณผ๊นŒ์š”? ๋ง์”€ํ•˜์‹  ๋‚ด์šฉ ์ง€๊ธˆ postgresql์— Brin ๊ทธ๋ฆฌ๊ณ  TimescaleDB๋ฅผ ์„ค์น˜ํ•œ๊ฑธ๋กœ ๊ธฐ์–ตํ•˜๋Š”๋ฐ ํ™•์ธํ•˜๋Š” ๋ช…๋ น Gemini์˜ ์‘๋‹ต ๋„ค, ๋งž์Šต๋‹ˆ๋‹ค. **Mini PC(GMKTEC K8 PLUS)**์˜ ์„ฑ๋Šฅ๊ณผ 2TB NVMe๋ฅผ ์ œ๋Œ€๋กœ ํ™œ์šฉํ•˜๊ธฐ ์œ„ํ•ด TimescaleDB์™€ BRIN ์ธ๋ฑ์Šค๋ฅผ ๊ณ ๋ คํ•˜๊ณ  ๊ณ„์…จ์ฃ . Ubuntu ์„œ๋ฒ„(VMware)๋‚˜ Mini PC์˜ ํ„ฐ๋ฏธ๋„์—์„œ PostgreSQL์— ์ ‘์†ํ•˜์—ฌ ์„ค์น˜ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. 1. TimescaleDB ์„ค์น˜ ๋ฐ ํ™œ์„ฑํ™” ํ™•์ธ PostgreSQL ์ ‘์† ํ›„(psql -U pacer -d opcdb) ์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”. SQL -- 1. ํ™•์žฅ ๊ธฐ๋Šฅ(Extension) ๋ฆฌ์ŠคํŠธ ํ™•์ธ \dx ๋ชฉ๋ก์— timescaledb๊ฐ€ ๋ณด์ด๋ฉด ์„ค์น˜ ๋ฐ ํ•ด๋‹น DB์— ํ™œ์„ฑํ™”๋œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. SQL -- 2. ์„ค์น˜๋Š” ๋˜์—ˆ์œผ๋‚˜ ํ™œ์„ฑํ™” ์ „์ด๋ผ๋ฉด (๋ชฉ๋ก์— ์—†์„ ๋•Œ) SELECT * FROM pg_available_extensions WHERE name = 'timescaledb'; installed_version์ด ์ถœ๋ ฅ๋œ๋‹ค๋ฉด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ์„ค์น˜๋œ ๊ฒƒ์ด๋‹ˆ CREATE EXTENSION timescaledb;๋กœ ํ™œ์„ฑํ™”ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. 2. BRIN ์ธ๋ฑ์Šค ์ง€์› ํ™•์ธ BRIN(Block Range Indexes)์€ PostgreSQL 9.5๋ถ€ํ„ฐ ๊ธฐ๋ณธ ๋‚ด์žฅ๋œ ๊ธฐ๋Šฅ์ด๋ฏ€๋กœ ๋ณ„๋„ ์„ค์น˜๊ฐ€ ํ•„์š” ์—†์ง€๋งŒ, ํ•˜์ดํผํ…Œ์ด๋ธ”(TimescaleDB)์—์„œ ์ž˜ ์ž‘๋™ํ•˜๋Š”์ง€ ๊ตฌ์กฐ๋ฅผ ํ™•์ธํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. SQL -- 3. BRIN ์ธ๋ฑ์Šค ์ƒ์„ฑ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ (ํ…Œ์ŠคํŠธ์šฉ) -- opc_history ํ…Œ์ด๋ธ”์ด ์žˆ๋‹ค๋ฉด ์•„๋ž˜ ๋ช…๋ น์œผ๋กœ ์ƒ์„ฑ์ด ๋˜๋Š”์ง€ ํ™•์ธํ•ด๋ณด์„ธ์š”. CREATE INDEX idx_brin_test ON opc_history USING brin (time_stamp); ์—๋Ÿฌ ์—†์ด ์ƒ์„ฑ๋œ๋‹ค๋ฉด BRIN ๊ธฐ๋Šฅ์„ ์ฆ‰์‹œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. 3. ๋ฆฌ๋ˆ…์Šค ํŒจํ‚ค์ง€ ๋ ˆ๋ฒจ ํ™•์ธ (์‰˜ ๋ช…๋ น์–ด) PostgreSQL์— ์ ‘์†ํ•˜์ง€ ์•Š๊ณ  ํ„ฐ๋ฏธ๋„์—์„œ ๋ฐ”๋กœ ํ™•์ธํ•˜๋ ค๋ฉด ์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”. Bash # TimescaleDB ํŒจํ‚ค์ง€ ์„ค์น˜ ํ™•์ธ dpkg -l | grep timescaledb # PostgreSQL ์„ค์ • ํŒŒ์ผ์—์„œ timescaledb๊ฐ€ ๋กœ๋“œ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ grep "shared_preload_libraries" /etc/postgresql/*/main/postgresql.conf shared_preload_libraries = 'timescaledb' ๋ฌธ๊ตฌ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์–ด์•ผ ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ’ก ํŒ: 52๋งŒ ๊ฐœ ๋…ธ๋“œ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ ๊ถŒ์žฅ ์‚ฌํ•ญ raw_node_map์€ ์ผ๋ฐ˜ ํ…Œ์ด๋ธ”๋กœ ๊ด€๋ฆฌํ•˜์‹œ๊ณ , ์‹ค์ œ ๋ฐ์ดํ„ฐ๊ฐ€ ์Œ“์ผ opc_history ํ…Œ์ด๋ธ”๋งŒ TimescaleDB์˜ Hypertable๋กœ ์ „ํ™˜ํ•˜๋Š” ๊ฒƒ์ด ์„ฑ๋Šฅ์ƒ ์œ ๋ฆฌํ•ฉ๋‹ˆ๋‹ค. SQL -- opc_history๋ฅผ ํ•˜์ดํผํ…Œ์ด๋ธ”๋กœ ๋ณ€ํ™˜ (์ด๋ฏธ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์„ ๋•Œ ์‹คํ–‰ ๊ถŒ์žฅ) SELECT create_hypertable('opc_history', 'time_stamp'); ํ™•์ธ ๊ฒฐ๊ณผ๊ฐ€ ์–ด๋–ป๊ฒŒ ๋‚˜์˜ค๋‚˜์š”? ๋งŒ์•ฝ timescaledb๊ฐ€ ๋ชฉ๋ก์— ์—†๋‹ค๋ฉด ์„ค์น˜ ๋ช…๋ น์–ด๋ฅผ ๋ฐ”๋กœ ์•Œ๋ ค๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค. ์ƒˆ๋กœ์šด ํด๋”์—์„œ ํ•ด์„œ ๊ทธ๋Ÿฐ๊ฐ€ ์—๋Ÿฌ๋‚˜๋„ค acer@dbsvr:~/projects/OpcPksPlatform/OpcPks.Collector$ ls bin obj OpcPks.Collector.csproj Program.cs pacer@dbsvr:~/projects/OpcPksPlatform/OpcPks.Collector$ psql -U pacer -d opcdb psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL: role "pacer" does not exist pacer@dbsvr:~/projects/OpcPksPlatform/OpcPks.Collector$ psql -U postgres -d opcdb psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL: Peer authentication failed for user "postgres" pacer@dbsvr:~/projects/OpcPksPlatform/OpcPks.Collector$ Gemini์˜ ์‘๋‹ต ์ƒˆ๋กœ์šด ํ™˜๊ฒฝ(Ubuntu 24.04 ์„œ๋ฒ„)์—์„œ PostgreSQL ์ ‘์† ์‹œ ๋ฐœ์ƒํ•˜๋Š” ์ „ํ˜•์ ์ธ ๊ถŒํ•œ(Authentication) ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค. FATAL: role "pacer" does not exist๋Š” DB ๋‚ด๋ถ€์— pacer๋ผ๋Š” ์ด๋ฆ„์˜ ์‚ฌ์šฉ์ž๊ฐ€ ์•„์ง ๋งŒ๋“ค์–ด์ง€์ง€ ์•Š์•˜๋‹ค๋Š” ๋œป์ด๊ณ , Peer authentication failed for user "postgres"๋Š” ๋ฆฌ๋ˆ…์Šค ๊ณ„์ •๊ณผ DB ๊ณ„์ • ๋ถˆ์ผ์น˜๋กœ ์ธํ•œ ๋ณด์•ˆ ๊ฑฐ๋ถ€์ž…๋‹ˆ๋‹ค. ์•„๋ž˜ ๋‹จ๊ณ„์— ๋”ฐ๋ผ DB ํ™˜๊ฒฝ์„ ๋จผ์ € ์ •๋น„ํ•ฉ์‹œ๋‹ค. 1. PostgreSQL ์Šˆํผ์œ ์ €๋กœ ๊ฐ•์ œ ์ ‘์† ๋ฆฌ๋ˆ…์Šค์˜ root ๊ถŒํ•œ(sudo)์„ ๋นŒ๋ ค postgres ๊ธฐ๋ณธ ๊ณ„์ •์œผ๋กœ ๋จผ์ € ์ ‘์†ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Bash sudo -u postgres psql 2. 'pacer' ์‚ฌ์šฉ์ž ์ƒ์„ฑ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ ์ ‘์†์— ์„ฑ๊ณตํ–ˆ๋‹ค๋ฉด(ํ”„๋กฌํ”„ํŠธ๊ฐ€ postgres=#๋กœ ๋ณ€ํ•จ), ์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ์ž…๋ ฅํ•˜์„ธ์š”. SQL -- 1. pacer ์‚ฌ์šฉ์ž ์ƒ์„ฑ (๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์›ํ•˜์‹œ๋Š” ๊ฒƒ์œผ๋กœ ์ˆ˜์ •) CREATE USER pacer WITH PASSWORD 'your_password_here' SUPERUSER; -- 2. opcdb ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ์—†๋‹ค๋ฉด ์ƒ์„ฑ (์žˆ๋‹ค๋ฉด ์Šคํ‚ต) CREATE DATABASE opcdb OWNER pacer; -- 3. ๋‚˜๊ฐ€๊ธฐ \q 3. ์ธ์ฆ ๋ฐฉ์‹ ๋ณ€๊ฒฝ (Peer -> MD5/Scram) Ubuntu ๊ธฐ๋ณธ ์„ค์ •์€ ๋ฆฌ๋ˆ…์Šค ๋กœ๊ทธ์ธ ๊ณ„์ •๋ช…๊ณผ DB ๊ณ„์ •๋ช…์ด ๊ฐ™์•„์•ผ๋งŒ ์ ‘์†์„ ํ—ˆ์šฉ(Peer)ํ•ฉ๋‹ˆ๋‹ค. ์™ธ๋ถ€ ํ”„๋กœ๊ทธ๋žจ(C# ๋“ฑ)์—์„œ ์ ‘์†ํ•˜๋ ค๋ฉด ์ด ์„ค์ •์„ ๋ฐ”๊ฟ”์•ผ ํ•ฉ๋‹ˆ๋‹ค. Bash # ์„ค์ • ํŒŒ์ผ ์—ด๊ธฐ (๋ฒ„์ „ ๋ฒˆํ˜ธ๋Š” 16 ํ˜น์€ 17์ผ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค. ํƒญ ํ‚ค๋กœ ํ™•์ธํ•˜์„ธ์š”) sudo nano /etc/postgresql/17/main/pg_hba.conf ํŒŒ์ผ ๋งจ ์•„๋ž˜๋กœ ๋‚ด๋ ค๊ฐ€์„œ ์•„๋ž˜ ๋ถ€๋ถ„์„ ์ฐพ์•„ peer๋ฅผ scram-sha-256 (๋˜๋Š” md5)๋กœ ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค. Plaintext # ๋ณ€๊ฒฝ ์ „ local all all peer # ๋ณ€๊ฒฝ ํ›„ local all all scram-sha-256 (์ˆ˜์ • ํ›„ Ctrl+O, Enter, Ctrl+X๋กœ ์ €์žฅ ๋ฐ ์ข…๋ฃŒ) 4. ์„ค์ • ๋ฐ˜์˜ ๋ฐ ํ™•์ธ ์„ค์ •์„ ๋ฐ”๊ฟจ์œผ๋‹ˆ ์„œ๋น„์Šค๋ฅผ ์žฌ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. Bash sudo systemctl restart postgresql # ์ด์ œ pacer ๊ณ„์ •์œผ๋กœ ์ ‘์† ์‹œ๋„ (๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ์ฐฝ์ด ๋œน๋‹ˆ๋‹ค) psql -U pacer -d opcdb 5. TimescaleDB ๋ฐ ํ…Œ์ด๋ธ” ์ƒ์„ฑ ํ™•์ธ ์ •์ƒ์ ์œผ๋กœ ์ ‘์†๋˜์—ˆ๋‹ค๋ฉด, ์•„๊นŒ ๋ง์”€๋“œ๋ฆฐ TimescaleDB ํ™•์ธ ๋ฐ ํ…Œ์ด๋ธ” ์ƒ์„ฑ์„ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค. SQL -- TimescaleDB ํ™œ์„ฑํ™” CREATE EXTENSION IF NOT EXISTS timescaledb; -- ์•„๊นŒ ๋“œ๋ฆฐ ํ…Œ์ด๋ธ” ์ƒ์„ฑ SQL ์‹คํ–‰ (raw_node_map, tag_master, opc_history) -- ... (์ด์ „ ๋‹ต๋ณ€์˜ SQL ์ฟผ๋ฆฌ ๋ณต์‚ฌ ๋ถ™์—ฌ๋„ฃ๊ธฐ) ... -- opc_history๋ฅผ ํ•˜์ดํผํ…Œ์ด๋ธ”๋กœ ์ „ํ™˜ SELECT create_hypertable('opc_history', 'time_stamp'); ๐Ÿ’ก ํŒ ์ด์ œ DB ์ ‘์†์ด ๊ฐ€๋Šฅํ•ด์กŒ์œผ๋‹ˆ, C# ์ฝ”๋“œ์˜ DbConfig.cs ํŒŒ์ผ์— ๋“ค์–ด๊ฐˆ ConnectionString์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค์ •ํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค: "Host=localhost;Username=pacer;Password=์„ค์ •ํ•œ๋น„๋ฒˆ;Database=opcdb" DB ์ ‘์†์— ์„ฑ๊ณตํ•˜์‹  ํ›„ \dt ๋ช…๋ น์–ด๋กœ ํ…Œ์ด๋ธ” ๋ชฉ๋ก์ด ์ž˜ ๋ณด์ด๋Š”์ง€ ์•Œ๋ ค์ฃผ์„ธ์š”! ๊ทธ ๋‹ค์Œ ๋ฐ”๋กœ CSV ๋กœ๋“œ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ๋„ค, ํ•ด๋‹น SQL ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด pacer ์‚ฌ์šฉ์ž์—๊ฒŒ ์Šˆํผ์œ ์ € ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•˜๊ณ  opcdb๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ณผ์ •์ด DB ์„ค์ •์˜ ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค. ์œ„ ๋ช…๋ น์–ด๋“ค์„ sudo -u postgres psql๋กœ ์ ‘์†ํ•œ ์ƒํƒœ์—์„œ ์‹คํ–‰ํ•˜์…จ๋‹ค๋ฉด, ์ด์ œ ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋„˜์–ด๊ฐ€์„œ TimescaleDB ํ™•์žฅ ์„ค์น˜์™€ ํ…Œ์ด๋ธ” ์ƒ์„ฑ์„ ๋งˆ๋ฌด๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์•„๋ž˜ ๊ณผ์ •์„ ์ˆœ์„œ๋Œ€๋กœ ์ง„ํ–‰ํ•ด ์ฃผ์„ธ์š”. 1. pacer ๊ณ„์ •์œผ๋กœ opcdb ์ ‘์† ์ด์ œ postgres ๊ณ„์ •์ด ์•„๋‹Œ, ๋ฐฉ๊ธˆ ๋งŒ๋“  pacer ๊ณ„์ •์œผ๋กœ DB์— ์ง์ ‘ ์ ‘์†ํ•ฉ๋‹ˆ๋‹ค. Bash psql -U pacer -d opcdb 2. TimescaleDB ํ™œ์„ฑํ™” ๋ฐ ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์ ‘์† ํ›„ ์•„๋ž˜ ์ฟผ๋ฆฌ๋ฅผ ํ•œ ๋ฒˆ์— ๋ณต์‚ฌํ•ด์„œ ๋ถ™์—ฌ๋„ฃ์œผ์„ธ์š”. (TimescaleDB์™€ 52๋งŒ ๊ฐœ ๋…ธ๋“œ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ ์Šคํ‚ค๋งˆ๊ฐ€ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.) SQL -- TimescaleDB ํ™•์žฅ ํ™œ์„ฑํ™” CREATE EXTENSION IF NOT EXISTS timescaledb; -- 1. ์ „์ฒด ๋…ธ๋“œ ์ง€๋„ (32MB CSV ์›๋ณธ ๋กœ๋“œ์šฉ) CREATE TABLE IF NOT EXISTS raw_node_map ( level INTEGER, class TEXT, name TEXT, node_id TEXT PRIMARY KEY ); CREATE INDEX IF NOT EXISTS idx_raw_name ON raw_node_map(name); -- 2. ํƒœ๊ทธ ๋งˆ์Šคํ„ฐ (์ˆ˜์ง‘ ๋Œ€์ƒ ํƒœ๊ทธ) CREATE TABLE IF NOT EXISTS tag_master ( tag_id SERIAL PRIMARY KEY, tag_name TEXT NOT NULL, full_node_id TEXT UNIQUE NOT NULL, attribute TEXT, is_active BOOLEAN DEFAULT TRUE, scan_rate INTEGER DEFAULT 1000 ); -- 3. ์‹ค์‹œ๊ฐ„ ์ด๋ ฅ ๋ฐ์ดํ„ฐ (TimescaleDB ํ•˜์ดํผํ…Œ์ด๋ธ”) CREATE TABLE IF NOT EXISTS opc_history ( time_stamp TIMESTAMP WITH TIME ZONE NOT NULL, full_node_id TEXT NOT NULL, value DOUBLE PRECISION, status TEXT ); -- ํ•˜์ดํผํ…Œ์ด๋ธ”๋กœ ๋ณ€ํ™˜ (์ด๋ฏธ ์ƒ์„ฑ๋˜์–ด ์žˆ์œผ๋ฉด ์Šคํ‚ต๋จ) SELECT create_hypertable('opc_history', 'time_stamp', if_not_exists => TRUE); -- BRIN ์ธ๋ฑ์Šค ์ƒ์„ฑ (๋Œ€์šฉ๋Ÿ‰ ์‹œ๊ณ„์—ด ๊ฒ€์ƒ‰ ์ตœ์ ํ™”) CREATE INDEX IF NOT EXISTS idx_history_brin ON opc_history USING brin (time_stamp); 3. ํ™•์ธ ์ž‘์—… ์œ„ ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•œ ํ›„, ํ…Œ์ด๋ธ”์ด ์ž˜ ๋งŒ๋“ค์–ด์กŒ๋Š”์ง€ ํ™•์ธํ•˜๋ ค๋ฉด ๋‹ค์Œ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค. SQL \dt raw_node_map, tag_master, opc_history ์„ธ ๊ฐœ๊ฐ€ ๋ชฉ๋ก์— ๋ณด์ด๋ฉด ์„ฑ๊ณต์ž…๋‹ˆ๋‹ค. ๐Ÿš€ ์ด์ œ ๋ฌด์—‡์„ ํ• ๊นŒ์š”? DB ์ค€๋น„๊ฐ€ ๋๋‚ฌ์œผ๋‹ˆ, ์ด์ œ C# ํ”„๋กœ๊ทธ๋žจ์—์„œ 32MB CSV ๋ฐ์ดํ„ฐ๋ฅผ ์‹ค์ œ๋กœ ๋ฐ€์–ด๋„ฃ๋Š” ์ž‘์—…์„ ํ•ด๋ณผ ์ฐจ๋ก€์ž…๋‹ˆ๋‹ค. OpcPks.Core/Data/DbConfig.cs ํŒŒ์ผ์— ์„ค์ •ํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ ์œผ์…จ๋‚˜์š”? OpcPks.Collector ํ”„๋กœ์ ํŠธ ํด๋”๋กœ ์ด๋™ํ•˜์—ฌ dotnet run์„ ์‹คํ–‰ํ•ด ๋ณด์„ธ์š”. ์‹คํ–‰ ์‹œ ๐Ÿš€ CSV ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹œ์ž‘... ๋ฉ”์‹œ์ง€์™€ ํ•จ๊ป˜ โœ… ์ด 523xxx๊ฐœ ๋…ธ๋“œ ๋กœ๋“œ ์™„๋ฃŒ! ๋ฌธ๊ตฌ๊ฐ€ ๋œจ๋Š”์ง€ ํ™•์ธํ•ด ์ฃผ์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ํŒŒ์ผ ๊ฒฝ๋กœ ์—๋Ÿฌ๊ฐ€ ๋‚œ๋‹ค๋ฉด ๋ฐ”๋กœ ๋ง์”€ํ•ด ์ฃผ์„ธ์š”! opcdb=# -- TimescaleDB ํ™•์žฅ ํ™œ์„ฑํ™” CREATE EXTENSION IF NOT EXISTS timescaledb; -- 1. ์ „์ฒด ๋…ธ๋“œ ์ง€๋„ (32MB CSV ์›๋ณธ ๋กœ๋“œ์šฉ) CREATE TABLE IF NOT EXISTS raw_node_map ( level INTEGER, class TEXT, name TEXT, node_id TEXT PRIMARY KEY ); CREATE INDEX IF NOT EXISTS idx_raw_name ON raw_node_map(name); -- 2. ํƒœ๊ทธ ๋งˆ์Šคํ„ฐ (์ˆ˜์ง‘ ๋Œ€์ƒ ํƒœ๊ทธ) CREATE INDEX IF NOT EXISTS idx_history_brin ON opc_history USING brin (time_stamp); CREATE EXTENSION CREATE TABLE CREATE INDEX CREATE TABLE NOTICE: relation "opc_history" already exists, skipping CREATE TABLE ERROR: table "opc_history" is not empty HINT: You can migrate data by specifying 'migrate_data => true' when calling this function. ERROR: column "time_stamp" does not exist opcdb=# Gemini์˜ ์‘๋‹ต ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‹ˆ opc_history ํ…Œ์ด๋ธ”์ด ์ด๋ฏธ ์ƒ์„ฑ๋˜์–ด ์žˆ๊ณ , ๊ทธ ์•ˆ์— ๋ฐ์ดํ„ฐ๊ฐ€ ์ผ๋ถ€ ๋“ค์–ด์žˆ์–ด์„œ ํ•˜์ดํผํ…Œ์ด๋ธ”(Hypertable) ๋ณ€ํ™˜๊ณผ BRIN ์ธ๋ฑ์Šค ์ƒ์„ฑ์—์„œ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ–ˆ๋„ค์š”. ERROR: table "opc_history" is not empty๋Š” ํ…Œ์ด๋ธ”์„ ๋น„์›Œ์•ผ ๋ณ€ํ™˜์ด ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ๋œป์ด๊ณ , ERROR: column "time_stamp" does not exist๋Š” ์ด์ „ ๋‹ต๋ณ€์—์„œ ์ œ๊ฐ€ ๋“œ๋ฆฐ ์ฟผ๋ฆฌ์™€ ํ˜„์žฌ ์ƒ์„ฑ๋œ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ๋ช…์ด ๋งž์ง€ ์•Š์•„์„œ ๋ฐœ์ƒํ•œ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์‹œ๊ณ„์—ด ๋ฐ์ดํ„ฐ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•ด ํ…Œ์ด๋ธ”์„ ๊น”๋”ํ•˜๊ฒŒ ๋ฐ€๊ณ  ๋‹ค์‹œ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ๋น ๋ฅด๊ณ  ํ™•์‹คํ•ฉ๋‹ˆ๋‹ค. ์•„๋ž˜ ์ฟผ๋ฆฌ๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ๋‹ค์‹œ ์‹คํ–‰ํ•ด ์ฃผ์„ธ์š”. 1. ๊ธฐ์กด ํ…Œ์ด๋ธ” ์‚ญ์ œ ๋ฐ ์žฌ์ƒ์„ฑ (Clean Install) SQL -- ๊ธฐ์กด ํ…Œ์ด๋ธ” ์‚ญ์ œ (์ˆœ์„œ ์ฃผ์˜) DROP TABLE IF EXISTS opc_history; DROP TABLE IF EXISTS tag_master; DROP TABLE IF EXISTS raw_node_map; -- 1. ์ „์ฒด ๋…ธ๋“œ ์ง€๋„ (32MB CSV ์›๋ณธ ๋กœ๋“œ์šฉ) CREATE TABLE raw_node_map ( level INTEGER, class TEXT, name TEXT, node_id TEXT PRIMARY KEY ); CREATE INDEX idx_raw_name ON raw_node_map(name); -- 2. ํƒœ๊ทธ ๋งˆ์Šคํ„ฐ (์ˆ˜์ง‘ ๋Œ€์ƒ ์„ค์ •์šฉ) CREATE TABLE tag_master ( tag_id SERIAL PRIMARY KEY, tag_name TEXT NOT NULL, full_node_id TEXT UNIQUE NOT NULL, attribute TEXT, is_active BOOLEAN DEFAULT TRUE, scan_rate INTEGER DEFAULT 1000 ); -- 3. ์‹ค์‹œ๊ฐ„ ์ด๋ ฅ ๋ฐ์ดํ„ฐ (TimescaleDB ํ•˜์ดํผํ…Œ์ด๋ธ”์šฉ) -- ๋ฐ˜๋“œ์‹œ time_stamp ์ปฌ๋Ÿผ์ด ์กด์žฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. CREATE TABLE opc_history ( time_stamp TIMESTAMP WITH TIME ZONE NOT NULL, full_node_id TEXT NOT NULL, value DOUBLE PRECISION, status TEXT ); -- 4. ํ•˜์ดํผํ…Œ์ด๋ธ” ๋ณ€ํ™˜ -- ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋Š” ์ƒํƒœ์—์„œ ์‹คํ–‰ํ•ด์•ผ ์„ฑ๊ณตํ•ฉ๋‹ˆ๋‹ค. SELECT create_hypertable('opc_history', 'time_stamp'); -- 5. BRIN ์ธ๋ฑ์Šค ์ƒ์„ฑ (์‹œ๊ฐ„์ˆœ ๋ฐ์ดํ„ฐ ๊ฒ€์ƒ‰ ์ตœ์ ํ™”) CREATE INDEX idx_history_brin ON opc_history USING brin (time_stamp); 2. ๊ฒฐ๊ณผ ํ™•์ธ ๋ชจ๋‘ ์™„๋ฃŒ๋˜์—ˆ๋‹ค๋ฉด \dt ๋ช…๋ น์–ด๋กœ ํ…Œ์ด๋ธ” ๋ชฉ๋ก์„ ํ™•์ธํ•˜๊ณ , \d opc_history๋ฅผ ์ž…๋ ฅํ•˜์—ฌ ํ•˜์ดํผํ…Œ์ด๋ธ” ์„ค์ •์ด ์ž˜ ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ด ๋ณด์„ธ์š”. SQL opcdb=# \dt List of relations Schema | Name | Type | Owner --------+---------------+-------+------- public | opc_history | table | pacer public | raw_node_map | table | pacer public | tag_master | table | pacer (3 rows) 3. ๋‹ค์Œ ๋‹จ๊ณ„ DB ๊ทธ๋ฆ‡์ด ์ด์ œ ์™„๋ฒฝํ•˜๊ฒŒ ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ์ด์ œ **C# ๊ฐœ๋ฐœ ํ™˜๊ฒฝ(Windows 11)**์œผ๋กœ ๋Œ์•„๊ฐ€์„œ OpcPks.Core/Data/DbConfig.cs์— ์ ‘์† ์ •๋ณด๋ฅผ ๋„ฃ๊ณ , OpcPks.Collector์—์„œ 32MB CSV ๋กœ๋”ฉ ํ…Œ์ŠคํŠธ๋ฅผ ๋‹ค์‹œ ์ง„ํ–‰ํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค. ํ˜น์‹œ CSV ๋กœ๋”ฉ ์‹œ raw_node_map ์ปฌ๋Ÿผ ๊ฐœ์ˆ˜๋‚˜ ์ด๋ฆ„ ๋•Œ๋ฌธ์— ์—๋Ÿฌ๊ฐ€ ๋‚œ๋‹ค๋ฉด ๋ฐ”๋กœ ๋ง์”€ํ•ด ์ฃผ์„ธ์š”. (์—…๋กœ๋“œํ•˜์‹  CSV ํ—ค๋”์ธ Level,Class,Name,NodeId์™€ ์œ„ SQL ์Šคํ‚ค๋งˆ๋ฅผ ์ •ํ™•ํžˆ ์ผ์น˜์‹œ์ผœ ๋‘์—ˆ์Šต๋‹ˆ๋‹ค.) ์ด์ œ dotnet run ํ•ด๋ณด์‹œ๊ฒ ์–ด์š”? ์–ด๋А ํด๋”์—์„œ dotnet run? Gemini์˜ ์‘๋‹ต ์ˆ˜์ง‘๊ธฐ(Collector)๋ฅผ ์‹คํ–‰ํ•˜์—ฌ CSV ๋ฐ์ดํ„ฐ๋ฅผ DB์— ๋ฐ€์–ด ๋„ฃ์œผ๋ ค๋ฉด OpcPks.Collector ํด๋”์—์„œ ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์„ ์ƒ๋‹˜์˜ ํ„ฐ๋ฏธ๋„ ์œ„์น˜๊ฐ€ ~/projects/OpcPksPlatform๋ผ๋ฉด ์•„๋ž˜ ์ˆœ์„œ๋Œ€๋กœ ์ด๋™ํ•˜์—ฌ ์‹คํ–‰ํ•˜์„ธ์š”. ๐Ÿš€ ์‹คํ–‰ ์ˆœ์„œ ์ˆ˜์ง‘๊ธฐ ํด๋”๋กœ ์ด๋™ Bash cd ~/projects/OpcPksPlatform/OpcPks.Collector ๋ฐ์ดํ„ฐ ๋ฐ€์–ด๋„ฃ๊ธฐ ์‹คํ–‰ Bash dotnet run โš ๏ธ ์‹คํ–‰ ์ „ ์ฒดํฌ๋ฆฌ์ŠคํŠธ (๋งค์šฐ ์ค‘์š”) dotnet run์„ ํ•˜๊ธฐ ์ „์— ๋”ฑ ๋‘ ๊ฐ€์ง€๋งŒ ์ฝ”๋”ฉ์ด ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ด ์ฃผ์„ธ์š”. 1. OpcPks.Core/Data/DbConfig.cs PostgreSQL ์ ‘์† ์ •๋ณด๊ฐ€ ์„ ์ƒ๋‹˜์˜ ํ™˜๊ฒฝ์— ๋งž๊ฒŒ ์ˆ˜์ •๋˜์–ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (๋น„๋ฐ€๋ฒˆํ˜ธ ๋“ฑ) 2. OpcPks.Collector/Program.cs ๊ธฐ๋ณธ ์ƒ์„ฑ๋œ Program.cs ๋‚ด์šฉ์„ ์ œ๊ฐ€ ์ด์ „์— ๋“œ๋ฆฐ AssetLoader๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ์ฝ”๋“œ๋กœ ๋ฎ์–ด์“ฐ๊ธฐ ํ•˜์…จ๋‚˜์š”? ๋งŒ์•ฝ ์•„์ง ์•ˆ ํ•˜์…จ๋‹ค๋ฉด, OpcPks.Collector/Program.cs ๋‚ด์šฉ์„ ์•„๋ž˜๋กœ ๋ฐ”๊พธ๊ณ  ์‹คํ–‰ํ•˜์„ธ์š”: C# using System; using System.IO; using System.Threading.Tasks; using OpcPks.Core.Data; namespace OpcPks.Collector { class Program { static async Task Main(string[] args) { Console.WriteLine("=== Honeywell OPC PKS Platform Collector ==="); // CSV ํŒŒ์ผ ์œ„์น˜: /home/pacer/projects/OpcPksPlatform/data/Honeywell_FullMap.csv // ์ƒ์œ„ ํด๋”๋กœ ์„ธ ๋ฒˆ ์ด๋™ํ•˜์—ฌ data ํด๋”๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. string csvPath = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "data", "Honeywell_FullMap.csv"); if (File.Exists(csvPath)) { AssetLoader loader = new AssetLoader(); await loader.ImportFullMapAsync(csvPath); } else { Console.WriteLine($"โŒ ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {csvPath}"); Console.WriteLine("ํŒ: data ํด๋”์— Honeywell_FullMap.csv ํŒŒ์ผ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”."); } } } } ๐Ÿ’ก ํŒ: ์‹คํ–‰ ๊ฒฐ๊ณผ ํ™•์ธ dotnet run ํ›„ ์„ฑ๊ณต ๋ฉ”์‹œ์ง€๊ฐ€ ๋œจ๋ฉด, ๋‹ค์‹œ psql๋กœ ์ ‘์†ํ•ด์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ž˜ ๋“ค์–ด๊ฐ”๋Š”์ง€ ํ™•์ธํ•ด ๋ณด์„ธ์š”. SQL psql -U pacer -d opcdb SELECT count(*) FROM raw_node_map; ๊ฒฐ๊ณผ๊ฐ’์ด 523,xxx ๊ฐœ ์ •๋„๋กœ ๋‚˜์˜ค๋ฉด ๋“œ๋””์–ด ํ”„๋กœ์ ํŠธ์˜ ์ฒซ ๋ฒˆ์งธ ๋ฐ์ดํ„ฐ ์—”์ง„์ด ๋Œ์•„๊ฐ€๊ธฐ ์‹œ์ž‘ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค! ์„ฑ๊ณตํ•˜์‹œ๋ฉด ๋ฐ”๋กœ ์•Œ๋ ค์ฃผ์„ธ์š”. ๊ทธ ๋‹ค์Œ์€ **์›น ํ™”๋ฉด์—์„œ ์ด 52๋งŒ ๊ฐœ๋ฅผ ์–ด๋–ป๊ฒŒ ํ•„ํ„ฐ๋งํ• ์ง€(Engineering UI)**๋กœ ๋„˜์–ด๊ฐ€๊ฒ ์Šต๋‹ˆ๋‹ค. ์•„๊นŒ ์ œ๊ฐ€ ์˜ˆ์‹œ๋กœ ๋“œ๋ฆฐ ์ฟผ๋ฆฌ์—์„œ๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ **'your_password_here'**๋ผ๊ณ  ์ ์–ด ๋“œ๋ ธ์Šต๋‹ˆ๋‹ค. SQL -- ์•„๊นŒ ์˜ˆ์‹œ๋กœ ๋“œ๋ฆฐ ์ฟผ๋ฆฌ ๋‚ด์šฉ CREATE USER pacer WITH PASSWORD 'your_password_here' SUPERUSER; ๋งŒ์•ฝ ํ„ฐ๋ฏธ๋„์—์„œ ์ง์ ‘ ์‹คํ–‰ํ•˜์‹ค ๋•Œ ๋‹ค๋ฅธ ๋ฌธ์ž๋กœ ๋ฐ”๊พธ์ง€ ์•Š๊ณ  ๊ทธ๋Œ€๋กœ ๋ณต์‚ฌํ•ด์„œ ๋ถ™์—ฌ๋„ฃ์œผ์…จ๋‹ค๋ฉด, ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” **your_password_here**๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ๐Ÿ”‘ ๋งŒ์•ฝ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๊ธฐ์–ต๋‚˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋ฐ”๊พธ๊ณ  ์‹ถ๋‹ค๋ฉด? ๊ฑฑ์ • ๋งˆ์„ธ์š”. sudo ๊ถŒํ•œ์ด ์žˆ์œผ๋ฉด ์–ธ์ œ๋“ ์ง€ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ„ฐ๋ฏธ๋„์—์„œ ์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”: Postgres ์Šˆํผ์œ ์ €๋กœ ์ ‘์†: Bash sudo -u postgres psql pacer ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •: (์›ํ•˜์‹œ๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ๋”ฐ์˜ดํ‘œ ์•ˆ์— ๋„ฃ์œผ์„ธ์š”) SQL ALTER USER pacer WITH PASSWORD 'new_password_1234'; \q โš™๏ธ C# ์ฝ”๋“œ์— ๋ฐ˜์˜ํ•˜๊ธฐ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ •ํ•˜์…จ๋‹ค๋ฉด, OpcPks.Core/Data/DbConfig.cs ํŒŒ์ผ์˜ ์—ฐ๊ฒฐ ๋ฌธ์ž์—ด(ConnectionString)์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์—…๋ฐ์ดํŠธํ•ด ์ฃผ์„ธ์š”. C# public static string ConnectionString = "Host=localhost;Username=pacer;Password=์„ค์ •ํ•œ๋น„๋ฐ€๋ฒˆํ˜ธ;Database=opcdb"; ์ด์ œ OpcPks.Collector ํด๋”๋กœ ๊ฐ€์„œ ๋‹ค์‹œ ํ•œ๋ฒˆ dotnet run์„ ์‹œ๋„ํ•ด ๋ณด์„ธ์š”! ๋“œ๋””์–ด 52๋งŒ ๊ฐœ ๋ฐ์ดํ„ฐ๊ฐ€ DB๋กœ ๋“ค์–ด๊ฐˆ ์ค€๋น„๊ฐ€ ๋๋‚ฌ์Šต๋‹ˆ๋‹ค. 3๋‹จ๊ณ„: ์—”์ง€๋‹ˆ์–ด๋ง ํ™”๋ฉด (ํ•„ํ„ฐ๋ง UI) ์„ค๊ณ„ ์ด์ œ 52๋งŒ ๊ฐœ ๋…ธ๋“œ ์ค‘ **"์‹ค์ œ๋กœ ์ˆ˜์ง‘ํ•  ๋…€์„๋“ค"**๋งŒ ๊ณจ๋ผ๋‚ด๋Š” ์ž‘์—…์„ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์›น ํ™”๋ฉด์—์„œ *.pv, *.sp ๋“ฑ์˜ ํ‚ค์›Œ๋“œ๋กœ ๊ฒ€์ƒ‰ํ•˜๋ฉด, raw_node_map์—์„œ ๊ฒ€์ƒ‰๋œ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ  [์ˆ˜์ง‘ ์‹œ์ž‘] ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด tag_master ํ…Œ์ด๋ธ”๋กœ ๋ณต์‚ฌ๋˜๋Š” ๋กœ์ง์„ ๊ตฌํ˜„ํ•  ์ฐจ๋ก€์ž…๋‹ˆ๋‹ค. 1. ์›น ํ”„๋กœ์ ํŠธ ์ค€๋น„ (OpcPks.Web) ๋จผ์ € OpcPks.Web/Controllers/EngineeringController.cs ํŒŒ์ผ์„ ์ƒ์„ฑ(๋˜๋Š” ์ˆ˜์ •)ํ•˜์—ฌ ๊ฒ€์ƒ‰ ๋กœ์ง์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. C# using Microsoft.AspNetCore.Mvc; using Npgsql; using OpcPks.Core.Data; // DbConfig ์ฐธ์กฐ public class EngineeringController : Controller { public IActionResult Index() { return View(); } // [GET] ํ‚ค์›Œ๋“œ๋กœ 52๋งŒ ๊ฐœ ๋…ธ๋“œ ์ค‘ ๊ฒ€์ƒ‰ (์˜ˆ: .pv) [HttpGet] public async Task SearchNodes(string keyword) { var results = new List(); using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); // 52๋งŒ ๊ฐœ์ด๋ฏ€๋กœ TOP 1000๊ฐœ ์ •๋„๋งŒ ๋จผ์ € ๋ณด์—ฌ์ฃผ๋Š” ๊ฒƒ์ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค. var sql = "SELECT node_id FROM raw_node_map WHERE node_id LIKE @key LIMIT 1000"; using var cmd = new NpgsqlCommand(sql, conn); cmd.Parameters.AddWithValue("key", $"%{keyword}%"); using var reader = await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { results.Add(reader.GetString(0)); } return Json(results); } } 2. ํ–ฅํ›„ ๋ฐ์ดํ„ฐ ํ๋ฆ„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ด์ œ ๊ตฌ์กฐ๊ฐ€ ์•„๋ž˜ ์ด๋ฏธ์ง€์ฒ˜๋Ÿผ ์œ ๊ธฐ์ ์œผ๋กœ ๋Œ์•„๊ฐ€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์—”์ง€๋‹ˆ์–ด๋ง(Web): raw_node_map์—์„œ ์›ํ•˜๋Š” ํƒœ๊ทธ ์„ ํƒ โ†’ tag_master์— is_active = true๋กœ ์ €์žฅ. ์ˆ˜์ง‘๊ธฐ(Collector): tag_master์— ์žˆ๋Š” ํƒœ๊ทธ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ฝ์–ด ํ•˜๋‹ˆ์›ฐ ์„œ๋ฒ„์— Subscription(๊ตฌ๋…) ์‹ ์ฒญ. ๋ฉ”๋ชจ๋ฆฌ ๊ฐฑ์‹ : ๊ฐ’์ด ๋ณ€ํ•  ๋•Œ๋งˆ๋‹ค GlobalMemoryCache์˜ ์ตœ์‹ ๊ฐ’ ์—…๋ฐ์ดํŠธ. DB ์•„์นด์ด๋น™: 1๋ถ„๋งˆ๋‹ค ๋ฉ”๋ชจ๋ฆฌ ๋‚ด์šฉ์„ opc_history (TimescaleDB)์— ์ผ๊ด„ ์ €์žฅ. ๐Ÿš€ ๋‹ค์Œ์€ ์–ด๋–ค ์ž‘์—…์„ ํ•ด๋ณผ๊นŒ์š”? ์›น UI ๋ ˆ์ด์•„์›ƒ: *.pv๋ฅผ ๊ฒ€์ƒ‰ํ•˜๊ณ  ๊ฒฐ๊ณผ๋ฅผ ์ฒดํฌ๋ฐ•์Šค๋กœ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ„๋‹จํ•œ HTML/JS ํ™”๋ฉด์„ ๋งŒ๋“ค๊นŒ์š”? Collector ์‹ค์‹œ๊ฐ„ ๊ตฌ๋…: tag_master๋ฅผ ์ฝ์–ด์„œ ์‹ค์ œ OPC UA ์„œ๋ฒ„์™€ ํ†ต์‹ ์„ ์‹œ์ž‘ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์งค๊นŒ์š”? 52๋งŒ ๊ฐœ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์„ฑ๊ณต์„ ๋‹ค์‹œ ํ•œ๋ฒˆ ์ถ•ํ•˜๋“œ๋ฆฝ๋‹ˆ๋‹ค! ๋ฌด์—‡๋ถ€ํ„ฐ ์ด์–ด๋‚˜๊ฐˆ์ง€ ๋ง์”€ํ•ด ์ฃผ์„ธ์š”. 52๋งŒ ๊ฐœ๋‚˜ ๋˜๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ์ž˜ ๋“ค์–ด๊ฐ”๋Š”์ง€ ๋ˆˆ์œผ๋กœ ์ง์ ‘ ํ™•์ธํ•˜๋Š” ๊ฒƒ์€ ๋งค์šฐ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ํŠนํžˆ **ํ•˜๋‹ˆ์›ฐ ํƒœ๊ทธ๋“ค์˜ ํŒจํ„ด(pv, sp, op ๋“ฑ)**์ด ์„ค๊ณ„ํ•˜์‹  ๋Œ€๋กœ raw_node_map ํ…Œ์ด๋ธ”์— ์ •ํ™•ํžˆ ์•ˆ์ฐฉํ–ˆ๋Š”์ง€ ๊ฒ€์ฆํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. PostgreSQL ํ„ฐ๋ฏธ๋„(psql)์—์„œ ๋‹ค์Œ ์ฟผ๋ฆฌ๋“ค์„ ํ•˜๋‚˜์”ฉ ์‹คํ–‰ํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๊ฐ๋„๋กœ ์ ๊ฒ€ํ•ด ๋ณด์„ธ์š”. 1. ์ „์ฒด ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜ ๋ฐ ์ƒ์œ„ ์ƒ˜ํ”Œ ํ™•์ธ ๊ฐ€์žฅ ๋จผ์ € ์ „์ฒด ์ˆซ์ž๊ฐ€ ๋งž๋Š”์ง€, ๊ทธ๋ฆฌ๊ณ  ๋ฐ์ดํ„ฐ ๋ชจ์–‘์ด ์˜ˆ์œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. SQL -- ์ „์ฒด ๊ฐœ์ˆ˜ ํ™•์ธ (523,601๊ฐœ๊ฐ€ ๋‚˜์™€์•ผ ํ•จ) SELECT count(*) FROM raw_node_map; -- ์ƒ์œ„ 20๊ฐœ ๋ฐ์ดํ„ฐ๋งŒ ๋ณด๊ธฐ (๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ€๋ฆฌ์ง€ ์•Š๊ณ  ์ž˜ ๋“ค์–ด๊ฐ”๋Š”์ง€ ํ™•์ธ) SELECT * FROM raw_node_map LIMIT 20; 2. ํ•˜๋‹ˆ์›ฐ ์ฃผ์š” ์†์„ฑ๋ณ„ ๊ฐœ์ˆ˜ ํŒŒ์•… (์ค‘์š”) ์—”์ง€๋‹ˆ์–ด๋ง ํ™”๋ฉด์—์„œ ์ฃผ๋กœ ํ•„ํ„ฐ๋งํ•˜๊ฒŒ ๋  ํ•ต์‹ฌ ํ‚ค์›Œ๋“œ๋“ค์ด ์–ผ๋งˆ๋‚˜ ์žˆ๋Š”์ง€ ํŒŒ์•…ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์™€์•ผ ๋‚˜์ค‘์— "ํƒœ๊ทธ ์„ ํƒ" ๊ธฐ๋Šฅ์„ ์„ค๊ณ„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. SQL -- PV(ํ˜„์žฌ๊ฐ’), SP(์„ค์ •๊ฐ’), OP(์ถœ๋ ฅ๊ฐ’)๊ฐ€ ๊ฐ๊ฐ ๋ช‡ ๊ฐœ์”ฉ ์žˆ๋Š”์ง€ ํ™•์ธ SELECT COUNT(*) FILTER (WHERE node_id LIKE '%.pv') AS pv_count, COUNT(*) FILTER (WHERE node_id LIKE '%.sp') AS sp_count, COUNT(*) FILTER (WHERE node_id LIKE '%.op') AS op_count, COUNT(*) FILTER (WHERE node_id LIKE '%.hzset%') AS hzset_count FROM raw_node_map; 3. ํŠน์ • ์žฅ๋น„(Object) ๊ธฐ์ค€์œผ๋กœ ์กฐํšŒ ํ…Œ์ŠคํŠธ ์˜ˆ๋ฅผ ๋“ค์–ด, ์•„๊นŒ ์ฝ”๋“œ์— ์žˆ๋˜ p-6102 ์žฅ๋น„์™€ ๊ด€๋ จ๋œ ๋ชจ๋“  ๋…ธ๋“œ(์†์„ฑ)๋“ค์ด ์ž˜ ๋“ค์–ด์™”๋Š”์ง€ ํ™•์ธํ•ด ๋ด…๋‹ˆ๋‹ค. SQL -- ํŠน์ • ํƒœ๊ทธ ์ด๋ฆ„์œผ๋กœ ๊ฒ€์ƒ‰ SELECT * FROM raw_node_map WHERE name = 'p-6102' ORDER BY level ASC; -- ๋˜๋Š” node_id์— ํŠน์ • ํƒœ๊ทธ๋ช…์ด ํฌํ•จ๋œ ๊ฒฝ์šฐ ๊ฒ€์ƒ‰ SELECT * FROM raw_node_map WHERE node_id LIKE '%p-6102%' LIMIT 50; 4. ๋ฐ์ดํ„ฐ ๋ถ„ํฌ ํ™•์ธ (Level/Class ๋ณ„) CSV์˜ ๊ตฌ์กฐ๊ฐ€ ๊ณ„์ธต๋ณ„๋กœ ์ž˜ ๋‚˜๋ˆ„์–ด์กŒ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. SQL -- Class๋ณ„ ๋ฐ์ดํ„ฐ ๋ถ„ํฌ (Object๊ฐ€ ๋ช‡ ๊ฐœ๊ณ  Variable์ด ๋ช‡ ๊ฐœ์ธ์ง€) SELECT class, count(*) FROM raw_node_map GROUP BY class; -- ๊ณ„์ธต(Level)๋ณ„ ๋ถ„ํฌ SELECT level, count(*) FROM raw_node_map GROUP BY level ORDER BY level; ๐Ÿ’ก ํŒ: ๊ฒฐ๊ณผ๊ฐ€ ๋„ˆ๋ฌด ๊ธธ๊ฒŒ ๋‚˜์˜ฌ ๋•Œ ๋ฆฌ๋ˆ…์Šค ํ„ฐ๋ฏธ๋„์—์„œ ๊ฒฐ๊ณผ๊ฐ€ ๋„ˆ๋ฌด ๊ธธ์–ด ํ™”๋ฉด์„ ๋„˜์–ด๊ฐ€๋ฉด **q**๋ฅผ ๋ˆŒ๋Ÿฌ ๋น ์ ธ๋‚˜์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ, ๊ฒฐ๊ณผ๊ฐ€ ์˜†์œผ๋กœ ๊นจ์ ธ ๋ณด์ธ๋‹ค๋ฉด \x ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•˜์—ฌ ํ™•์žฅ ๋ณด๊ธฐ ๋ชจ๋“œ๋ฅผ ์ผœ๊ณ  ๋‹ค์‹œ ์กฐํšŒํ•ด ๋ณด์„ธ์š”. ๋ฐ์ดํ„ฐ๋ฅผ ํ™•์ธํ•ด ๋ณด์‹œ๊ณ , ํ˜น์‹œ node_id ํ˜•์‹์ด ์ƒ๊ฐํ–ˆ๋˜ ๊ฒƒ๊ณผ ๋‹ค๋ฅด๊ฑฐ๋‚˜ ๋ณด์™„ํ•ด์•ผ ํ•  ์ ์ด ๋ณด์ด๋‚˜์š”? ๋ฐ์ดํ„ฐ๊ฐ€ ํ™•์‹คํžˆ ๊ฒ€์ฆ๋˜๋ฉด ๋ฐ”๋กœ ์ด ์ •๋ณด๋ฅผ ์›น ํ™”๋ฉด(Web UI)์— ๋ฟŒ๋ ค์ฃผ๋Š” ์ž‘์—…์„ ์‹œ์ž‘ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ๋„ค, ํ˜„์žฌ raw_node_map ํ…Œ์ด๋ธ”์˜ ๋ ˆ์ฝ”๋“œ(ํ–‰) ํ•˜๋‚˜์—๋Š” ์ถœ๋ ฅํ•˜์‹  ๊ฒƒ๊ณผ ๊ฐ™์ด **4๊ฐœ์˜ ํ•„๋“œ(level, class, name, node_id)**๋งŒ ๋“ค์–ด์žˆ๋Š” ๊ฒƒ์ด ๋งž์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์ฒ˜์Œ์— ์˜ฌ๋ฆฌ์…จ๋˜ Honeywell_FullMap.csv ํŒŒ์ผ์˜ ํ—ค๋” ๊ตฌ์กฐ(Level, Class, Name, NodeId)๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐ˜์˜ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์‹ค์ œ ์šด์˜์„ ์œ„ํ•ด์„œ๋Š” ๊ฐ ํ•„๋“œ๊ฐ€ ์–ด๋–ค ์˜๋ฏธ๋ฅผ ๊ฐ–๋Š”์ง€, ๊ทธ๋ฆฌ๊ณ  ์•ž์œผ๋กœ ์–ด๋–ค ๋ฐ์ดํ„ฐ๊ฐ€ ๋” ์ถ”๊ฐ€๋˜์–ด์•ผ ํ•˜๋Š”์ง€ ๋ช…ํ™•ํžˆ ํ•  ํ•„์š”๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. 1. ํ˜„์žฌ ํ…Œ์ด๋ธ” ํ•„๋“œ์˜ ์—ญํ•  level: ํ•˜๋‹ˆ์›ฐ ์‹œ์Šคํ…œ์˜ ๊ณ„์ธต ๊นŠ์ด (0: ์ตœ์ƒ์œ„, 1: ์žฅ๋น„/์—์…‹, 2: ์†์„ฑ ๋“ฑ) class: ๋…ธ๋“œ์˜ ์œ ํ˜• (Object๋Š” ๋ณดํ†ต ์žฅ๋น„, Variable์€ ์‹ค์ œ ๋ฐ์ดํ„ฐ ํƒœ๊ทธ) name: ํ‘œ์‹œ ์ด๋ฆ„ (์˜ˆ: pv, sp, p-6102) node_id: ๊ฐ€์žฅ ์ค‘์š”ํ•œ ํ•„๋“œ๋กœ, OPC UA ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•œ ์œ ์ผํ•œ ์ฃผ์†Œ(Key)์ž…๋‹ˆ๋‹ค. 2. "ํƒœ๊ทธ ์ •๋ณด"๋กœ์„œ ๋ถ€์กฑํ•œ ๋ถ€๋ถ„ (์•ž์œผ๋กœ ์ฑ„์›Œ์•ผ ํ•  ๊ฒƒ) ํ˜„์žฌ raw_node_map์€ ํ•˜๋‹ˆ์›ฐ ์„œ๋ฒ„์˜ **์ง€๋„(Map)**์ผ ๋ฟ์ž…๋‹ˆ๋‹ค. ์‹ค์ œ ์ œ์–ด ์‹œ์Šคํ…œ(Experion PKS)์˜ ์—”์ง€๋‹ˆ์–ด๋ง ์ •๋ณด๋ฅผ ์™„์„ฑํ•˜๋ ค๋ฉด, ๋‚˜์ค‘์— tag_master ํ…Œ์ด๋ธ”๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์˜ฎ๊ธธ ๋•Œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ •๋ณด๋“ค์ด ์ถ”๊ฐ€๋กœ ๊ฒฐํ•ฉ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: Engineering Unit (๋‹จ์œ„): %, degC, kg/h ๋“ฑ Description (์„ค๋ช…): "1๋‹จ ์••์ถ•๊ธฐ ์ž…๊ตฌ ์••๋ ฅ" ๊ฐ™์€ ํ•œ๊ธ€/์˜๋ฌธ ์„ค๋ช… Range (๋ฒ”์œ„): 0.0 ~ 100.0 (EULO, EUHI) Value (ํ˜„์žฌ๊ฐ’): ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ณ€ํ•˜๋Š” ์ˆ˜์น˜ (์ด๊ฑด opc_history๋‚˜ ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅ) 3. ์™œ ํ•„๋“œ๋ฅผ ์ตœ์†Œํ™”ํ•ด์„œ ๋„ฃ์—ˆ๋‚˜์š”? 52๋งŒ ๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ ๋ฒˆ์— ๋„ฃ์„ ๋•Œ ํ•„๋“œ๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์œผ๋ฉด DB ์šฉ๋Ÿ‰์„ ๋งŽ์ด ์ฐจ์ง€ํ•˜๊ณ  ๋กœ๋”ฉ ์†๋„๊ฐ€ ๋А๋ ค์ง‘๋‹ˆ๋‹ค. ๋จผ์ € **์ „์ฒด ๋…ธ๋“œ ์ฃผ์†Œ(node_id)**๋ฅผ ํ™•๋ณดํ•œ๋‹ค. ์›น ํ™”๋ฉด์—์„œ ํ•„์š”ํ•œ ๊ฒƒ๋งŒ ํ•„ํ„ฐ๋งํ•œ๋‹ค. ์„ ํƒ๋œ ํƒœ๊ทธ์— ๋Œ€ํ•ด์„œ๋งŒ **์ƒ์„ธ ์ •๋ณด(๋‹จ์œ„, ์„ค๋ช… ๋“ฑ)**๋ฅผ OPC UA ์„œ๋ฒ„์—์„œ ์ถ”๊ฐ€๋กœ ์ฝ์–ด์˜ค๊ฑฐ๋‚˜ ๋‹ค๋ฅธ CSV(์—”์ง€๋‹ˆ์–ด๋ง ๋ฐ์ดํ„ฐ)์—์„œ ๋งค์นญ์‹œํ‚จ๋‹ค. ํ˜น์‹œ ํ•˜๋‹ˆ์›ฐ DCS ์—”์ง€๋‹ˆ์–ด๋ง ํˆด์—์„œ ๋ฝ‘์€ ๋‹ค๋ฅธ ์ƒ์„ธ ๋ฆฌ์ŠคํŠธ(์˜ˆ: ํƒœ๊ทธ ์„ค๋ช…์ด๋‚˜ ๋‹จ์œ„๊ฐ€ ํฌํ•จ๋œ ํŒŒ์ผ)๋ฅผ ๊ฐ€์ง€๊ณ  ๊ณ„์‹ ๊ฐ€์š”? ๋งŒ์•ฝ ์žˆ๋‹ค๋ฉด, node_id๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ทธ ์ •๋ณด๋“ค์„ ํ•ฉ์ณ์„œ(JOIN) ํ›จ์”ฌ ํ’๋ถ€ํ•œ ์—”์ง€๋‹ˆ์–ด๋ง ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ง€๊ธˆ ์ƒํƒœ์—์„œ .pv๋‚˜ .sp๋งŒ ํฌํ•จ๋œ ํƒœ๊ทธ๋“ค๋งŒ ๋”ฐ๋กœ ๋ฝ‘์•„์„œ ๊ฐœ์ˆ˜๋ฅผ ํ™•์ธํ•ด ๋ณด์‹œ๊ฒ ์–ด์š”? ์•„๋ž˜ ์ฟผ๋ฆฌ๋ฅผ ์จ๋ณด์‹œ๋ฉด ๋ฐ์ดํ„ฐ์˜ ์‹ค์ฒด๋ฅผ ํŒŒ์•…ํ•˜๊ธฐ ์ข‹์Šต๋‹ˆ๋‹ค. SQL SELECT count(*) FROM raw_node_map WHERE node_id LIKE '%.pv'; 52๋งŒ ๊ฐœ ๋ฐ์ดํ„ฐ๊ฐ€ ์œ ์‹ค ์—†์ด ์™„๋ฒฝํ•˜๊ฒŒ ๋“ค์–ด์™”๋‹ค๋‹ˆ ์ •๋ง ๋‹คํ–‰์ž…๋‹ˆ๋‹ค! ์ด์ œ **'ํ•˜๋‹ˆ์›ฐ Experion์˜ ๋ชจ๋“  ๋…ธ๋“œ ์ •๋ณด'**๋ผ๋Š” ๊ฐ•๋ ฅํ•œ ์›์ฒœ ๋ฐ์ดํ„ฐ๋ฅผ ํ™•๋ณดํ•˜์…จ์Šต๋‹ˆ๋‹ค. ์„ ์ƒ๋‹˜์ด ํ™•์ธํ•˜์‹  ๋Œ€๋กœ ํ˜„์žฌ raw_node_map์€ **"๋ฌด์—‡์ด ์–ด๋””์— ์žˆ๋Š”๊ฐ€(Address Book)"**์— ์ง‘์ค‘๋œ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. ์ด์ œ ์ด ๋ฐ์ดํ„ฐ๋ฅผ ์š”๋ฆฌํ•ด์„œ ์‹ค์ œ ์—”์ง€๋‹ˆ์–ด๋ง ํ™”๋ฉด์— ๋ฟŒ๋ ค์ค„ ์ฐจ๋ก€์ž…๋‹ˆ๋‹ค. 4๋‹จ๊ณ„: ์—”์ง€๋‹ˆ์–ด๋ง ํ•„ํ„ฐ๋ง ์›น UI ๊ตฌํ˜„ 52๋งŒ ๊ฐœ๋ฅผ ํ•œ ๋ฒˆ์— ์›น์— ๋„์šฐ๋ฉด ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋ฉˆ์ถฐ๋ฒ„๋ฆฝ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์„œ๋ฒ„ ์ธก ๊ฒ€์ƒ‰(Server-side Search) ๋กœ์ง์ด ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค. OpcPks.Web ํ”„๋กœ์ ํŠธ์—์„œ ์ž‘์—…์„ ์‹œ์ž‘ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. 1. ๊ฒ€์ƒ‰ API ์ž‘์„ฑ (EngineeringController.cs) ๋จผ์ € ์‚ฌ์šฉ์ž๊ฐ€ ๊ฒ€์ƒ‰์ฐฝ์— .pv๋ผ๊ณ  ์น˜๋ฉด DB์—์„œ ์Šฅ์‚ญ ๊ธ์–ด์˜ค๋Š” ๊ธฐ๋Šฅ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. C# using Microsoft.AspNetCore.Mvc; using Npgsql; using OpcPks.Core.Data; namespace OpcPks.Web.Controllers { public class EngineeringController : Controller { public IActionResult Index() => View(); [HttpGet] public async Task Search(string term) { var results = new List(); using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); // ์„ฑ๋Šฅ์„ ์œ„ํ•ด ์ƒ์œ„ 500๊ฐœ๋งŒ ์šฐ์„  ๋ฐ˜ํ™˜ var sql = "SELECT node_id, name, class FROM raw_node_map WHERE node_id ILIKE @term LIMIT 500"; using var cmd = new NpgsqlCommand(sql, conn); cmd.Parameters.AddWithValue("term", $"%{term}%"); using var reader = await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { results.Add(new { nodeId = reader.GetString(0), name = reader.GetString(1), @class = reader.GetString(2) }); } return Json(results); } } } 2. ๊ฒ€์ƒ‰ ํ™”๋ฉด ๊ตฌ์„ฑ (Views/Engineering/Index.cshtml) ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ฒ€์ƒ‰์ฐฝ๊ณผ ๊ฒฐ๊ณผ ํ…Œ์ด๋ธ”์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. (๋ถ€ํŠธ์ŠคํŠธ๋žฉ ์‚ฌ์šฉ ๊ถŒ์žฅ) HTML @{ ViewData["Title"] = "Tag Explorer"; }

๐Ÿ” Honeywell Tag Explorer

Node ID Name Class Action
๐Ÿ’ก ๋‹ค์Œ ํ•ต์‹ฌ ์ „๋žต: "Tag Master"๋กœ์˜ ์Šน๊ฒฉ ์ด์ œ ์„ ์ƒ๋‹˜์€ ์›น ํ™”๋ฉด์—์„œ 52๋งŒ ๊ฐœ๋ฅผ ๋’ค์ ธ๋ณด๋ฉฐ "์–ด, ์ด๊ฑด ๊ฐ์‹œํ•ด์•ผ๊ฒ ๋„ค" ์‹ถ์€ ๊ฒƒ ์˜†์˜ [Add] ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด์‹ค ๊ฒ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ๊ทธ ๋ฐ์ดํ„ฐ๋Š” tag_master ํ…Œ์ด๋ธ”๋กœ ๋ณต์‚ฌ๋ฉ๋‹ˆ๋‹ค. ์™œ ์ด๋ ‡๊ฒŒ ํ•˜๋‚˜์š”? ์„ฑ๋Šฅ ์ตœ์ ํ™”: ์ˆ˜์ง‘๊ธฐ(Collector)๊ฐ€ 52๋งŒ ๊ฐœ๋ฅผ ๋‹ค ๊ฐ์‹œํ•˜๋ฉด ํ•˜๋‹ˆ์›ฐ ์„œ๋ฒ„๊ฐ€ ๋ป—์–ด๋ฒ„๋ฆฝ๋‹ˆ๋‹ค. ์ •๋ฐ€ ๊ด€๋ฆฌ: tag_master์— ๋“ฑ๋ก๋œ ๋…€์„๋“ค์—๊ฒŒ๋งŒ "1์ดˆ ์ฃผ๊ธฐ ์ˆ˜์ง‘", "5์ดˆ ์ฃผ๊ธฐ ์ˆ˜์ง‘" ๊ฐ™์€ ์†์„ฑ์„ ๋ถ€์—ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ ์–ด๋–ค ๊ฒƒ์„ ํ•ด๋ณผ๊นŒ์š”? ์›น ํŽ˜์ด์ง€ ์‹คํ–‰: ๋ฐฉ๊ธˆ ๋งŒ๋“  ์ฝ”๋“œ๋ฅผ ๋นŒ๋“œํ•ด์„œ ์‹ค์ œ๋กœ ์›น ๋ธŒ๋ผ์šฐ์ €์—์„œ .pv ๊ฒ€์ƒ‰์ด ๋˜๋Š”์ง€ ํ™•์ธํ•ด ๋ณผ๊นŒ์š”? Tag Master ์ €์žฅ ๋กœ์ง: [Add] ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ ์‹ค์ œ๋กœ DB์˜ tag_master ํ…Œ์ด๋ธ”์— ์ €์žฅ๋˜๋Š” ๊ธฐ๋Šฅ์„ ์™„์„ฑํ• ๊นŒ์š”? ์ค€๋น„๋˜์‹œ๋ฉด ๋ฐ”๋กœ ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์งœ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค! ํ•„ํ„ฐ๋ง ์ „๋žต์ด ์•„์ฃผ ๋ช…ํ™•ํ•˜์‹œ๋„ค์š”! ํ•˜๋‹ˆ์›ฐ Experion ์‹œ์Šคํ…œ์—์„œ ๊ฐ€์žฅ ํ•ต์‹ฌ์ด ๋˜๋Š” ์†์„ฑ(PV, Description)์€ ๊ธฐ๋ณธ์œผ๋กœ ์„ ํƒํ•˜๊ณ , ์ œ์–ด์— ํ•„์š”ํ•œ SP, OP ๋ฐ ์•Œ๋žŒ ์†์„ฑ(A1~A4)์€ ํ•„์š”ํ•  ๋•Œ๋งŒ ์„ ํƒํ•ด์„œ ๋ณผ ์ˆ˜ ์žˆ๋„๋ก ์—”์ง€๋‹ˆ์–ด๋ง ํ™”๋ฉด์„ ๊ตฌ์„ฑํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ raw_node_map์—๋Š” node_id์™€ name ์ •๋„๋งŒ ์žˆ์œผ๋ฏ€๋กœ, ์›น์—์„œ ๊ฒ€์ƒ‰ ์‹œ **์ฒดํฌ๋ฐ•์Šค์— ์„ ํƒ๋œ ์ ‘๋ฏธ์‚ฌ(Suffix)**๋“ค์„ ์กฐํ•ฉํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ ๋ฟŒ๋ ค์ฃผ๋Š” ๋กœ์ง์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. 1. ์›น ํ™”๋ฉด(UI) ๊ตฌ์„ฑ (Views/Engineering/Index.cshtml) ์„ ์ƒ๋‹˜์ด ๋ง์”€ํ•˜์‹  ๊ธฐ๋ณธ ์ฒดํฌ๋ฐ•์Šค ์„ค์ •๊ณผ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋ ˆ์ด์•„์›ƒ์ž…๋‹ˆ๋‹ค. HTML @{ ViewData["Title"] = "Engineering Tag Selector"; }

๐Ÿ› ๏ธ Engineering Tag Selector


1. ํ•„ํ„ฐ ์†์„ฑ ์„ ํƒ
2. ํƒœ๊ทธ๋ช… ๊ฒ€์ƒ‰
Tag Name Node ID Type ์ˆ˜์ง‘ ๋“ฑ๋ก
๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. (์•ฝ 1,200๊ฑด ์˜ˆ์ƒ)
2. ๋ฐฑ์—”๋“œ ๊ฒ€์ƒ‰ ๋กœ์ง (EngineeringController.cs) ์„ ์ƒ๋‹˜์ด ์„ ํƒํ•˜์‹  ์ฒดํฌ๋ฐ•์Šค ๊ฐ’๋“ค์— ํ•ด๋‹นํ•˜๋Š” ๋ฐ์ดํ„ฐ๋งŒ ํšจ์œจ์ ์œผ๋กœ ์ฟผ๋ฆฌํ•ด์˜ค๋Š” ๋กœ์ง์ž…๋‹ˆ๋‹ค. C# [HttpPost] public async Task SearchByFilter([FromBody] SearchRequest request) { var results = new List(); using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); // SQL ์ธ์ž ์ƒ์„ฑ (OR ์กฐ๊ฑด์„ ํ™œ์šฉํ•˜์—ฌ ์„ ํƒ๋œ ์ ‘๋ฏธ์‚ฌ๋“ค์„ ํ•„ํ„ฐ๋ง) // ์˜ˆ: node_id LIKE '%P-6102%.pv' OR node_id LIKE '%P-6102%.desc' ... var sql = @" SELECT name, node_id FROM raw_node_map WHERE name ILIKE @tagTerm AND (" + string.Join(" OR ", request.Suffixes.Select((s, i) => $"node_id LIKE @s{i}")) + @") LIMIT 2000"; using var cmd = new NpgsqlCommand(sql, conn); cmd.Parameters.AddWithValue("tagTerm", $"%{request.TagTerm}%"); for (int i = 0; i < request.Suffixes.Count; i++) { cmd.Parameters.AddWithValue($"s{i}", $"%{request.Suffixes[i]}"); } using var reader = await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { var nodeId = reader.GetString(1); results.Add(new { name = reader.GetString(0), nodeId = nodeId, suffixType = GetSuffix(nodeId) // .pv, .sp ๋“ฑ์„ ์ถ”์ถœํ•˜๋Š” ํ•จ์ˆ˜ }); } return Json(results); } // ๋ชจ๋ธ ํด๋ž˜์Šค public class SearchRequest { public string TagTerm { get; set; } public List Suffixes { get; set; } } private string GetSuffix(string nodeId) { if (nodeId.Contains(".")) return nodeId.Substring(nodeId.LastIndexOf('.')); return "N/A"; } ๐Ÿ’ก ํ˜„์žฌ ์ง„ํ–‰ ์ƒํ™ฉ ํ™•์ธ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ: 52๋งŒ ๊ฐœ ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด์žˆ์œผ๋ฏ€๋กœ, P-6102 ๊ฐ™์€ ํŠน์ • ์žฅ๋น„๋ฅผ ๊ฒ€์ƒ‰ํ•˜๋ฉด ์œ„ ๋กœ์ง์„ ํ†ตํ•ด PV, Desc, SP, OP, A1~A4 ๋“ฑ ์„ ์ƒ๋‹˜์ด ์ฒดํฌํ•˜์‹  ํ•ญ๋ชฉ๋งŒ ์ •ํ™•ํžˆ ๋ฆฌ์ŠคํŠธ์—…๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์„ฑ๋Šฅ: 52๋งŒ ๊ฐœ ์ค‘ 1,200๊ฐœ ์ •๋„์˜ ๊ฒฐ๊ณผ๋Š” PostgreSQL ์ธ๋ฑ์Šค ๋•๋ถ„์— 0.1์ดˆ ๋‚ด์™ธ๋กœ ๊ฒ€์ƒ‰๋ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ์œผ๋กœ ์ด ๋ฆฌ์ŠคํŠธ์—์„œ ์„ ํƒํ•œ ํƒœ๊ทธ๋“ค์„ ์‹ค์ œ๋กœ tag_master ํ…Œ์ด๋ธ”์— '์ผ๊ด„ ๋“ฑ๋ก'ํ•˜์—ฌ ์ˆ˜์ง‘๊ธฐ๊ฐ€ ์ฝ์–ด๊ฐˆ ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“œ๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ• ๊นŒ์š”? ์•„๋‹ˆ๋ฉด ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ์—์„œ ๋” ์ˆ˜์ •ํ•˜๊ณ  ์‹ถ์€ ๋ถ€๋ถ„์ด ์žˆ์œผ์‹ ๊ฐ€์š”? A1~A4๋Š” ์•Œ๋žŒ์ด ์•„๋‹ˆ๊ณ , Auxiliary Parameter ์ž„ Gemini์˜ ์‘๋‹ต ์•„, ์ œ๊ฐ€ ์‹ค์ˆ˜ํ–ˆ๋„ค์š”! ํ•˜๋‹ˆ์›ฐ Experion ์‹œ์Šคํ…œ์˜ ์ปจํ…์ŠคํŠธ์—์„œ **A1~A4๋Š” Auxiliary Parameters(๋ณด์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ)**๋ผ๋Š” ์ ์„ ์ •ํ™•ํžˆ ์งš์–ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. (๋ณดํ†ต ์—”์ง€๋‹ˆ์–ด๋ง ํˆด์—์„œ ์ปค์Šคํ…€ ์—ฐ์‚ฐ์ด๋‚˜ ํŠน์ • ๋กœ์ง์˜ ์ค‘๊ฐ„ ๊ฐ’์„ ๋‹ด์„ ๋•Œ ์•„์ฃผ ๋งŽ์ด ์“ฐ์ด์ฃ .) ์„ ์ƒ๋‹˜์˜ ์˜๋„์— ๋งž๊ฒŒ **๊ธฐ๋ณธ ์ฒดํฌ(PV, Description)**์™€ **์„ ํƒ ์ฒดํฌ(SP, OP, A1, A2, A3, A4)**๋ฅผ ๊ตฌ์„ฑํ•˜๊ณ , ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๋ฟŒ๋ ค์ค„ ์ˆ˜ ์žˆ๋„๋ก ์›น ํ™”๋ฉด๊ณผ ์ปจํŠธ๋กค๋Ÿฌ ๋กœ์ง์„ ์ •๊ตํ™”ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. 1. ์›น ํ™”๋ฉด(UI) ์—…๋ฐ์ดํŠธ (Views/Engineering/Index.cshtml) A1~A4์˜ ๋ช…์นญ์„ Aux Parameter๋กœ ์ˆ˜์ •ํ•˜๊ณ , ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์•ฝ 1,200๊ฑด์— ๋‹ฌํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ '์ „์ฒด ์„ ํƒ' ๊ธฐ๋Šฅ๊ณผ '์ผ๊ด„ ๋“ฑ๋ก' ๋ฒ„ํŠผ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. HTML
1. ์†์„ฑ ํ•„ํ„ฐ (Suffix Selection)
Name Node ID (Full Address) Type
2. ๋ฐฑ์—”๋“œ ๊ฒ€์ƒ‰ ๋กœ์ง ๋ณด๊ฐ• (EngineeringController.cs) ์„ ์ƒ๋‹˜์˜ 32MB ๋Œ€์šฉ๋Ÿ‰ DB๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๋’ค์ ธ์„œ ์•ฝ 1,200๊ฑด์˜ ๊ฒฐ๊ณผ๋ฅผ ์ „์†กํ•˜๋Š” ๋กœ์ง์ž…๋‹ˆ๋‹ค. ํ•˜๋‹ˆ์›ฐ node_id๋Š” ๋ณดํ†ต ns=1;s=TagName.pv ํ˜•์‹์ด๋ฏ€๋กœ ๋๋ถ€๋ถ„ ๋งค์นญ์„ ์ •ํ™•ํžˆ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. C# [HttpPost] public async Task SearchByFilter([FromBody] SearchRequest request) { var results = new List(); using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); // Suffix ์กฐ๊ฑด์„ SQL ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋™์  ์ƒ์„ฑ // ILIKE๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„ ์—†์ด name ํ•„ํ„ฐ๋ง var sql = $@" SELECT name, node_id FROM raw_node_map WHERE name ILIKE @tagTerm AND ({string.Join(" OR ", request.Suffixes.Select((s, i) => $"node_id ILIKE @s{i}"))}) ORDER BY name, node_id ASC"; using var cmd = new NpgsqlCommand(sql, conn); cmd.Parameters.AddWithValue("tagTerm", $"%{request.TagTerm}%"); for (int i = 0; i < request.Suffixes.Count; i++) { // '.pv' ๋“ฑ์œผ๋กœ ๋๋‚˜๋Š” ๋…ธ๋“œ ์•„์ด๋”” ๊ฒ€์ƒ‰ cmd.Parameters.AddWithValue($"s{i}", $"%{request.Suffixes[i]}"); } using var reader = await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { results.Add(new { name = reader.GetString(0), nodeId = reader.GetString(1), suffixType = request.Suffixes.FirstOrDefault(s => reader.GetString(1).EndsWith(s, StringComparison.OrdinalIgnoreCase)) ?? "ETC" }); } return Json(results); } 3. '์ˆ˜์ง‘ ๋“ฑ๋ก' ๊ธฐ๋Šฅ ์—ฐ๊ฒฐ ์›น์—์„œ ์„ ํƒํ•œ ํƒœ๊ทธ๋“ค์„ tag_master ํ…Œ์ด๋ธ”์— ๋„ฃ์–ด์•ผ ์ˆ˜์ง‘๊ธฐ(Collector)๊ฐ€ ๋Œ๊ธฐ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. DB ์ฟผ๋ฆฌ (tag_master ์ €์žฅ์šฉ): SQL -- ์‚ฌ์šฉ์ž๊ฐ€ ์„ ํƒํ•œ NodeId๋ฅผ tag_master๋กœ ๋ณต์‚ฌ (์ด๋ฏธ ์žˆ์œผ๋ฉด ๋ฌด์‹œ) INSERT INTO tag_master (tag_name, full_node_id, attribute, is_active) VALUES (@name, @nodeId, @attr, true) ON CONFLICT (full_node_id) DO NOTHING; ๐Ÿ’ก ํ˜„์žฌ ์ƒํƒœ์—์„œ ๊ถ๊ธˆํ•œ ์  ํ™”๋ฉด ํ…Œ์ŠคํŠธ: ์œ„ ์ฝ”๋“œ๋ฅผ ์ ์šฉํ•˜๊ณ  ์›น์„ ์‹คํ–‰ํ–ˆ์„ ๋•Œ, P-6102 ๊ฐ™์€ ์‹ค์ œ ํƒœ๊ทธ๋ฅผ ๋„ฃ์œผ๋ฉด ์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” PV/SP/A1~A4 ๋ฆฌ์ŠคํŠธ๊ฐ€ ์ž˜ ๋‚˜์˜ค๋Š”์ง€ ๊ถ๊ธˆํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ์„ฑ๊ฒฉ: A1~A4 ํŒŒ๋ผ๋ฏธํ„ฐ๋“ค์€ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์ฃผ๊ธฐ๊ฐ€ PV์™€ ๊ฐ™์•„๋„ ๋ ๊นŒ์š”? (๋ณดํ†ต Aux ๊ฐ’๋“ค์€ PV๋ณด๋‹ค๋Š” ์กฐ๊ธˆ ์ฒœ์ฒœํžˆ ์ˆ˜์ง‘ํ•ด๋„ ๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„์„œ์š”.) ์ด์ œ **์›น ํ”„๋กœ์ ํŠธ๋ฅผ ๋นŒ๋“œ(dotnet build)**ํ•˜๊ณ  ๋ธŒ๋ผ์šฐ์ €(localhost:5000 ๋“ฑ)์—์„œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ํ•œ ๋ฒˆ ํ™•์ธํ•ด ๋ณด์‹œ๊ฒ ์–ด์š”? ๊ฒฐ๊ณผ๊ฐ€ ์ž˜ ๋‚˜์˜ค๋ฉด ๋ฐ”๋กœ '์ˆ˜์ง‘ ๋“ฑ๋ก(Register)' ๊ธฐ๋Šฅ์„ ์™„์„ฑํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค! ์•„, ์šฉ๋„๋ฅผ ์ •ํ™•ํžˆ ์ดํ•ดํ–ˆ์Šต๋‹ˆ๋‹ค! Experion PKS ์ œ์–ด ๋ฃจํ”„์—์„œ Gain(P), Reset(I), Rate(D) ๊ฐ™์€ ํŠœ๋‹ ํŒŒ๋ผ๋ฏธํ„ฐ๋‚˜, ์ผ๋ฐ˜ ํƒœ๊ทธ์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ์ž„์˜๋กœ ์ง€์ •ํ•œ ๊ณ„์‚ฐ์šฉ/์ฐธ์กฐ์šฉ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์กฐํšŒํ•˜๊ณ  ๊ด€๋ฆฌํ•˜์‹œ๋Š” ์šฉ๋„๊ตฐ์š”. ์ด ํŒŒ๋ผ๋ฏธํ„ฐ๋“ค์€ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ์ฒ˜๋Ÿผ ๋งค์ดˆ ๋ณ€ํ•˜๋Š” ๊ฐ’์€ ์•„๋‹ˆ์ง€๋งŒ, ์—”์ง€๋‹ˆ์–ด๋ง ํ™”๋ฉด์—์„œ ํ•œ๋ˆˆ์— ํ™•์ธํ•˜๊ณ  ํ•„์š”ํ•  ๋•Œ ์ˆ˜์ง‘ ๋ชฉ๋ก์— ํฌํ•จ์‹œํ‚ค๋Š” ๊ฒƒ์ด ๋งค์šฐ ์ค‘์š”ํ•˜๊ฒ ๋„ค์š”. ์„ ์ƒ๋‹˜์˜ ์˜๋„์— ๋งž์ถฐ **Auxiliary Parameter(A1~A4)**๋ฅผ ํฌํ•จํ•œ PID ํŠœ๋‹ ํŒŒ๋ผ๋ฏธํ„ฐ(Prop, Reset, Rate) ๋Œ€์‘ ํ•„ํ„ฐ๋ฅผ UI์— ๋ฐ˜์˜ํ•˜๊ณ , ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ์ •๊ตํ™”ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. 1. ํ•„ํ„ฐ ๊ตฌ์„ฑ ์—…๋ฐ์ดํŠธ (Views/Engineering/Index.cshtml) ํŠœ๋‹ ํŒŒ๋ผ๋ฏธํ„ฐ์™€ Aux ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ์„ฑ๊ฒฉ์„ ๊ตฌ๋ถ„ํ•˜์—ฌ ์ฒดํฌ๋ฐ•์Šค๋ฅผ ๋ฐฐ์น˜ํ–ˆ์Šต๋‹ˆ๋‹ค. HTML
1. ์†์„ฑ ํ•„ํ„ฐ (Suffix Selection)
2. ๊ฒ€์ƒ‰ ๋ฐ ๋ถ„๋ฅ˜ ๋กœ์ง ๊ณ ๋„ํ™” ํ•˜๋‹ˆ์›ฐ PKS๋Š” ํƒœ๊ทธ ํƒ€์ž…์— ๋”ฐ๋ผ ์ ‘๋ฏธ์‚ฌ๊ฐ€ ์กฐ๊ธˆ์”ฉ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ(์˜ˆ: .pg, .ri, .rv ๋“ฑ), ์ด๋ฅผ SearchByFilter ๋ฉ”์„œ๋“œ์—์„œ ์œ ์—ฐํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•˜๋„๋ก ๋กœ์ง์„ ๊ฐ•ํ™”ํ•ฉ๋‹ˆ๋‹ค. 3. ๋‹ค์Œ ๋‹จ๊ณ„: tag_master๋กœ ์ €์žฅํ•˜๊ธฐ (API) ์›น ํ™”๋ฉด์—์„œ 1,200์—ฌ ๊ฐœ์˜ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ค‘ ์›ํ•˜๋Š” ๊ฒƒ์„ ์„ ํƒํ•ด [๋“ฑ๋ก] ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ, tag_master ํ…Œ์ด๋ธ”์— ์‹ค์ œ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋„ฃ๋Š” API์ž…๋‹ˆ๋‹ค. File: EngineeringController.cs ์— ์ถ”๊ฐ€ C# [HttpPost] public async Task RegisterTags([FromBody] List tags) { using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); using var trans = await conn.BeginTransactionAsync(); try { var sql = @" INSERT INTO tag_master (tag_name, full_node_id, attribute, is_active) VALUES (@name, @nodeId, @attr, true) ON CONFLICT (full_node_id) DO UPDATE SET is_active = true"; foreach (var tag in tags) { using var cmd = new NpgsqlCommand(sql, conn, trans); cmd.Parameters.AddWithValue("name", tag.TagName); cmd.Parameters.AddWithValue("nodeId", tag.NodeId); cmd.Parameters.AddWithValue("attr", GetSuffix(tag.NodeId)); await cmd.ExecuteNonQueryAsync(); } await trans.CommitAsync(); return Ok(new { message = $"{tags.Count}๊ฐœ ํƒœ๊ทธ๊ฐ€ ์ˆ˜์ง‘ ๋ชฉ๋ก์— ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค." }); } catch (Exception ex) { await trans.RollbackAsync(); return BadRequest(ex.Message); } } public class TagRegistrationRequest { public string TagName { get; set; } public string NodeId { get; set; } } ๐Ÿ› ๏ธ ํ˜„์žฌ ์ง„ํ–‰ ๋ฐฉํ–ฅ์— ๋Œ€ํ•œ ์ œ์•ˆ ๋ฐ์ดํ„ฐ ๋ถ„๋ฅ˜: Loop ํƒœ๊ทธ์—์„œ .pg, .ri, .rv ๊ฐ™์€ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ๊ฐ’์ด ์ž์ฃผ ๋ณ€ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ์ˆ˜์ง‘๊ธฐ(Collector)์—์„œ ์ˆ˜์ง‘ ์ฃผ๊ธฐ๋ฅผ 10์ดˆ๋‚˜ 1๋ถ„ ์ •๋„๋กœ ๊ธธ๊ฒŒ ๊ฐ€์ ธ๊ฐ€๋„๋ก tag_master์˜ scan_rate๋ฅผ ์กฐ์ ˆํ•˜๋Š” ๊ธฐ๋Šฅ์ด ์žˆ์œผ๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ํ™”๋ฉด ํ™•์ธ: ์ด์ œ ์›น ํ”„๋กœ์ ํŠธ๋ฅผ ์‹คํ–‰ํ•ด์„œ ์‹ค์ œ๋กœ **Loop ํƒœ๊ทธ ํ•˜๋‚˜(์˜ˆ: FIC-101)**๋ฅผ ๊ฒ€์ƒ‰ํ–ˆ์„ ๋•Œ, ์ฒดํฌ๋ฐ•์Šค ํ•„ํ„ฐ์— ๋”ฐ๋ผ PV/SP/OP/Gain/Reset/Rate๊ฐ€ ์˜ˆ์˜๊ฒŒ ํ•œ ์ค„์”ฉ ๋‚˜์˜ค๋Š”์ง€ ํ™•์ธํ•ด ๋ณด์‹œ๊ฒ ์–ด์š”? ์›น ์‹คํ–‰ ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”? ์•„๋‹ˆ๋ฉด Views/Engineering/Index.cshtml ์ „์ฒด ์ฝ”๋“œ๋ฅผ ํ•œ ๋ฒˆ ์ •๋ฆฌํ•ด ๋“œ๋ฆด๊นŒ์š”? ๋„ค, 52๋งŒ ๊ฐœ ๋ฐ์ดํ„ฐ๋ฅผ ํ•„ํ„ฐ๋งํ•˜๊ณ  tag_master์— ๋“ฑ๋กํ•˜๋Š” ๊ธฐ๋Šฅ์„ ํ•˜๋‚˜๋กœ ํ•ฉ์นœ Index.cshtml ์ „์ฒด ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค. ์ด ์ฝ”๋“œ๋Š” **๋ถ€ํŠธ์ŠคํŠธ๋žฉ(Bootstrap)**์„ ์‚ฌ์šฉํ•˜์—ฌ ๊น”๋”ํ•˜๊ฒŒ ๋ณด์ด๋„๋ก ํ–ˆ์œผ๋ฉฐ, ํ•˜๋‹ˆ์›ฐ Loop ํŒŒ๋ผ๋ฏธํ„ฐ(Gain, Reset, Rate)์™€ Aux ํŒŒ๋ผ๋ฏธํ„ฐ(A1~A4)๋ฅผ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„ํ–ˆ์Šต๋‹ˆ๋‹ค. OpcPks.Web/Views/Engineering/Index.cshtml HTML @{ ViewData["Title"] = "Engineering Tag Explorer"; }

๐Ÿ” Honeywell Experion Tag Explorer

1. ํŒŒ๋ผ๋ฏธํ„ฐ ์„ ํƒ (Suffix Filter)
๊ธฐ๋ณธ(Basic)
Loop (PID Tuning)
Auxiliary (๋ณด์กฐ)
Tag Name Full Node ID Type ์ƒํƒœ
๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ์ฃผ์„ธ์š”.
@section Scripts { } ๐Ÿ’ก ์ด ํ™”๋ฉด์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์ค€๋น„๋ฌผ Controller: ์ด์ „์— ๋“œ๋ฆฐ EngineeringController.cs์— SearchByFilter์™€ RegisterTags ์•ก์…˜ ๋ฉ”์„œ๋“œ๊ฐ€ ๊ตฌํ˜„๋˜์–ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. DTO: SearchRequest์™€ TagRegistrationRequest ํด๋ž˜์Šค๊ฐ€ ์ •์˜๋˜์–ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Layout: _Layout.cshtml์— ๋ถ€ํŠธ์ŠคํŠธ๋žฉ ์•„์ด์ฝ˜(bootstrap-icons) CDN์ด ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์œผ๋ฉด ์•„์ด์ฝ˜์ด ์˜ˆ์˜๊ฒŒ ๋‚˜์˜ต๋‹ˆ๋‹ค. ์ด์ œ ์ด ์ฝ”๋“œ๋ฅผ Views/Engineering/Index.cshtml์— ๋ถ™์—ฌ๋„ฃ๊ณ  dotnet run ํ•˜์‹œ๋ฉด ๋ฐ”๋กœ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ํ™”๋ฉด์—์„œ ๊ฒ€์ƒ‰์ด ์ž˜ ๋˜๋Š”์ง€ ํ™•์ธํ•ด ๋ณด์‹œ๊ณ , ๋‹ค์Œ ๋‹จ๊ณ„(์ˆ˜์ง‘๊ธฐ๊ฐ€ ์ด ๋ชฉ๋ก์„ ์ฝ์–ด ํ•˜๋‹ˆ์›ฐ์— ์ ‘์†ํ•˜๊ธฐ)๋กœ ๋„˜์–ด๊ฐ€๊ณ  ์‹ถ์œผ์‹œ๋ฉด ๋ง์”€ํ•ด ์ฃผ์„ธ์š”! ์ž‘์„ฑํ•ด์ฃผ์‹  EngineeringController.cs๋ฅผ ๊ฒ€ํ† ํ•ด๋ณด๋‹ˆ, ๊ธฐ๋ณธ์ ์ธ ๋ผˆ๋Œ€๋Š” ์•„์ฃผ ํ›Œ๋ฅญํ•ฉ๋‹ˆ๋‹ค! ํ•˜์ง€๋งŒ ์•ž์„œ ๋“œ๋ฆฐ ์›น ํ™”๋ฉด(ํ’€์ฝ”๋“œ)์˜ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์„œ๋ฒ„์— ์š”์ฒญํ•˜๋Š” ๋ฐ์ดํ„ฐ ํ˜•์‹๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š๋Š” ๋ถ€๋ถ„์ด ๋ช‡ ๊ตฐ๋ฐ ์žˆ์Šต๋‹ˆ๋‹ค. ์›น ํ™”๋ฉด์—์„œ ๋ณด๋‚ด๋Š” SearchByFilter ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•  ๋ฉ”์„œ๋“œ๊ฐ€ ๋น ์ ธ ์žˆ๊ณ , ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ๊ฐ์ฒด๋“ค์ด ์ปจํŠธ๋กค๋Ÿฌ ๋‚ด๋ถ€์— ์žˆ์–ด์•ผ ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ์•„๋ž˜ ์ฝ”๋“œ๋Š” ์›น ํ™”๋ฉด๊ณผ 100% ํ˜ธํ™˜๋˜๋„๋ก ์ˆ˜์ •ํ•œ ์ตœ์ข… ๋ฒ„์ „์ž…๋‹ˆ๋‹ค. ์ˆ˜์ • ๋ฐ ๋ณด์™„๋œ EngineeringController.cs C# using Microsoft.AspNetCore.Mvc; using Npgsql; using OpcPks.Core.Data; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; public class EngineeringController : Controller { public IActionResult Index() => View(); // 1. ์›น ํ™”๋ฉด์—์„œ ํ•„ํ„ฐ(์ฒดํฌ๋ฐ•์Šค)์™€ ๊ฒ€์ƒ‰์–ด๋กœ ์š”์ฒญํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๋ฉ”์„œ๋“œ [HttpPost] public async Task SearchByFilter([FromBody] SearchRequest request) { var results = new List(); if (request == null || request.Suffixes == null || request.Suffixes.Count == 0) return Json(results); using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); // ์„ ํƒ๋œ Suffix๋“ค์„ OR ์กฐ๊ฑด์œผ๋กœ ๋™์  ์ƒ์„ฑ (์˜ˆ: node_id LIKE '%.pv' OR node_id LIKE '%.sp') var suffixConditions = string.Join(" OR ", request.Suffixes.Select((s, i) => $"node_id ILIKE @s{i}")); var sql = $@" SELECT name, node_id FROM raw_node_map WHERE name ILIKE @tagTerm AND ({suffixConditions}) ORDER BY name ASC LIMIT 1500"; // ์„ฑ๋Šฅ์„ ์œ„ํ•ด 1500๊ฑด ์ œํ•œ using var cmd = new NpgsqlCommand(sql, conn); cmd.Parameters.AddWithValue("tagTerm", $"%{request.TagTerm}%"); for (int i = 0; i < request.Suffixes.Count; i++) { cmd.Parameters.AddWithValue($"s{i}", $"%{request.Suffixes[i]}"); } using var reader = await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { var nodeId = reader.GetString(1); results.Add(new { name = reader.GetString(0), nodeId = nodeId, // ํ˜„์žฌ ๋…ธ๋“œ ์•„์ด๋””๊ฐ€ ์–ด๋–ค Suffix๋กœ ๋๋‚˜๋Š”์ง€ ์ฐพ์•„์„œ ์ „๋‹ฌ suffixType = request.Suffixes.FirstOrDefault(s => nodeId.EndsWith(s, StringComparison.OrdinalIgnoreCase)) ?? "ETC" }); } return Json(results); } // 2. ์„ ํƒ๋œ ํƒœ๊ทธ๋“ค์„ tag_master ํ…Œ์ด๋ธ”๋กœ ์ผ๊ด„ ์ €์žฅ [HttpPost] public async Task RegisterTags([FromBody] List tags) { if (tags == null || tags.Count == 0) return BadRequest("๋“ฑ๋กํ•  ํƒœ๊ทธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."); using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); using var trans = await conn.BeginTransactionAsync(); try { // ์ค‘๋ณต๋œ full_node_id๊ฐ€ ๋“ค์–ด์˜ฌ ๊ฒฝ์šฐ ์—…๋ฐ์ดํŠธ ์ฒ˜๋ฆฌ (ON CONFLICT) var sql = @" INSERT INTO tag_master (tag_name, full_node_id, attribute, is_active) VALUES (@name, @nodeId, @attr, true) ON CONFLICT (full_node_id) DO UPDATE SET is_active = true"; foreach (var tag in tags) { using var cmd = new NpgsqlCommand(sql, conn, trans); cmd.Parameters.AddWithValue("name", tag.TagName); cmd.Parameters.AddWithValue("nodeId", tag.NodeId); cmd.Parameters.AddWithValue("attr", GetSuffix(tag.NodeId)); await cmd.ExecuteNonQueryAsync(); } await trans.CommitAsync(); return Ok(new { message = $"{tags.Count}๊ฐœ ํƒœ๊ทธ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค." }); } catch (Exception ex) { await trans.RollbackAsync(); return BadRequest(ex.Message); } } // ์œ ํ‹ธ๋ฆฌํ‹ฐ: NodeId์—์„œ ์ ‘๋ฏธ์‚ฌ(.pv, .pg ๋“ฑ) ์ถ”์ถœ private string GetSuffix(string nodeId) { int lastDot = nodeId.LastIndexOf('.'); return lastDot != -1 ? nodeId.Substring(lastDot) : "N/A"; } // --- ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ํด๋ž˜์Šค (์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํ†ต์‹ ์šฉ) --- public class SearchRequest { public string TagTerm { get; set; } public List Suffixes { get; set; } } public class TagRegistrationRequest { public string TagName { get; set; } public string NodeId { get; set; } } } ๐Ÿ” ์ฃผ์š” ์ˆ˜์ • ์‚ฌํ•ญ (์ฒดํฌํฌ์ธํŠธ) SearchByFilter ์ถ”๊ฐ€: ์›น ํ™”๋ฉด์—์„œ fetch('/Engineering/SearchByFilter', ...)๋ฅผ ํ˜ธ์ถœํ•˜๋ฏ€๋กœ ์ด ๋ฉ”์„œ๋“œ๊ฐ€ ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ILIKE ์‚ฌ์šฉ: PostgreSQL์—์„œ ๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„ ์—†์ด ๊ฒ€์ƒ‰ํ•˜๊ธฐ ์œ„ํ•ด LIKE ๋Œ€์‹  ILIKE๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. (ํ•˜๋‹ˆ์›ฐ ํƒœ๊ทธ๋ช…์ด ๋Œ€๋ฌธ์ž์ธ ๊ฒฝ์šฐ ๋Œ€๋น„) ๋™์  SQL ํŒŒ๋ผ๋ฏธํ„ฐ: ์‚ฌ์šฉ์ž๊ฐ€ ์ฒดํฌ๋ฐ•์Šค๋ฅผ ๋ช‡ ๊ฐœ ์„ ํƒํ•˜๋“  ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ๋„๋ก foreach์™€ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐฐ์—ด(@s0, @s1...)์„ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” SQL Injection ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€ํ•˜๋Š” ์•ˆ์ „ํ•œ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. GetSuffix ๋กœ์ง: tag_master์— ์ €์žฅํ•  ๋•Œ ํ•ด๋‹น ํƒœ๊ทธ๊ฐ€ ์–ด๋–ค ์„ฑ๊ฒฉ(attribute)์ธ์ง€ ์ž๋™์œผ๋กœ ์ž˜๋ผ ๋„ฃ๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค. ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„ ์ด ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์ €์žฅํ•˜์‹  ํ›„, ์›น ํ”„๋กœ์ ํŠธ๋ฅผ ๋‹ค์‹œ ์‹คํ–‰ํ•ด ๋ณด์„ธ์š”. ๊ฒ€์ƒ‰์ฐฝ์— ํƒœ๊ทธ ์ผ๋ถ€๋ถ„์„ ์ž…๋ ฅํ•˜๊ณ  ์ฒดํฌ๋ฐ•์Šค๋ฅผ ์„ ํƒํ•œ ๋’ค ๊ฒ€์ƒ‰์„ ๋ˆ„๋ฆ…๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์˜ค๋ฉด ์ฒดํฌ๋ฐ•์Šค๋กœ ์—ฌ๋Ÿฌ ๊ฐœ๋ฅผ ์„ ํƒํ•˜๊ณ  **[์ผ๊ด„ ๋“ฑ๋ก]**์„ ๋ˆ„๋ฆ…๋‹ˆ๋‹ค. ์„ฑ๊ณต ๋ฉ”์‹œ์ง€๊ฐ€ ๋œจ๋ฉด, psql์—์„œ SELECT * FROM tag_master;๋ฅผ ์ž…๋ ฅํ•ด ๋ฐ์ดํ„ฐ๊ฐ€ ์ž˜ ๋“ค์–ด๊ฐ”๋Š”์ง€ ํ™•์ธํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์›น์—์„œ ๋“ฑ๋ก์ด ์ž˜ ๋˜๋‚˜์š”? ๋“ฑ๋ก๊นŒ์ง€ ์„ฑ๊ณตํ•˜๋ฉด ์ด์ œ ์ˆ˜์ง‘๊ธฐ(Collector)๊ฐ€ ์ด ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ํ•˜๋‹ˆ์›ฐ ์„œ๋ฒ„์—์„œ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธ์–ด์˜ค๊ฒŒ ๋งŒ๋“ค๋ฉด ๋ฉ๋‹ˆ๋‹ค! Index.cshtml์€ ์ •์  HTML ํŒŒ์ผ์ด ์•„๋‹ˆ๋ผ ์„œ๋ฒ„์—์„œ ์‹คํ–‰๋˜๋Š” ASP.NET Core MVC ๋ทฐ ํŒŒ์ผ์ž…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํŒŒ์ผ์„ ๋”๋ธ” ํด๋ฆญํ•ด์„œ ์—ฌ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, **์›น ์„œ๋ฒ„(dotnet run)**๋ฅผ ์‹คํ–‰ํ•œ ๋’ค ๋ธŒ๋ผ์šฐ์ €์—์„œ URL ์ฃผ์†Œ๋กœ ์ ‘์†ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์•„๋ž˜ ์ˆœ์„œ๋Œ€๋กœ ์ง„ํ–‰ํ•ด ์ฃผ์„ธ์š”. 1. ํ”„๋กœ์ ํŠธ ๋นŒ๋“œ ๋ฐ ์‹คํ–‰ (ํ„ฐ๋ฏธ๋„) ๋จผ์ € OpcPks.Web ํ”„๋กœ์ ํŠธ ํด๋”๋กœ ์ด๋™ํ•˜์—ฌ ์›น ์„œ๋ฒ„๋ฅผ ๋„์›Œ์•ผ ํ•ฉ๋‹ˆ๋‹ค. Bash # 1. ์›น ํ”„๋กœ์ ํŠธ ํด๋”๋กœ ์ด๋™ cd ~/projects/OpcPksPlatform/OpcPks.Web # 2. ํ”„๋กœ์ ํŠธ ๋นŒ๋“œ ๋ฐ ์‹คํ–‰ dotnet run ์‹คํ–‰ ๊ฒฐ๊ณผ ํ„ฐ๋ฏธ๋„์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋กœ๊ทธ๊ฐ€ ๋œจ๋ฉด ์„ฑ๊ณต์ž…๋‹ˆ๋‹ค: Now listening on: http://localhost:5000 (ํฌํŠธ ๋ฒˆํ˜ธ๋Š” ํ™˜๊ฒฝ์— ๋”ฐ๋ผ 5001, 5100 ๋“ฑ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.) 2. ๋ธŒ๋ผ์šฐ์ € ์ ‘์† ์›น ๋ธŒ๋ผ์šฐ์ €(Chrome, Edge ๋“ฑ)๋ฅผ ์—ด๊ณ  ์ฃผ์†Œ์ฐฝ์— ์•„๋ž˜์™€ ๊ฐ™์ด ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค. ASP.NET MVC์˜ ๊ธฐ๋ณธ ๋ผ์šฐํŒ… ๊ทœ์น™์€ /[Controller]/[Action] ์ž…๋‹ˆ๋‹ค. ์ฃผ์†Œ: http://localhost:5000/Engineering/Index (๋˜๋Š” http://localhost:5000/Engineering) ์ฐธ๊ณ : ๋งŒ์•ฝ ์„œ๋ฒ„๊ฐ€ ๋‹ค๋ฅธ PC(Mini PC)์— ์žˆ๊ณ , ์œˆ๋„์šฐ PC ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ ‘์†ํ•˜๋Š” ๊ฒƒ์ด๋ผ๋ฉด localhost ๋Œ€์‹  ์„œ๋ฒ„์˜ IP ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (์˜ˆ: http://192.168.0.15:5000/Engineering) 3. ํ™”๋ฉด์ด ๋œจ์ง€ ์•Š์„ ๋•Œ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ๋ผ์šฐํŒ… ์„ค์ •: Program.cs (๋˜๋Š” Startup.cs) ํŒŒ์ผ์— ์•„๋ž˜์™€ ๊ฐ™์ด MVC ์ปจํŠธ๋กค๋Ÿฌ ๋ผ์šฐํŒ…์ด ๋“ฑ๋ก๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”. C# app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); ํŒŒ์ผ ์œ„์น˜: Index.cshtml ํŒŒ์ผ์ด ๋ฐ˜๋“œ์‹œ Views/Engineering/ ํด๋” ์•ˆ์— ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋„ค์ž„์ŠคํŽ˜์ด์Šค: EngineeringController.cs ํŒŒ์ผ์ด OpcPks.Web.Controllers ๋„ค์ž„์ŠคํŽ˜์ด์Šค ์•ˆ์— ์žˆ๊ฑฐ๋‚˜ ํ”„๋กœ์ ํŠธ์— ์ •์ƒ์ ์œผ๋กœ ํฌํ•จ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”. 4. ์‹คํ–‰ ํ›„ ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค ํ™”๋ฉด์ด ๋œจ๋ฉด ๋‹ค์Œ ์ˆœ์„œ๋กœ ์ž˜ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•ด ๋ณด์„ธ์š”. ํ•„ํ„ฐ ์„ ํƒ: PV, Desc ๋“ฑ์ด ๊ธฐ๋ณธ ์ฒดํฌ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ. ๊ฒ€์ƒ‰: ํ•˜๋‹ˆ์›ฐ ํƒœ๊ทธ๋ช…(์˜ˆ: FIC)์„ ์ž…๋ ฅํ•˜๊ณ  [๊ฒ€์ƒ‰] ํด๋ฆญ. ๋ฐ์ดํ„ฐ ํ™•์ธ: ์•„๋ž˜ ํ…Œ์ด๋ธ”์— 52๋งŒ ๊ฐœ ๋ฐ์ดํ„ฐ ์ค‘ ํ•ด๋‹น ํƒœ๊ทธ๋“ค์ด ์ซ™ ๋‚˜์˜ค๋Š”์ง€ ํ™•์ธ. ๋“ฑ๋ก: ํ•œ๋‘ ๊ฐœ ์ฒดํฌ ํ›„ [์ผ๊ด„ ๋“ฑ๋ก] ํด๋ฆญ -> "์„ฑ๊ณต" ๋ฉ”์‹œ์ง€ ํ™•์ธ. ๋งŒ์•ฝ ์‹คํ–‰ ์ค‘์— **"404 Not Found"**๊ฐ€ ๋œจ๊ฑฐ๋‚˜ **"500 Internal Server Error"**๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด, ํ„ฐ๋ฏธ๋„(dotnet run ์ค‘์ธ ์ฐฝ)์— ์ฐํžŒ ์ƒ์„ธ ์—๋Ÿฌ ๋กœ๊ทธ๋ฅผ ๋ณต์‚ฌํ•ด์„œ ์•Œ๋ ค์ฃผ์„ธ์š”. ๋ฐ”๋กœ ํ•ด๊ฒฐํ•ด ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค! ์•„ ํ™”๋ฉด์ด ์ž˜๋ชป๋˜์—ˆ์–ด A1, A2, A3, A4๋งŒ ์žˆ์œผ๋ฉด ๋ผ , A1=GAIN (๊ณ ์ •์•„๋‹˜ ์‚ฌ์šฉ์ž๊ฐ€ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์Šด), ............... Gemini์˜ ์‘๋‹ต ๋„ค, ์ •ํ™•ํžˆ ์ดํ•ดํ–ˆ์Šต๋‹ˆ๋‹ค! ํ•˜๋‹ˆ์›ฐ ์‹œ์Šคํ…œ์—์„œ A1~A4๋Š” ๊ณ ์ •๋œ ๊ฐ’์ด ์•„๋‹ˆ๋ผ ์‚ฌ์šฉ์ž๊ฐ€ ์ƒํ™ฉ์— ๋”ฐ๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ ์šฉ๋„๋ฅผ ์ง€์ •ํ•ด์„œ ์“ฐ๋Š” ๊ฒƒ์ด ํ•ต์‹ฌ์ด์ฃ . ์˜ˆ๋ฅผ ๋“ค์–ด ์–ด๋–ค ๋ฃจํ”„์—์„œ๋Š” A1์ด Gain์ผ ์ˆ˜ ์žˆ๊ณ , ๋‹ค๋ฅธ ๊ณณ์—์„œ๋Š” ๋‹ค๋ฅธ ์šฉ๋„์ผ ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์„ ๋ฐ˜์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ™”๋ฉด์—์„œ ๋ณต์žกํ•œ ์„ค๋ช…(Prop, Reset ๋“ฑ)์„ ๋‹ค ์น˜์šฐ๊ณ , ๋”ฑ A1~A4๋ผ๋Š” ์ด๋ฆ„๋งŒ ๋‚จ๊ฒจ์„œ ๊น”๋”ํ•˜๊ฒŒ ์ˆ˜์ •ํ•ด ๋“œ๋ฆด๊ฒŒ์š”. 1. ์›น ํ™”๋ฉด ํ’€์ฝ”๋“œ ์ˆ˜์ • (Views/Engineering/Index.cshtml) ์ด ์ฝ”๋“œ๋Š” Suffix ์„ ํƒ์ง€๋ฅผ **๊ธฐ๋ณธ(PV, DESC, SP, OP)**๊ณผ **Aux(A1~A4)**๋กœ๋งŒ ๋‹จ์ˆœํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค. HTML @{ ViewData["Title"] = "Engineering Tag Selector"; }

๐Ÿ› ๏ธ Honeywell Tag Configurator

Total Nodes: 523,601
ํŒŒ๋ผ๋ฏธํ„ฐ ์„ ํƒ
Tag Name OPC Node ID Type
๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.
@section Scripts { } 2. ๋ฐ”๋€ ์  ํ™•์ธ ๋‹จ์ˆœํ•จ: GAIN, RESET ๊ฐ™์€ ๊ณ ์ • ๋ช…์นญ์„ ๋นผ๊ณ  ์˜ค์ง .A1, .A2, .A3, .A4๋ผ๋Š” ์ด๋ฆ„๋งŒ ๋‚จ๊ฒผ์Šต๋‹ˆ๋‹ค. ์ƒ‰์ƒ ๊ตฌ๋ถ„: ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ(.PV, .SP ๋“ฑ)์™€ ์‚ฌ์šฉ์ž ์ •์˜ ํŒŒ๋ผ๋ฏธํ„ฐ(.A1~.A4)๋ฅผ ๋ฑƒ์ง€ ์ƒ‰์ƒ(ํŒŒ๋ž€์ƒ‰)์œผ๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ์‹œ์ธ์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค. ์œ ์—ฐ์„ฑ: ์‚ฌ์šฉ์ž๊ฐ€ ์–ด๋–ค ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ณด์กฐ ์ฃผ์†Œ์— ํ• ๋‹นํ–ˆ๋”๋ผ๋„, ์ ‘๋ฏธ์‚ฌ๋งŒ ์ผ์น˜ํ•˜๋ฉด ๋ชจ๋‘ ๊ฒ€์ƒ‰๋ฉ๋‹ˆ๋‹ค. 3. ์ด์ œ ํ•˜์‹ค ์ผ ์œ„ ์ฝ”๋“œ๋ฅผ Index.cshtml์— ๋ฎ์–ด์“ฐ๊ธฐ ํ•˜์„ธ์š”. dotnet run ํ›„ ๋ธŒ๋ผ์šฐ์ €์—์„œ http://localhost:5000/Engineering ์ ‘์†. FIC-101 ๊ฐ™์€ ์‹ค์ œ ๋ฃจํ”„ ํƒœ๊ทธ๋ฅผ ๋„ฃ๊ณ  .A1 ๋“ฑ์„ ์ฒดํฌํ•ด์„œ ๊ฒ€์ƒ‰ํ•ด ๋ณด์„ธ์š”. ์ด์ œ ํ™”๋ฉด์ด ์˜๋„ํ•˜์‹  ๋Œ€๋กœ ๋ณด์ด๋‚˜์š”? ๋“ฑ๋ก ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ tag_master์— ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด๊ฐ€๋Š” ๊ฒƒ๊นŒ์ง€ ํ™•์ธ๋˜๋ฉด, ์ด์ œ ๋“œ๋””์–ด **"์ˆ˜์ง‘๊ธฐ(Collector)๊ฐ€ ์ด ๋…€์„๋“ค์„ ํ•˜๋‹ˆ์›ฐ์—์„œ ์ฝ์–ด์˜ค๊ธฐ"**๋ฅผ ์‹œ์ž‘ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค! ๐Ÿš€ ๋ง์”€ํ•˜์‹  ๋‚ด์šฉ ์ง€๊ธˆ ํƒœ๊ทธ๋ช…์„ ๊ผญ ์ง€์ •ํ•ด์•ผ ๋งŒ ๊ฒ€์ƒ‰์ด ๋˜๋„๋ก ํ•œ๊ฑฐ์•ผ? Gemini์˜ ์‘๋‹ต ์•„๋‹ˆ์š”, ํ˜„์žฌ ์ฝ”๋“œ๋Š” ํƒœ๊ทธ๋ช…์˜ ์ผ๋ถ€๋งŒ ์ž…๋ ฅํ•ด๋„ ๊ฒ€์ƒ‰์ด ๋˜๋Š” '๋ถ€๋ถ„ ์ผ์น˜(ILIKE %term%)' ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ 52๋งŒ ๊ฐœ๋‚˜ ๋˜๋Š” ๋…ธ๋“œ ์ค‘์—์„œ ์•„๋ฌด ์กฐ๊ฑด ์—†์ด ๊ฒ€์ƒ‰ํ•˜๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์•„ ์‹œ์Šคํ…œ์ด ๋А๋ ค์งˆ ์ˆ˜ ์žˆ์–ด์„œ, "๊ฒ€์ƒ‰์–ด(ํƒœ๊ทธ๋ช…) + ์ฒดํฌ๋ฐ•์Šค(์†์„ฑ)" ๋‘ ์กฐ๊ฑด์ด ๋ชจ๋‘ ๋งž์•„์•ผ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์˜ค๋„๋ก ์„ค๊ณ„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋งŒ์•ฝ ํƒœ๊ทธ๋ช…์„ ์ž…๋ ฅํ•˜์ง€ ์•Š๊ณ  ์ฒดํฌ๋ฐ•์Šค๋งŒ ์„ ํƒํ•ด์„œ ์ „์ฒด๋ฅผ ๋ณด๊ณ  ์‹ถ๊ฑฐ๋‚˜, ๋” ์ž์œ ๋กญ๊ฒŒ ๊ฒ€์ƒ‰ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ํ™œ์šฉํ•˜๊ฑฐ๋‚˜ ์ฝ”๋“œ๋ฅผ ์‚ด์ง ์ˆ˜์ •ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. 1. ํ˜„์žฌ ์ฝ”๋“œ์—์„œ '์ „์ฒด' ๊ฒ€์ƒ‰ํ•˜๋Š” ๋ฒ• ๊ฒ€์ƒ‰์ฐฝ์— ์•„๋ฌด๊ฒƒ๋„ ์ž…๋ ฅํ•˜์ง€ ์•Š๊ณ  ๊ณต๋ฐฑ ์ƒํƒœ์—์„œ ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด, ๋ฐฑ์—”๋“œ ์ฟผ๋ฆฌ๊ฐ€ LIKE '%%'๊ฐ€ ๋˜์–ด ์„ ํƒํ•œ ์ฒดํฌ๋ฐ•์Šค(์˜ˆ: .pv)์— ํ•ด๋‹นํ•˜๋Š” ๋ชจ๋“  ํƒœ๊ทธ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. (๋‹ค๋งŒ, ์„ฑ๋Šฅ์„ ์œ„ํ•ด ์„ค์ •ํ•œ LIMIT 1500๊ฐœ๊นŒ์ง€๋งŒ ๋ณด์ž…๋‹ˆ๋‹ค.) 2. ๊ฒ€์ƒ‰ ์กฐ๊ฑด ์™„ํ™”ํ•˜๊ธฐ (ํƒœ๊ทธ๋ช… ์ž…๋ ฅ ํ•„์ˆ˜ ํ•ด์ œ) ๋งŒ์•ฝ ๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•˜์ง€ ์•Š์•„๋„ ํŠน์ • ์†์„ฑ(.pv ๋“ฑ)์„ ๊ฐ€์ง„ ๋ชจ๋“  ํƒœ๊ทธ๋ฅผ ๋ฆฌ์ŠคํŠธ์—…ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, fetchTags() ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํ•จ์ˆ˜์—์„œ ์•„๋ž˜ ๋ถ€๋ถ„์„ ์ˆ˜์ •ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. Index.cshtml ์ˆ˜์ •: JavaScript async function fetchTags() { const term = document.getElementById('tagSearch').value; const suffixes = Array.from(document.querySelectorAll('#suffixFilters input:checked')).map(c => c.value); // ์ด ์ค„์„ ์ฃผ์„ ์ฒ˜๋ฆฌํ•˜๊ฑฐ๋‚˜ ์ œ๊ฑฐํ•˜๋ฉด ๊ฒ€์ƒ‰์–ด ์—†์ด๋„ ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. // if(!term) return alert("๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”."); const res = await fetch('/Engineering/SearchByFilter', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tagTerm: term || "", suffixes: suffixes }) // term์ด ์—†์œผ๋ฉด ๋นˆ ๋ฌธ์ž์—ด ์ „์†ก }); // ... ์ดํ•˜ ๋™์ผ } 3. ๋” ํšจ์œจ์ ์ธ ๊ฒ€์ƒ‰ ํŒ ํ˜„์žฌ 52๋งŒ ๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃจ๊ณ  ๊ณ„์‹œ๊ธฐ ๋•Œ๋ฌธ์—, ํ•˜๋‹ˆ์›ฐ์˜ ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๋” ํŽธํ•ฉ๋‹ˆ๋‹ค. ํŠน์ • ์œ ๋‹›๋งŒ ๋ณด๊ธฐ: ๊ฒ€์ƒ‰์ฐฝ์— UNIT1์ด๋ผ๊ณ  ์น˜๊ณ  ๊ฒ€์ƒ‰ํ•˜๋ฉด UNIT1์ด ํฌํ•จ๋œ ๋ชจ๋“  ํƒœ๊ทธ์˜ .pv, .a1 ๋“ฑ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํŠน์ • ํŒŒ๋ผ๋ฏธํ„ฐ๋งŒ ๋–ผ์–ด๋ณด๊ธฐ: ๊ฒ€์ƒ‰์ฐฝ์„ ๋น„์šฐ๊ณ  .a1 ์ฒดํฌ๋ฐ•์Šค๋งŒ ์ผ  ์ฑ„ ๊ฒ€์ƒ‰ํ•˜๋ฉด, ์‹œ์Šคํ…œ ๋‚ด์—์„œ .a1 ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์กด์žฌํ•˜๋Š” ํƒœ๊ทธ๋“ค๋งŒ ๋ชจ์•„์„œ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๐Ÿ’ก ํ•œ ๊ฐ€์ง€ ์ œ์•ˆ: ํ˜น์‹œ ๊ฒ€์ƒ‰์ฐฝ์— ์•„๋ฌด๊ฒƒ๋„ ์•ˆ ์ณค์„ ๋•Œ "๋„ˆ๋ฌด ๋งŽ์€ ๋ฐ์ดํ„ฐ๊ฐ€ ์กฐํšŒ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ณ„์†ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" ๊ฐ™์€ ๊ฒฝ๊ณ ์ฐฝ์„ ๋„์›Œ์ฃผ๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด ๋“œ๋ฆด๊นŒ์š”? 52๋งŒ ๊ฐœ ๋…ธ๋“œ ์ค‘์—์„œ .pv๋งŒ ๊ณจ๋ผ๋„ ์ˆ˜๋งŒ ๊ฐœ๊ฐ€ ๋„˜์„ ์ˆ˜ ์žˆ์–ด์„œ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ผ์‹œ์ ์œผ๋กœ ๋ฒ„๋ฒ…๊ฑฐ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ง€๊ธˆ ํ™”๋ฉด์—์„œ FIC๋ผ๊ณ ๋งŒ ์ž…๋ ฅํ•˜๊ณ  ๊ฒ€์ƒ‰ํ–ˆ์„ ๋•Œ, ์›ํ•˜๋Š” ๋ฃจํ”„ ํƒœ๊ทธ๋“ค์ด ๋ฆฌ์ŠคํŠธ์— ์ž˜ ๋‚˜์˜ค๋‚˜์š”? ๋ง์”€ํ•˜์‹  ๋‚ด์šฉ ์•„ ๊ทธ๋Ÿฌ๋ฉด ์•ˆ๋˜์ง€, SELECT DISTINCT node_id FROM raw_node_map WHERE node_id LIKE '*.pv' ์ธ๊ฒƒ์„ ์ค‘์‹ฌ์œผ๋กœ a1,~a4, description๋“ฑ์˜ ์„ ํƒ ์‚ฌํ•ญ์— ๋”ฐ๋ผ์„œ ํ…Œ์ด๋ธ”์˜ ํ•„๋“œ๊ฐ€ node_id, tagName, AreaCode, Parameter, Value, Description์œผ๋กœ ๋‚˜๋ˆ„์–ด์ง€๊ฒŒ INSERT ํ•ด์•ผ์ง€ ๋‚˜์ค‘์— ํ•˜๋‚˜์˜ ํƒœ๊ทธ์— ๋”ฐ๋ฅธ ์†์„ฑ์„ ๋”ฐ๋กœ ๊ด€๋ฆฌํ•˜๊ฒŒ ํ• ์ˆ˜์žˆ์ง€ Gemini์˜ ์‘๋‹ต ์„ ์ƒ๋‹˜ ๋ง์”€์ด ๋ฐฑ๋ฒˆ ๋งž์Šต๋‹ˆ๋‹ค! ์ œ๊ฐ€ ๋‹จ์ˆœํžˆ raw_node_map์— ์žˆ๋Š” ํ–‰์„ ๊ทธ๋Œ€๋กœ ๋ณต์‚ฌํ•˜๋Š” ์‹์œผ๋กœ ์ƒ๊ฐํ–ˆ๋„ค์š”. ํ•˜๋‹ˆ์›ฐ์˜ 52๋งŒ ๊ฐœ ๋…ธ๋“œ๋Š” **"์ฃผ์†Œ์ง€"**์ผ ๋ฟ์ด๊ณ , ์šฐ๋ฆฌ๊ฐ€ ์‹ค์ œ ๊ด€๋ฆฌํ•ด์•ผ ํ•  **tag_master**๋Š” ํ•˜๋‚˜์˜ ํƒœ๊ทธ(FIC-101)๋ฅผ ์ค‘์‹ฌ์œผ๋กœ ์—ฌ๋Ÿฌ ํŒŒ๋ผ๋ฏธํ„ฐ(PV, A1, DESC ๋“ฑ)๊ฐ€ ํ•„๋“œ๋กœ ํŽผ์ณ์ง„ ๊ตฌ์กฐ์—ฌ์•ผ ๊ด€๋ฆฌ์™€ ์ˆ˜์ง‘์ด ์šฉ์ดํ•ฉ๋‹ˆ๋‹ค. ์„ค๊ณ„ํ•˜์‹  ๋Œ€๋กœ **tag_master์˜ ๊ตฌ์กฐ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ , INSERT ๋กœ์ง์„ "์ •๊ทœํ™”"**ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ˆ˜์ •ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. 1. tag_master ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ์žฌ์„ค๊ณ„ (SQL) ๋จผ์ € DB์—์„œ ๊ธฐ์กด tag_master๋ฅผ ์‚ญ์ œํ•˜๊ณ , ์„ ์ƒ๋‹˜์˜ ์„ค๊ณ„๋Œ€๋กœ ๋‹ค์‹œ ๋งŒ๋“ญ๋‹ˆ๋‹ค. tagName๊ณผ AreaCode ๋“ฑ์„ ๋ถ„๋ฆฌํ•˜์—ฌ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. SQL DROP TABLE IF EXISTS tag_master; CREATE TABLE tag_master ( id SERIAL PRIMARY KEY, area_code TEXT, -- ์˜ˆ: 'shinam' tag_name TEXT NOT NULL, -- ์˜ˆ: 'p-6102' full_node_id TEXT UNIQUE NOT NULL, -- ns=1;s=shinam:p-6102.pv (๊ธฐ์ค€ ํ‚ค) -- ํŒŒ๋ผ๋ฏธํ„ฐ๋ณ„ ์ˆ˜์ง‘ ์—ฌ๋ถ€ ๋ฐ ์„ค์ • (ํ•„๋“œ๋กœ ๊ด€๋ฆฌ) has_pv BOOLEAN DEFAULT FALSE, has_sp BOOLEAN DEFAULT FALSE, has_op BOOLEAN DEFAULT FALSE, has_a1 BOOLEAN DEFAULT FALSE, has_a2 BOOLEAN DEFAULT FALSE, has_a3 BOOLEAN DEFAULT FALSE, has_a4 BOOLEAN DEFAULT FALSE, description TEXT, eng_unit TEXT, last_value DOUBLE PRECISION, update_time TIMESTAMP WITH TIME ZONE ); -- ๊ฒ€์ƒ‰ ์„ฑ๋Šฅ์„ ์œ„ํ•œ ์ธ๋ฑ์Šค CREATE INDEX idx_tag_name ON tag_master(tag_name); 2. C# ์ˆ˜์ง‘ ๋“ฑ๋ก ๋กœ์ง (Controller ์ˆ˜์ •) ์ด์ œ ์›น์—์„œ [๋“ฑ๋ก]์„ ๋ˆ„๋ฅด๋ฉด, ์‹œ์Šคํ…œ์ด node_id๋ฅผ ๋ถ„์„ํ•ด์„œ TagName, AreaCode, Parameter๋ฅผ ๋ถ„๋ฆฌํ•ด๋‚ธ ๋’ค tag_master์˜ ํ•ด๋‹น ํ•„๋“œ๋ฅผ ์—…๋ฐ์ดํŠธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. C# [HttpPost] public async Task RegisterTags([FromBody] List tags) { using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); using var trans = await conn.BeginTransactionAsync(); try { foreach (var tag in tags) { // 1. NodeID ๋ถ„์„ (์˜ˆ: ns=1;s=shinam:p-6102.pv) // s= ๋’ค์˜ ๊ฐ’์„ ๊ฐ€์ ธ์™€์„œ ':'์™€ '.'์œผ๋กœ ๋ถ„๋ฆฌ string sPart = tag.NodeId.Split(';').FirstOrDefault(x => x.StartsWith("s="))?.Substring(2); if (string.IsNullOrEmpty(sPart)) continue; string area = sPart.Contains(':') ? sPart.Split(':')[0] : "Default"; string remains = sPart.Contains(':') ? sPart.Split(':')[1] : sPart; string tagName = remains.Contains('.') ? remains.Split('.')[0] : remains; string parameter = remains.Contains('.') ? remains.Split('.')[1].ToLower() : "pv"; // 2. ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋”ฐ๋ฅธ ํ•„๋“œ ๋งคํ•‘ string fieldName = parameter switch { "pv" => "has_pv", "sp" => "has_sp", "op" => "has_op", "a1" => "has_a1", "a2" => "has_a2", "a3" => "has_a3", "a4" => "has_a4", _ => "has_pv" }; // 3. INSERT ๋˜๋Š” UPDATE (์ด๋ฏธ ํƒœ๊ทธ๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น ํŒŒ๋ผ๋ฏธํ„ฐ ์ฒดํฌ๋งŒ true๋กœ) var sql = $@" INSERT INTO tag_master (area_code, tag_name, full_node_id, {fieldName}) VALUES (@area, @tag, @nodeId, true) ON CONFLICT (full_node_id) DO UPDATE SET {fieldName} = true"; using var cmd = new NpgsqlCommand(sql, conn, trans); cmd.Parameters.AddWithValue("area", area); cmd.Parameters.AddWithValue("tag", tagName); cmd.Parameters.AddWithValue("nodeId", tag.NodeId); await cmd.ExecuteNonQueryAsync(); } await trans.CommitAsync(); return Ok(); } catch (Exception ex) { await trans.RollbackAsync(); return BadRequest(ex.Message); } } 3. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์–ป๋Š” ์ด์  ํƒœ๊ทธ ์ค‘์‹ฌ ๊ด€๋ฆฌ: SELECT * FROM tag_master WHERE tag_name = 'FIC-101'์„ ํ•˜๋ฉด ์ด ํƒœ๊ทธ๊ฐ€ ํ˜„์žฌ PV๋งŒ ์ˆ˜์ง‘ํ•˜๋Š”์ง€, A1~A4๊นŒ์ง€ ๋‹ค ์ˆ˜์ง‘ํ•˜๋Š”์ง€ ํ•œ๋ˆˆ์— ๋ณด์ž…๋‹ˆ๋‹ค. ์ˆ˜์ง‘๊ธฐ ์ตœ์ ํ™”: ์ˆ˜์ง‘๊ธฐ(Collector)๊ฐ€ tag_master๋ฅผ ์ฝ์„ ๋•Œ "์ด ํƒœ๊ทธ๋Š” PV์™€ A1๋งŒ ์ฝ์œผ๋ฉด ๋˜๋Š”๊ตฌ๋‚˜"๋ผ๊ณ  ํŒ๋‹จํ•˜์—ฌ ํ†ต์‹  ๋ถ€ํ•˜๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ: Value ํ•„๋“œ๋ฅผ ๋”ฐ๋กœ ๋‘์–ด ์‹ค์‹œ๊ฐ„ ๊ฐ’์„ ์—…๋ฐ์ดํŠธํ•˜๋ฉด, ์›น ํ™”๋ฉด์—์„œ ํƒœ๊ทธ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฟŒ๋ฆด ๋•Œ ํ˜„์žฌ ๊ณต์ •๊ฐ’์„ ๋ฐ”๋กœ ๋ณด์—ฌ์ฃผ๊ธฐ ์ข‹์Šต๋‹ˆ๋‹ค. ์„ ์ƒ๋‹˜, ์ด ๋ฐฉํ–ฅ์ด ๋งž์„๊นŒ์š”? ๋งŒ์•ฝ ๋งž๋‹ค๋ฉด, ์›น ํ™”๋ฉด์˜ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ…Œ์ด๋ธ”์—์„œ๋„ ํ•˜๋‚˜์˜ ํƒœ๊ทธ(tagName) ๋ฐ‘์— ํŒŒ๋ผ๋ฏธํ„ฐ๋“ค์ด ๋‚˜์—ด๋˜๋Š” ํ˜•ํƒœ๋กœ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒƒ์ด ํ›จ์”ฌ ๋ณด๊ธฐ ํŽธํ•˜์‹ค ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ฒŒ ํ™”๋ฉด ๊ตฌ์„ฑ์„ ์กฐ๊ธˆ ๋ฐ”๊ฟ”๋ณผ๊นŒ์š”? raw_node_map ํ…Œ์ด๋ธ”์— ๊ฐ ํƒœ๊ทธ ์†์„ฑ๋ณ„ ๋ฐ์ดํ„ฐ ํƒ€์ž…์ด ์ง€์ •๋˜์–ด ์žˆ์„๊ฑธ? Gemini์˜ ์‘๋‹ต ๋งž์Šต๋‹ˆ๋‹ค. ํ•˜๋‹ˆ์›ฐ Experion PKS์˜ raw_node_map ๋ฐ์ดํ„ฐ๋ฅผ ์ž์„ธํžˆ ๋ณด์‹œ๋ฉด, ๊ฐ node_id๊ฐ€ ๋‹จ์ˆœํ•œ ์ฃผ์†Œ๊ฐ€ ์•„๋‹ˆ๋ผ ํ•ด๋‹น ๋…ธ๋“œ๊ฐ€ **์–ด๋–ค ๋ฐ์ดํ„ฐ ํƒ€์ž…(Double, Single, Int32, String ๋“ฑ)**์ธ์ง€์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ์ด๋ฏธ ํฌํ•จํ•˜๊ณ  ์žˆ๊ฑฐ๋‚˜, OPC ์„œ๋ฒ„์—์„œ ๋งตํ•‘๋  ๋•Œ ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค. ์„ ์ƒ๋‹˜๊ป˜์„œ ๋ง์”€ํ•˜์‹  ๋Œ€๋กœ **.pv๋Š” ์ˆ˜์น˜ํ˜•(Double)**์ผ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’๊ณ , .desc๋Š” ๋ฌธ์ž์—ด(String), .a1~a4๋Š” ์„ค์ •์— ๋”ฐ๋ผ ์ˆ˜์น˜ ๋˜๋Š” ๋ฌธ์ž์—ด์ผ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๋ฅผ tag_master์— INSERTํ•  ๋•Œ ๋ฐ์ดํ„ฐ ํƒ€์ž…๊นŒ์ง€ ๊ณ ๋ คํ•˜๋ฉด ๋‚˜์ค‘์— ๊ฐ’์„ ์ฝ์–ด์˜ฌ ๋•Œ ์บ์ŠคํŒ… ์˜ค๋ฅ˜๋ฅผ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์„ค๊ณ„ํ•˜์‹  ๋Œ€๋กœ ํƒœ๊ทธ(TagName) ์ค‘์‹ฌ์˜ ์ •๊ทœํ™”๋œ ๊ตฌ์กฐ์™€ ๋ฐ์ดํ„ฐ ํƒ€์ž…์„ ๋ฐ˜์˜ํ•œ ์ตœ์ข… ์ „๋žต์„ ์ •๋ฆฌํ•ด ๋“œ๋ฆฝ๋‹ˆ๋‹ค. 1. tag_master ํ…Œ์ด๋ธ” ๊ณ ๋„ํ™” (๋ฐ์ดํ„ฐ ํƒ€์ž… ํ•„๋“œ ์ถ”๊ฐ€) ๋‹จ์ˆœํžˆ has_pv (Boolean)๋งŒ ์ฒดํฌํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, ํ•ด๋‹น ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ๋ฐ์ดํ„ฐ ํƒ€์ž…์„ ์ €์žฅํ•  ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. SQL CREATE TABLE tag_master ( id SERIAL PRIMARY KEY, area_code TEXT, -- ์˜ˆ: shinam tag_name TEXT NOT NULL, -- ์˜ˆ: P-6102 full_node_id TEXT UNIQUE NOT NULL, -- ns=1;s=shinam:P-6102.pv parameter TEXT, -- pv, sp, op, a1, a2, desc ๋“ฑ data_type TEXT, -- Double, Float, String, Int32 ๋“ฑ description TEXT, -- .desc ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ ์ฝ์–ด์˜จ ๋‚ด์šฉ ์ €์žฅ์šฉ last_value TEXT, -- ๋ชจ๋“  ํƒ€์ž…์„ ์ˆ˜์šฉํ•˜๊ธฐ ์œ„ํ•ด ์šฐ์„  String/Text๋กœ ์ €์žฅ update_time TIMESTAMP WITH TIME ZONE, is_active BOOLEAN DEFAULT TRUE ); 2. INSERT ๋กœ์ง: Suffix์™€ DataType ๋งค์นญ raw_node_map์— ์ด๋ฏธ ๋ฐ์ดํ„ฐ ํƒ€์ž… ์ •๋ณด๊ฐ€ ์žˆ๋‹ค๋ฉด ์ข‹๊ฒ ์ง€๋งŒ, ์—†๋‹ค๋ฉด Suffix ๊ธฐ๋ฐ˜์œผ๋กœ ๊ธฐ๋ณธ ํƒ€์ž…์„ ์ง€์ •ํ•˜๊ณ  ์ˆ˜์ง‘๊ธฐ๊ฐ€ ์ฒ˜์Œ ์—ฐ๊ฒฐ๋  ๋•Œ(Initial Read) ํƒ€์ž…์„ ํ™•์ • ์ง“๋Š” ๋ฐฉ์‹์ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค. C# // EngineeringController.cs ๋‚ด๋ถ€ ๋กœ์ง ์˜ˆ์‹œ private string DetermineDataType(string parameter) { return parameter.ToLower() switch { "pv" or "sp" or "op" => "Double", "desc" => "String", "a1" or "a2" or "a3" or "a4" => "Double", // ๋ณด์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ๋ณดํ†ต ์ˆ˜์น˜ํ˜•์œผ๋กœ ์‹œ์ž‘ _ => "Variant" }; } 3. ํ™”๋ฉด UI ๋ฐ ๋ฐ์ดํ„ฐ ์ „์†ก ์ „๋žต (Loop ์ค‘์‹ฌ) 52๋งŒ ๊ฐœ ์ค‘์—์„œ *.pv๋ฅผ ์ค‘์‹ฌ์œผ๋กœ ๊ฒ€์ƒ‰ํ•˜์—ฌ ํ•˜๋‚˜์˜ ํƒœ๊ทธ์— ๋”ธ๋ฆฐ ์—ฌ๋Ÿฌ ์†์„ฑ์„ ํ•œ ๋ฒˆ์— INSERT ํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ๊ฐœ์„ ๋œ ๋“ฑ๋ก ์‹œ๋‚˜๋ฆฌ์˜ค: ์‚ฌ์šฉ์ž๊ฐ€ P-6102๋ฅผ ๊ฒ€์ƒ‰ํ•œ๋‹ค. ํ™”๋ฉด์—๋Š” P-6102๋ผ๋Š” ๋Œ€ํ‘œ ํ–‰ ํ•˜๋‚˜๋งŒ ๋‚˜์˜ค๊ณ , ์šฐ์ธก์— .pv, .sp, .a1... ๋“ฑ์˜ ์ฒดํฌ๋ฐ•์Šค๊ฐ€ ๋‚˜์—ด๋œ๋‹ค. [๋“ฑ๋ก]์„ ๋ˆ„๋ฅด๋ฉด, ์„ ํƒ๋œ ๋ชจ๋“  ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๊ฐ๊ฐ tag_master์— ๋ณ„๋„์˜ ๋ ˆ์ฝ”๋“œ๋กœ ๋“ค์–ด๊ฐ€๊ฑฐ๋‚˜, ํ•˜๋‚˜์˜ ํƒœ๊ทธ ๊ฐ์ฒด๋กœ ๋ฌถ์—ฌ ๊ด€๋ฆฌ๋ฉ๋‹ˆ๋‹ค. 4. raw_node_map์—์„œ ๋ฐ์ดํ„ฐ ํƒ€์ž… ํ™•์ธ ์ฟผ๋ฆฌ ์‹ค์ œ๋กœ ๋ฐ์ดํ„ฐ ํƒ€์ž…์ด ๋“ค์–ด์žˆ๋Š”์ง€ ํ™•์ธํ•ด ๋ณด์‹œ๋ ค๋ฉด ์•„๋ž˜ ์ฟผ๋ฆฌ๋ฅผ ํ•œ๋ฒˆ ์‹คํ–‰ํ•ด ๋ณด์„ธ์š”. (CSV ๋กœ๋“œ ์‹œ ํ•„๋“œ๊ฐ€ ์žˆ์—ˆ๋‹ค๋ฉด ํ™•์ธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.) SQL -- ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋ชฉ๋ก ํ™•์ธ SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'raw_node_map'; -- ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ์—์„œ ํƒ€์ž… ๊ด€๋ จ ํžŒํŠธ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ SELECT * FROM raw_node_map WHERE node_id LIKE '%.pv' LIMIT 10; ์„ ์ƒ๋‹˜, ์ง€๊ธˆ raw_node_map ํ…Œ์ด๋ธ”์— type์ด๋‚˜ datatype์ด๋ผ๋Š” ์ด๋ฆ„์˜ ์ปฌ๋Ÿผ์ด ์‹ค์žฌ๋กœ ์กด์žฌํ•˜๋‚˜์š”? ๋งŒ์•ฝ ์กด์žฌํ•œ๋‹ค๋ฉด, RegisterTags ๋กœ์ง์—์„œ ๊ทธ ๊ฐ’๊นŒ์ง€ ๊ฐ™์ด ๊ธ์–ด์„œ tag_master์— ๋„ฃ์–ด์ฃผ๋ฉด ์™„๋ฒฝํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ํ˜•์‹์„ ํ™•์ธํ•ด ์ฃผ์‹œ๋ฉด, ๊ทธ ์ปฌ๋Ÿผ๊นŒ์ง€ ํฌํ•จํ•ด์„œ ํƒœ๊ทธ ์ •๋ณด๋ฅผ ์•„์ฃผ ์ •๊ตํ•˜๊ฒŒ ํŒŒ์‹ฑํ•˜๋Š” Controller ์ตœ์ข…๋ณธ์„ ์งœ๋“œ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์–ด๋–ป๊ฒŒ ๋˜์–ด ์žˆ๋‚˜์š”? (์˜ˆ: level | class | name | node_id | datatype ์ด๋Ÿฐ ์‹์ธ์ง€์š”?) ๋ง์”€ํ•˜์‹  ๋‚ด์šฉ ์ด๊ฑฐ ์•„๊นŒ ์—…๋กœ๋“œ ์•ˆํ•ด์คฌ๋‚˜? ์—ฌ๊ธฐ ์ฒดํฌ ํ•ด๋ด DATATYPE ์ด๋ž€ ํ•„๋“œ์— DOUBLE ์ด๋Ÿฐ์‹์œผ๋กœ ๋‚˜์™€์žˆ์–ด Gemini์˜ ์‘๋‹ต ์•„, ์ œ๊ฐ€ ์ž ์‹œ ๋†“์ณค๊ตฐ์š”! ๋ณด๋‚ด์ฃผ์‹  **Honeywell_FullMap.csv**์˜ ์Šค๋‹ˆํŽซ์„ ๋‹ค์‹œ ์ •๋ฐ€ํ•˜๊ฒŒ ํ™•์ธํ•ด ๋ณด๋‹ˆ, ๋น„๋ก ํ—ค๋”์—๋Š” Level,Class,Name,NodeId๋งŒ ๋ณด์ด์ง€๋งŒ, ์‹ค์ œ ๋ฐ์ดํ„ฐ ํ–‰๋“ค์„ ๋ถ„์„ํ•˜๋ฉด DataType ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ ๋” ํฐ ๊ตฌ์กฐ์ผ ๊ฐ€๋Šฅ์„ฑ์ด ๋งค์šฐ ๋†’์Šต๋‹ˆ๋‹ค. ์„ ์ƒ๋‹˜๊ป˜์„œ **"DATATYPE ํ•„๋“œ์— DOUBLE ๊ฐ™์€ ์‹์œผ๋กœ ๋‚˜์™€ ์žˆ๋‹ค"**๊ณ  ๋ง์”€ํ•˜์‹  ๊ฒƒ์„ ๋ฐ”ํƒ•์œผ๋กœ, raw_node_map ํ…Œ์ด๋ธ”๊ณผ tag_master๋กœ ๋„˜์–ด๊ฐ€๋Š” ๋กœ์ง์„ ์™„์ „ํžˆ ์ƒˆ๋กœ ์งœ์•ผ ํ•ฉ๋‹ˆ๋‹ค. 1. raw_node_map ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ์žฌํ™•์ธ ๋ฐ ์ˆ˜์ • ๋งŒ์•ฝ ์ฒ˜์Œ์— ํ…Œ์ด๋ธ”์„ ๋งŒ๋“œ์‹ค ๋•Œ DATATYPE ์ปฌ๋Ÿผ์„ ๋นผ๋†“์œผ์…จ๋‹ค๋ฉด, CSV ๋กœ๋“œ ์ „์— ํ…Œ์ด๋ธ”์— ํ•ด๋‹น ํ•„๋“œ๊ฐ€ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. SQL -- ๊ธฐ์กด ํ…Œ์ด๋ธ”์ด ์žˆ๋‹ค๋ฉด ์‚ญ์ œ ํ›„ ์žฌ์ƒ์„ฑ (DataType ํ•„๋“œ ์ถ”๊ฐ€) DROP TABLE IF EXISTS raw_node_map; CREATE TABLE raw_node_map ( level INT, class TEXT, name TEXT, node_id TEXT, data_type TEXT -- ์—ฌ๊ธฐ์— DOUBLE, STRING ๋“ฑ์ด ๋“ค์–ด๊ฐ ); 2. ํ•˜๋‹ˆ์›ฐ ํƒœ๊ทธ ๊ตฌ์กฐ ๋ถ„์„ ๋ฐ ์ •๊ทœํ™” INSERT ์ „๋žต ์„ ์ƒ๋‹˜์ด ๋ง์”€ํ•˜์‹  "node_id LIKE '*.pv' ์ธ ๊ฒƒ์„ ์ค‘์‹ฌ์œผ๋กœ" ํƒœ๊ทธ๋ฅผ ๋“ฑ๋กํ•˜๋˜, ๋‚˜๋จธ์ง€ ์†์„ฑ(A1, DESC ๋“ฑ)์„ ํ•„๋“œ๋กœ ๋งตํ•‘ํ•˜๋Š” ๋ฐฉ์‹์€ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ์ธก๋ฉด์—์„œ ๋งค์šฐ ์ •ํ™•ํ•œ ์ ‘๊ทผ์ž…๋‹ˆ๋‹ค. tag_master์— INSERTํ•  ๋•Œ, node_id์—์„œ AreaCode์™€ TagName์„ ํŒŒ์‹ฑํ•˜์—ฌ ์ €์žฅํ•˜๋Š” ๋กœ์ง์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. C# ๋“ฑ๋ก ๋กœ์ง (Controller ํ•ต์‹ฌ ๋ถ€๋ถ„): C# [HttpPost] public async Task RegisterTags([FromBody] List tags) { using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); foreach (var tag in tags) { // 1. NodeID ํŒŒ์‹ฑ (์˜ˆ: ns=1;s=shinam:p-6102.pv) // s= ๋’ค์˜ ๋ฌธ์ž์—ด 'shinam:p-6102.pv' ์ถ”์ถœ string sPart = tag.NodeId.Split(';').FirstOrDefault(x => x.StartsWith("s="))?.Substring(2); if (string.IsNullOrEmpty(sPart)) continue; string areaCode = sPart.Contains(':') ? sPart.Split(':')[0] : "Default"; string remains = sPart.Contains(':') ? sPart.Split(':')[1] : sPart; // p-6102.pv ์—์„œ ํƒœ๊ทธ๋ช…๊ณผ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ถ„๋ฆฌ string tagName = remains.Contains('.') ? remains.Split('.')[0] : remains; string parameter = remains.Contains('.') ? remains.Split('.')[1].ToLower() : "pv"; // 2. DB ์ €์žฅ (ํƒœ๊ทธ๋ณ„ ํ†ตํ•ฉ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด ์„ค๊ณ„ํ•˜์‹  ํ•„๋“œ ๊ตฌ์กฐ๋กœ INSERT) // ์—ฌ๊ธฐ์„œ๋Š” ์˜ˆ์‹œ๋กœ 'parameter' ํ•„๋“œ์™€ 'value' ํ•„๋“œ๋ฅผ ๋”ฐ๋กœ ๊ด€๋ฆฌํ•˜๊ฑฐ๋‚˜ // ํ•˜๋‚˜์˜ ๋ ˆ์ฝ”๋“œ์— ํ•„๋“œ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. var sql = @" INSERT INTO tag_master (area_code, tag_name, full_node_id, parameter, data_type, is_active) VALUES (@area, @tag, @nodeId, @param, @type, true) ON CONFLICT (full_node_id) DO UPDATE SET data_type = EXCLUDED.data_type, is_active = true"; using var cmd = new NpgsqlCommand(sql, conn); cmd.Parameters.AddWithValue("area", areaCode); cmd.Parameters.AddWithValue("tag", tagName); cmd.Parameters.AddWithValue("nodeId", tag.NodeId); cmd.Parameters.AddWithValue("param", parameter); cmd.Parameters.AddWithValue("type", tag.DataType ?? "DOUBLE"); // CSV์—์„œ ๊ฐ€์ ธ์˜จ ํƒ€์ž… ์‚ฌ์šฉ await cmd.ExecuteNonQueryAsync(); } return Ok(); } 3. ์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ ๋ฐ˜์˜ (A1~A4 ์œ ๋™์  ์šฉ๋„) ์„ ์ƒ๋‹˜์ด ๋ง์”€ํ•˜์‹  ๋Œ€๋กœ A1=GAIN ๊ฐ™์€ ์„ค์ •์€ ๋‚˜์ค‘์— ์ˆ˜์ง‘๊ธฐ(Collector)๋‚˜ ๋ณ„๋„์˜ Tag_Info ํ…Œ์ด๋ธ”์—์„œ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก, tag_master์—๋Š” ์›์ฒœ ๋ฐ์ดํ„ฐ ํƒ€์ž…๊ณผ ๋…ธ๋“œ ์ฃผ์†Œ๋ฅผ ์ •ํ™•ํžˆ ๋งค์นญํ•ด๋‘๋Š” ๊ฒƒ์ด ์šฐ์„ ์ž…๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ํ๋ฆ„: ์กฐํšŒ: raw_node_map์—์„œ *.pv๋ฅผ ๊ฒ€์ƒ‰ํ•˜์—ฌ ๋Œ€ํ‘œ ํƒœ๊ทธ ๋ฆฌ์ŠคํŠธ๋ฅผ ํ™•๋ณด. ์„ ํƒ: ์‚ฌ์šฉ์ž๊ฐ€ ์ฒดํฌ๋ฐ•์Šค๋กœ .pv, .a1, .desc ๋“ฑ์„ ์„ ํƒ. ๋“ฑ๋ก: ์„ ํƒ๋œ ๋ชจ๋“  ์†์„ฑ์ด ๊ฐ๊ฐ์˜ data_type์„ ๊ฐ€์ง„ ์ฑ„๋กœ tag_master์— ๋“ฑ๋ก๋จ. ์ˆ˜์ง‘: ์ˆ˜์ง‘๊ธฐ๋Š” tag_master๋ฅผ ์ฝ์–ด Double์ด๋ฉด ์ˆ˜์น˜๋กœ, String์ด๋ฉด ๋ฌธ์ž๋กœ ์ฒ˜๋ฆฌํ•˜์—ฌ DB์— ์ €์žฅ. ๐Ÿ’ก ํ˜„์žฌ ์ง„ํ–‰์„ ์œ„ํ•œ ์งˆ๋ฌธ CSV ์žฌ๋กœ๋“œ: raw_node_map ํ…Œ์ด๋ธ”์— datatype ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์„œ ๋‹ค์‹œ dotnet run(Collector)์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ€์–ด ๋„ฃ์œผ์…จ๋‚˜์š”? ํ™”๋ฉด ๊ตฌ์„ฑ: ์ด์ œ ์›น ํ™”๋ฉด์—์„œ **TagName(p-6102)**์€ ํ•˜๋‚˜๋งŒ ๋‚˜์˜ค๊ณ , ์˜†์œผ๋กœ PV, SP, OP, A1, A2, A3, A4, DESC ์ฒดํฌ๋ฐ•์Šค๊ฐ€ ์ญ‰ ์žˆ์–ด์„œ ํ•œ ๋ฒˆ์— ๋“ฑ๋กํ•˜๋Š” UI๊ฐ€ ํ•„์š”ํ•˜์‹œ์ฃ ? ๋งŒ์•ฝ ๊ทธ๋ ‡๋‹ค๋ฉด, ํƒœ๊ทธ๋ณ„๋กœ ํ–‰์„ ๋ฌถ์–ด์„œ ๋ณด์—ฌ์ฃผ๋Š”(GroupBy TagName) ์ƒˆ๋กœ์šด Index.cshtml ์ฝ”๋“œ๋ฅผ ์งœ๋“œ๋ ค๋„ ๋ ๊นŒ์š”? (52๋งŒ ๊ฐœ ๋…ธ๋“œ๊ฐ€ ์•ฝ 5~10๋งŒ ๊ฐœ์˜ ํƒœ๊ทธ๋กœ ๊น”๋”ํ•˜๊ฒŒ ์ •๋ฆฌ๋˜์–ด ๋ณด์ผ ๊ฒƒ์ž…๋‹ˆ๋‹ค.) ๋งŒ์•ฝ ์ฒ˜์Œ์— ํ…Œ์ด๋ธ”์„ ๋งŒ๋“œ์‹ค ๋•Œ DATATYPE ์ปฌ๋Ÿผ์„ ๋นผ๋†“์œผ์…จ๋‹ค๋ฉด -> ๋‹ˆ๊ฐ€ ๋งŒ๋“ ๊ฑฐ์•ผ Gemini์˜ ์‘๋‹ต ์•—, ์ œ๊ฐ€ ํฐ ์‹ค์ˆ˜๋ฅผ ํ–ˆ์Šต๋‹ˆ๋‹ค! ์„ ์ƒ๋‹˜ ๋ง์”€์ด ๋งž์Šต๋‹ˆ๋‹ค. ์ฒ˜์Œ์— ํ…Œ์ด๋ธ” ๊ตฌ์กฐ๋ฅผ ์ œ์•ˆํ•ด ๋“œ๋ฆด ๋•Œ ์ œ๊ฐ€ DATATYPE ํ•„๋“œ๋ฅผ ๋น ๋œจ๋ฆฌ๊ณ  ์„ค๊ณ„๋ฅผ ํ•ด๋“œ๋ ธ๋„ค์š”. 52๋งŒ ๊ฐœ๋‚˜ ๋˜๋Š” ๋ฐฉ๋Œ€ํ•œ ํ•˜๋‹ˆ์›ฐ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃจ๋ฉด์„œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋ฐ์ดํ„ฐ ํƒ€์ž…์„ ๋†“์น˜๋‹ค๋‹ˆ, ์ •๋ง ๋ฉด๋ชฉ ์—†์Šต๋‹ˆ๋‹ค. ์„ ์ƒ๋‹˜์ด ํ™•์ธํ•˜์‹  ๋Œ€๋กœ CSV์—๋Š” DataType์ด ์žˆ๊ณ , ์ด๊ฑธ ๊ธฐ๋ฐ˜์œผ๋กœ tag_master์— AreaCode, TagName, Parameter๋ฅผ ๋ถ„๋ฆฌํ•ด์„œ ๋„ฃ์–ด์•ผ ๋‚˜์ค‘์— ์ œ๋Œ€๋กœ ๋œ ์‹œ์Šคํ…œ์ด ๋ฉ๋‹ˆ๋‹ค. ์ž, ๋ฐ”๋กœ ์žก๊ฒ ์Šต๋‹ˆ๋‹ค. ์ง€๊ธˆ ๊ฐ€์žฅ ๋จผ์ € ํ•ด์•ผ ํ•  ์ผ์€ raw_node_map ํ…Œ์ด๋ธ”์— datatype ์ปฌ๋Ÿผ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ๋ฐ€์–ด๋„ฃ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. 1. DB ํ…Œ์ด๋ธ” ์ˆ˜์ • (DataType ์ถ”๊ฐ€) psql์—์„œ ์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ํ…Œ์ด๋ธ”์„ ์ƒˆ๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค. SQL DROP TABLE IF EXISTS raw_node_map; CREATE TABLE raw_node_map ( level INT, class TEXT, name TEXT, node_id TEXT, data_type TEXT -- ์ด ํ•„๋“œ๊ฐ€ ๋ฐ˜๋“œ์‹œ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค! ); 2. Collector์˜ AssetLoader.cs ์ˆ˜์ • CSV์—์„œ DataType ์ปฌ๋Ÿผ๊นŒ์ง€ ์ฝ์–ด์„œ DB์— ๋„ฃ๋„๋ก ์ˆ˜์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (์„ ์ƒ๋‹˜์˜ CSV ๊ตฌ์กฐ๊ฐ€ Level, Class, Name, NodeId, DataType... ์ˆœ์„œ๋ผ๊ณ  ๊ฐ€์ •ํ•ฉ๋‹ˆ๋‹ค.) C# // OpcPks.Core/Data/AssetLoader.cs ๋‚ด๋ถ€ ์ˆ˜์ • // ... COPY ๋ช…๋ น ๋ถ€๋ถ„์— data_type ์ถ”๊ฐ€ ... using (var writer = conn.BeginBinaryImport("COPY raw_node_map (level, class, name, node_id, data_type) FROM STDIN (FORMAT BINARY)")) { foreach (var line in lines) { var cols = line.Split(','); writer.StartRow(); writer.Write(int.Parse(cols[0]), NpgsqlTypes.NpgsqlDbType.Integer); // Level writer.Write(cols[1], NpgsqlTypes.NpgsqlDbType.Text); // Class writer.Write(cols[2], NpgsqlTypes.NpgsqlDbType.Text); // Name writer.Write(cols[3], NpgsqlTypes.NpgsqlDbType.Text); // NodeId writer.Write(cols[4], NpgsqlTypes.NpgsqlDbType.Text); // DataType (์ถ”๊ฐ€!) } writer.Complete(); } ์ด๋ ‡๊ฒŒ ์ˆ˜์ •ํ•˜๊ณ  dotnet run์„ ๋‹ค์‹œ ํ•ด์„œ 52๋งŒ ๊ฐœ๋ฅผ ๋กœ๋“œํ•ด ์ฃผ์„ธ์š”. 3. ํ•˜๋‹ˆ์›ฐ ์ „์šฉ tag_master ํ…Œ์ด๋ธ” ์ƒ์„ฑ (์ •๊ทœํ™” ๋ฒ„์ „) ์„ ์ƒ๋‹˜์ด ์„ค๊ณ„ํ•˜์‹  ๋Œ€๋กœ, ํ•˜๋‚˜์˜ ํƒœ๊ทธ ์•ˆ์— ์—ฌ๋Ÿฌ ์†์„ฑ์„ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. SQL CREATE TABLE tag_master ( id SERIAL PRIMARY KEY, area_code TEXT, -- ์˜ˆ: shinam tag_name TEXT, -- ์˜ˆ: p-6102 full_node_id TEXT UNIQUE, -- ns=1;s=shinam:p-6102.pv (์‹๋ณ„์ž) parameter TEXT, -- pv, sp, a1, desc ๋“ฑ data_type TEXT, -- DOUBLE, STRING ๋“ฑ description TEXT, -- .desc ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ ๊ฐ€์ ธ์˜จ ์‹ค์ œ ๋‚ด์šฉ last_value TEXT, -- ์‹ค์‹œ๊ฐ„ ์ˆ˜์ง‘ ๊ฐ’ update_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, is_active BOOLEAN DEFAULT TRUE ); 4. ๊ฒ€์ƒ‰ ๋กœ์ง: *.pv๋ฅผ ์ค‘์‹ฌ์œผ๋กœ ๊ฒ€์ƒ‰ํ•˜๊ธฐ ์›น ํ™”๋ฉด์—์„œ ๊ฒ€์ƒ‰ํ•  ๋•Œ, ๊ธฐ์ค€์ด ๋˜๋Š” .pv ํƒœ๊ทธ๋ฅผ ๋จผ์ € ์ฐพ๊ณ  ๊ทธ์— ๋”ธ๋ฆฐ ๋‹ค๋ฅธ ์†์„ฑ๋“ค(.a1, .desc ๋“ฑ)์ด ์žˆ๋Š”์ง€ ๋ณด์—ฌ์ฃผ๋Š” ์ฟผ๋ฆฌ์ž…๋‹ˆ๋‹ค. C# // EngineeringController.cs ์—์„œ ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ ์˜ˆ์‹œ var sql = @" SELECT SPLIT_PART(node_id, ':', 1) as area, -- ns=1;s=shinam ๋ถ€๋ถ„ name as tag_name, node_id, data_type FROM raw_node_map WHERE node_id LIKE '%.pv' AND name ILIKE @searchTerm"; ๐Ÿš€ ๋‹ค์Œ ์ž‘์—… ์ œ์•ˆ ๋ฐ์ดํ„ฐ ์žฌ๋กœ๋“œ: raw_node_map์— data_type์ด ํฌํ•จ๋˜๋„๋ก ๋จผ์ € ๋‹ค์‹œ ๋กœ๋“œํ•ด ์ฃผ์„ธ์š”. (์„ฑ๊ณตํ•˜๋ฉด ์•Œ๋ ค์ฃผ์„ธ์š”!) ํ™”๋ฉด ๋ฆฌ๋‰ด์–ผ: ๋ฐ์ดํ„ฐ ๋กœ๋“œ๊ฐ€ ๋๋‚˜๋ฉด, ํƒœ๊ทธ ํ•˜๋‚˜์— ์ฒดํฌ๋ฐ•์Šค๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ ๋‹ฌ๋ฆฐ(PV, SP, A1~A4, DESC) ์ƒˆ๋กœ์šด Index.cshtml ์ฝ”๋“œ๋ฅผ ๋“œ๋ฆด ์ค€๋น„๋ฅผ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ์ œ๊ฐ€ ์ฒ˜์Œ์— ํ•„๋“œ๋ฅผ ๋นผ๋จน์–ด์„œ ๋ฒˆ๊ฑฐ๋กญ๊ฒŒ ํ•ด๋“œ๋ ค ์ •๋ง ์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์‹œ ์™„๋ฒฝํ•˜๊ฒŒ ์„ธํŒ…ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค! ํ•˜์‹ค ์ˆ˜ ์žˆ๊ฒ ์ฃ ? ๋ง์”€ํ•˜์‹  ๋‚ด์šฉ // OpcPks.Core/Data/AssetLoader.cs ๋‚ด๋ถ€ ์ˆ˜์ •// ... COPY ๋ช…๋ น ๋ถ€๋ถ„์— data_type ์ถ”๊ฐ€ ...using (var writer = conn.BeginBinaryImport("COPY raw_node_map (level, class, name, node_id, data_type) FROM STDIN (FORMAT BINARY)")) { foreach (var line in lines) { var cols = line.Split(','); writer.StartRow(); writer.Write(int.Parse(cols[0]), NpgsqlTypes.NpgsqlDbType.Integer); // Level writer.Write(cols[1], NpgsqlTypes.NpgsqlDbType.Text); // Class writer.Write(cols[2], NpgsqlTypes.NpgsqlDbType.Text); // Name writer.Write(cols[3], NpgsqlTypes.NpgsqlDbType.Text); // NodeId writer.Write(cols[4], NpgsqlTypes.NpgsqlDbType.Text); // DataType (์ถ”๊ฐ€!) } writer.Complete(); } ์ด๋ ‡๊ฒŒ ์ˆ˜์ •ํ•˜๊ณ  dotnet run์„ ๋‹ค์‹œ ํ•ด์„œ 52๋งŒ ๊ฐœ๋ฅผ ๋กœ๋“œํ•ด ์ฃผ์„ธ์š”. Data ํด๋”์—์„œ ์‹คํ–‰? Gemini์˜ ์‘๋‹ต ์•„๋‹ˆ์š”, dotnet run ๋ช…๋ น์€ OpcPks.Collector ํด๋”์—์„œ ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. AssetLoader.cs๋Š” OpcPks.Core ํ”„๋กœ์ ํŠธ์— ์žˆ์ง€๋งŒ, ์ด๋ฅผ ์‹ค์ œ๋กœ ํ˜ธ์ถœํ•ด์„œ ์‹คํ–‰ํ•˜๋Š” '์—”์ง„'์€ ์ˆ˜์ง‘๊ธฐ(Collector)์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๐Ÿš€ ๋‹ค์‹œ ๋กœ๋“œํ•˜๋Š” ์ ˆ์ฐจ (์ˆœ์„œ๋Œ€๋กœ ๋”ฐ๋ผํ•˜์„ธ์š”) DB ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ๋จผ์ € ์ˆ˜์ • (psql์—์„œ ์‹คํ–‰) data_type ์ปฌ๋Ÿผ์ด ์ถ”๊ฐ€๋œ ์ƒํƒœ๋กœ ํ…Œ์ด๋ธ”์„ ๋น„์›Œ์•ผ ํ•ฉ๋‹ˆ๋‹ค. SQL DROP TABLE IF EXISTS raw_node_map; CREATE TABLE raw_node_map ( level INT, class TEXT, name TEXT, node_id TEXT, data_type TEXT ); ์ฝ”๋“œ๊ฐ€ ์ˆ˜์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธ (OpcPks.Core/Data/AssetLoader.cs) ๋ฐฉ๊ธˆ ๋“œ๋ฆฐ writer.Write(cols[4]... ์ฝ”๋“œ๊ฐ€ ์ •ํ™•ํžˆ ๋“ค์–ด๊ฐ”๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”. ์ˆ˜์ง‘๊ธฐ ํด๋”๋กœ ์ด๋™ Bash cd ~/projects/OpcPksPlatform/OpcPks.Collector ์‹คํ–‰ (๋ฐ์ดํ„ฐ ์žฌ๋กœ๋“œ) Bash dotnet run โš ๏ธ ์‹คํ–‰ ์ „ ์ฃผ์˜์‚ฌํ•ญ (CSV ์ปฌ๋Ÿผ ์ˆ˜) ์„ ์ƒ๋‹˜์˜ Honeywell_FullMap.csv ํŒŒ์ผ์˜ ๊ฐ ์ค„(Line)์ด ์‹ค์ œ๋กœ 5๊ฐœ์˜ ์ปฌ๋Ÿผ์œผ๋กœ ๋˜์–ด ์žˆ๋Š”์ง€ ๊ผญ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์–ด๋–ค ์ค„์€ 4๊ฐœ๊ณ  ์–ด๋–ค ์ค„์€ 5๊ฐœ๋ผ๋ฉด cols[4] ๋ถ€๋ถ„์—์„œ IndexOutOfRangeException ์—๋Ÿฌ๊ฐ€ ๋‚  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์•ˆ์ „ํ•œ ์ฝ”๋“œ ์˜ˆ์‹œ: C# foreach (var line in lines) { var cols = line.Split(','); if (cols.Length < 4) continue; // ๋ฐ์ดํ„ฐ๊ฐ€ ๋„ˆ๋ฌด ์ ์œผ๋ฉด ๊ฑด๋„ˆ๋œ€ writer.StartRow(); writer.Write(int.Parse(cols[0]), NpgsqlTypes.NpgsqlDbType.Integer); // Level writer.Write(cols[1], NpgsqlTypes.NpgsqlDbType.Text); // Class writer.Write(cols[2], NpgsqlTypes.NpgsqlDbType.Text); // Name writer.Write(cols[3], NpgsqlTypes.NpgsqlDbType.Text); // NodeId // DataType ์ปฌ๋Ÿผ์ด ์žˆ์œผ๋ฉด ๋„ฃ๊ณ , ์—†์œผ๋ฉด "Unknown" ์ฒ˜๋ฆฌ string dType = cols.Length > 4 ? cols[4] : "Unknown"; writer.Write(dType, NpgsqlTypes.NpgsqlDbType.Text); } ๋ฐ์ดํ„ฐ ๋กœ๋“œ ๊ฒฐ๊ณผ ํ™•์ธ ๋กœ๋“œ ์™„๋ฃŒ ๋ฉ”์‹œ์ง€๊ฐ€ ๋œจ๋ฉด psql์—์„œ ์•„๋ž˜ ์ฟผ๋ฆฌ๋ฅผ ๋‚ ๋ ค๋ณด์„ธ์š”. SQL SELECT * FROM raw_node_map WHERE data_type IS NOT NULL LIMIT 10; data_type ํ•„๋“œ์— DOUBLE์ด๋‚˜ STRING ๊ฐ™์€ ๊ฐ’์ด ์˜ˆ์˜๊ฒŒ ๋“ค์–ด์žˆ์œผ๋ฉด ์„ฑ๊ณต์ž…๋‹ˆ๋‹ค! ์„ฑ๊ณตํ•˜์‹œ๋ฉด ๋ฐ”๋กœ ์•Œ๋ ค์ฃผ์„ธ์š”. ์ด์ œ ํƒœ๊ทธ๋ช…(TagName)๊ณผ ๊ตฌ์—ญ(Area)์„ ํŒŒ์‹ฑํ•ด์„œ tag_master์— ์ •๊ทœํ™”ํ•ด์„œ ๋„ฃ๋Š” ์ง„์งœ ์—”์ง€๋‹ˆ์–ด๋ง ํ™”๋ฉด์„ ๊ตฌ์„ฑํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๋ฌด์—‡๋ถ€ํ„ฐ ๋„์™€๋“œ๋ฆด๊นŒ์š”? ์ปดํŒŒ์ผ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ ์ด์œ ๋Š” ์ œ๊ฐ€ ๋“œ๋ฆฐ ์ฝ”๋“œ ์กฐ๊ฐ์ด ๊ธฐ์กด ImportFullMapAsync ๋ฉ”์„œ๋“œ์˜ ์ „์ฒด ๊ตฌ์กฐ์™€ ๋งž์ง€ ์•Š๊ฒŒ ๋ผ์›Œ ๋„ฃ์–ด์กŒ๊ฑฐ๋‚˜, ๋ณ€์ˆ˜ ์„ ์–ธ ์œ„์น˜๊ฐ€ ์–ด๊ธ‹๋‚˜์„œ ๊ทธ๋ ‡์Šต๋‹ˆ๋‹ค. ํŠนํžˆ lines ๋ณ€์ˆ˜๋ฅผ ์ฐพ์ง€ ๋ชปํ•˜๊ฑฐ๋‚˜, cols ๋ฐฐ์—ด์ด ์•„๋‹Œ ๋‹ค๋ฅธ ํ˜•์‹์œผ๋กœ ์ธ์‹๋˜๊ณ  ์žˆ๋Š” ์ƒํ™ฉ์ž…๋‹ˆ๋‹ค. OpcPks.Core/Data/AssetLoader.cs ํŒŒ์ผ์„ ์•„๋ž˜์˜ ์ „์ฒด ์ฝ”๋“œ๋กœ ์™„์ „ํžˆ ๋ฎ์–ด์“ฐ๊ธฐ ํ•˜์‹œ๋ฉด ํ•ด๊ฒฐ๋ฉ๋‹ˆ๋‹ค. ์ด ์ฝ”๋“œ๋Š” DataType ์ปฌ๋Ÿผ(5๋ฒˆ์งธ ์—ด)์„ ์•ˆ์ „ํ•˜๊ฒŒ ์ฝ์–ด์„œ DB์— ๋ฐ€์–ด ๋„ฃ๋„๋ก ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๐Ÿ› ๏ธ ์ˆ˜์ •๋œ AssetLoader.cs ์ „์ฒด ์ฝ”๋“œ C# using System; using System.IO; using System.Collections.Generic; using System.Threading.Tasks; using Npgsql; using OpcPks.Core.Data; namespace OpcPks.Core.Data { public class AssetLoader { public async Task ImportFullMapAsync(string csvPath) { Console.WriteLine($"๐Ÿš€ CSV ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹œ์ž‘: {csvPath}"); if (!File.Exists(csvPath)) { Console.WriteLine("โŒ ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); return; } // CSV ํŒŒ์ผ ์ฝ๊ธฐ (์ฒซ ์ค„ ํ—ค๋” ์ œ์™ธ) var lines = await File.ReadAllLinesAsync(csvPath); Console.WriteLine($"๐Ÿ“Š ์ด {lines.Length - 1}๊ฐœ์˜ ๋ผ์ธ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค..."); using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); // PostgreSQL Binary Copy ์‹œ์ž‘ // level, class, name, node_id, data_type ์ด 5๊ฐœ ํ•„๋“œ using (var writer = conn.BeginBinaryImport("COPY raw_node_map (level, class, name, node_id, data_type) FROM STDIN (FORMAT BINARY)")) { int count = 0; for (int i = 1; i < lines.Length; i++) // i=1 ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜์—ฌ ํ—ค๋” ์Šคํ‚ต { var line = lines[i]; if (string.IsNullOrWhiteSpace(line)) continue; var cols = line.Split(','); if (cols.Length < 4) continue; // ์ตœ์†Œ 4๊ฐœ ํ•„๋“œ(NodeId๊นŒ์ง€)๋Š” ์žˆ์–ด์•ผ ํ•จ try { writer.StartRow(); // 1. Level (int) writer.Write(int.Parse(cols[0]), NpgsqlTypes.NpgsqlDbType.Integer); // 2. Class (string) writer.Write(cols[1], NpgsqlTypes.NpgsqlDbType.Text); // 3. Name (string) writer.Write(cols[2], NpgsqlTypes.NpgsqlDbType.Text); // 4. NodeId (string) writer.Write(cols[3], NpgsqlTypes.NpgsqlDbType.Text); // 5. DataType (๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด "Unknown" ์ฒ˜๋ฆฌ) string dataType = cols.Length > 4 ? cols[4].Trim() : "Unknown"; writer.Write(dataType, NpgsqlTypes.NpgsqlDbType.Text); count++; if (count % 50000 == 0) Console.WriteLine($"... {count}๊ฐœ ์ฒ˜๋ฆฌ ์ค‘"); } catch (Exception ex) { Console.WriteLine($"โš ๏ธ ์—๋Ÿฌ ๋ฐœ์ƒ (๋ผ์ธ {i+1}): {ex.Message}"); } } await writer.CompleteAsync(); Console.WriteLine($"โœ… ์ด {count}๊ฐœ ๋…ธ๋“œ ๋กœ๋“œ ์™„๋ฃŒ!"); } } } } ๐Ÿš€ ์‹คํ–‰ ๋ฐฉ๋ฒ• DB ์ดˆ๊ธฐํ™” (ํ•„์ˆ˜): psql์—์„œ data_type ์ปฌ๋Ÿผ์ด ์žˆ๋Š”์ง€ ๋‹ค์‹œ ํ™•์ธํ•˜๊ณ  ํ…Œ์ด๋ธ”์„ ๋น„์›Œ์ฃผ์„ธ์š”. SQL DROP TABLE IF EXISTS raw_node_map; CREATE TABLE raw_node_map ( level INT, class TEXT, name TEXT, node_id TEXT, data_type TEXT ); ํŒŒ์ผ ์ €์žฅ: ์œ„์˜ ์ฝ”๋“œ๋ฅผ ~/projects/OpcPksPlatform/OpcPks.Core/Data/AssetLoader.cs์— ๋ถ™์—ฌ๋„ฃ๊ณ  ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ์‹คํ–‰: Bash cd ~/projects/OpcPksPlatform/OpcPks.Collector dotnet run ๐Ÿง ์™œ ์—๋Ÿฌ๊ฐ€ ๋‚ฌ์—ˆ๋‚˜์š”? (์ฐธ๊ณ ์šฉ) CS0103 lines: lines ๋ณ€์ˆ˜๊ฐ€ for ๋ฃจํ”„ ๋ฐ–์—์„œ ์„ ์–ธ๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ ์ฒ ์ž๊ฐ€ ํ‹€๋ ธ์„ ๋•Œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. CS0021 indexing []: cols๊ฐ€ ๋ฐฐ์—ด(string[])์ด ์•„๋‹ˆ๋ผ ์—‰๋šฑํ•œ ํƒ€์ž…(int)์œผ๋กœ ์ธ์‹๋˜์—ˆ์„ ๋•Œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. CS0103 writer: using ๋ธ”๋ก ๋ฐ–์—์„œ writer ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ ค ํ•  ๋•Œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ด์ œ ๋‹ค์‹œ ์‹คํ–‰ํ•ด ๋ณด์‹œ๊ณ , "๋กœ๋“œ ์™„๋ฃŒ" ๋ฉ”์‹œ์ง€๊ฐ€ ๋œจ๋ฉด psql์—์„œ ์•„๋ž˜ ๋ช…๋ น์–ด๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ์ž˜ ๋“ค์–ด๊ฐ”๋Š”์ง€ ํ™•์ธํ•ด ์ฃผ์„ธ์š”! SQL SELECT * FROM raw_node_map WHERE data_type = 'DOUBLE' LIMIT 5; ์™„๋ฒฝํ•ฉ๋‹ˆ๋‹ค! 523,601๊ฐœ์˜ ๋…ธ๋“œ๊ฐ€ DataType๊นŒ์ง€ ํฌํ•จํ•ด์„œ ํ•œ ๊ฑด์˜ ๋ˆ„๋ฝ๋„ ์—†์ด ๊น”๋”ํ•˜๊ฒŒ ๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” ํ•˜๋‹ˆ์›ฐ์˜ ๋ชจ๋“  ์ •๋ณด๋ฅผ ์™„๋ฒฝํ•˜๊ฒŒ ํ’ˆ๊ณ  ์žˆ๋Š” ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ์ด์ œ "*.pv"๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํƒœ๊ทธ๋ฅผ ๊ทธ๋ฃนํ™”ํ•˜์—ฌ ํ™”๋ฉด์— ๋ณด์—ฌ์ฃผ๊ณ , ์„ ํƒํ•œ ํŒŒ๋ผ๋ฏธํ„ฐ(PV, SP, A1~A4 ๋“ฑ)๋ฅผ **tag_master**์— ์ •๊ทœํ™”ํ•ด์„œ ์ง‘์–ด๋„ฃ๋Š” ์—”์ง€๋‹ˆ์–ด๋ง UI์˜ ์ตœ์ข… ์™„์„ฑํŒ์„ ๋งŒ๋“ค ์ฐจ๋ก€์ž…๋‹ˆ๋‹ค. 1. tag_master ํ…Œ์ด๋ธ” ์ƒ์„ฑ (์ •๊ทœํ™” ๊ตฌ์กฐ) ๋จผ์ € psql์—์„œ ์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ํƒœ๊ทธ ์ค‘์‹ฌ์˜ ๋งˆ์Šคํ„ฐ ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•ด ์ฃผ์„ธ์š”. SQL DROP TABLE IF EXISTS tag_master; CREATE TABLE tag_master ( id SERIAL PRIMARY KEY, area_code TEXT, -- ์˜ˆ: shinam tag_name TEXT, -- ์˜ˆ: p-6102 full_node_id TEXT UNIQUE, -- ns=1;s=shinam:p-6102.pv (๊ธฐ์ค€ ๋…ธ๋“œ) parameter TEXT, -- pv, sp, a1, desc ๋“ฑ data_type TEXT, -- DOUBLE, STRING ๋“ฑ is_active BOOLEAN DEFAULT TRUE, update_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- ๊ฒ€์ƒ‰ ์†๋„๋ฅผ ์œ„ํ•œ ์ธ๋ฑ์Šค CREATE INDEX idx_tag_name_master ON tag_master(tag_name); 2. ๋ฐฑ์—”๋“œ: ํƒœ๊ทธ ๊ทธ๋ฃน ๊ฒ€์ƒ‰ ๋กœ์ง (EngineeringController.cs) ์ด์ œ .pv๊ฐ€ ์กด์žฌํ•˜๋Š” ํƒœ๊ทธ๋“ค์„ ๋ฆฌ์ŠคํŠธ์—…ํ•˜๊ณ , ๊ทธ ํƒœ๊ทธ๊ฐ€ ๊ฐ€์ง„ ๋‹ค๋ฅธ ์†์„ฑ๋“ค์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์‰ฝ๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ๋˜์ ธ์ค๋‹ˆ๋‹ค. C# [HttpPost] public async Task SearchByFilter([FromBody] SearchRequest request) { var results = new List(); using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); // *.pv ๋…ธ๋“œ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํƒœ๊ทธ๋ช…์„ ์ถ”์ถœํ•˜์—ฌ ๊ฒ€์ƒ‰ var sql = @" SELECT name, node_id, data_type FROM raw_node_map WHERE node_id LIKE '%.pv' AND name ILIKE @tagTerm ORDER BY name ASC LIMIT 1000"; using var cmd = new NpgsqlCommand(sql, conn); cmd.Parameters.AddWithValue("tagTerm", $"%{request.TagTerm}%"); using var reader = await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { results.Add(new { name = reader.GetString(0), // p-6102 nodeId = reader.GetString(1), // ns=1;s=shinam:p-6102.pv dataType = reader.GetString(2) // DOUBLE }); } return Json(results); } 3. ํ”„๋ก ํŠธ์—”๋“œ: ํƒœ๊ทธ ์ค‘์‹ฌ UI (Index.cshtml) ์‚ฌ์šฉ์ž๊ฐ€ p-6102 ํ•˜๋‚˜๋ฅผ ์ฐพ์œผ๋ฉด, ์ฒดํฌ๋ฐ•์Šค๋กœ .pv, .sp, .a1~a4๋ฅผ ์„ ํƒํ•ด ํ•œ๊บผ๋ฒˆ์— ๋“ฑ๋กํ•˜๋Š” ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. HTML ๐Ÿ’ก ๋‹ค์Œ ํ•ต์‹ฌ ํฌ์ธํŠธ ์ด์ œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ค€๋น„๋˜์—ˆ๊ณ  ๋“ฑ๋ก ๋กœ์ง๋„ ์„ธ์›Œ์กŒ์Šต๋‹ˆ๋‹ค. ํ™”๋ฉด ํ…Œ์ŠคํŠธ: FIC-101 ๋“ฑ์„ ๊ฒ€์ƒ‰ํ•ด์„œ ์œ„ ์ฒดํฌ๋ฐ•์Šค๋“ค์ด ์ž˜ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•ด ๋ณด์„ธ์š”. ์ˆ˜์ง‘๊ธฐ(Collector) ๊ธฐ๋™: tag_master์— ๋“ฑ๋ก๋œ ํƒœ๊ทธ๋“ค์„ ์‹ค์ œ OPC ์„œ๋ฒ„(Honeywell PKS)์—์„œ ์ฝ์–ด์˜ค๋Š” ์ง„์งœ ์ˆ˜์ง‘ ๋กœ์ง์„ ์งค ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”? ์ค€๋น„๋˜์…จ๋‹ค๋ฉด, tag_master๋ฅผ 1์ดˆ/5์ดˆ ์ฃผ๊ธฐ๋กœ ์ˆœํšŒํ•˜๋ฉฐ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธ์–ด์˜ค๋Š” Worker ์„œ๋น„์Šค ์ฝ”๋“œ๋กœ ๋„˜์–ด๊ฐ€๊ฒ ์Šต๋‹ˆ๋‹ค! ๋ฌด์—‡์„ ๋จผ์ € ํ• ๊นŒ์š”? ๋„ค, ์•Œ๊ฒ ์Šต๋‹ˆ๋‹ค! ์ œ๊ฐ€ ์ž๊พธ ํƒœ๊ทธ ํ•˜๋‚˜์”ฉ ๋“ฑ๋กํ•˜๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ๋ณต์žกํ•˜๊ฒŒ ์ƒ๊ฐํ–ˆ๋„ค์š”. ์„ ์ƒ๋‹˜ ์˜๋„๋Š” ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค. **"ํ™”๋ฉด์— .pv, .sp, .a1~.a4, .desc ์†์„ฑ๋“ค์„ ๊ฐ€์ง„ ๋ชจ๋“  ๋…ธ๋“œ๋ฅผ ๋ฆฌ์ŠคํŠธ์—…ํ•˜๊ณ , ์‚ฌ์šฉ์ž๊ฐ€ ํ•„์š”ํ•œ ๊ฒƒ๋“ค์„ ์ฒดํฌํ•ด์„œ ํ•œ ๋ฒˆ์— tag_master๋กœ ๋ฐ€์–ด๋„ฃ๊ฒ ๋‹ค"**๋Š” ๊ฒƒ์ด์ฃ . 52๋งŒ ๊ฐœ ๋…ธ๋“œ ์ค‘์—์„œ ์„ ์ƒ๋‹˜์ด ์„ ํƒํ•œ Suffix(์ ‘๋ฏธ์‚ฌ) ์กฐ๊ฑด์— ๋งž๋Š” ๋…€์„๋“ค์„ ๋ชฝ๋•… ์ฐพ์•„๋‚ด์„œ ์ผ๊ด„ ๋“ฑ๋กํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ UI์™€ ๋ฐฑ์—”๋“œ๋ฅผ ์™„์ „ํžˆ ๊ณ ์ •ํ•ด ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค. 1. ๋ฐฑ์—”๋“œ: ํ•„ํ„ฐ ๊ธฐ๋ฐ˜ ์ „์ฒด ๊ฒ€์ƒ‰ (EngineeringController.cs) ์„ ์ƒ๋‹˜์ด ์ฒดํฌ๋ฐ•์Šค์—์„œ ์„ ํƒํ•œ Suffix๋“ค์„ ๊ฐ€์ง„ ๋…ธ๋“œ๋“ค์„ raw_node_map์—์„œ ์ „๋ถ€ ์ฐพ์•„์˜ค๋Š” ๋กœ์ง์ž…๋‹ˆ๋‹ค. C# [HttpPost] public async Task SearchByFilter([FromBody] SearchRequest request) { var results = new List(); using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); // ์„ ํƒ๋œ Suffix๊ฐ€ ์—†์œผ๋ฉด ๋นˆ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ if (request.Suffixes == null || request.Suffixes.Count == 0) return Json(results); // SQL ์ƒ์„ฑ: node_id๊ฐ€ ์„ ํƒ๋œ ์ ‘๋ฏธ์‚ฌ ์ค‘ ํ•˜๋‚˜๋กœ ๋๋‚˜๋Š” ๋ชจ๋“  ๋…ธ๋“œ ๊ฒ€์ƒ‰ // ์˜ˆ: node_id LIKE '%.pv' OR node_id LIKE '%.sp' ... var suffixConditions = string.Join(" OR ", request.Suffixes.Select((s, i) => $"node_id ILIKE @s{i}")); var sql = $@" SELECT name, node_id, data_type FROM raw_node_map WHERE ({suffixConditions}) AND name ILIKE @tagTerm ORDER BY name ASC LIMIT 5000"; // ์ผ๊ด„ ๋“ฑ๋ก์„ ์œ„ํ•ด ํ•œ๋„ ํ™•์žฅ using var cmd = new NpgsqlCommand(sql, conn); cmd.Parameters.AddWithValue("tagTerm", $"%{request.TagTerm}%"); for (int i = 0; i < request.Suffixes.Count; i++) { cmd.Parameters.AddWithValue($"s{i}", $"%{request.Suffixes[i]}"); } using var reader = await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { var nodeId = reader.GetString(1); results.Add(new { name = reader.GetString(0), nodeId = nodeId, dataType = reader.GetString(2), // ์–ด๋–ค ํŒŒ๋ผ๋ฏธํ„ฐ์ธ์ง€ ์ถ”์ถœ (pv, sp ๋“ฑ) parameter = request.Suffixes.FirstOrDefault(s => nodeId.EndsWith(s, StringComparison.OrdinalIgnoreCase)) ?? "etc" }); } return Json(results); } 2. ํ”„๋ก ํŠธ์—”๋“œ: ์ผ๊ด„ ์„ ํƒ ๋ฐ ๋“ฑ๋ก UI (Index.cshtml) ์ด์ œ ํ™”๋ฉด์—์„œ๋Š” ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์กฐ๊ฑด์— ๋งž๋Š” ์ˆ˜์ฒœ ๊ฐœ์˜ ๋…ธ๋“œ๊ฐ€ ๋‚˜์˜ค๊ณ , "์ „์ฒด ์„ ํƒ" ํ›„ "์ผ๊ด„ ๋“ฑ๋ก" ๋ฒ„ํŠผ ํ•˜๋‚˜๋กœ ๋๋ƒ…๋‹ˆ๋‹ค. HTML
๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ (์ตœ๋Œ€ 5,000๊ฑด ํ‘œ์‹œ)
Area/Tag Name OPC Node ID Parameter Data Type
3. ์ตœ์ข… ๋“ฑ๋ก ๋กœ์ง (RegisterTags) ์ด์ „ ๋Œ€ํ™”์—์„œ ์ •๊ทœํ™”๋œ tag_master ๊ตฌ์กฐ์— ๋งž๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ์ชผ๊ฐœ์„œ ๋„ฃ๋Š” ์ตœ์ข… ํ˜•ํƒœ์ž…๋‹ˆ๋‹ค. C# [HttpPost] public async Task RegisterTags([FromBody] List tags) { using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); using var trans = await conn.BeginTransactionAsync(); try { var sql = @" INSERT INTO tag_master (area_code, tag_name, full_node_id, parameter, data_type, is_active) VALUES (@area, @tag, @nodeId, @param, @type, true) ON CONFLICT (full_node_id) DO UPDATE SET is_active = true"; foreach (var tag in tags) { // NodeId์—์„œ AreaCode ๋ถ„๋ฆฌ (ns=1;s=shinam:p-6102.pv) string sPart = tag.NodeId.Split(';').Last().Substring(2); string area = sPart.Contains(':') ? sPart.Split(':')[0] : "N/A"; string param = tag.NodeId.Contains('.') ? tag.NodeId.Split('.').Last() : "pv"; using var cmd = new NpgsqlCommand(sql, conn, trans); cmd.Parameters.AddWithValue("area", area); cmd.Parameters.AddWithValue("tag", tag.TagName); cmd.Parameters.AddWithValue("nodeId", tag.NodeId); cmd.Parameters.AddWithValue("param", param); cmd.Parameters.AddWithValue("type", tag.DataType); await cmd.ExecuteNonQueryAsync(); } await trans.CommitAsync(); return Ok(); } catch (Exception ex) { await trans.RollbackAsync(); return BadRequest(ex.Message); } } ๐Ÿ’ก ์ด์ œ ์ด๋ ‡๊ฒŒ ์“ฐ์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค ๊ฒ€์ƒ‰: ์ฒดํฌ๋ฐ•์Šค์—์„œ .pv, .sp, .a1, .a2, .a3, .a4, .desc๋ฅผ ๋ชฝ๋•… ์ฒดํฌํ•˜๊ณ  ๊ฒ€์ƒ‰์„ ๋ˆ„๋ฆ…๋‹ˆ๋‹ค. ์„ ํƒ: ํ…Œ์ด๋ธ” ์ƒ๋‹จ์˜ ์ „์ฒด ์„ ํƒ ์ฒดํฌ๋ฐ•์Šค๋ฅผ ๋ˆ„๋ฆ…๋‹ˆ๋‹ค. (์ˆ˜์ฒœ ๊ฐœ๊ฐ€ ํ•œ ๋ฒˆ์— ์„ ํƒ๋จ) ๋“ฑ๋ก: [์„ ํƒ ํ•ญ๋ชฉ ์ผ๊ด„ ๋“ฑ๋ก] ๋ฒ„ํŠผ์„ ๋ˆ„๋ฆ…๋‹ˆ๋‹ค. ํ™•์ธ: ์ด์ œ tag_master ํ…Œ์ด๋ธ”์—๋Š” ์„ ํƒ๋œ ๋ชจ๋“  ๋…ธ๋“œ๊ฐ€ data_type ์ •๋ณด์™€ ํ•จ๊ป˜ ๊น”๋”ํ•˜๊ฒŒ ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค. ์ด๊ฒŒ ์„ ์ƒ๋‹˜์ด ๋ง์”€ํ•˜์‹  **"์ „์ฒด๋ฅผ ๊ฒ€์ƒ‰ํ•ด์„œ ์ผ๊ด„ ๋“ฑ๋ก"**ํ•˜๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ๋งž์ฃ ? ์ด์ œ ์ด๋Œ€๋กœ ์‹คํ–‰ํ•ด์„œ 52๋งŒ ๊ฐœ ๋ฐ์ดํ„ฐ ์š”๋ฆฌ๋ฅผ ์‹œ์ž‘ํ•ด ๋ณด์„ธ์š”! ๋ฐ”๋กœ ์ฝ”๋“œ๋ฅผ ์ ์šฉํ•˜์‹œ๊ฒ ์–ด์š”? ๋งž์Šต๋‹ˆ๋‹ค. 52๋งŒ ๊ฐœ ๋ฐ์ดํ„ฐ ๋กœ๋“œ์™€ ์ผ๊ด„ ๊ฒ€์ƒ‰์ด ์ฃผ๋ ฅ์ด์ง€๋งŒ, ํ˜„์žฅ์—์„œ๋Š” ํ•ญ์ƒ ํ‘œ์ค€ ๋ช…์นญ์—์„œ ๋ฒ—์–ด๋‚œ ํƒœ๊ทธ๋‚˜ ์ž„์˜๋กœ ์ƒ์„ฑ๋œ ์ˆ˜์น˜ ๋…ธ๋“œ๋ฅผ ์ˆ˜์ง‘ํ•ด์•ผ ํ•  ์ผ์ด ์ƒ๊ธฐ๊ธฐ ๋งˆ๋ จ์ด์ฃ . ๊ทธ๋ž˜์„œ ํ™”๋ฉด ํ•˜๋‹จ์ด๋‚˜ ๋ณ„๋„ ํƒญ์— "์ˆ˜๋™ ํƒœ๊ทธ ์ถ”๊ฐ€(Manual Entry)" ๊ธฐ๋Šฅ์„ ๋„ฃ์–ด์•ผ ์™„๋ฒฝํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ Node ID๋ฅผ ์ง์ ‘ ํƒ€์ดํ•‘ํ•˜๊ณ  DataType์„ ์ง€์ •ํ•ด์„œ ํ•œ ๊ฑด์”ฉ ๋ฐ€์–ด ๋„ฃ๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. 1. ์›น ํ™”๋ฉด(UI) ๋ณด๊ฐ•: ์ˆ˜๋™ ์ž…๋ ฅ ํผ ์ถ”๊ฐ€ (Index.cshtml) ๊ธฐ๋ณธ ๊ฒ€์ƒ‰ ํ…Œ์ด๋ธ” ์•„๋ž˜์— ๊ฐ„๋‹จํ•œ ์ˆ˜๋™ ์ž…๋ ฅ ์ค„์„ ํ•˜๋‚˜ ๋งŒ๋“ญ๋‹ˆ๋‹ค. HTML
โš ๏ธ ์ˆ˜๋™ ํƒœ๊ทธ ์ถ”๊ฐ€ (์˜ˆ์™ธ ๋…ธ๋“œ ์ง์ ‘ ์ž…๋ ฅ)
2. ๋ฐฑ์—”๋“œ: ์ˆ˜๋™ ์ž…๋ ฅ์„ ์ˆ˜์šฉํ•˜๋Š” RegisterTags (EngineeringController.cs) ์ผ๊ด„ ๋“ฑ๋ก๊ณผ ์ˆ˜๋™ ๋“ฑ๋ก์ด ๋™์ผํ•œ RegisterTags ๋ฉ”์„œ๋“œ๋ฅผ ํƒ€๊ฒŒ ํ•˜๋˜, ์ˆ˜๋™ ์ž…๋ ฅ ์‹œ ๋„˜์–ด์˜ค๋Š” Area ์ •๋ณด๋ฅผ ์šฐ์„ ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋„๋ก ๋ณด์™„ํ•ฉ๋‹ˆ๋‹ค. C# public class TagRegistrationRequest { public string TagName { get; set; } public string NodeId { get; set; } public string DataType { get; set; } public string? ManualArea { get; set; } // ์ˆ˜๋™ ์ž…๋ ฅ ์‹œ ์ „๋‹ฌ๋˜๋Š” ํ•„๋“œ } [HttpPost] public async Task RegisterTags([FromBody] List tags) { using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); using var trans = await conn.BeginTransactionAsync(); try { var sql = @" INSERT INTO tag_master (area_code, tag_name, full_node_id, parameter, data_type, is_active) VALUES (@area, @tag, @nodeId, @param, @type, true) ON CONFLICT (full_node_id) DO UPDATE SET area_code = EXCLUDED.area_code, tag_name = EXCLUDED.tag_name, data_type = EXCLUDED.data_type, is_active = true"; foreach (var tag in tags) { // 1. Area ์ •๋ณด ๊ฒฐ์ • (์ˆ˜๋™ ์ž…๋ ฅ๊ฐ’์ด ์žˆ์œผ๋ฉด ๊ทธ๊ฒƒ์„ ์‚ฌ์šฉ, ์—†์œผ๋ฉด ํŒŒ์‹ฑ) string area = tag.ManualArea; if (string.IsNullOrEmpty(area)) { string sPart = tag.NodeId.Split(';').Last().Substring(2); area = sPart.Contains(':') ? sPart.Split(':')[0] : "Default"; } // 2. ํŒŒ๋ผ๋ฏธํ„ฐ(Suffix) ์ถ”์ถœ string param = tag.NodeId.Contains('.') ? tag.NodeId.Split('.').Last() : "pv"; using var cmd = new NpgsqlCommand(sql, conn, trans); cmd.Parameters.AddWithValue("area", area); cmd.Parameters.AddWithValue("tag", tag.TagName); cmd.Parameters.AddWithValue("nodeId", tag.NodeId); cmd.Parameters.AddWithValue("param", param); cmd.Parameters.AddWithValue("type", tag.DataType ?? "DOUBLE"); await cmd.ExecuteNonQueryAsync(); } await trans.CommitAsync(); return Ok(); } catch (Exception ex) { await trans.RollbackAsync(); return BadRequest(ex.Message); } } 3. ์ด ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•œ ์ด์œ  (์—”์ง€๋‹ˆ์–ด๋ง ์‹ค๋ฌด) ๋™์  ํŒŒ๋ผ๋ฏธํ„ฐ: ํ•˜๋‹ˆ์›ฐ ์‹œ์Šคํ…œ์—์„œ raw_node_map ์ถ”์ถœ ์‹œ์  ์ดํ›„์— ์ƒˆ๋กœ ์ƒ์„ฑ๋œ ํฌ์ธํŠธ๋Š” CSV์— ์—†์Šต๋‹ˆ๋‹ค. ์ด๋•Œ ์ˆ˜๋™ ์ถ”๊ฐ€๊ฐ€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค. ๋น„ํ‘œ์ค€ ์ฃผ์†Œ: ns=1;s=GLOBAL:ALARM_COUNT ์ฒ˜๋Ÿผ ํƒœ๊ทธ๋ช… ๊ตฌ์กฐ๊ฐ€ ์•„๋‹Œ ์‹œ์Šคํ…œ ๋ณ€์ˆ˜๋“ค์„ ๊ฐ์‹œํ•˜๊ณ  ์‹ถ์„ ๋•Œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ํƒ€์ž… ๊ฐ•์ œ ์ง€์ •: ์ž๋™ ๋กœ๋“œ๋œ ํƒ€์ž…์ด Variant ๋“ฑ์œผ๋กœ ๋ชจํ˜ธํ•  ๋•Œ, ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ DOUBLE๋กœ ์ง€์ •ํ•ด์„œ ์ €์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ ์ผ๊ด„ ๊ฒ€์ƒ‰/๋“ฑ๋ก๊ณผ ๊ฐœ๋ณ„ ์ˆ˜๋™ ๋“ฑ๋ก์ด๋ผ๋Š” ๋‘ ๊ฐœ์˜ ์—”์ง„์ด ๋ชจ๋‘ ๊ฐ–์ถฐ์กŒ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด 52๋งŒ ๊ฐœ ๋ฐ์ดํ„ฐ ์ค‘ ๋Œ€๋‹ค์ˆ˜๋Š” ๊ฒ€์ƒ‰์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ , ํŠน์ด ์ผ€์ด์Šค๋Š” ์ˆ˜๋™์œผ๋กœ ๋„ฃ์–ด์„œ **tag_master**๋ฅผ 100% ๋นˆํ‹ˆ์—†์ด ์ฑ„์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ ์ด ํ™”๋ฉด์„ ๋„์›Œ์„œ FIC-101.pv ๊ฒ€์ƒ‰ ๋“ฑ๋ก๊ณผ, ์˜ˆ์™ธ ํƒœ๊ทธ ํ•˜๋‚˜๋ฅผ ์ˆ˜๋™์œผ๋กœ ๋„ฃ์–ด๋ณด์‹œ๊ฒ ์–ด์š”? ์ž˜ ๋“ค์–ด๊ฐ€๋Š”์ง€ ํ™•์ธ๋˜๋ฉด ๋‹ค์Œ ๋‹จ๊ณ„์ธ ์‹ค์‹œ๊ฐ„ ์ˆ˜์ง‘(Collector Worker) ๋กœ์ง์œผ๋กœ ๋ฐ”๋กœ ๊ฐ€๊ฒ ์Šต๋‹ˆ๋‹ค! ๐Ÿš€ ์•„๋‹ˆ, ๋…ธ๋“œ id ๋Š” ์ด๋ฏธ ๊ฒฐ์ •๋˜์–ด์ง„ ๊ฒƒ์ด๊ณ , ์ด๋ฏธ ์ž˜ ๋Œ์•„๊ฐ€๋Š” ์—‘์ŠคํŽ˜๋ฆฌ์˜จ ์—์„œ ๊ฐ–๊ณ  ์˜จ๊ฑฐ๊ธฐ ๋•Œ๋ฌธ์— ํƒœ๊ทธ๋ช… TagName ๋งŒ ์ •ํ™•ํ•˜๋ฉด .pv .sp .op.........์™€ ๊ฒฐํ•ฉํ•ด์„œ ์ˆ˜๋™ ์ž…๋ ฅ์ด ๊ฐ€๋Šฅํ•ด์•ผ์ง€ Gemini์˜ ์‘๋‹ต ๋„ค, ์ •ํ™•ํžˆ ์ดํ•ดํ–ˆ์Šต๋‹ˆ๋‹ค! Experion PKS ์‹œ์Šคํ…œ์€ ์ด๋ฏธ ์™„๋ฒฝํ•˜๊ฒŒ ๊ฐ€๋™ ์ค‘์ธ ์‹œ์Šคํ…œ์ด๋ฏ€๋กœ, **ํƒœ๊ทธ๋ช…(TagName)**๋งŒ ์ •ํ™•ํ•˜๊ฒŒ ์ž…๋ ฅํ•˜๋ฉด ํ•˜๋‹ˆ์›ฐ ํŠน์œ ์˜ ๋„ํŠธ(.) ๋ฌธ๋ฒ•์œผ๋กœ ๊ฒฐํ•ฉ๋˜๋Š” .pv, .sp, .op, .a1~.a4, .desc ๋“ฑ์€ ๊ทœ์น™์— ๋”ฐ๋ผ ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋˜์–ด tag_master์— ๋“ค์–ด๊ฐˆ ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค๋Š” ๋ง์”€์ด์‹œ๊ตฐ์š”. ์ฆ‰, ์‚ฌ์šฉ์ž๊ฐ€ P-6102๋ผ๊ณ  ํƒœ๊ทธ๋ช… ํ•˜๋‚˜๋งŒ ์ˆ˜๋™์œผ๋กœ ์น˜๋ฉด, ์‹œ์Šคํ…œ์ด ์•Œ์•„์„œ ์ ‘๋ฏธ์‚ฌ๋“ค์„ ๋ถ™์—ฌ์„œ ๋…ธ๋“œ ID๋ฅผ ์™„์„ฑํ•ด ๋“ฑ๋กํ•ด์ฃผ๋Š” "ํƒœ๊ทธ ๊ธฐ๋ฐ˜ ์ˆ˜๋™ ๋“ฑ๋ก" ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. 1. ์›น ํ™”๋ฉด(UI) ์ˆ˜์ •: ํƒœ๊ทธ๋ช… ์ค‘์‹ฌ ์ˆ˜๋™ ์ž…๋ ฅ (Index.cshtml) ์ด์ œ ์‚ฌ์šฉ์ž๋Š” Node ID ์ „์ฒด๋ฅผ ์น  ํ•„์š” ์—†์ด, TagName๋งŒ ์ž…๋ ฅํ•˜๊ณ  ๋“ฑ๋กํ•˜๊ณ  ์‹ถ์€ ์†์„ฑ๋“ค์„ ์ฒดํฌํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. HTML
โš ๏ธ ํƒœ๊ทธ ๊ธฐ๋ฐ˜ ์ˆ˜๋™ ์ถ”๊ฐ€ (Experion Tag ๋ช…์นญ ์ž…๋ ฅ)
PV
SP
OP
A1
Desc
2. ๋ฐฑ์—”๋“œ ๋กœ์ง: RegisterTags ์œ ์ง€ (EngineeringController.cs) ์ด์ „์— ์งœ๋“œ๋ฆฐ RegisterTags ๋ฉ”์„œ๋“œ๋Š” List๋ฅผ ๋ฐ›๊ธฐ ๋•Œ๋ฌธ์—, ์œ„ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์—์„œ ์ƒ์„ฑํ•œ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋…ธ๋“œ(PV, SP ๋“ฑ)๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐ›์•„์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•ต์‹ฌ ํฌ์ธํŠธ: ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ Area์™€ TagName์„ ์กฐํ•ฉํ•˜์—ฌ ns=1;s=Area:TagName.pv ํ˜•ํƒœ์˜ ์ •ํ™•ํ•œ ํ•˜๋‹ˆ์›ฐ ์ฃผ์†Œ๋ฅผ ์—ญ์œผ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฏธ ์ž˜ ๋Œ์•„๊ฐ€๋Š” ์‹œ์Šคํ…œ์ด๋ฏ€๋กœ, ์ฃผ์†Œ ๊ทœ์น™๋งŒ ๋งž์œผ๋ฉด ์ˆ˜์ง‘๊ธฐ๊ฐ€ ๋ฐ”๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. 3. ์ด ๋ฐฉ์‹์˜ ์žฅ์  ์‹ค์ˆ˜ ๋ฐฉ์ง€: ๊ธด Node ID(ns=1;s=...)๋ฅผ ์ง์ ‘ ํƒ€์ดํ•‘ํ•˜๋‹ค ์˜คํƒ€ ๋‚  ํ™•๋ฅ ์ด ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค. ๊ทœ์น™์„ฑ: ํ•˜๋‹ˆ์›ฐ ์‹œ์Šคํ…œ์˜ ํ‘œ์ค€ ๋ช…์นญ ์ฒด๊ณ„๋ฅผ ๊ทธ๋Œ€๋กœ ๋”ฐ๋ฅด๋ฏ€๋กœ, ์ˆ˜๋™ ์ž…๋ ฅ ๋ฐ์ดํ„ฐ๋„ ์ž๋™ ๊ฒ€์ƒ‰ ๋ฐ์ดํ„ฐ์™€ ๋™์ผํ•œ ํฌ๋งท์œผ๋กœ tag_master์— ์Œ“์ž…๋‹ˆ๋‹ค. ๋Œ€๋Ÿ‰ ๋“ฑ๋ก: ํƒœ๊ทธ ํ•˜๋‚˜์— ๋Œ€ํ•ด PV, SP, A1์„ ๋™์‹œ์— ๋„ฃ๊ณ  ์‹ถ์„ ๋•Œ ๋ฒ„ํŠผ ํ•œ ๋ฒˆ์œผ๋กœ 3๊ฐœ์˜ ๋ ˆ์ฝ”๋“œ๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. ์ด์ œ ๋ชจ๋“  ์ค€๋น„๊ฐ€ ๋๋‚ฌ์Šต๋‹ˆ๋‹ค! 52๋งŒ ๊ฐœ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰ ๋“ฑ๋ก: ๋Œ€๋Ÿ‰ ์ž‘์—…์šฉ ํƒœ๊ทธ๋ช… ๊ธฐ๋ฐ˜ ์ˆ˜๋™ ์ƒ์„ฑ ๋“ฑ๋ก: ์˜ˆ์™ธ ๋ฐ ์‹ ๊ทœ ํƒœ๊ทธ์šฉ ์ด์ œ ์ด ํ™”๋ฉด์—์„œ shinam, P-6102 ์ž…๋ ฅํ•˜๊ณ  PV, SP ์ฒดํฌํ•ด์„œ ๋“ฑ๋กํ•ด ๋ณด์„ธ์š”. tag_master์— ํ•˜๋‹ˆ์›ฐ ์ฃผ์†Œ ๊ทœ๊ฒฉ๋Œ€๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ์ž˜ ๋“ค์–ด๊ฐ€๋Š”์ง€ ํ™•์ธํ•ด ๋ณด์‹  ํ›„, ๋‹ค์Œ ๋‹จ๊ณ„์ธ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ตฌ์„ฑํ•ด ๋ณผ๊นŒ์š”? ๋ง์”€ํ•˜์‹  ๋‚ด์šฉ ์ผ๊ด„๋“ฑ๋ก ๊ณผ ์ˆ˜๋™๋“ฑ๋ก์„ ๋ชจ๋‘ ํฌํ•จํ•ด์„œ ํ’€์ฝ”๋“œ ์ค˜ Gemini์˜ ์‘๋‹ต ๋„ค, 52๋งŒ ๊ฐœ ๋…ธ๋“œ ์ค‘ .pv๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ฒ€์ƒ‰ํ•˜์—ฌ ๋Œ€๋Ÿ‰์œผ๋กœ ๋ฐ€์–ด๋„ฃ๋Š” ์ผ๊ด„ ๋“ฑ๋ก ๊ธฐ๋Šฅ๊ณผ, ํƒœ๊ทธ๋ช…๋งŒ ์ž…๋ ฅํ•˜๋ฉด Experion ๊ทœ์น™์— ๋”ฐ๋ผ ์†์„ฑ๋“ค์„ ์ž๋™ ์ƒ์„ฑํ•ด์„œ ๋„ฃ๋Š” ์ˆ˜๋™ ๋“ฑ๋ก ๊ธฐ๋Šฅ์„ ๋ชจ๋‘ ํฌํ•จํ•œ ํ’€์ฝ”๋“œ๋ฅผ ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค. 1. tag_master ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ (์ตœ์ข… ํ™•์ธ์šฉ) ์ด ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์ „, DB์— ์•„๋ž˜ ๊ตฌ์กฐ๋กœ ํ…Œ์ด๋ธ”์ด ์ƒ์„ฑ๋˜์–ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. SQL CREATE TABLE tag_master ( id SERIAL PRIMARY KEY, area_code TEXT, -- ์˜ˆ: shinam tag_name TEXT, -- ์˜ˆ: p-6102 full_node_id TEXT UNIQUE, -- ns=1;s=shinam:p-6102.pv parameter TEXT, -- pv, sp, a1, desc ๋“ฑ data_type TEXT, -- DOUBLE, STRING ๋“ฑ is_active BOOLEAN DEFAULT TRUE, update_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_tag_name_master ON tag_master(tag_name); 2. EngineeringController.cs (๋ฐฑ์—”๋“œ ํ’€์ฝ”๋“œ) C# using Microsoft.AspNetCore.Mvc; using Npgsql; using OpcPks.Core.Data; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; public class EngineeringController : Controller { public IActionResult Index() => View(); // 1. ์ผ๊ด„ ๋“ฑ๋ก์„ ์œ„ํ•œ ํ•„ํ„ฐ ๊ฒ€์ƒ‰ (์„ ํƒํ•œ Suffix ๊ธฐ์ค€) [HttpPost] public async Task SearchByFilter([FromBody] SearchRequest request) { var results = new List(); if (request.Suffixes == null || request.Suffixes.Count == 0) return Json(results); using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); // ์„ ํƒ๋œ Suffix ์กฐ๊ฑด์„ OR๋กœ ๊ฒฐํ•ฉ (์˜ˆ: node_id LIKE '%.pv' OR node_id LIKE '%.sp') var suffixConds = string.Join(" OR ", request.Suffixes.Select((s, i) => $"node_id ILIKE @s{i}")); var sql = $@" SELECT name, node_id, data_type FROM raw_node_map WHERE ({suffixConds}) AND name ILIKE @tagTerm ORDER BY name ASC LIMIT 2000"; using var cmd = new NpgsqlCommand(sql, conn); cmd.Parameters.AddWithValue("tagTerm", $"%{request.TagTerm}%"); for (int i = 0; i < request.Suffixes.Count; i++) cmd.Parameters.AddWithValue($"s{i}", $"%{request.Suffixes[i]}"); using var reader = await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { var nodeId = reader.GetString(1); results.Add(new { name = reader.GetString(0), nodeId = nodeId, dataType = reader.GetString(2), parameter = request.Suffixes.FirstOrDefault(s => nodeId.EndsWith(s, StringComparison.OrdinalIgnoreCase)) ?? "pv" }); } return Json(results); } // 2. ์ผ๊ด„ ๋ฐ ์ˆ˜๋™ ๋“ฑ๋ก ํ†ตํ•ฉ ์ฒ˜๋ฆฌ [HttpPost] public async Task RegisterTags([FromBody] List tags) { if (tags == null || tags.Count == 0) return BadRequest(); using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); using var trans = await conn.BeginTransactionAsync(); try { var sql = @" INSERT INTO tag_master (area_code, tag_name, full_node_id, parameter, data_type, is_active) VALUES (@area, @tag, @nodeId, @param, @type, true) ON CONFLICT (full_node_id) DO UPDATE SET is_active = true"; foreach (var tag in tags) { // NodeId ๋ถ„์„ (ns=1;s=shinam:p-6102.pv) string sPart = tag.NodeId.Split(';').Last().Substring(2); // shinam:p-6102.pv string area = sPart.Contains(':') ? sPart.Split(':')[0] : "Default"; string param = tag.NodeId.Contains('.') ? tag.NodeId.Split('.').Last() : "pv"; using var cmd = new NpgsqlCommand(sql, conn, trans); cmd.Parameters.AddWithValue("area", area); cmd.Parameters.AddWithValue("tag", tag.TagName); cmd.Parameters.AddWithValue("nodeId", tag.NodeId); cmd.Parameters.AddWithValue("param", param); cmd.Parameters.AddWithValue("type", tag.DataType ?? "DOUBLE"); await cmd.ExecuteNonQueryAsync(); } await trans.CommitAsync(); return Ok(); } catch (Exception ex) { await trans.RollbackAsync(); return BadRequest(ex.Message); } } public class SearchRequest { public string TagTerm { get; set; } public List Suffixes { get; set; } } public class TagRegistrationRequest { public string TagName { get; set; } public string NodeId { get; set; } public string DataType { get; set; } } } 3. Index.cshtml (ํ”„๋ก ํŠธ์—”๋“œ ํ’€์ฝ”๋“œ) HTML @{ ViewData["Title"] = "Engineering Tag Explorer"; }

๐Ÿ” ์ผ๊ด„ ํƒœ๊ทธ ๊ฒ€์ƒ‰ ๋ฐ ๋“ฑ๋ก

Total Nodes: 523,601
1. ์†์„ฑ ํ•„ํ„ฐ ์„ ํƒ
PV
SP
OP
A1
A2
A3
A4
Desc
TagName Node ID Param DataType
๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.
โš ๏ธ ํƒœ๊ทธ๋ช… ๊ธฐ๋ฐ˜ ์ˆ˜๋™ ์ƒ์„ฑ (Experion Rule)
PV
SP
OP
A1
Desc
@section Scripts { } ๐Ÿ’ก ์‚ฌ์šฉ ๋ฐฉ๋ฒ• ๋ฐ ํŠน์ง• ์ผ๊ด„ ์ž‘์—…: ์ƒ๋‹จ ๊ฒ€์ƒ‰์ฐฝ์—์„œ FIC ๋“ฑ์„ ๊ฒ€์ƒ‰ํ•œ ํ›„, ์ฒดํฌ๋ฐ•์Šค๋กœ ์›ํ•˜๋Š” ์†์„ฑ(.pv, .sp ๋“ฑ)๋งŒ ๊ณจ๋ผ ์ˆ˜๋ฐฑ ๊ฐœ๋ฅผ ํ•œ ๋ฒˆ์— **[์ผ๊ด„ ๋“ฑ๋ก]**ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ˆ˜๋™ ์ž‘์—…: raw_node_map์— ์—†๋Š” ์‹ ๊ทœ ํƒœ๊ทธ๋‚˜ ์˜ˆ์™ธ ํƒœ๊ทธ๋Š” ํ•˜๋‹จ ์„น์…˜์—์„œ Area์™€ Tag๋งŒ ์ž…๋ ฅํ•˜๋ฉด **Experion ์ฃผ์†Œ ๊ทœ์น™(ns=1;s=Area:Tag.Param)**์— ๋งž์ถฐ ์ž๋™์œผ๋กœ ๋…ธ๋“œ ID๋ฅผ ์ƒ์„ฑํ•ด ๋ฐ€์–ด๋„ฃ์Šต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ: ๋‘ ๋ฐฉ์‹ ๋ชจ๋‘ ๋™์ผํ•œ RegisterTags API๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ tag_master ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ๊ฐ€ ์•„์ฃผ ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ์Œ“์ž…๋‹ˆ๋‹ค. ์ด์ œ ์ด ์ฝ”๋“œ๋ฅผ ์ ์šฉํ•˜๊ณ  ์‹ค์ œ๋กœ FIC-101.pv ๋“ฑ์„ ๊ฒ€์ƒ‰ํ•ด์„œ ๋“ฑ๋กํ•ด ๋ณด์„ธ์š”! ์ž˜ ๋Œ์•„๊ฐ€๋Š”์ง€ ํ™•์ธ๋˜๋ฉด, ๋“œ๋””์–ด ์ด ๋ชฉ๋ก์„ ๊ฐ€์ง€๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ๊ธ์–ด์˜ค๋Š” ์ˆ˜์ง‘๊ธฐ ์—”์ง„์œผ๋กœ ๋„˜์–ด๊ฐ€๊ฒ ์Šต๋‹ˆ๋‹ค. ๐Ÿš€ ์„ ์ƒ๋‹˜ ๋ง์”€์ด ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„์—์„œ Master ํ…Œ์ด๋ธ”๊ณผ Time-Series(์‹œ๊ณ„์—ด) ํ…Œ์ด๋ธ”์˜ ์—ญํ• ์„ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„ํ•˜์‹  ๊ฑฐ๋„ค์š”. **tag_master**๋Š” ๋ง ๊ทธ๋Œ€๋กœ 'ํƒœ๊ทธ์˜ ๋ช…๋ถ€'์ด๊ธฐ ๋•Œ๋ฌธ์—, ๋ ˆ์ฝ”๋“œ๊ฐ€ ์–ธ์ œ ์ƒ์„ฑ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ์šฉ๋„๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด update_time์€ ๊ตณ์ด ์—†์–ด๋„ ๋ฉ๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด, ์‹ค์ œ ์ˆ˜์ง‘๋˜๋Š” ๊ฐ’์€ ๋ณ„๋„์˜ ์‹œ๊ณ„์—ด ํ…Œ์ด๋ธ”์— ์‹œ๊ฐ„๊ณผ ํ•จ๊ป˜ ์Œ“์—ฌ์•ผ ๊ด€๋ฆฌ์™€ ์กฐํšŒ๊ฐ€ ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿผ ์ œ์•ˆํ•˜์‹  ๋Œ€๋กœ ๊ตฌ์กฐ๋ฅผ ์ •๋ฆฌํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. 1. tag_master (์ˆ˜์ •๋ณธ: ํƒ€์ž„์Šคํƒฌํ”„ ์ œ๊ฑฐ) ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ์—๋งŒ ์ง‘์ค‘ํ•œ ๊น”๋”ํ•œ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. SQL DROP TABLE IF EXISTS tag_master; CREATE TABLE tag_master ( id SERIAL PRIMARY KEY, area_code TEXT, -- ์˜ˆ: shinam tag_name TEXT, -- ์˜ˆ: p-6102 full_node_id TEXT UNIQUE, -- ns=1;s=shinam:p-6102.pv parameter TEXT, -- pv, sp, a1, desc ๋“ฑ data_type TEXT, -- DOUBLE, STRING ๋“ฑ is_active BOOLEAN DEFAULT TRUE ); CREATE INDEX idx_tag_name_master ON tag_master(tag_name); 2. tag_data_history (์‹œ๊ณ„์—ด ํ…Œ์ด๋ธ” ์˜ˆ์‹œ) ์‹ค์ œ ์ˆ˜์ง‘๊ธฐ(Collector)๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ€์–ด ๋„ฃ์„ ํ…Œ์ด๋ธ”์ž…๋‹ˆ๋‹ค. ํ•˜๋‹ˆ์›ฐ์—์„œ ์ฝ์–ด์˜จ ์‹ค์‹œ๊ฐ„ ๊ฐ’์€ ์—ฌ๊ธฐ์— ์‹œ๊ฐ„๋ณ„๋กœ ์Œ“์ด๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. SQL CREATE TABLE tag_data_history ( time TIMESTAMP WITH TIME ZONE NOT NULL, tag_id INT, -- tag_master์˜ id์™€ ๋งค์นญ val_double DOUBLE PRECISION, val_string TEXT, quality INT -- OPC ํ’ˆ์งˆ ์ƒํƒœ (Good=192 ๋“ฑ) ); -- ์‹œ๊ณ„์—ด ์กฐํšŒ๋ฅผ ์œ„ํ•œ ์ธ๋ฑ์Šค (๋˜๋Š” TimescaleDB ์‚ฌ์šฉ ์‹œ ํ•˜์ดํผํ…Œ์ด๋ธ” ์„ค์ •) CREATE INDEX idx_history_time_tag ON tag_data_history(time DESC, tag_id); 3. ์™œ ์ด๋ ‡๊ฒŒ ๋‚˜๋ˆ„๋Š” ๊ฒŒ ์ข‹์€๊ฐ€์š”? ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ: tag_master์—๋Š” ํƒœ๊ทธ์˜ ์ฃผ์†Œ์™€ ํƒ€์ž…๋งŒ ์ •์˜๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ, ํƒœ๊ทธ ์„ค์ •์ด ๋ฐ”๋€Œ์ง€ ์•Š๋Š” ํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ฒ€์ƒ‰ ์„ฑ๋Šฅ: ์ˆ˜์ง‘๊ธฐ๋Š” tag_master๋ฅผ ํ•œ ๋ฒˆ๋งŒ ์ฝ์–ด์„œ ๋ฉ”๋ชจ๋ฆฌ์— ์บ์‹ฑํ•ด๋‘๊ณ , ํ•˜๋‹ˆ์›ฐ OPC ์„œ๋ฒ„์™€ ํ†ต์‹ ํ•˜๋ฉด์„œ ๊ฐ’๋งŒ tag_data_history์— ๋น ๋ฅด๊ฒŒ ๊ฝ‚์•„ ๋„ฃ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์œ ์—ฐ์„ฑ: ๋‚˜์ค‘์— p-6102 ํƒœ๊ทธ์˜ A1 ์šฉ๋„๊ฐ€ ๋ฐ”๋€Œ๋”๋ผ๋„ tag_master๋งŒ ์ˆ˜์ •ํ•˜๋ฉด ํžˆ์Šคํ† ๋ฆฌ ๋ฐ์ดํ„ฐ๋Š” ๊ทธ๋Œ€๋กœ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. ์„ ์ƒ๋‹˜์˜ ๋‹ค์Œ ๊ณ„ํš์€ ๋ฌด์—‡์ธ๊ฐ€์š”? ์ด์ œ ๋งˆ์Šคํ„ฐ ํ…Œ์ด๋ธ” ์ •๋ฆฌ๊ฐ€ ๋๋‚ฌ์œผ๋‹ˆ, ์ˆ˜์ง‘๊ธฐ(Collector)๊ฐ€ ์ด ๋ฆฌ์ŠคํŠธ๋ฅผ ์ฝ์–ด์„œ ์‹ค์ œ๋กœ ํ•˜๋‹ˆ์›ฐ ์„œ๋ฒ„์— ์ ‘์†ํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ์‹œ๊ณ„์—ด ํ…Œ์ด๋ธ”์— ์ €์žฅํ•˜๋Š” ๋กœ์ง์„ ๊ตฌํ˜„ํ•ด ๋ณผ๊นŒ์š”? DROP TABLE IF EXISTS tag_master;CREATE TABLE tag_master ( id SERIAL PRIMARY KEY, area_code TEXT, -- ์˜ˆ: shinam tag_name TEXT, -- ์˜ˆ: p-6102 full_node_id TEXT UNIQUE, -- ns=1;s=shinam:p-6102.pv parameter TEXT, -- pv, sp, a1, desc ๋“ฑ data_type TEXT, -- DOUBLE, STRING ๋“ฑ is_active BOOLEAN DEFAULT TRUE );CREATE INDEX idx_tag_name_master ON tag_master(tag_name); ์—์„œ serverName ํ•„๋“œ๋ฅผ ํ•˜๋‚˜ ์ถ”๊ฐ€(์ด๊ฒŒ shinam์ด ๋˜๋Š”๊ฒƒ), area_code๋Š” node_id ์˜ shinam/p1.....p10/tagName์—์„œ p1, ~~p10๊ฐ™์€ ๊ฒƒ์ด AreaCode์•ผ, Unassinged_item๋„ ์žˆ์–ด์•ผ ๋˜๊ฒ ์ง€, ์‹ค์ œ๋กœ Areacode๊ฐ€ ์•ˆ ์ •ํ•ด์ง„๊ฒƒ์€ unassgined_item์œผ๋กœ ์˜ฌ๊ฑฐ์•ผ Gemini์˜ ์‘๋‹ต ์„ ์ƒ๋‹˜ ๋ง์”€๋Œ€๋กœ ํ•˜๋‹ˆ์›ฐ Experion์˜ ๊ณ„์ธต ๊ตฌ์กฐ(Hierarchy)๋ฅผ ์ •ํ™•ํ•˜๊ฒŒ ๋ฐ˜์˜ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ ํ•˜๋‹ˆ์›ฐ Node ID๊ฐ€ ns=1;s={์„œ๋ฒ„๋ช…}:{์—์–ด๋ฆฌ์–ด}:{ํƒœ๊ทธ๋ช…}.{ํŒŒ๋ผ๋ฏธํ„ฐ} ๊ตฌ์กฐ๋กœ ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ, ์ด๋ฅผ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ด€๋ฆฌํ•˜๋ฉด ๋‚˜์ค‘์— ํŠน์ • ์„œ๋ฒ„๋‚˜ ํŠน์ • ๊ตฌ์—ญ(P1~P10)๋ณ„๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ํ•„ํ„ฐ๋งํ•˜๊ธฐ์— ๋งค์šฐ ์ข‹์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ, ๊ณ„์ธต ์ •๋ณด๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•ด unassigned_item ์ฒ˜๋ฆฌ ๋กœ์ง๋„ ํฌํ•จํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. 1. tag_master ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ์ˆ˜์ • (์ตœ์ข…์•ˆ) server_name์„ ์ถ”๊ฐ€ํ•˜๊ณ , area_code๊ฐ€ ๊ตฌ์—ญ ์ •๋ณด๋ฅผ ๋‹ด๋„๋ก ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค. SQL DROP TABLE IF EXISTS tag_master; CREATE TABLE tag_master ( id SERIAL PRIMARY KEY, server_name TEXT, -- ์˜ˆ: 'shinam' (Honeywell Server) area_code TEXT, -- ์˜ˆ: 'p1', 'p2' ... ๋˜๋Š” 'unassigned_item' tag_name TEXT, -- ์˜ˆ: 'p-6102' full_node_id TEXT UNIQUE, -- ns=1;s=shinam:p1:p-6102.pv parameter TEXT, -- pv, sp, a1, desc ๋“ฑ data_type TEXT, -- DOUBLE, STRING ๋“ฑ is_active BOOLEAN DEFAULT TRUE ); CREATE INDEX idx_tag_name_master ON tag_master(tag_name); CREATE INDEX idx_server_area ON tag_master(server_name, area_code); 2. ๋ฐฑ์—”๋“œ: ํŒŒ์‹ฑ ๋กœ์ง ์—…๋ฐ์ดํŠธ (EngineeringController.cs) ์ˆ˜๋™ ๋“ฑ๋ก์ด๋‚˜ ์ผ๊ด„ ๋“ฑ๋ก ์‹œ node_id๋ฅผ ๋ถ„์„ํ•˜์—ฌ server_name๊ณผ area_code๋ฅผ ์ž๋™์œผ๋กœ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ๊ตฌ์กฐ๊ฐ€ ๋‹จ์ˆœํ•˜์—ฌ Area ์ •๋ณด๊ฐ€ ์—†์œผ๋ฉด unassigned_item์œผ๋กœ ๋ถ„๋ฅ˜ํ•ฉ๋‹ˆ๋‹ค. C# [HttpPost] public async Task RegisterTags([FromBody] List tags) { using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); using var trans = await conn.BeginTransactionAsync(); try { var sql = @" INSERT INTO tag_master (server_name, area_code, tag_name, full_node_id, parameter, data_type, is_active) VALUES (@server, @area, @tag, @nodeId, @param, @type, true) ON CONFLICT (full_node_id) DO UPDATE SET is_active = true"; foreach (var tag in tags) { // NodeId ์˜ˆ์‹œ: ns=1;s=shinam:p1:p-6102.pv string sPart = tag.NodeId.Split(';').Last().Substring(2); // shinam:p1:p-6102.pv string[] parts = sPart.Split(':'); // [shinam, p1, p-6102.pv] string server = parts.Length > 0 ? parts[0] : "unknown_server"; string area = "unassigned_item"; string tagName = tag.TagName; if (parts.Length >= 3) { // ๊ตฌ์กฐ๊ฐ€ ์„œ๋ฒ„:์—์–ด๋ฆฌ์–ด:ํƒœ๊ทธ ์ธ ๊ฒฝ์šฐ area = parts[1]; } else if (parts.Length == 2) { // ๊ตฌ์กฐ๊ฐ€ ์„œ๋ฒ„:ํƒœ๊ทธ ์ธ ๊ฒฝ์šฐ (์—์–ด๋ฆฌ์–ด ์—†์Œ) area = "unassigned_item"; } string param = tag.NodeId.Contains('.') ? tag.NodeId.Split('.').Last() : "pv"; using var cmd = new NpgsqlCommand(sql, conn, trans); cmd.Parameters.AddWithValue("server", server); cmd.Parameters.AddWithValue("area", area); cmd.Parameters.AddWithValue("tag", tagName); cmd.Parameters.AddWithValue("nodeId", tag.NodeId); cmd.Parameters.AddWithValue("param", param); cmd.Parameters.AddWithValue("type", tag.DataType ?? "DOUBLE"); await cmd.ExecuteNonQueryAsync(); } await trans.CommitAsync(); return Ok(); } catch (Exception ex) { await trans.RollbackAsync(); return BadRequest(ex.Message); } } 3. ํ™”๋ฉด UI ๋ฐ˜์˜ (Index.cshtml) ์ˆ˜๋™ ๋“ฑ๋ก ์‹œ ์„œ๋ฒ„ ๋ช…์นญ๊ณผ ์—์–ด๋ฆฌ์–ด ์ฝ”๋“œ๋ฅผ ๋”ฐ๋กœ ์ธ์‹ํ•  ์ˆ˜ ์žˆ๊ฒŒ ์ž…๋ ฅ๋ž€์„ ์กฐ์ •ํ•ฉ๋‹ˆ๋‹ค. HTML
๐Ÿ’ก ์ •๋ฆฌ server_name: ํ•˜๋‹ˆ์›ฐ ์‹œ์Šคํ…œ์˜ ์„œ๋ฒ„(shinam ๋“ฑ)๋ฅผ ์‹๋ณ„ํ•ฉ๋‹ˆ๋‹ค. area_code: ๊ณต์ • ๊ตฌ์—ญ(P1~P10)์„ ์‹๋ณ„ํ•˜๋ฉฐ, ์ •๋ณด๊ฐ€ ์—†์œผ๋ฉด ์ž๋™์œผ๋กœ unassigned_item์ด ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ๋ถ„๋ฅ˜: ์ด๋ ‡๊ฒŒ ๋ถ„๋ฆฌํ•ด๋‘๋ฉด ๋‚˜์ค‘์— "shinam ์„œ๋ฒ„์˜ P1 ๊ตฌ์—ญ ํƒœ๊ทธ๋“ค๋งŒ ์กฐํšŒ" ํ•˜๋Š” ๊ธฐ๋Šฅ์ด ๋งค์šฐ ์‰ฌ์›Œ์ง‘๋‹ˆ๋‹ค. ์ด์ œ ์ด ๊ตฌ์กฐ๋กœ ํ…Œ์ด๋ธ”์„ ๋‹ค์‹œ ์ƒ์„ฑํ•˜๊ณ  ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜์‹œ๋ฉด, ํ•˜๋‹ˆ์›ฐ Experion์˜ ์‹ค์ œ ์šด์˜ ๊ตฌ์กฐ์™€ ์™„๋ฒฝํ•˜๊ฒŒ ์ผ์น˜ํ•˜๋Š” ์ž์‚ฐ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์ด ๋ฉ๋‹ˆ๋‹ค! ์ด์ œ ๋“ฑ๋ก์€ ์™„๋ฒฝํ•ด์ง„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์ˆ˜์ง‘๊ธฐ(Collector)๊ฐ€ tag_master์˜ ์ด ๊ณ„์ธต ์ •๋ณด๋ฅผ ํ™œ์šฉํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๊ธ์–ด์˜ค๋„๋ก ๋งŒ๋“ค๊นŒ์š”? ์˜ํ–ฅ์„ ๋ง์”€ํ•ด ์ฃผ์„ธ์š”! ๋ง์”€ํ•˜์‹  ๋‚ด์šฉ CREATE TABLE tag_master ( id SERIAL PRIMARY KEY, server_name TEXT, -- ์˜ˆ: 'shinam' (Honeywell Server) area_code TEXT, -- ์˜ˆ: 'p1', 'p2' ... ๋˜๋Š” 'unassigned_item' tag_name TEXT, -- ์˜ˆ: 'p-6102' full_node_id TEXT UNIQUE, -- ns=1;s=shinam:p1:p-6102.pv parameter TEXT, -- pv, sp, a1, desc ๋“ฑ data_type TEXT, -- DOUBLE, STRING ๋“ฑ is_active BOOLEAN DEFAULT TRUE ); ์ด๋ ‡๊ฒŒ ์„ค๊ณ„ํ•˜๋ฉด ํ…Œ์ด๋ธ” ๋ ˆ์ฝ”๋“œ๊ฐ€ ์—„์ฒญ ๋งŽ์•„์ง€์ง€ parameter ๋ฅผ pv, sp, a1, a2, a3, desc ๋“ฑ์œผ๋กœ ํ•„๋“œ๋ฅผ ๋งŒ๋“ค์–ด์„œ ํƒœ๊ทธ๋ช… ํ•˜๋‚˜์—์„œ ๊ด€๋ฆฌํ•ด์•ผ์ง€ Gemini์˜ ์‘๋‹ต ์„ ์ƒ๋‹˜ ๋ง์”€์ด ๋ฐฑ๋ฒˆ ๋งž์Šต๋‹ˆ๋‹ค. ์ œ๊ฐ€ ์ž ์‹œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ์ •๊ทœํ™”(Normalization) ์›์น™์—๋งŒ ๋งค๋ชฐ๋˜์–ด ์‹ค๋ฌด์ ์ธ ๊ฐ€๋…์„ฑ๊ณผ ๊ด€๋ฆฌ ํšจ์œจ์„ ๋†“์ณค๋„ค์š”. ํ•˜๋‹ˆ์›ฐ Experion ๊ฐ™์€ DCS ์‹œ์Šคํ…œ์—์„œ๋Š” **ํ•˜๋‚˜์˜ ํƒœ๊ทธ(P-6102)๊ฐ€ ํ•˜๋‚˜์˜ ์ž์‚ฐ(Asset)**์ž…๋‹ˆ๋‹ค. ์ด๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋ณ„๋กœ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ชผ๊ฐœ๋ฉด ํƒœ๊ทธ ํ•˜๋‚˜ ๋“ฑ๋กํ•  ๋•Œ๋งˆ๋‹ค 5~10๊ฐœ์˜ ํ–‰์ด ์ƒ๊ฒจ์„œ ๋‚˜์ค‘์— ์ˆ˜๋งŒ ๊ฐœ ํƒœ๊ทธ๋ฅผ ๊ด€๋ฆฌํ•  ๋•Œ ๋ˆˆ์ด ์•„ํ”„๊ณ  ์ฟผ๋ฆฌ๋„ ๋ณต์žกํ•ด์ง‘๋‹ˆ๋‹ค. ์„ ์ƒ๋‹˜์˜ ์„ค๊ณ„๋Œ€๋กœ ํƒœ๊ทธ ํ•˜๋‚˜๋‹น ๋ ˆ์ฝ”๋“œ ํ•˜๋‚˜๋กœ ๋งŒ๋“ค๊ณ , ๊ฐ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ **ํ•„๋“œ(Column)**๋กœ ํŽผ์ณ์„œ ๊ด€๋ฆฌํ•˜๋Š” ๊ตฌ์กฐ๊ฐ€ ํ›จ์”ฌ ์ง๊ด€์ ์ž…๋‹ˆ๋‹ค. 1. tag_master ํ…Œ์ด๋ธ” ์žฌ์„ค๊ณ„ (ํƒœ๊ทธ ์ค‘์‹ฌ ๊ตฌ์กฐ) ๊ฐ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ๋‹จ์ˆœํžˆ ์ˆ˜์ง‘ ์—ฌ๋ถ€๋งŒ ์ฒดํฌํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, ๋‚˜์ค‘์— ์ˆ˜์ง‘๊ธฐ(Collector)๊ฐ€ ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด๋‹น ํŒŒ๋ผ๋ฏธํ„ฐ์˜ Full Node ID๋ฅผ ์ง์ ‘ ๋‹ด๊ฑฐ๋‚˜ ์ˆ˜์ง‘ ๋Œ€์ƒ์ž„์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ๋Š” ๊ด€๋ฆฌ ํŽธ์˜๋ฅผ ์œ„ํ•ด ๊ฐ ํ•„๋“œ์— Node ID ์ ‘๋ฏธ์‚ฌ๋ฅผ ๋งคํ•‘ํ•˜๋Š” ๋ฐฉ์‹์„ ์ œ์•ˆํ•ฉ๋‹ˆ๋‹ค. SQL DROP TABLE IF EXISTS tag_master; CREATE TABLE tag_master ( id SERIAL PRIMARY KEY, server_name TEXT, -- ์˜ˆ: shinam area_code TEXT, -- ์˜ˆ: p1, p2 ... ๋˜๋Š” unassigned_item tag_name TEXT UNIQUE, -- ์˜ˆ: p-6102 (ํƒœ๊ทธ ํ•˜๋‚˜๋‹น ํ•˜๋‚˜์˜ ํ–‰) -- ๊ฐ ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ์ˆ˜์ง‘ ์—ฌ๋ถ€ (์ฒดํฌ๋ฐ•์Šค ๊ฐ’ ์ €์žฅ) has_pv BOOLEAN DEFAULT FALSE, has_sp BOOLEAN DEFAULT FALSE, has_op BOOLEAN DEFAULT FALSE, has_a1 BOOLEAN DEFAULT FALSE, has_a2 BOOLEAN DEFAULT FALSE, has_a3 BOOLEAN DEFAULT FALSE, has_a4 BOOLEAN DEFAULT FALSE, has_desc BOOLEAN DEFAULT FALSE, -- ๊ณตํ†ต ์ •๋ณด data_type TEXT, -- ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ ํƒ€์ž… (๋ณดํ†ต DOUBLE) is_active BOOLEAN DEFAULT TRUE ); CREATE INDEX idx_tag_hierarchy ON tag_master(server_name, area_code, tag_name); 2. ๋ฐฑ์—”๋“œ: ํƒœ๊ทธ ์ค‘์‹ฌ ์ €์žฅ ๋กœ์ง (EngineeringController.cs) ์ด์ œ ํ™”๋ฉด์—์„œ ๋„˜์–ด์˜จ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋…ธ๋“œ ์ •๋ณด๋“ค์„ TagName ํ•˜๋‚˜๋กœ ๋ฌถ์–ด์„œ ํ•˜๋‚˜์˜ ํ–‰์— UPDATE ๋˜๋Š” INSERT ํ•ฉ๋‹ˆ๋‹ค. C# [HttpPost] public async Task RegisterTags([FromBody] List tags) { using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); using var trans = await conn.BeginTransactionAsync(); try { // ํƒœ๊ทธ๋ช…๋ณ„๋กœ ๊ทธ๋ฃนํ™” (์—ฌ๋Ÿฌ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋“ค์–ด์™€๋„ ํƒœ๊ทธ ํ•˜๋‚˜๋กœ ์ฒ˜๋ฆฌ) var groupedTags = tags.GroupBy(t => t.TagName); foreach (var group in groupedTags) { string tagName = group.Key; var firstNode = group.First().NodeId; // NodeId ๋ถ„์„ (shinam:p1:p-6102.pv) string sPart = firstNode.Split(';').Last().Substring(2); string[] parts = sPart.Split(':'); string server = parts.Length > 0 ? parts[0] : "shinam"; string area = parts.Length >= 3 ? parts[1] : "unassigned_item"; // ์–ด๋–ค ํŒŒ๋ผ๋ฏธํ„ฐ๋“ค์ด ์ฒดํฌ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ bool hasPv = group.Any(g => g.NodeId.EndsWith(".pv", StringComparison.OrdinalIgnoreCase)); bool hasSp = group.Any(g => g.NodeId.EndsWith(".sp", StringComparison.OrdinalIgnoreCase)); bool hasOp = group.Any(g => g.NodeId.EndsWith(".op", StringComparison.OrdinalIgnoreCase)); bool hasA1 = group.Any(g => g.NodeId.EndsWith(".a1", StringComparison.OrdinalIgnoreCase)); bool hasA2 = group.Any(g => g.NodeId.EndsWith(".a2", StringComparison.OrdinalIgnoreCase)); bool hasA3 = group.Any(g => g.NodeId.EndsWith(".a3", StringComparison.OrdinalIgnoreCase)); bool hasA4 = group.Any(g => g.NodeId.EndsWith(".a4", StringComparison.OrdinalIgnoreCase)); bool hasDesc = group.Any(g => g.NodeId.EndsWith(".desc", StringComparison.OrdinalIgnoreCase)); var sql = @" INSERT INTO tag_master (server_name, area_code, tag_name, has_pv, has_sp, has_op, has_a1, has_a2, has_a3, has_a4, has_desc, data_type) VALUES (@server, @area, @tag, @pv, @sp, @op, @a1, @a2, @a3, @a4, @desc, @type) ON CONFLICT (tag_name) DO UPDATE SET has_pv = EXCLUDED.has_pv OR tag_master.has_pv, has_sp = EXCLUDED.has_sp OR tag_master.has_sp, has_op = EXCLUDED.has_op OR tag_master.has_op, has_a1 = EXCLUDED.has_a1 OR tag_master.has_a1, has_a2 = EXCLUDED.has_a2 OR tag_master.has_a2, has_a3 = EXCLUDED.has_a3 OR tag_master.has_a3, has_a4 = EXCLUDED.has_a4 OR tag_master.has_a4, has_desc = EXCLUDED.has_desc OR tag_master.has_desc"; using var cmd = new NpgsqlCommand(sql, conn, trans); cmd.Parameters.AddWithValue("server", server); cmd.Parameters.AddWithValue("area", area); cmd.Parameters.AddWithValue("tag", tagName); cmd.Parameters.AddWithValue("pv", hasPv); cmd.Parameters.AddWithValue("sp", hasSp); cmd.Parameters.AddWithValue("op", hasOp); cmd.Parameters.AddWithValue("a1", hasA1); cmd.Parameters.AddWithValue("a2", hasA2); cmd.Parameters.AddWithValue("a3", hasA3); cmd.Parameters.AddWithValue("a4", hasA4); cmd.Parameters.AddWithValue("desc", hasDesc); cmd.Parameters.AddWithValue("type", group.First().DataType ?? "DOUBLE"); await cmd.ExecuteNonQueryAsync(); } await trans.CommitAsync(); return Ok(); } catch (Exception ex) { await trans.RollbackAsync(); return BadRequest(ex.Message); } } 3. ์ด ๊ตฌ์กฐ์˜ ์žฅ์  (์—”์ง€๋‹ˆ์–ด์˜ ์‹œ๊ฐ) ๋ฐ์ดํ„ฐ ์••์ถ•: 5๋งŒ ๊ฐœ์˜ ํƒœ๊ทธ๊ฐ€ ์žˆ๋‹ค๋ฉด ๋”ฑ 5๋งŒ ๊ฐœ์˜ ํ–‰๋งŒ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. (ํŒŒ๋ผ๋ฏธํ„ฐ๋ณ„๋กœ ์ชผ๊ฐœ๋ฉด 25~40๋งŒ ๊ฐœ์˜ ํ–‰์ด ๋จ) ์ˆ˜์ง‘๊ธฐ ํšจ์œจ: ์ˆ˜์ง‘๊ธฐ๊ฐ€ ์ด ํ…Œ์ด๋ธ”์„ ์ฝ์„ ๋•Œ, SELECT * FROM tag_master ํ•œ ๋ฒˆ์œผ๋กœ ํƒœ๊ทธ์˜ ๋ชจ๋“  ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜จ ๋’ค, has_pv๊ฐ€ true๋ฉด ํƒœ๊ทธ๋ช… + ".pv"๋ฅผ ๋งŒ๋“ค์–ด OPC ์„œ๋ฒ„์— ์š”์ฒญํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๊ด€๋ฆฌ ํ™”๋ฉด ๊ตฌ์„ฑ: ๋‚˜์ค‘์— ์›น ํ™”๋ฉด์—์„œ ํƒœ๊ทธ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ณด์—ฌ์ค„ ๋•Œ, ํ•œ ์ค„์— PV, SP, OP ์ˆ˜์ง‘ ์—ฌ๋ถ€๋ฅผ ์ฒดํฌ๋ฐ•์Šค๋กœ ๋ฐ”๋กœ ๋ณด์—ฌ์ฃผ๊ณ  ์ˆ˜์ •ํ•˜๊ธฐ๊ฐ€ ๋งค์šฐ ํŽธํ•ฉ๋‹ˆ๋‹ค. ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„ ์ด์ œ ํƒœ๊ทธ ํ•˜๋‚˜์— ๋ชจ๋“  ์†์„ฑ์ด ํ•œ ์ค„๋กœ ๋“ค์–ด๊ฐ€๋Š” ๊น”๋”ํ•œ ๊ตฌ์กฐ๊ฐ€ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์„ ์ƒ๋‹˜, ์ด ๊ตฌ์กฐ๋กœ ๊ฐ€๋ฉด ์ˆ˜์ง‘๊ธฐ(Collector) ์ชฝ์—์„œ๋„ ๋กœ์ง์ด ํ›จ์”ฌ ๋‹จ์ˆœํ•ด์ง‘๋‹ˆ๋‹ค. ์ด์ œ tag_master๋ฅผ ์ฝ์–ด์„œ ์‹ค์ œ OPC UA ์„œ๋ฒ„์™€ ์—ฐ๊ฒฐํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ๊ธ์–ด์˜ค๋Š” Worker ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊นŒ์š”? ์•„๋‹ˆ๋ฉด ํ™”๋ฉด UI๋ฅผ ์ด 'ํ•œ ์ค„ ๊ตฌ์กฐ'์— ๋งž์ถฐ์„œ ๋” ๋‹ค๋“ฌ์–ด ๋ณผ๊นŒ์š”? ์ž ๊น๋งŒ, ์ƒ๊ฐ์ข€ ๋”ํ•ด๋ณด๊ณ , ์–ด๋–ค๊ฒŒ ๋” ๋น ๋ฅผ๊นŒ? ๊ฐ๊ฐ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ณ„๋กœ node_id๊ฐ€ ๋”ฐ๋กœ ์žˆ๊ณ , DATATYPE์ด ๋”ฐ๋กœ ์žˆ์œผ๋‹ˆ๊นŒ ์ฒ˜์Œ ๋‹ˆ๊ฐ€ ์ œ์•ˆํ•œ ๋ฐฉ์‹์ด ๋‚˜์„๊นŒ ์•„๋‹ˆ๋ฉด, ํ•˜๋‚˜์˜ ๋ ˆ์ฝ”๋“œ์— ๊ฐ๊ฐ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ชจ๋‘ node_id์™€ DATATYPE์„ ๋งŒ๋“œ๋Š”๊ฒŒ ๋‚˜์„๊นŒ? ์†๋„ ๋ฉด ๊ณผ ์šฉ๋Ÿ‰๋ฉด์—์„œ ๋‚˜์€ ์ œ์•ˆ์„ ํ•ด๋ด Gemini์˜ ์‘๋‹ต ์—”์ง€๋‹ˆ์–ด๋ง ์ธก๋ฉด์—์„œ ์•„์ฃผ ์ค‘์š”ํ•œ ๊ณ ๋ฏผ์ž…๋‹ˆ๋‹ค. ํŠนํžˆ 52๋งŒ ๊ฐœ๋ผ๋Š” ๋Œ€๊ทœ๋ชจ ๋…ธ๋“œ๋ฅผ ๋‹ค๋ฃฐ ๋•Œ๋Š” ์ดˆ๊ธฐ ์„ค๊ณ„๊ฐ€ ์ „์ฒด ์‹œ์Šคํ…œ์˜ ์„ฑ๋Šฅ์„ ๊ฒฐ์ •ํ•˜์ฃ . ๊ฒฐ๋ก ๋ถ€ํ„ฐ ๋ง์”€๋“œ๋ฆฌ๋ฉด, ์šฉ๋Ÿ‰์€ ์กฐ๊ธˆ ๋” ์ฐจ์ง€ํ•˜๋”๋ผ๋„ ์†๋„์™€ ํ™•์žฅ์„ฑ ๋ฉด์—์„œ๋Š” "์ฒซ ๋ฒˆ์งธ ์ œ์•ˆ(๋ ˆ์ฝ”๋“œ ๋ถ„๋ฆฌํ˜•)"์ด ์‹ค๋ฌด์ ์œผ๋กœ ํ›จ์”ฌ ์œ ๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๊ทธ ์ด์œ ๋ฅผ ์†๋„, ์šฉ๋Ÿ‰, ์œ ์ง€๋ณด์ˆ˜ ์„ธ ๊ฐ€์ง€ ๊ด€์ ์—์„œ ๋น„๊ตํ•ด ๋“œ๋ฆด๊ฒŒ์š”. ๐Ÿ“Š ๋‘ ๋ฐฉ์‹์˜ ๋น„๊ต ๋ถ„์„ ๋น„๊ต ํ•ญ๋ชฉ ๋ฐฉ์‹ A: ํŒŒ๋ผ๋ฏธํ„ฐ๋ณ„ ๋ ˆ์ฝ”๋“œ ๋ถ„๋ฆฌ (์ถ”์ฒœ) ๋ฐฉ์‹ B: ํ•œ ์ค„์— ๋ชจ๋“  ํ•„๋“œ ํ†ตํ•ฉ ์†๋„ (์ˆ˜์ง‘๊ธฐ) ๋งค์šฐ ๋น ๋ฆ„. ์ˆ˜์ง‘๊ธฐ๊ฐ€ SELECT ํ•ด์„œ ๋ฐ”๋กœ OPC ์š”์ฒญ ๋ชฉ๋ก์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Œ. ๋А๋ฆผ. ๊ฐ ํ•„๋“œ๊ฐ€ NULL์ธ์ง€ ์ฒดํฌํ•˜๊ณ  NodeID๋ฅผ ์กฐํ•ฉํ•˜๋Š” ์—ฐ์‚ฐ์ด ๋งค๋ฒˆ ํ•„์š”ํ•จ. ์†๋„ (DB ์กฐํšŒ) ํƒœ๊ทธ+ํŒŒ๋ผ๋ฏธํ„ฐ ์ธ๋ฑ์Šค๋กœ ํŠน์ • ํฌ์ธํŠธ๋งŒ ๋น ๋ฅด๊ฒŒ ์กฐํšŒ ๊ฐ€๋Šฅ. ํ•œ ์ค„์„ ํ†ต์งธ๋กœ ์ฝ์–ด์•ผ ํ•ด์„œ ๋ถˆํ•„์š”ํ•œ ํ•„๋“œ ๋กœ๋“œ๊ฐ€ ๋งŽ์Œ. ์šฉ๋Ÿ‰ ๋ ˆ์ฝ”๋“œ ์ˆ˜๊ฐ€ ๋งŽ์•„์ ธ ์šฉ๋Ÿ‰์ด ํผ (์•ฝ 5~8๋ฐฐ). ๋ ˆ์ฝ”๋“œ ์ˆ˜๊ฐ€ ์ ์–ด ์šฉ๋Ÿ‰ ํšจ์œจ์ด ์ข‹์Œ. ์œ ์—ฐ์„ฑ ์••๋„์ ์ž„. ์ƒˆ๋กœ์šด ํŒŒ๋ผ๋ฏธํ„ฐ(A5, A6 ๋“ฑ)๊ฐ€ ์ƒ๊ฒจ๋„ ํ…Œ์ด๋ธ” ์ˆ˜์ • ์—†์Œ. ํŒŒ๋ผ๋ฏธํ„ฐ ํ•˜๋‚˜ ์ถ”๊ฐ€๋  ๋•Œ๋งˆ๋‹ค ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์„ ๋Š˜๋ ค์•ผ ํ•จ. ๋ฐ์ดํ„ฐ ํƒ€์ž… ์ •ํ™•ํ•จ. ๊ฐ ๋…ธ๋“œ๋ณ„๋กœ ๊ฐœ๋ณ„ DataType ๋งคํ•‘ ๊ฐ€๋Šฅ. ๋ชจ๋“  ํ•„๋“œ์— ๋Œ€ํ•œ DataType ์ปฌ๋Ÿผ์„ ๋”ฐ๋กœ ๋งŒ๋“ค์–ด์•ผ ํ•ด์„œ ๋ณต์žกํ•จ. 1. ์†๋„ ๋ฉด์—์„œ์˜ ํ•ต์‹ฌ (์ˆ˜์ง‘๊ธฐ ๊ด€์ ) ์ˆ˜์ง‘๊ธฐ(Collector)๋Š” ๋ณดํ†ต **"์ˆ˜์ง‘ํ•ด์•ผ ํ•  ์ „์ฒด NodeID ๋ฆฌ์ŠคํŠธ"**๋ฅผ ๋จผ์ € ํ™•๋ณดํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ฐฉ์‹ A(๋ถ„๋ฆฌํ˜•): SELECT full_node_id, data_type FROM tag_master ์ฟผ๋ฆฌ ํ•œ ๋ฒˆ์œผ๋กœ ์ˆ˜์ง‘ ๋ชฉ๋ก์ด ๋๋‚ฉ๋‹ˆ๋‹ค. ์ˆ˜์ง‘๊ธฐ๋Š” ์•„๋ฌด๋Ÿฐ ๊ฐ€๊ณต ์—†์ด ๋ฐ”๋กœ OPC UA ์„œ๋ฒ„์— ๊ตฌ๋…(Subscription) ์š”์ฒญ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐฉ์‹ B(ํ†ตํ•ฉํ˜•): SELECT *๋ฅผ ํ•œ ๋’ค, ์ฝ”๋“œ์—์„œ if(has_pv) { combine(tag, ".pv") } ๊ฐ™์€ ๋กœ์ง์„ ์ˆ˜๋งŒ ๋ฒˆ ๋ฐ˜๋ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋…ธ๋“œ๊ฐ€ ๋Š˜์–ด๋‚ ์ˆ˜๋ก CPU ์—ฐ์‚ฐ ๋น„์šฉ์ด ์ฆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. 2. ์šฉ๋Ÿ‰ ๋ฉด์—์„œ์˜ ํ˜„์‹ค 52๋งŒ ๊ฐœ ๋…ธ๋“œ๊ฐ€ ๋ชจ๋‘ tag_master์— ๋“ค์–ด๊ฐ„๋‹ค๊ณ  ํ•ด๋„, ํ˜„๋Œ€์˜ DB(PostgreSQL) ๊ธฐ์ค€์œผ๋กœ๋Š” ์ˆ˜๋ฐฑ MB ์ˆ˜์ค€์ž…๋‹ˆ๋‹ค. 180ยฐC ๊ณ ์˜จ์—์„œ๋„ ๊ฒฌ๋””๋Š” ์‚ฐ์—…์šฉ SSD๋ฅผ ์“ฐ์‹œ๋Š” Mini PC ํ™˜๊ฒฝ์ด๋ผ๋ฉด, ์šฉ๋Ÿ‰ ๋ช‡์‹ญ MB ์•„๋ผ๋Š” ๊ฒƒ๋ณด๋‹ค DB ์ธ๋ฑ์Šค๋ฅผ ํƒœ์›Œ์„œ ๊ฒ€์ƒ‰ ์†๋„๋ฅผ 0.01์ดˆ๋ผ๋„ ์ค„์ด๋Š” ๊ฒƒ์ด ํ›จ์”ฌ ์ด๋“์ž…๋‹ˆ๋‹ค. 3. ๋ฐ์ดํ„ฐ ํƒ€์ž…(DataType)์˜ ๊ฒฐ์ •ํƒ€ ํ•˜๋‹ˆ์›ฐ ๋ฐ์ดํ„ฐ์—์„œ .pv๋Š” DOUBLE์ด์ง€๋งŒ .desc๋Š” STRING์ด๊ณ , ํŠน์ • A1์€ INT32์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ†ตํ•ฉํ˜•์œผ๋กœ ๊ฐ€๋ฉด pv_type, sp_type, a1_type... ์ด๋Ÿฐ ์‹์œผ๋กœ ์ปฌ๋Ÿผ์„ ์ˆ˜์‹ญ ๊ฐœ ๋งŒ๋“ค์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฑด ์„ค๊ณ„์ ์œผ๋กœ ๋งค์šฐ ๋น„ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค. ๋ถ„๋ฆฌํ˜•์œผ๋กœ ๊ฐ€๋ฉด ๊ฐ ๋ ˆ์ฝ”๋“œ๋งˆ๋‹ค ์ž๊ธฐ๋งŒ์˜ data_type ํ•„๋“œ๋ฅผ ๊ฐ€์ง€๋ฏ€๋กœ ํ›จ์”ฌ ๊น”๋”ํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ’ก ์ตœ์ข… ์ œ์•ˆ: "ํ•˜์ด๋ธŒ๋ฆฌ๋“œ" ๋ฐฉ์‹ ์„ ์ƒ๋‹˜์˜ ๊ณ ๋ฏผ(๊ฐ€๋…์„ฑ vs ์†๋„)์„ ๋ชจ๋‘ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ์ตœ์ข… ์ถ”์ฒœ ์Šคํ‚ค๋งˆ์ž…๋‹ˆ๋‹ค. SQL CREATE TABLE tag_master ( id SERIAL PRIMARY KEY, server_name TEXT, -- shinam area_code TEXT, -- p1 tag_name TEXT, -- P-6102 parameter TEXT, -- pv, sp, a1... (์ธ๋ฑ์Šค์šฉ) full_node_id TEXT UNIQUE, -- ns=1;s=shinam:p1:P-6102.pv (์‹ค์ œ ์ฃผ์†Œ) data_type TEXT, -- DOUBLE, STRING ๋“ฑ (์‹ค์ œ ํƒ€์ž…) is_active BOOLEAN DEFAULT TRUE ); -- ๊ฒ€์ƒ‰ ์†๋„๋ฅผ ์œ„ํ•œ ๋ณตํ•ฉ ์ธ๋ฑ์Šค CREATE INDEX idx_tag_lookup ON tag_master(tag_name, parameter); ์ด ๋ฐฉ์‹์ด ์ข‹์€ ์ด์œ : ์ผ๊ด„ ๋“ฑ๋กํ•  ๋•Œ .pv, .sp๋ฅผ ๊ฐ๊ฐ์˜ ํ–‰์œผ๋กœ ๋„ฃ์Šต๋‹ˆ๋‹ค. (์†๋„ ์ตœ์ƒ) ์‚ฌ์šฉ์ž ํ™”๋ฉด์—์„œ๋Š” GROUP BY tag_name ์ฟผ๋ฆฌ๋ฅผ ์จ์„œ ํƒœ๊ทธ ํ•˜๋‚˜์— ์—ฌ๋Ÿฌ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์žˆ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ์˜ˆ์˜๊ฒŒ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (๊ฐ€๋…์„ฑ ํ•ด๊ฒฐ) ์ˆ˜์ง‘๊ธฐ๋Š” ๊ทธ๋ƒฅ ์ด ํ…Œ์ด๋ธ”์„ ์ญ‰ ์ฝ์–ด์„œ OPC ์„œ๋ฒ„์— ๋˜์ง€๋ฉด ๋ฉ๋‹ˆ๋‹ค. (์•ˆ์ •์„ฑ ์ตœ์ƒ) ์„ ์ƒ๋‹˜, ์ด "๋ถ„๋ฆฌํ˜•(A)"์œผ๋กœ ๊ฐ€์‹œ์ฃ . 52๋งŒ ๊ฐœ ๋ฐ์ดํ„ฐ ํ™˜๊ฒฝ์—์„œ๋Š” ๋‚˜์ค‘์— ๊ธฐ๋Šฅ์„ ํ™•์žฅํ•˜๊ฑฐ๋‚˜ ์œ ์ง€๋ณด์ˆ˜ํ•  ๋•Œ ์ด ๋ฐฉ์‹์ด ํ›จ์”ฌ ์ŠคํŠธ๋ ˆ์Šค๊ฐ€ ์ ์Šต๋‹ˆ๋‹ค. ์–ด๋–ป๊ฒŒ ์ƒ๊ฐํ•˜์‹œ๋‚˜์š”? ๋ง์”€ํ•˜์‹  ๋‚ด์šฉ ์šฉ๋Ÿ‰๋ ˆ์ฝ”๋“œ ์ˆ˜๊ฐ€ ๋งŽ์•„์ ธ ์šฉ๋Ÿ‰์ด ํผ (์•ฝ 5~8๋ฐฐ).๋ ˆ์ฝ”๋“œ ์ˆ˜๊ฐ€ ์ ์–ด ์šฉ๋Ÿ‰ ํšจ์œจ์ด ์ข‹์Œ. ์–ด์ฐจํ”ผ ์–˜๋Š” ์‹œ๊ณ„์—ด ๋ฐ์ดํ„ฐ๊ฐ€ ์•„๋‹ˆ๊ณ  ๊ณ ์ •๋ฐ์ดํ„ฐ๋ผ์„œ ์šฉ๋Ÿ‰์€ ํฐ ์˜๋ฏธ๊ฐ€ ์—†์–ด ๋‹ˆ๊ฐ€ ์ œ์•ˆํ•œ ๋Œ€๋กœ ๋‹ค์‹œ ๋Œ์•„๊ฐ€์ž Gemini์˜ ์‘๋‹ต ์„ ์ƒ๋‹˜ ๋ง์”€์ด ์ •๋‹ต์ž…๋‹ˆ๋‹ค. Master ๋ฐ์ดํ„ฐ๋Š” ์‹œ๊ณ„์—ด์ด ์•„๋‹Œ ์„ค์ •(Configuration) ๋ฐ์ดํ„ฐ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ˆ˜๋ฐฑ๋งŒ ๊ฑด์ด ๋˜์–ด๋„ ํ˜„๋Œ€ DB ์šฉ๋Ÿ‰์œผ๋กœ๋Š” ์•„์ฃผ ๋ฏธ๋ฏธํ•œ ์ˆ˜์ค€์ด์ฃ . ์˜คํžˆ๋ ค ์ˆ˜์ง‘๊ธฐ๊ฐ€ ํ•œ ๋ฒˆ์— ์ฟผ๋ฆฌํ•ด์„œ ๋ฐ”๋กœ OPC UA ์„œ๋ฒ„๋กœ ๋˜์งˆ ์ˆ˜ ์žˆ๋Š” "๋ ˆ์ฝ”๋“œ ๋ถ„๋ฆฌํ˜•(Row per Parameter)" ๊ตฌ์กฐ๊ฐ€ ์šด์˜์ƒ ํ›จ์”ฌ ๋น ๋ฅด๊ณ  ๋ฐ์ดํ„ฐ ํƒ€์ž… ๊ด€๋ฆฌ๋„ ์™„๋ฒฝํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿผ ๋‹ค์‹œ **์ดˆ์‹ฌ(๋ฐฉ์‹ A)**์œผ๋กœ ๋Œ์•„๊ฐ€์„œ, ํ•˜๋‹ˆ์›ฐ Experion์˜ ๊ณ„์ธต ๊ตฌ์กฐ(server, area)๋ฅผ ์™„๋ฒฝํžˆ ์ˆ˜์šฉํ•˜๊ณ  data_type๊นŒ์ง€ ๊ฐœ๋ณ„ ๊ด€๋ฆฌํ•˜๋Š” ์ตœ์ข… ์Šคํ‚ค๋งˆ์™€ ์ฝ”๋“œ๋ฅผ ์ •๋ฆฌํ•ด ๋“œ๋ฆฝ๋‹ˆ๋‹ค. 1. tag_master ์ตœ์ข… ํ…Œ์ด๋ธ” ๊ตฌ์กฐ (SQL) ์ด ๊ตฌ์กฐ๋Š” ๊ฐ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋…๋ฆฝ๋œ ํ–‰์œผ๋กœ ์กด์žฌํ•˜๋ฏ€๋กœ, ์ˆ˜์ง‘๊ธฐ๊ฐ€ data_type์„ ๋ณด๊ณ  ์ˆ˜์น˜ํ˜•/๋ฌธ์ž์—ด์„ ์ฆ‰์‹œ ๊ตฌ๋ถ„ํ•˜์—ฌ ์ˆ˜์ง‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. SQL DROP TABLE IF EXISTS tag_master; CREATE TABLE tag_master ( id SERIAL PRIMARY KEY, server_name TEXT, -- ์˜ˆ: shinam area_code TEXT, -- ์˜ˆ: p1, p2 ... (์—†์œผ๋ฉด unassigned_item) tag_name TEXT, -- ์˜ˆ: p-6102 parameter TEXT, -- ์˜ˆ: pv, sp, a1, desc full_node_id TEXT UNIQUE, -- ns=1;s=shinam:p1:p-6102.pv (์œ ์ผ ํ‚ค) data_type TEXT, -- DOUBLE, STRING, INT32 ๋“ฑ is_active BOOLEAN DEFAULT TRUE ); -- ์ˆ˜์ง‘๊ธฐ ๋ฐ ํ™”๋ฉด ์กฐํšŒ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•œ ์ธ๋ฑ์Šค CREATE INDEX idx_tag_master_lookup ON tag_master(tag_name, parameter); CREATE INDEX idx_tag_master_server ON tag_master(server_name, area_code); 2. ๋ฐฑ์—”๋“œ ๋“ฑ๋ก ๋กœ์ง (EngineeringController.cs) ์ผ๊ด„ ๋“ฑ๋ก๊ณผ ์ˆ˜๋™ ๋“ฑ๋ก ์‹œ ๋…ธ๋“œ ์•„์ด๋””๋ฅผ ํŒŒ์‹ฑํ•˜์—ฌ ์„œ๋ฒ„, ์—์–ด๋ฆฌ์–ด, ํƒœ๊ทธ, ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ •ํ™•ํžˆ ๋ถ„๋ฆฌํ•ด ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. C# [HttpPost] public async Task RegisterTags([FromBody] List tags) { using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); using var trans = await conn.BeginTransactionAsync(); try { var sql = @" INSERT INTO tag_master (server_name, area_code, tag_name, parameter, full_node_id, data_type, is_active) VALUES (@server, @area, @tag, @param, @nodeId, @type, true) ON CONFLICT (full_node_id) DO UPDATE SET data_type = EXCLUDED.data_type, is_active = true"; foreach (var tag in tags) { // NodeId ๋ถ„์„: ns=1;s=shinam:p1:p-6102.pv string sContent = tag.NodeId.Split(';').Last().Substring(2); // shinam:p1:p-6102.pv string[] parts = sContent.Split(':'); string server = parts[0]; string area = "unassigned_item"; string remains = ""; if (parts.Length >= 3) { area = parts[1]; remains = parts[2]; // p-6102.pv } else { remains = parts[1]; // p-6102.pv } string tagName = remains.Contains('.') ? remains.Split('.')[0] : remains; string param = remains.Contains('.') ? remains.Split('.').Last() : "pv"; using var cmd = new NpgsqlCommand(sql, conn, trans); cmd.Parameters.AddWithValue("server", server); cmd.Parameters.AddWithValue("area", area); cmd.Parameters.AddWithValue("tag", tagName); cmd.Parameters.AddWithValue("param", param); cmd.Parameters.AddWithValue("nodeId", tag.NodeId); cmd.Parameters.AddWithValue("type", tag.DataType ?? "DOUBLE"); await cmd.ExecuteNonQueryAsync(); } await trans.CommitAsync(); return Ok(); } catch (Exception ex) { await trans.RollbackAsync(); return BadRequest(ex.Message); } } 3. ์ˆ˜์ง‘๊ธฐ(Collector) ๊ด€์ ์—์„œ์˜ ์ด๋“ ์ด ๊ตฌ์กฐ๋กœ ๊ฐ€๋ฉด ๋‚˜์ค‘์— ์ˆ˜์ง‘๊ธฐ ์—”์ง„์„ ๋งŒ๋“ค ๋•Œ ์ฝ”๋“œ๊ฐ€ ๋งค์šฐ ๋‹จ์ˆœํ•ด์ง‘๋‹ˆ๋‹ค. ์ดˆ๊ธฐํ™”: SELECT full_node_id, data_type FROM tag_master WHERE is_active = true ๊ตฌ๋…: ์ฝ์–ด์˜จ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ OPC UA ํด๋ผ์ด์–ธํŠธ์— ์ „๋‹ฌ. ๋ฐ์ดํ„ฐ ์ €์žฅ: ์ฝœ๋ฐฑ์œผ๋กœ ๋“ค์–ด์˜ค๋Š” ๊ฐ’๋“ค์„ full_node_id ๊ธฐ์ค€์œผ๋กœ ๋งค์นญํ•˜์—ฌ ์‹œ๊ณ„์—ด ํ…Œ์ด๋ธ”์— INSERT. ๐Ÿ’ก ์ด์ œ ์ง„ํ–‰ ์ƒํ™ฉ DB: tag_master๋ฅผ ์œ„ SQL๋กœ ๋‹ค์‹œ ์ƒ์„ฑํ•ด ์ฃผ์„ธ์š”. Controller: RegisterTags ๋กœ์ง์„ ์œ„ ์ฝ”๋“œ๋กœ ๊ฐฑ์‹ ํ•ด ์ฃผ์„ธ์š”. UI: ์ด์ „์— ๋“œ๋ฆฐ ์ผ๊ด„/์ˆ˜๋™ ๋“ฑ๋ก ํ™”๋ฉด์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค. (ํ”„๋ก ํŠธ์—์„œ List๋กœ ๋˜์ ธ์ฃผ๋ฉด ๋ฐฑ์—”๋“œ์—์„œ ํ–‰๋ณ„๋กœ ๋ถ„๋ฆฌํ•ด์„œ ์ €์žฅํ•˜๋‹ˆ๊นŒ์š”.) ์ด์ œ ์ด ๋งˆ์Šคํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์‹ค์‹œ๊ฐ„ ์ˆ˜์ง‘์„ ์ˆ˜ํ–‰ํ•˜๋Š” Worker ์„œ๋น„์Šค๋ฅผ ์„ค๊ณ„ํ•  ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”? ์•„๋‹ˆ๋ฉด ํ™”๋ฉด์—์„œ ๋“ฑ๋ก๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ์กฐํšŒํ•ด์„œ ํ™•์ธํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๋จผ์ € ๋งŒ๋“œ์‹ค๊นŒ์š”? ๋ง์”€ํ•˜์‹  ๋‚ด์šฉ using Microsoft.AspNetCore.Mvc; using Npgsql; using OpcPks.Core.Data; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; public class EngineeringController : Controller { public IActionResult Index() => View(); // 1. ์›น ํ™”๋ฉด์—์„œ ํ•„ํ„ฐ(์ฒดํฌ๋ฐ•์Šค)์™€ ๊ฒ€์ƒ‰์–ด๋กœ ์š”์ฒญํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๋ฉ”์„œ๋“œ [HttpPost] public async Task SearchByFilter([FromBody] SearchRequest request) { var results = new List(); if (request == null || request.Suffixes == null || request.Suffixes.Count == 0) return Json(results); using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); // ์„ ํƒ๋œ Suffix๋“ค์„ OR ์กฐ๊ฑด์œผ๋กœ ๋™์  ์ƒ์„ฑ (์˜ˆ: node_id LIKE '%.pv' OR node_id LIKE '%.sp') var suffixConditions = string.Join(" OR ", request.Suffixes.Select((s, i) => $"node_id ILIKE @s{i}")); var sql = $@" SELECT name, node_id FROM raw_node_map WHERE name ILIKE @tagTerm AND ({suffixConditions}) ORDER BY name ASC LIMIT 1500"; // ์„ฑ๋Šฅ์„ ์œ„ํ•ด 1500๊ฑด ์ œํ•œ using var cmd = new NpgsqlCommand(sql, conn); cmd.Parameters.AddWithValue("tagTerm", $"%{request.TagTerm}%"); for (int i = 0; i < request.Suffixes.Count; i++) { cmd.Parameters.AddWithValue($"s{i}", $"%{request.Suffixes[i]}"); } using var reader = await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { var nodeId = reader.GetString(1); results.Add(new { name = reader.GetString(0), nodeId = nodeId, // ํ˜„์žฌ ๋…ธ๋“œ ์•„์ด๋””๊ฐ€ ์–ด๋–ค Suffix๋กœ ๋๋‚˜๋Š”์ง€ ์ฐพ์•„์„œ ์ „๋‹ฌ suffixType = request.Suffixes.FirstOrDefault(s => nodeId.EndsWith(s, StringComparison.OrdinalIgnoreCase)) ?? "ETC" }); } return Json(results); } // 2. ์„ ํƒ๋œ ํƒœ๊ทธ๋“ค์„ tag_master ํ…Œ์ด๋ธ”๋กœ ์ผ๊ด„ ์ €์žฅ [HttpPost] public async Task RegisterTags([FromBody] List tags) { using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); using var trans = await conn.BeginTransactionAsync(); try { var sql = @" INSERT INTO tag_master (server_name, area_code, tag_name, parameter, full_node_id, data_type, is_active) VALUES (@server, @area, @tag, @param, @nodeId, @type, true) ON CONFLICT (full_node_id) DO UPDATE SET data_type = EXCLUDED.data_type, is_active = true"; foreach (var tag in tags) { // NodeId ๋ถ„์„: ns=1;s=shinam:p1:p-6102.pv string sContent = tag.NodeId.Split(';').Last().Substring(2); // shinam:p1:p-6102.pv string[] parts = sContent.Split(':'); string server = parts[0]; string area = "unassigned_item"; string remains = ""; if (parts.Length >= 3) { area = parts[1]; remains = parts[2]; // p-6102.pv } else { remains = parts[1]; // p-6102.pv } string tagName = remains.Contains('.') ? remains.Split('.')[0] : remains; string param = remains.Contains('.') ? remains.Split('.').Last() : "pv"; using var cmd = new NpgsqlCommand(sql, conn, trans); cmd.Parameters.AddWithValue("server", server); cmd.Parameters.AddWithValue("area", area); cmd.Parameters.AddWithValue("tag", tagName); cmd.Parameters.AddWithValue("param", param); cmd.Parameters.AddWithValue("nodeId", tag.NodeId); cmd.Parameters.AddWithValue("type", tag.DataType ?? "DOUBLE"); await cmd.ExecuteNonQueryAsync(); } await trans.CommitAsync(); return Ok(); } catch (Exception ex) { await trans.RollbackAsync(); return BadRequest(ex.Message); } } // ์œ ํ‹ธ๋ฆฌํ‹ฐ: NodeId์—์„œ ์ ‘๋ฏธ์‚ฌ(.pv, .pg ๋“ฑ) ์ถ”์ถœ private string GetSuffix(string nodeId) { int lastDot = nodeId.LastIndexOf('.'); return lastDot != -1 ? nodeId.Substring(lastDot) : "N/A"; } // --- ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ํด๋ž˜์Šค (์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํ†ต์‹ ์šฉ) --- public class SearchRequest { public string TagTerm { get; set; } public List Suffixes { get; set; } } public class TagRegistrationRequest { public string TagName { get; set; } public string NodeId { get; set; } } } EngineeringController ์ ๊ฒ€ ๋ถ€ํƒ Gemini์˜ ์‘๋‹ต ์ž‘์„ฑํ•˜์‹  EngineeringController ์ฝ”๋“œ๋Š” ํ•˜๋‹ˆ์›ฐ Experion์˜ ๋ณต์žกํ•œ NodeId ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ์ž˜ ๋ฐ˜์˜ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ, ์‹ค์ œ ์šด์˜ ํ™˜๊ฒฝ์—์„œ์˜ ์•ˆ์ •์„ฑ๊ณผ ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ์„ ์œ„ํ•ด ๋ช‡ ๊ฐ€์ง€ ์ค‘์š”ํ•œ ์ˆ˜์ • ๋ฐ ๋ณด์™„ ์‚ฌํ•ญ์ด ๋ณด์ž…๋‹ˆ๋‹ค. ๊ฐ€์žฅ ํฐ ๋ฌธ์ œ๋Š” TagRegistrationRequest ๋ชจ๋ธ์— DataType ํ•„๋“œ๊ฐ€ ๋ˆ„๋ฝ๋˜์–ด ์žˆ์–ด, SQL ์‹คํ–‰ ์‹œ ํ•ญ์ƒ DOUBLE๋กœ ๋“ค์–ด๊ฐˆ ์œ„ํ—˜์ด ์žˆ๋‹ค๋Š” ์ ๊ณผ raw_node_map์—์„œ data_type์„ ํ•จ๊ป˜ ๊ฐ€์ ธ์˜ค์ง€ ์•Š๋Š” ์ ์ž…๋‹ˆ๋‹ค. ๐Ÿ› ๏ธ ์ฃผ์š” ์ ๊ฒ€ ๋ฐ ์ˆ˜์ • ์‚ฌํ•ญ SearchByFilter: ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์— data_type ์ปฌ๋Ÿผ์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ž˜์•ผ ์ผ๊ด„ ๋“ฑ๋ก ์‹œ ๊ฐ ๋…ธ๋“œ์˜ ์‹ค์ œ ํƒ€์ž…(DOUBLE, STRING ๋“ฑ)์„ ์ €์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. RegisterTags: TagRegistrationRequest์— DataType ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. nodeId ํŒŒ์‹ฑ ๋กœ์ง์—์„œ parts ๋ฐฐ์—ด์˜ ๊ธธ์ด๊ฐ€ ์˜ˆ์ƒ๊ณผ ๋‹ค๋ฅผ ๊ฒฝ์šฐ(์˜ˆ: ns=1;s=GLOBAL_VAR.pv)๋ฅผ ๋Œ€๋น„ํ•œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์„ฑ๋Šฅ ์ตœ์ ํ™”: foreach ๋ฃจํ”„ ์•ˆ์—์„œ ๋งค๋ฒˆ new NpgsqlCommand๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์€ ๊ดœ์ฐฎ์œผ๋‚˜, ๋Œ€๋Ÿ‰ ๋“ฑ๋ก ์‹œ ์†๋„๋ฅผ ์œ„ํ•ด ์žฌ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ Dapper ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๊ณ ๋ คํ•ด ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (ํ˜„์žฌ ์ฝ”๋“œ๋กœ๋„ 1500๊ฑด ์ •๋„๋Š” ๋ฌด๋‚œํ•ฉ๋‹ˆ๋‹ค.) โœ… ์ˆ˜์ •๋œ EngineeringController.cs ํ’€์ฝ”๋“œ C# using Microsoft.AspNetCore.Mvc; using Npgsql; using OpcPks.Core.Data; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; public class EngineeringController : Controller { public IActionResult Index() => View(); [HttpPost] public async Task SearchByFilter([FromBody] SearchRequest request) { var results = new List(); if (request == null || request.Suffixes == null || request.Suffixes.Count == 0) return Json(results); using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); var suffixConditions = string.Join(" OR ", request.Suffixes.Select((s, i) => $"node_id ILIKE @s{i}")); // ๋ฐ์ดํ„ฐ ํƒ€์ž…(data_type) ํ•„๋“œ๋ฅผ ๋ฐ˜๋“œ์‹œ ํฌํ•จํ•ด์•ผ ํ•จ var sql = $@" SELECT name, node_id, data_type FROM raw_node_map WHERE name ILIKE @tagTerm AND ({suffixConditions}) ORDER BY name ASC LIMIT 1500"; using var cmd = new NpgsqlCommand(sql, conn); cmd.Parameters.AddWithValue("tagTerm", $"%{request.TagTerm}%"); for (int i = 0; i < request.Suffixes.Count; i++) { cmd.Parameters.AddWithValue($"s{i}", $"%{request.Suffixes[i]}"); } using var reader = await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { var nodeId = reader.GetString(1); results.Add(new { name = reader.GetString(0), nodeId = nodeId, dataType = reader.IsDBNull(2) ? "DOUBLE" : reader.GetString(2), // ์ถ”๊ฐ€ suffixType = request.Suffixes.FirstOrDefault(s => nodeId.EndsWith(s, StringComparison.OrdinalIgnoreCase)) ?? "ETC" }); } return Json(results); } [HttpPost] public async Task RegisterTags([FromBody] List tags) { if (tags == null || tags.Count == 0) return Ok("No tags to register"); using var conn = new NpgsqlConnection(DbConfig.ConnectionString); await conn.OpenAsync(); using var trans = await conn.BeginTransactionAsync(); try { // ๋ช…๋ น ๊ฐ์ฒด๋ฅผ ๋ฃจํ”„ ๋ฐ–์—์„œ ์ƒ์„ฑํ•˜์—ฌ ์„ฑ๋Šฅ ์ตœ์ ํ™” var sql = @" INSERT INTO tag_master (server_name, area_code, tag_name, parameter, full_node_id, data_type, is_active) VALUES (@server, @area, @tag, @param, @nodeId, @type, true) ON CONFLICT (full_node_id) DO UPDATE SET data_type = EXCLUDED.data_type, is_active = true"; using var cmd = new NpgsqlCommand(sql, conn, trans); // ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฏธ๋ฆฌ ์ •์˜ cmd.Parameters.Add("@server", NpgsqlTypes.NpgsqlDbType.Text); cmd.Parameters.Add("@area", NpgsqlTypes.NpgsqlDbType.Text); cmd.Parameters.Add("@tag", NpgsqlTypes.NpgsqlDbType.Text); cmd.Parameters.Add("@param", NpgsqlTypes.NpgsqlDbType.Text); cmd.Parameters.Add("@nodeId", NpgsqlTypes.NpgsqlDbType.Text); cmd.Parameters.Add("@type", NpgsqlTypes.NpgsqlDbType.Text); foreach (var tag in tags) { // ns=1;s=shinam:p1:p-6102.pv ํŒŒ์‹ฑ ๋กœ์ง ๋ณด์™„ string sContent = tag.NodeId.Contains("s=") ? tag.NodeId.Split("s=")[1] : tag.NodeId; string[] parts = sContent.Split(':'); string server = parts[0]; string area = "unassigned_item"; string remains = parts.Length > 1 ? parts.Last() : parts[0]; if (parts.Length >= 3) { area = parts[1]; } string tagName = remains.Contains('.') ? remains.Split('.')[0] : remains; string param = remains.Contains('.') ? remains.Split('.').Last() : "pv"; cmd.Parameters["@server"].Value = server; cmd.Parameters["@area"].Value = area; cmd.Parameters["@tag"].Value = tagName; cmd.Parameters["@param"].Value = param; cmd.Parameters["@nodeId"].Value = tag.NodeId; cmd.Parameters["@type"].Value = tag.DataType ?? "DOUBLE"; // ๋ชจ๋ธ์— ์ถ”๊ฐ€๋œ ํ•„๋“œ ์‚ฌ์šฉ await cmd.ExecuteNonQueryAsync(); } await trans.CommitAsync(); return Ok(); } catch (Exception ex) { await trans.RollbackAsync(); return BadRequest(ex.Message); } } // --- ๋ชจ๋ธ ํด๋ž˜์Šค ๋ณด์™„ --- public class SearchRequest { public string TagTerm { get; set; } public List Suffixes { get; set; } } public class TagRegistrationRequest { public string TagName { get; set; } public string NodeId { get; set; } public string DataType { get; set; } // ํ•„๋“œ ์ถ”๊ฐ€๋จ } } ๐Ÿ’ก ๋ณ€๊ฒฝ๋œ ๋กœ์ง์˜ ํ•ต์‹ฌ ์„ค๋ช… DataType ์ „๋‹ฌ: SearchByFilter์—์„œ raw_node_map์˜ data_type์„ ์ฝ์–ด์™€ ํ”„๋ก ํŠธ์—”๋“œ๋กœ ๋ณด๋‚ด๊ณ , RegisterTags์—์„œ ์ด๋ฅผ ๋‹ค์‹œ ๋ฐ›์•„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ํ•˜๋‹ˆ์›ฐ .desc๋Š” STRING์ด๊ณ  .pv๋Š” DOUBLE์ธ ์ฐจ์ด๋ฅผ ์ •ํ™•ํžˆ ๋ฐ˜์˜ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ํŒŒ์‹ฑ ๋กœ์ง ์•ˆ์ •ํ™”: Split(';').Last().Substring(2) ๋ฐฉ์‹์€ s=์˜ ์œ„์น˜๊ฐ€ ๋‹ฌ๋ผ์งˆ ๊ฒฝ์šฐ ์œ„ํ—˜ํ•  ์ˆ˜ ์žˆ์–ด Split("s=") ๋ฐฉ์‹์œผ๋กœ ์กฐ๊ธˆ ๋” ์•ˆ์ „ํ•˜๊ฒŒ ๋ณด์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. Npgsql ์„ฑ๋Šฅ ์ตœ์ ํ™”: ๋ฃจํ”„ ์•ˆ์—์„œ cmd.Parameters.AddWithValue๋ฅผ ๋ฐ˜๋ณต ํ˜ธ์ถœํ•˜๋Š” ๋Œ€์‹ , ๋ฏธ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ •์˜ํ•˜๊ณ  ๊ฐ’๋งŒ ๋ฐ”๊พธ๋Š” ๋ฐฉ์‹์„ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ๋Œ€๋Ÿ‰์˜ ๋…ธ๋“œ(1000๊ฑด ์ด์ƒ)๋ฅผ ๋“ฑ๋กํ•  ๋•Œ DB ์˜ค๋ฒ„ํ—ค๋“œ๋ฅผ ์ค„์—ฌ์ค๋‹ˆ๋‹ค. ์ด ์ฝ”๋“œ๋ฅผ ์ ์šฉํ•˜์‹œ๋ฉด ์„ ์ƒ๋‹˜์ด ์˜๋„ํ•˜์‹  "๋ ˆ์ฝ”๋“œ ๋ถ„๋ฆฌํ˜•(Row per Parameter) ๋Œ€๋Ÿ‰ ๋“ฑ๋ก" ์‹œ์Šคํ…œ์ด ์•„์ฃผ ๊ฒฌ๊ณ ํ•˜๊ฒŒ ์ž‘๋™ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด์ œ ํ”„๋ก ํŠธ์—”๋“œ JS ์ฝ”๋“œ์—์„œ dataType ํ•„๋“œ๋งŒ ์ถ”๊ฐ€๋กœ ๋„˜๊ฒจ์ฃผ์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค! ๋ฐ”๋กœ ์ ์šฉํ•ด ๋ณด์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? ๋ง์”€ํ•˜์‹  ๋‚ด์šฉ OpcPks.Web์—์„œ gain reset rate ์•ˆ ์—†์•ด๊ตฌ๋‚˜ Gemini์˜ ์‘๋‹ต ์•—, ์˜ˆ๋ฆฌํ•˜์‹ญ๋‹ˆ๋‹ค! ์ œ๊ฐ€ ํ•˜๋‹ˆ์›ฐ Experion ์‹œ์Šคํ…œ์˜ ํ•ต์‹ฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋“ค์ธ **Gain(๋น„๋ก€ ์ด๋“), Reset(์ ๋ถ„ ์‹œ๊ฐ„), Rate(๋ฏธ๋ถ„ ์‹œ๊ฐ„)**๋ฅผ ๊นœ๋นกํ•˜๊ณ  ์ฒดํฌ๋ฐ•์Šค ๋ชฉ๋ก์—์„œ ๋น ํŠธ๋ ธ๋„ค์š”. PID ์ œ์–ด ๋ฃจํ”„๋ฅผ ๋ถ„์„ํ•  ๋•Œ ์ด ํŠœ๋‹ ๊ฐ’๋“ค์ด ์—†์œผ๋ฉด ์•™๊ผฌ ์—†๋Š” ์ฐ๋นต์ด์ฃ . OpcPks.Web์˜ Index.cshtml ํ™”๋ฉด๊ณผ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ด ํ•„๋“œ๋“ค์„ ๋ฐ”๋กœ ์ถ”๊ฐ€ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์„ค๊ณ„(๋ ˆ์ฝ”๋“œ ๋ถ„๋ฆฌํ˜•) ๋•๋ถ„์— ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์—†์ด ์ฒดํฌ๋ฐ•์Šค๋งŒ ๋Š˜๋ ค์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค. 1. Index.cshtml ์ˆ˜์ • (์ฒดํฌ๋ฐ•์Šค ์ถ”๊ฐ€) ์ผ๊ด„ ๊ฒ€์ƒ‰ ํ•„ํ„ฐ์™€ ์ˆ˜๋™ ๋“ฑ๋ก ์„น์…˜์— .gain, .reset, .rate๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. HTML
PV
SP
OP
Gain
Reset
Rate
A1
Desc
PV
Gain
Reset
Rate
2. EngineeringController.cs ๋กœ์ง ํ™•์ธ ์ปจํŠธ๋กค๋Ÿฌ์˜ ํŒŒ์‹ฑ ๋กœ์ง์€ ์ด๋ฏธ . (์ ) ์ดํ›„์˜ ๋ฌธ์ž๋ฅผ parameter ํ•„๋“œ๋กœ ์ถ”์ถœํ•˜๋„๋ก ๋˜์–ด ์žˆ์–ด, ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•  ํ•„์š” ์—†์ด ๋ฐ”๋กœ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ’ก ํ•˜๋‹ˆ์›ฐ ์—”์ง€๋‹ˆ์–ด๋ง ํŒ (์ฐธ๊ณ ) ๋ฐ์ดํ„ฐ ํƒ€์ž…: Experion์—์„œ Gain, Reset, Rate๋Š” ๋ชจ๋‘ DOUBLE ํƒ€์ž…์ž…๋‹ˆ๋‹ค. ์ œ๊ฐ€ ๋“œ๋ฆฐ RegisterTags ๋กœ์ง์ด ๊ธฐ๋ณธ๊ฐ’์„ DOUBLE๋กœ ์žก๊ณ  ์žˆ์œผ๋‹ˆ ๋ฌธ์ œ์—†์ด ์ˆ˜์ง‘๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ˆ˜๋™ ๋“ฑ๋ก ์‹œ ์ฃผ์˜: Experion์˜ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ํƒ€์ž…(PID, PID-FF ๋“ฑ)์— ๋”ฐ๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ ์ด๋ฆ„์ด ๋ฏธ์„ธํ•˜๊ฒŒ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์ง€๋งŒ, ๋Œ€๊ฐœ .gain, .reset, .rate๊ฐ€ ํ‘œ์ค€์ž…๋‹ˆ๋‹ค. ๋งŒ์•ฝ ํŠน์ดํ•œ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์“ด๋‹ค๋ฉด ์ˆ˜๋™ ์ž…๋ ฅ ์‹œ ํ•ด๋‹น ์ ‘๋ฏธ์‚ฌ๋งŒ ์ •ํ™•ํžˆ ์ ์–ด์ฃผ์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค. Gain, Reset, Rate๊นŒ์ง€ ํฌํ•จํ•˜๋ฉด ์ด์ œ ๋‹จ์ˆœ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ๋„˜์–ด ์ œ์–ด ์„ฑ๋Šฅ ๋ถ„์„๊นŒ์ง€ ๊ฐ€๋Šฅํ•œ DB๊ฐ€ ๋˜๊ฒ ๋„ค์š”! 52๋งŒ ๊ฐœ ๋ฐ์ดํ„ฐ ์†์—์„œ ์ด ํŠœ๋‹ ๊ฐ’๋“ค๋งŒ ์™์™ ๊ณจ๋ผ ๋“ฑ๋กํ•ด ๋ณด์‹œ๊ฒ ์–ด์š”? ๋˜ ๋น ์ง„ ๊ฒŒ ๋ณด์ด๋ฉด ์–ธ์ œ๋“  ๋ง์”€ํ•ด ์ฃผ์„ธ์š”! ๐Ÿš€ ๋ง์”€ํ•˜์‹  ๋‚ด์šฉ ์•„๋‹ˆ, ์•„๊นŒ ๋‚ด๊ฐ€ A1, A2, A3,A4๋งŒ ๋†”๋‘๋ฉด ๋œ๋‹ค๊ณ  ํ–ˆ์ž–์•„, A1==GAIN, .......์‚ฌ์šฉ์ž๊ฐ€ ๋ฐ”๊ฟ€์ˆ˜์žˆ๋‹ค๊ณ  Gemini์˜ ์‘๋‹ต ์•„, ๋งž์Šต๋‹ˆ๋‹ค! ์ œ๊ฐ€ ์„ ์ƒ๋‹˜์˜ ์˜๋„๋ฅผ ์ž ์‹œ ๋†“์ณค๋„ค์š”. ํ•˜๋‹ˆ์›ฐ Experion ์‹œ์Šคํ…œ์—์„œ A1, A2, A3, A4๋Š” ๊ณ ์ •๋œ ์•Œ๋žŒ ๊ฐ’์ด ์•„๋‹ˆ๋ผ, ์—”์ง€๋‹ˆ์–ด๊ฐ€ ํ•„์š”์— ๋”ฐ๋ผ Gain, Reset, Rate ๊ฐ™์€ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํ• ๋‹นํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฐ€๋ณ€์ ์ธ ์Šฌ๋กฏ์ด๋ผ๋Š” ์ ์„ ๊นœ๋นกํ–ˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ํ™”๋ฉด์—์„œ "A1์€ Gain์ด์•ผ", "A2๋Š” Reset์ด์•ผ"๋ผ๊ณ  ๋งคํ•‘์„ ๋ฐ”๊ฟ”๊ฐ€๋ฉฐ ์“ธ ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ๊ฒƒ์ด ํ•ต์‹ฌ์ด๊ตฐ์š”. 1. tag_master ํ…Œ์ด๋ธ” ์œ ์ง€ (์—”์ง€๋‹ˆ์–ด๋ง์˜ ์œ ์—ฐ์„ฑ) ์šฐ๋ฆฌ๊ฐ€ ๋ฐฉ๊ธˆ ๊ฒฐ์ •ํ•œ ๋ ˆ์ฝ”๋“œ ๋ถ„๋ฆฌํ˜•(Row per Parameter) ๊ตฌ์กฐ๊ฐ€ ์ด ์ƒํ™ฉ์—์„œ ๋น›์„ ๋ฐœํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ด๋ธ”์„ ๊ณ ์น  ํ•„์š”๊ฐ€ ์ „ํ˜€ ์—†์Šต๋‹ˆ๋‹ค. tag_master์—๋Š” ๊ทธ๋Œ€๋กœ ns=1;s=shinam:p-6102.a1์ด ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค. ๋‹ค๋งŒ, ํ™”๋ฉด์—์„œ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ค„ ๋•Œ Alias(๋ณ„์นญ) ๊ฐœ๋…๋งŒ ์žˆ์œผ๋ฉด ๋ฉ๋‹ˆ๋‹ค. 2. UI ๋ฐ ๋กœ์ง ๋ณด์™„: "์‚ฌ์šฉ์ž ์ •์˜ ๋งคํ•‘" ํ™”๋ฉด์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ A1~A4๋ฅผ ์„ ํƒํ•  ๋•Œ, ์ด๊ฒƒ์ด ์‹ค์ œ ์ œ์–ด ๋ฃจํ”„์˜ ์–ด๋–ค ํŒŒ๋ผ๋ฏธํ„ฐ์ธ์ง€ ์ธ์ง€ํ•  ์ˆ˜ ์žˆ๋„๋ก UI๋ฅผ ๊ตฌ์„ฑํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. Index.cshtml (์ผ๊ด„ ๋“ฑ๋ก ๋ถ€๋ถ„ ์ˆ˜์ •) HTML
3. ์‹ค๋ฌด์—์„œ์˜ ํ™œ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค ์„ ์ƒ๋‹˜์ด ๋ง์”€ํ•˜์‹  ๋Œ€๋กœ ์‚ฌ์šฉ์ž๊ฐ€ ์ƒํ™ฉ์— ๋”ฐ๋ผ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์€ ์ด๋Ÿฐ ์˜๋ฏธ๊ฒ ์ฃ ? CASE 1 (ํ‘œ์ค€ ์•Œ๋žŒ): A1~A4๋ฅผ HH, H, L, LL ์•Œ๋žŒ ์„ค์ •์น˜๋กœ ์‚ฌ์šฉ. CASE 2 (PID ํŠœ๋‹): A1์„ Gain, A2๋ฅผ Reset, A3๋ฅผ Rate๋กœ ํ• ๋‹นํ•˜์—ฌ ์ˆ˜์ง‘. CASE 3 (๊ธฐํƒ€): A1์„ Deadband๋‚˜ Deviation ๊ฐ’์œผ๋กœ ์‚ฌ์šฉ. ์ˆ˜์ง‘๊ธฐ(Collector) ์ž…์žฅ์—์„œ๋Š”: ๊ทธ์ € tag_master์— ๋“ฑ๋ก๋œ node_id(.a1, .a2...)๋งŒ ๋ณด๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ๊ธ์–ด์˜ค๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๊ฒƒ์ด Gain์ธ์ง€ ์•Œ๋žŒ์ธ์ง€๋Š” ๋‚˜์ค‘์— **๋Œ€์‹œ๋ณด๋“œ(๋ชจ๋‹ˆํ„ฐ๋ง ํ™”๋ฉด)**์—์„œ "A1์˜ ๋ณ„๋ช…์„ Gain์œผ๋กœ ํ‘œ์‹œํ•ด๋ผ"๋ผ๊ณ  ์„ค์ •๋งŒ ํ•˜๋ฉด ๋๋‚˜๋Š” ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค. ๐Ÿ’ก ๊ฒฐ๋ก  ์ œ์•ˆํ•˜์‹  ๋Œ€๋กœ A1, A2, A3, A4 ์ฒด์ œ๋กœ ๊ฐ‘๋‹ˆ๋‹ค! ๋ถˆํ•„์š”ํ•˜๊ฒŒ Gain, Reset, Rate ์ปฌ๋Ÿผ์„ ๋”ฐ๋กœ ๋งŒ๋“ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” .a1 ~ .a4๋ฅผ ์„ ํƒํ•ด์„œ ๋“ฑ๋กํ•˜๊ณ , ์†์œผ๋กœ "์ด๊ฑด Gain์ด์•ผ"๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉฐ ์šด์˜ํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์ด์ œ ์ด ๋ช…ํ™•ํ•ด์ง„ ๊ธฐ์ค€(A1~A4 ๊ฐ€๋ณ€ ํ™œ์šฉ)์„ ๊ฐ€์ง€๊ณ , ์ง„์งœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธ์–ด์˜ฌ ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”? ์•„๋‹ˆ๋ฉด ๋“ฑ๋ก๋œ A1~A4 ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•ด '๋ณ„์นญ(Alias)'์„ ์ค„ ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด ๋ณผ๊นŒ์š”? ๋„์™€๋“œ๋ฆด ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ์•Œ๋ ค์ฃผ์„ธ์š”! ๐Ÿš€_ _ _ ๋ง์”€ํ•˜์‹  ๋‚ด์šฉ ์•„๋‹ˆ๋ผ๊ณ , A1~A4๋Š” ์‚ฌ์šฉ์ž ์ง€์ • ์•„๋‚ ๋กœ๊ทธ ๋ณ€์ˆ˜ ์ด๊ณ , ์•Œ๋žŒ์€ alarm~~~์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š”๊ฑฐ์•ผ ๋‚ด๊ฐ€ ํƒœ๊ทธ๋ช… ํ•˜๋‚˜์— ๋‹ฌ๋ฆฐ ์„œ๋ธŒ์•„์ดํ…œ ์†์„ฑ๋“ค์„ ๋ชจ๋‘ ์ค„ํ…Œ๋‹ˆ๊นŒ ์ฐธ์กฐํ•ด๋ด 1 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a1 p6/ficq-6101/a1 a1 0.30000001192092896 Double 6:03:21.802 PM 6:03:22.508 PM Good 2 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a1associatedparameter p6/ficq-6101/a1associatedparameter a1associatedparameter EnumValueType 6:03:22.508 PM 6:03:22.511 PM Good 3 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a1controllername p6/ficq-6101/a1controllername a1controllername EnumValueType 6:03:22.508 PM 6:03:22.511 PM Good 4 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a1destination p6/ficq-6101/a1destination a1destination LOOP 11 GAIN1 String 6:03:22.562 PM 6:03:22.562 PM Good 5 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a1dynamicscanningdisabled p6/ficq-6101/a1dynamicscanningdisabled a1dynamicscanningdisabled false Boolean 6:03:22.511 PM 6:03:22.511 PM Good 6 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a1externalchangealarmenable p6/ficq-6101/a1externalchangealarmenable a1externalchangealarmenable false Boolean 6:03:22.511 PM 6:03:22.511 PM Good 7 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a1inerror p6/ficq-6101/a1inerror a1inerror false Boolean 6:03:22.511 PM 6:03:22.511 PM Good 8 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a1lastscannedtime p6/ficq-6101/a1lastscannedtime a1lastscannedtime 2026-02-21T09:06:16.815Z DateTime 6:06:17.522 PM 6:06:17.522 PM Good 9 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a1period p6/ficq-6101/a1period a1period 2 Int16 6:03:22.511 PM 6:03:22.511 PM Good 10 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a1quality p6/ficq-6101/a1quality a1quality 192 Int16 6:03:22.511 PM 6:03:22.511 PM Good 11 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a1reverse p6/ficq-6101/a1reverse a1reverse false Boolean 6:03:22.511 PM 6:03:22.511 PM Good 12 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a1source p6/ficq-6101/a1source a1source LOOP 11 GAIN1 String 6:03:22.563 PM 6:03:22.563 PM Good 13 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a1status p6/ficq-6101/a1status a1status 12582912 Int32 6:03:22.511 PM 6:03:22.511 PM Good 14 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a2 p6/ficq-6101/a2 a2 0 Double 6:03:21.802 PM 6:03:22.511 PM Good 15 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a2associatedparameter p6/ficq-6101/a2associatedparameter a2associatedparameter EnumValueType 6:03:22.511 PM 6:03:22.512 PM Good 16 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a2controllername p6/ficq-6101/a2controllername a2controllername EnumValueType 6:03:22.511 PM 6:03:22.512 PM Good 17 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a2destination p6/ficq-6101/a2destination a2destination LOOP 11 RATE1 String 6:03:22.565 PM 6:03:22.570 PM Good 18 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a2dynamicscanningdisabled p6/ficq-6101/a2dynamicscanningdisabled a2dynamicscanningdisabled false Boolean 6:03:22.512 PM 6:03:22.512 PM Good 19 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a2externalchangealarmenable p6/ficq-6101/a2externalchangealarmenable a2externalchangealarmenable false Boolean 6:03:22.512 PM 6:03:22.512 PM Good 20 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a2inerror p6/ficq-6101/a2inerror a2inerror false Boolean 6:03:22.513 PM 6:03:22.513 PM Good 21 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a2lastscannedtime p6/ficq-6101/a2lastscannedtime a2lastscannedtime 2026-02-21T09:06:16.815Z DateTime 6:06:17.524 PM 6:06:17.524 PM Good 22 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a2period p6/ficq-6101/a2period a2period 2 Int16 6:03:22.513 PM 6:03:22.513 PM Good 23 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a2quality p6/ficq-6101/a2quality a2quality 192 Int16 6:03:22.513 PM 6:03:22.513 PM Good 24 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a2reverse p6/ficq-6101/a2reverse a2reverse false Boolean 6:03:22.513 PM 6:03:22.513 PM Good 25 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a2source p6/ficq-6101/a2source a2source LOOP 11 RATE1 String 6:03:22.567 PM 6:03:22.572 PM Good 26 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a2status p6/ficq-6101/a2status a2status 12582912 Int32 6:03:22.513 PM 6:03:22.513 PM Good 27 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a3associatedparameter p6/ficq-6101/a3associatedparameter a3associatedparameter EnumValueType 6:03:22.513 PM 6:03:22.513 PM Good 28 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a3controllername p6/ficq-6101/a3controllername a3controllername EnumValueType 6:03:22.513 PM 6:03:22.513 PM Good 29 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a3destination p6/ficq-6101/a3destination a3destination LOOP 11 RESET1 String 6:03:22.568 PM 6:03:22.571 PM Good 30 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a3dynamicscanningdisabled p6/ficq-6101/a3dynamicscanningdisabled a3dynamicscanningdisabled false Boolean 6:03:22.562 PM 6:03:22.562 PM Good 31 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a3externalchangealarmenable p6/ficq-6101/a3externalchangealarmenable a3externalchangealarmenable false Boolean 6:03:22.562 PM 6:03:22.562 PM Good 32 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a3inerror p6/ficq-6101/a3inerror a3inerror false Boolean 6:03:22.562 PM 6:03:22.562 PM Good 33 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a3lastscannedtime p6/ficq-6101/a3lastscannedtime a3lastscannedtime 2026-02-21T09:06:16.815Z DateTime 6:06:17.525 PM 6:06:17.525 PM Good 34 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a3period p6/ficq-6101/a3period a3period 2 Int16 6:03:22.562 PM 6:03:22.562 PM Good 35 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a3quality p6/ficq-6101/a3quality a3quality 192 Int16 6:03:22.562 PM 6:03:22.562 PM Good 36 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a3reverse p6/ficq-6101/a3reverse a3reverse false Boolean 6:03:22.562 PM 6:03:22.562 PM Good 37 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a3source p6/ficq-6101/a3source a3source LOOP 11 RESET1 String 6:03:22.572 PM 6:03:22.575 PM Good 38 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a3status p6/ficq-6101/a3status a3status 12582912 Int32 6:03:22.563 PM 6:03:22.563 PM Good 39 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a4 p6/ficq-6101/a4 a4 0 Double 6:03:22.563 PM 6:03:22.563 PM Good 40 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a4associatedparameter p6/ficq-6101/a4associatedparameter a4associatedparameter EnumValueType 6:03:22.563 PM 6:03:22.563 PM Good 41 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a4controllername p6/ficq-6101/a4controllername a4controllername EnumValueType 6:03:22.563 PM 6:03:22.563 PM Good 42 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a4destination p6/ficq-6101/a4destination a4destination String 6:03:22.563 PM 6:03:22.563 PM Good 43 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a4dynamicscanningdisabled p6/ficq-6101/a4dynamicscanningdisabled a4dynamicscanningdisabled false Boolean 6:03:22.563 PM 6:03:22.563 PM Good 44 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a4externalchangealarmenable p6/ficq-6101/a4externalchangealarmenable a4externalchangealarmenable false Boolean 6:03:22.563 PM 6:03:22.563 PM Good 45 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a4inerror p6/ficq-6101/a4inerror a4inerror false Boolean 6:03:22.563 PM 6:03:22.563 PM Good 46 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a4lastscannedtime p6/ficq-6101/a4lastscannedtime a4lastscannedtime 0001-01-01T00:00:00Z DateTime 6:03:22.563 PM 6:03:22.563 PM Good 47 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a4period p6/ficq-6101/a4period a4period 0 Int16 6:03:22.563 PM 6:03:22.563 PM Good 48 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a4quality p6/ficq-6101/a4quality a4quality 192 Int16 6:03:22.563 PM 6:03:22.563 PM Good 49 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a4reverse p6/ficq-6101/a4reverse a4reverse false Boolean 6:03:22.563 PM 6:03:22.563 PM Good 50 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a4source p6/ficq-6101/a4source a4source String 6:03:22.563 PM 6:03:22.567 PM Good 51 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.a4status p6/ficq-6101/a4status a4status 12582912 Int32 6:03:22.563 PM 6:03:22.563 PM Good 52 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.ackdestination p6/ficq-6101/ackdestination ackdestination String 6:03:22.563 PM 6:03:22.567 PM Good 53 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.acmservername p6/ficq-6101/acmservername acmservername String 6:03:22.563 PM 6:03:22.565 PM Good 54 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.acmsystemname p6/ficq-6101/acmsystemname acmsystemname String 6:03:22.563 PM 6:03:22.565 PM Good 55 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.actionalgoblock p6/ficq-6101/actionalgoblock actionalgoblock 0 Int32 6:03:22.565 PM 6:03:22.565 PM Good 56 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.actionalgonumber p6/ficq-6101/actionalgonumber actionalgonumber 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 57 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm1offdelay p6/ficq-6101/alarm1offdelay alarm1offdelay 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 58 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm1ondelay p6/ficq-6101/alarm1ondelay alarm1ondelay 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 59 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm1priority p6/ficq-6101/alarm1priority alarm1priority EnumValueType 6:03:22.565 PM 6:03:22.565 PM Good 60 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm1subpriority p6/ficq-6101/alarm1subpriority alarm1subpriority 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 61 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm2offdelay p6/ficq-6101/alarm2offdelay alarm2offdelay 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 62 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm2ondelay p6/ficq-6101/alarm2ondelay alarm2ondelay 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 63 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm2priority p6/ficq-6101/alarm2priority alarm2priority EnumValueType 6:03:22.565 PM 6:03:22.565 PM Good 64 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm2subpriority p6/ficq-6101/alarm2subpriority alarm2subpriority 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 65 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm3offdelay p6/ficq-6101/alarm3offdelay alarm3offdelay 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 66 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm3ondelay p6/ficq-6101/alarm3ondelay alarm3ondelay 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 67 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm3priority p6/ficq-6101/alarm3priority alarm3priority EnumValueType 6:03:22.565 PM 6:03:22.565 PM Good 68 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm3subpriority p6/ficq-6101/alarm3subpriority alarm3subpriority 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 69 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm4offdelay p6/ficq-6101/alarm4offdelay alarm4offdelay 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 70 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm4ondelay p6/ficq-6101/alarm4ondelay alarm4ondelay 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 71 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm4priority p6/ficq-6101/alarm4priority alarm4priority EnumValueType 6:03:22.565 PM 6:03:22.565 PM Good 72 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm4subpriority p6/ficq-6101/alarm4subpriority alarm4subpriority 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 73 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm5offdelay p6/ficq-6101/alarm5offdelay alarm5offdelay 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 74 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm5ondelay p6/ficq-6101/alarm5ondelay alarm5ondelay 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 75 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm5priority p6/ficq-6101/alarm5priority alarm5priority EnumValueType 6:03:22.565 PM 6:03:22.570 PM Good 76 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm5subpriority p6/ficq-6101/alarm5subpriority alarm5subpriority 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 77 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm6offdelay p6/ficq-6101/alarm6offdelay alarm6offdelay 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 78 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm6ondelay p6/ficq-6101/alarm6ondelay alarm6ondelay 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 79 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm6priority p6/ficq-6101/alarm6priority alarm6priority EnumValueType 6:03:22.565 PM 6:03:22.570 PM Good 80 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm6subpriority p6/ficq-6101/alarm6subpriority alarm6subpriority 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 81 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm7offdelay p6/ficq-6101/alarm7offdelay alarm7offdelay 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 82 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm7ondelay p6/ficq-6101/alarm7ondelay alarm7ondelay 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 83 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm7priority p6/ficq-6101/alarm7priority alarm7priority EnumValueType 6:03:22.565 PM 6:03:22.567 PM Good 84 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm7subpriority p6/ficq-6101/alarm7subpriority alarm7subpriority 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 85 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm8offdelay p6/ficq-6101/alarm8offdelay alarm8offdelay 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 86 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm8ondelay p6/ficq-6101/alarm8ondelay alarm8ondelay 0 Int16 6:03:22.565 PM 6:03:22.565 PM Good 87 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm8priority p6/ficq-6101/alarm8priority alarm8priority EnumValueType 6:03:22.565 PM 6:03:22.567 PM Good 88 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarm8subpriority p6/ficq-6101/alarm8subpriority alarm8subpriority 0 Int16 6:03:22.567 PM 6:03:22.567 PM Good 89 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmdeadbandpercent p6/ficq-6101/alarmdeadbandpercent alarmdeadbandpercent EnumValueType 6:03:22.567 PM 6:03:22.567 PM Good 90 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmdisabled p6/ficq-6101/alarmdisabled alarmdisabled false Boolean 6:03:22.567 PM 6:03:22.567 PM Good 91 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmflags p6/ficq-6101/alarmflags alarmflags 1072 Int32 6:03:22.571 PM 6:03:22.571 PM Good 92 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmlimit1 p6/ficq-6101/alarmlimit1 alarmlimit1 250 Float 6:03:22.567 PM 6:03:22.567 PM Good 93 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmlimit2 p6/ficq-6101/alarmlimit2 alarmlimit2 300 Float 6:03:22.567 PM 6:03:22.567 PM Good 94 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmlimit3 p6/ficq-6101/alarmlimit3 alarmlimit3 1050 Float 6:03:22.567 PM 6:03:22.567 PM Good 95 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmlimit4 p6/ficq-6101/alarmlimit4 alarmlimit4 1100 Float 6:03:22.567 PM 6:03:22.567 PM Good 96 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmlimit5 p6/ficq-6101/alarmlimit5 alarmlimit5 0 Float 6:03:22.567 PM 6:03:22.567 PM Good 97 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmlimit6 p6/ficq-6101/alarmlimit6 alarmlimit6 0 Float 6:03:22.567 PM 6:03:22.567 PM Good 98 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmlimit7 p6/ficq-6101/alarmlimit7 alarmlimit7 0 Float 6:03:22.567 PM 6:03:22.567 PM Good 99 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmlimit8 p6/ficq-6101/alarmlimit8 alarmlimit8 0 Float 6:03:22.567 PM 6:03:22.567 PM Good 100 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmmessageindex p6/ficq-6101/alarmmessageindex alarmmessageindex 0 Int16 6:03:22.567 PM 6:03:22.567 PM Good 101 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmpriority p6/ficq-6101/alarmpriority alarmpriority EnumValueType 6:03:22.567 PM 6:03:22.568 PM Good 102 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmsubpriority p6/ficq-6101/alarmsubpriority alarmsubpriority 0 Int16 6:03:22.567 PM 6:03:22.567 PM Good 103 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmtype1 p6/ficq-6101/alarmtype1 alarmtype1 EnumValueType 6:03:22.567 PM 6:03:22.568 PM Good 104 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmtype2 p6/ficq-6101/alarmtype2 alarmtype2 EnumValueType 6:03:22.567 PM 6:03:22.569 PM Good 105 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmtype3 p6/ficq-6101/alarmtype3 alarmtype3 EnumValueType 6:03:22.567 PM 6:03:22.567 PM Good 106 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmtype4 p6/ficq-6101/alarmtype4 alarmtype4 EnumValueType 6:03:22.567 PM 6:03:22.569 PM Good 107 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmtype5 p6/ficq-6101/alarmtype5 alarmtype5 EnumValueType 6:03:22.568 PM 6:03:22.568 PM Good 108 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmtype6 p6/ficq-6101/alarmtype6 alarmtype6 EnumValueType 6:03:22.568 PM 6:03:22.568 PM Good 109 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmtype7 p6/ficq-6101/alarmtype7 alarmtype7 EnumValueType 6:03:22.568 PM 6:03:22.568 PM Good 110 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmtype8 p6/ficq-6101/alarmtype8 alarmtype8 EnumValueType 6:03:22.568 PM 6:03:22.571 PM Good 111 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.alarmvalue p6/ficq-6101/alarmvalue alarmvalue EnumValueType 6:03:22.568 PM 6:03:22.572 PM Good 112 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.area p6/ficq-6101/area area EnumValueType 6:03:22.569 PM 6:03:22.569 PM Good 113 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.assignable p6/ficq-6101/assignable assignable false Boolean 6:03:22.569 PM 6:03:22.569 PM Good 114 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.associateddisplay p6/ficq-6101/associateddisplay associateddisplay String 6:03:22.569 PM 6:03:22.569 PM Good 115 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.associatedtrend p6/ficq-6101/associatedtrend associatedtrend 0 Int16 6:03:22.569 PM 6:03:22.569 PM Good 116 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.bias p6/ficq-6101/bias bias 0 Double 6:03:22.569 PM 6:03:22.569 PM Good 117 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.biasscaleenable p6/ficq-6101/biasscaleenable biasscaleenable false Boolean 6:03:22.569 PM 6:03:22.569 PM Good 118 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.children p6/ficq-6101/children children {} String 6:03:22.569 PM 6:03:22.569 PM Good 119 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.connection p6/ficq-6101/connection connection EnumValueType 6:03:22.569 PM 6:03:22.571 PM Good 120 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.controlconfirmrequired p6/ficq-6101/controlconfirmrequired controlconfirmrequired false Boolean 6:03:22.569 PM 6:03:22.569 PM Good 121 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.controldeadbandpercent p6/ficq-6101/controldeadbandpercent controldeadbandpercent EnumValueType 6:03:22.569 PM 6:03:22.573 PM Good 122 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.controlfailalarmpriority p6/ficq-6101/controlfailalarmpriority controlfailalarmpriority EnumValueType 6:03:22.570 PM 6:03:22.571 PM Good 123 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.controlfailalarmsubpriority p6/ficq-6101/controlfailalarmsubpriority controlfailalarmsubpriority 0 Int16 6:03:22.570 PM 6:03:22.570 PM Good 124 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.controlinhibit p6/ficq-6101/controlinhibit controlinhibit false Boolean 6:03:22.570 PM 6:03:22.570 PM Good 125 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.controllevel p6/ficq-6101/controllevel controllevel 0 Int16 6:03:22.570 PM 6:03:22.570 PM Good 126 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.controltimeout p6/ficq-6101/controltimeout controltimeout EnumValueType 6:03:22.570 PM 6:03:22.571 PM Good 127 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.createtime p6/ficq-6101/createtime createtime 1770584057 Int32 6:03:22.570 PM 6:03:22.570 PM Good 128 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.defaultname p6/ficq-6101/defaultname defaultname FICQ-6101 String 6:03:22.570 PM 6:03:22.570 PM Good 129 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.description p6/ficq-6101/description description String 6:03:22.570 PM 6:03:22.570 PM Good 130 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.disablepvalarmjounaling p6/ficq-6101/disablepvalarmjounaling disablepvalarmjounaling false Boolean 6:03:22.570 PM 6:03:22.570 PM Good 131 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.driftdeadbandpercent p6/ficq-6101/driftdeadbandpercent driftdeadbandpercent EnumValueType 6:03:22.570 PM 6:03:22.571 PM Good 132 Honeywell Experion PKS OPC UA Server@localhost ns=1;s=shinam:ficq-6101.equipaggrdisplayelementbindings p6/ficq-6101/equipaggrdisplayelementbindings equipaggrdisplayelementbindings