feat: 웹UI Phase 4 완료 — CSS 분리, pane 중첩 버그 수정, app.js 제거
Phase 4 — CSS 분리: - style.css(2,230→670줄)에서 탭별 스타일을 css/<tab>.css 8개로 분할 (t2s:437, pid:236, pb:106, hist:100, evt:50, opcsvr:14, llmchat:501, kbadmin:109) - 크로스탭 공유 스타일(nm-*, hist-status, dt-picker 등)은 style.css 잔류 - index.html head에 11개 CSS link 태그 (1 style.css + 8 tab + 2 lib) app.js 제거: - index.html에서 <script src=/js/app.js> 참조 제거 - app.js → 10줄 placeholder (이미 Phase 0-3에서 모든 로직 이전 완료) Pane wrapper 버그 수정: - 16개 pane 파일에서 <section class=pane id=pane-xxx> wrapper 제거 - activateTab이 innerHTML로 주입 시 중첩 section + display:none 발생 - 내용이 전혀 안 보이는 문제 해결 문서 갱신: - AGENTS.md: Frontend Architecture 섹션 추가 - 웹UI-개선플랜-byOPUS.md: Phase 0-4 완료 상태로 갱신, 결과 검증 추가 MCP: - server.py: timestamp 정밀도 개선 등
This commit is contained in:
@@ -964,6 +964,137 @@ async def run_sql(sql: str) -> str:
|
||||
return await _execute_sql_internal(sql)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def upsert_pid_connection(
|
||||
tag_no: str,
|
||||
from_tag: str | None = None,
|
||||
to_tag: str | None = None,
|
||||
from_at: str | None = None,
|
||||
to_at: str | None = None,
|
||||
role: str | None = None,
|
||||
category: str | None = None,
|
||||
tag_class: str | None = None,
|
||||
) -> str:
|
||||
"""운전자가 작성한 from/to 연결 1건을 pid_equipment 에 멱등(idempotent) 반영.
|
||||
|
||||
⚠️ 오직 pid_equipment 테이블만, 아래 컬럼만 변경한다(나머지는 절대 안 건드림):
|
||||
from_tag, to_tag, from_at, to_at, role, category, tag_class, connection_locked, updated_at
|
||||
equipment_name/instrument_type/line_number/pid_drawing_no/pos_x/pos_y/confidence/is_active 는 보존.
|
||||
|
||||
매칭 규칙(문서규칙 §5):
|
||||
(1) 같은 연결((tag_no, from_tag, to_tag) 동일)이 있으면 그 행 UPDATE ← 재실행 안전
|
||||
(2) 없고, 해당 tag_no 의 from_tag·to_tag 가 비어있는 미완성 행(DXF)이 있으면 그 행을 채워 UPDATE
|
||||
(3) 둘 다 없으면 INSERT (같은 tag 추가 경로면 기존 행의 equipment_name/type/line/drawing/category 복사)
|
||||
|
||||
자동 처리:
|
||||
- from_tag 또는 to_tag 가 있으면 connection_locked=TRUE (연결분석이 덮어쓰지 않게)
|
||||
- updated_at = now()
|
||||
|
||||
Args:
|
||||
tag_no: 태그번호 (필수, 대소문자 무시 매칭).
|
||||
from_tag/to_tag: 상류/하류 태그. 병렬이면 호출 측에서 "A, B" 콤마 병합해 전달(이 도구는 분리하지 않음).
|
||||
from_at/to_at: 위치/서술 텍스트. role/category/tag_class: 그대로 저장(None이면 기존 유지).
|
||||
|
||||
Returns:
|
||||
JSON { success, action(update_existing|update_filled|insert), id, tag_no, before, after }
|
||||
"""
|
||||
def _n(v):
|
||||
if v is None:
|
||||
return None
|
||||
v = str(v).strip()
|
||||
return v or None
|
||||
|
||||
tag_no = _n(tag_no)
|
||||
if not tag_no:
|
||||
return json.dumps({"success": False, "error": "tag_no 가 비었습니다."}, ensure_ascii=False)
|
||||
from_tag, to_tag = _n(from_tag), _n(to_tag)
|
||||
from_at, to_at = _n(from_at), _n(to_at)
|
||||
role, category, tag_class = _n(role), _n(category), _n(tag_class)
|
||||
locked = (from_tag is not None) or (to_tag is not None)
|
||||
_SNAP = ["tag_no", "from_tag", "to_tag", "from_at", "to_at", "role", "category", "tag_class", "connection_locked"]
|
||||
|
||||
def _snap(cur, _id):
|
||||
cur.execute("""SELECT tag_no, from_tag, to_tag, from_at, to_at, role, category, tag_class, connection_locked
|
||||
FROM pid_equipment WHERE id=%s""", (_id,))
|
||||
r = cur.fetchone()
|
||||
return dict(zip(_SNAP, r)) if r else None
|
||||
|
||||
conn = None
|
||||
try:
|
||||
conn = await _get_db_connection()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(f"SET statement_timeout = {SQL_STATEMENT_TIMEOUT_MS}")
|
||||
# (1) 같은 연결 (재실행 멱등)
|
||||
cur.execute(
|
||||
"""SELECT id FROM pid_equipment
|
||||
WHERE lower(tag_no)=lower(%s)
|
||||
AND from_tag IS NOT DISTINCT FROM %s
|
||||
AND to_tag IS NOT DISTINCT FROM %s
|
||||
ORDER BY id LIMIT 1""",
|
||||
(tag_no, from_tag, to_tag))
|
||||
row = cur.fetchone()
|
||||
target_id = row[0] if row else None
|
||||
action = "update_existing" if row else None
|
||||
|
||||
# (2) 빈 미완성(DXF) 행 채우기
|
||||
if target_id is None:
|
||||
cur.execute(
|
||||
"""SELECT id FROM pid_equipment
|
||||
WHERE lower(tag_no)=lower(%s)
|
||||
AND (from_tag IS NULL OR from_tag='')
|
||||
AND (to_tag IS NULL OR to_tag='')
|
||||
ORDER BY id LIMIT 1""",
|
||||
(tag_no,))
|
||||
r2 = cur.fetchone()
|
||||
if r2:
|
||||
target_id, action = r2[0], "update_filled"
|
||||
|
||||
if target_id is not None:
|
||||
before = _snap(cur, target_id)
|
||||
cur.execute(
|
||||
"""UPDATE pid_equipment SET
|
||||
from_tag=%s, to_tag=%s, from_at=%s, to_at=%s, role=%s,
|
||||
category=COALESCE(%s, category),
|
||||
tag_class=COALESCE(%s, tag_class),
|
||||
connection_locked=%s, updated_at=now()
|
||||
WHERE id=%s""",
|
||||
(from_tag, to_tag, from_at, to_at, role, category, tag_class, locked, target_id))
|
||||
else:
|
||||
action = "insert"
|
||||
before = None
|
||||
# 같은 tag 기존 행에서 정적 메타 복사 (없으면 NULL)
|
||||
cur.execute("""SELECT equipment_name, instrument_type, line_number, pid_drawing_no, category
|
||||
FROM pid_equipment WHERE lower(tag_no)=lower(%s) ORDER BY id LIMIT 1""", (tag_no,))
|
||||
meta = cur.fetchone() or (None, None, None, None, None)
|
||||
eq_name, inst_type, line_no, draw_no, ex_cat = meta
|
||||
cur.execute(
|
||||
"""INSERT INTO pid_equipment
|
||||
(tag_no, equipment_name, instrument_type, line_number, pid_drawing_no,
|
||||
from_tag, to_tag, from_at, to_at, role, category, tag_class,
|
||||
connection_locked, confidence, is_active, extracted_at, updated_at)
|
||||
VALUES (%s,%s,%s,%s,%s, %s,%s,%s,%s,%s, COALESCE(%s,%s),%s, %s, 0, TRUE, now(), now())
|
||||
RETURNING id""",
|
||||
(tag_no, eq_name, inst_type, line_no, draw_no,
|
||||
from_tag, to_tag, from_at, to_at, role, category, ex_cat, tag_class, locked))
|
||||
target_id = cur.fetchone()[0]
|
||||
|
||||
after = _snap(cur, target_id)
|
||||
conn.commit()
|
||||
return json.dumps({"success": True, "action": action, "id": target_id,
|
||||
"tag_no": tag_no, "before": before, "after": after},
|
||||
ensure_ascii=False, default=str)
|
||||
except Exception as e:
|
||||
if conn:
|
||||
try:
|
||||
conn.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
return json.dumps({"success": False, "error": f"upsert 실패: {e}"}, ensure_ascii=False)
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def query_pv_history(tag_names: list[str], time_from: str, time_to: str, limit: int = 100) -> str:
|
||||
"""과거 값(PV) 히스토리 조회.
|
||||
|
||||
Reference in New Issue
Block a user