2026년 5월 4주차(2026-05-25 ~ 2026-06-01) 주간 리뷰. 한 주 동안 푼 문제 중심으로 정리한다.
룰·데이터 분석 파트는 산출물이 코드가 아니라 커밋 수로 잡히지 않으므로, 아래 별도 섹션으로 따로 정리한다.
서버 백엔드 전방위 진전
프로세스 영향도 위젯 — 서버 구현 4-레포 동시 정리 (OR-1442). OR-1334 분석가 가이드(V5.15)를 서버 입장에서 검토·확장해 rest-api · service · migrations · package 네 서브모듈에 동시 머지. system aggregation의 평균값 소스가 OHLC close(통상 마지막 raw)와 어긋나는 점을 잡아 average 필드를 OHLC와 별도로 신설했고(MIN_COUNT_FOR_OHLC=5 미달이어도 첫 sample부터 sum/count로 채움), daily 경계는 서버 로컬 timezone 자정으로 통일하되 DB 저장 값은 모두 UTC로 일관화. aggregated/list는 같은 SUID가 여러 hour|day에 등장할 때 row가 중복되던 P-1을 버리고, aggregator가 이미 보관해 둔 *_sum / CounterCount_sum으로 SUID 단위 가중평균(cpu_percentage = ProcessTime_total / SystemTime_total * 100)을 즉시 산출하는 P-2로 재설계했다. fast path 자동 흡수 — 시간 범위 안에 bucket이 1개뿐이면 $group도 doc 1개만 통과 → 단일 pipeline로 최적.
핵심은 subscriber-driven publish 모델이다. 31k SUID 전수 publish는 시간당 ~224k msg로 99%+가 아무도 안 듣는 publish이므로, manager-web이 관심 SUID를 PUT /v3/sprocess/subscribe로 등록하면 Redis ZSET에 ZADD-only(3분 keepalive·5분 TTL score 자연 만료)로 추적하고 aggregator는 ZRANGEBYSCORE로 활성 SUID set만 publish한다. abuse 가드로 한 bucket의 ZSET member 수가 MAX_ZSET_SIZE = 100,000을 넘으면 방금 추가한 SUID들을 ZREM으로 롤백 + 429. 정상 운영(매니저 수 × 위젯 × SUID 수십개 ≈ ~100)이 cap의 1000배 여유 안에 있다. 별도 systemd 서비스(orange.process-aggregator.service)로 격리, hourly cron :02 + partial :05/:15/:25/:35/:45/:55로 위젯이 새 hour 시작 후 최대 5분 안에 진행 중 hour를 볼 수 있게 했다. 이번 주 39커밋의 최대 작업.
조직도 일괄 로드 단일 트랜잭션화 + Beanie bulk_writer 단일 RTT (OR-1435). POST /v3/organization/load의 clear → load_groups → load_users 사이 중간 실패에 따른 부분 적재 노출을 단일 MongoDB 트랜잭션으로 묶었다(전제: 아래 OR-1439). load_groups · load_users · recompute_all_node_user_fields를 Beanie bulk_writer로 전환해 RTT를 1회로 줄였고, User.group_code 사전 검증을 엄격 모드로 추가, 조직도 로드 후 ghost 그룹의 활성 Command 자동 정리·Node 비정규화 필드 일괄 재계산·고아 parent 처리 옵션화(strict_orphan_parents)까지 시나리오 6번을 모두 정리했다. 5번 시나리오(코드 관성)는 Agent 측 동작 실측으로 무영향임을 확인하고 별도 리팩터링 이슈로 분리.
MongoDB single-node replica set 전환 — multi-document 트랜잭션 인프라 (OR-1439). OR-1435 트랜잭션을 가능하게 하기 위해 standalone MongoDB를 single-node replica set으로 전환. setup.sh/update.sh에 keyFile(openssl rand -base64 756, mongodb:mongodb, 400) 생성과 rs.initiate 3단 분기(무인증 OK / AlreadyInitialized SKIP / Unauthorized → admin auth fallback)를 idempotent로 짜고, PyMongo의 hello 응답 자동 발견을 위해 멤버 host를 rest-api · service 양쪽이 도달 가능한 docker network gateway IP로 설정(orange_net 172.30.0.1 / default bridge 172.17.0.1). 테스트 · 개발 서버 모두 데이터 100% 보존 마이그레이션 검증, rest-api/service 코드 변경 0건.
KISA SW 개발보안 가이드 — Phase 1~3 시큐어코딩 마무리 (OR-994). KISA(2021) 제4장 구현단계를 기준으로 servers/rest-api와 servers/service에서 도출한 20건의 보안 결함을 일괄 처리하고 본 이슈 종료. CWE-798 마스터키·MD5·deploy.sh 평문 비밀번호, CWE-22 Path Traversal·Zip Slip(extractall 시 ../../ 차단), CWE-918 모니터 SSRF(loopback 127.0.0.0/8 ::1·link-local 169.254.0.0/16 fe80::/10 차단을 문자열 비교가 아닌 DNS 후 IP 레벨에서), CWE-307 로그인 IP Rate Limiting(POST /v3/manager/login 60초/20회 Redis pipeline INCR+EXPIRE 원자 처리), CWE-330 Node ticket UUID v1→v4, CWE-434 업로드 크기·Zip bomb 가드(/v3/rule/upload 10MB + 전개 200MB, /v3/organization/load 10MB, /v3/upload 청크 2MB / 전체 5GB), CWE-312 v1 logon 응답에서 비밀번호 해시 제거까지. CORS 와일드카드와 verify=False(자체 서명 인증서 환경, 사내 PKI 미비) 두 건은 위협 모델 범위 밖으로 분리하면서도 향후 활성화를 위해 # WARN: cert verification disabled — internal CA 도입 시 활성화 주석을 7개 위치에 박아 두었다.
REST API 3181 nginx 우회 스캔 차단 — fail2ban HTTP jail + journald 로그 정합 (OR-1448). Agent 직접 접속용으로 의도된 3181 노출에 스캐너가 nginx 우회로 GET /·/favicon.ico·/.well-known/security.txt 404 폭주를 일으키던 케이스. compose 로그를 journald + logging.options.tag로 CONTAINER_TAG=orange-rest-api로 고정해 컨테이너 이름이 환경마다 달라도(고객사 orange-rest-api / dev rest-api) 한 벌의 jail이 동작하게 했고, banaction_allports(%(banaction_allports)s)로 시스템 방화벽 백엔드(nftables/iptables)를 자동 추종해 스캐너의 전 포트를 차단. /api 404는 ignoreregex로 분리해 정상 요청을 살리고, fail2ban-regex 샘플 + dev 실측 ban(TEST-NET) + 임시 트리로 전체 config 파싱까지 검증.
WebSocket 인증 실패 시 rest-api 예외 핸들러 크래시 정리 — AppException catch + Request/WS 분리 (OR-1432). /v3/ws/node/stats 토큰 누락/만료 시 AttributeError: 'WebSocket' object has no attribute 'method'로 ASGI 핸들러가 2차 폭발하던 버그. WS 엔드포인트가 HTTPException만 catch하던 부분에 AppException을 추가하고, app_exception_handler가 Request로만 가정하던 부분을 WebSocket 분기로 분리. accept 이전 close 호출이 uvicorn에 의해 HTTP 403 핸드셰이크 거절로 변해 클라가 1006으로 무한 재시도 루프에 빠지던 부작용도 같이 정리(OR-1129 자동 재연결 클라이언트와 직결되는 서버 측 안전화). 기타 4개 WS 엔드포인트도 close-before-accept 정리(OR-1433).
Manager·Agent 회귀 추적과 BSOD 분석
nodeInfo will aux abrupt close 우회 — 크롬 148 + OpenSSL 3.0.x 잔여 경로 회피 (OR-1418). OR-1409로 위젯 MQTT를 글로벌 일원화한 직후 nodeInfo cleanup 일부가 빠져 위젯이 닫혀도 Agent가 "이 화면 더 안 본다" 신호를 받지 못해 전용 패킷이 누적되던 회귀. broker 디버그 로그(log_type all + connection_messages true) · tcpdump · 엣지 vs 크롬 격리 비교로 크롬 148의 WebSocket close 시퀀스가 broker OpenSSL 3.0.x record-layer 버그의 error:0A000123:SSL routines::application data after close notify 잔여 경로를 다시 트리거하고, 그 결과 무관한 Agent 연결이 끊긴다는 사실을 확정. OR-1414 본문이 "운영 로그상 0건이라 100% 커버"라고 한 잔여 경로가 크롬 148 업데이트로 0건이 아니게 된 것이다. SSL_OP_IGNORE_UNEXPECTED_EOF는 0126 전용이고 0123을 무시할 옵션은 OpenSSL 3.0.13 헤더에 존재하지 않아 shim 확장도 불가능.
해결: will 보조 연결을 abrupt close (end(true) = stream.destroy())로 전환. graceful close가 0123을 트리거하는 것이 핵심이므로 abrupt는 close_notify를 생략해 0123을 원천 발생 불가로 만들고, 대신 발생하는 0A000126:unexpected eof는 OR-1414 shim이 흡수한다. 동시에 abrupt close가 broker의 WILL을 즉시(~1ms) 발사시켜 통지 안전망이 살아남는다. 매니저 측 두 곳에 상세한 [중요] 경고 주석과 비활성 publish 백업 코드를 박아 "graceful이 정상적인 종료"라는 단순 웹개발 상식으로 되돌리면 0123 재발 + WILL 미발사 두 문제가 컴파일·런타임 에러 없이 조용히 동시 발생하는 함정임을 박제했다. 실측 — nodeInfo 10회+ 반복 여닫이에 0A000123 6건 → 0건, agent Protocol error drop 발생 → 0건, broker 소켓 CLOSE-WAIT 누적 0건. 근본 처방(broker OpenSSL 3.2+ 또는 1.1.1 교체)은 별도 진행.
Manager > 노드 통계 WebSocket 야간 유실 — close 코드별 정책 정립 + 인스턴스 가드 (OR-1129). OR-1105 상태 패널로 적발된 야간 노드 통계 WebSocket(/api/v3/ws/node/stats) 단일화 현상에 대해, wasClean === true라도 close 코드는 정상이 아닐 수 있다(서버 WS_1011 내부 에러도 핸드셰이크가 정상 완료되면 wasClean = true)는 점을 짚고, 클라이언트가 close 코드별로 자동 복구하도록 정책을 정립. 1006(네트워크 끊김) / 1011(서버 내부 오류)은 지수 백오프 재연결(1s → 2s → … → 30s), 1000(정상) / 1005(코드 없음)은 무동작, 그 외 + 1008(인증/정책 위반)은 강제 로그아웃. 정상 메시지 수신 시 백오프 카운트 리셋으로 "수락 직후 끊김"이 1초마다 반복되는 플래핑을 차단하고, 인스턴스 가드("지금도 내가 현재 소켓일 때만" 동작)로 늦게 도착한 옛 소켓 이벤트가 새 연결을 덮어쓰는 것까지 막았다. 로그아웃 시 useNodeStatsStore.getState().reset()으로 옛 토큰 좀비 재연결도 차단. "메인 MQTT는 끊기면 무조건 로그아웃, node-stats만 degraded 복구"라는 운영 원칙도 명시화.
[Manager] ESLint no-shadow 규칙 + git pre-commit 게이트 — OR-1028 11일 잠복 버그 재발 차단 (OR-1029). OR-1028 파일 다운로드 버그가 import { downloadFile }과 컴포넌트 내부 const downloadFile = useCallback(...)의 변수 섀도잉으로 11일간(03-17~03-28) 잠복했던 사고를 받아, 새로 추가·수정하는 .ts/.tsx에 no-shadow 위반이 있으면 커밋이 거부되는 pre-commit 훅을 도입. yarn install 한 번에 윈도·맥·WSL 공통 자동 설치(설치 스크립트는 Node로 동작하므로 bash 불필요), Node가 없으면 통과가 아니라 차단으로 통일. 의도적 섀도잉만 인라인 // eslint-disable-next-line @typescript-eslint/no-shadow 주석으로 허용. 같은 사고를 도구 차원에서 들어오지 못하게 막은 마무리.
MSI 노트북 BSOD #2 — vmmemWSL 어서션 + e1dn.sys D3 전환 두 케이스 windbg 분석 (OR-1307). WSL2 vmmemWSL이 일으킨 0x7E SYSTEM_THREAD_EXCEPTION_NOT_HANDLED(STATUS_ASSERTION_FAILURE 0xC0000420 in nt!VmpInvalidateSingleGpaRange+0x44)는 Windows 11 24H2 26100.8246의 WSL2 메모리 관리 회귀로 추정. 별도 발생한 0x9F DRIVER_POWER_STATE_FAILURE는 windbg !irp로 IRP_MJ_POWER → SET_POWER D3가 \Driver\e1dnexpress에 도달했다가 시한 내 완료되지 못한 케이스로 확정. 충돌 위치 NxNblDatapath::SetRxHandler+30(nxnbldatapath.cpp:92)에서 NetAdapterCx의 RX 핸들러 분리 단계(SetRxHandler → rundown wait)에서 멈춘 것. 2024년 9월 빌드 e1dn.sys(Intel PCI\VEN_8086&DEV_550F 이더넷 컨트롤러)는 그동안 Intel·MS가 NetAdapterCx D3 전환 패치를 여러 차례 배포한 상태라 벤더 본 드라이버 업데이트와 어댑터 전원 관리 옵션 해제로 회피하도록 조치 정리.
sprocess 1시간 단위 hourly 집계 인프라 — 부팅 누적 → 시간 집계 의미 교체 (OR-1340). OR-1306이 깐 1시간 주기 송신 인프라 위에서 송신 수치를 부팅 누적이 아니라 1시간 단위 집계로 바꾸는 작업. CSProcessHourly 클래스(yagent21/CSProcessHourly.h)에 SUID 기반 메모리 버킷 + Rotate baseline carry(new.start2 = MAX(old(start2+start-stop), 0))·Cleanup 2시간 비활성 evict·DebugFlush dev 모드를 한 클래스로 모으고, 저장 Running 컬럼을 sprocess/sprocess_all에서 제거해 8개 SELECT에서 MAX(Start-Stop, 0) AS Running 파생으로 전환(저장 컬럼 없음 + UPDATE 쿼리 없음 + 0복귀 자동). wal_autocheckpoint 노브를 db/summary.json 헤더에 신설해 외부 reader(orangedb.exe)에서 라이브 데이터 즉시 visible. 28시간 무중단 운영에서 40회 hour rollover carry mismatch 0건, msedge SUID 3081108326 02:00 Running=4 → 03:00 Start2=4 carry 일치 검증. SystemStat MQTT 송신 빈도 폭증도 평상시 60초 / LiveResponse 5초로 같이 정리. 송신 메시지 스키마 합의(서버 백엔드 파트)와 Safe 모드 정책만 남김.
Agent doc.Servers 잔재로 인한 접속 불가 — dedup_sink + UI 저장 무효 가드 제거 (OR-1449). OR-1274 이전 옛 코드가 단말의 doc.Servers에 사설망 IP(192.168.100.211 4종 포트)를 남겨두고 신 코드는 그것을 갱신하지 않아, doc.Server.Address가 이미 당사 운영 서버로 정상 설정되어 있어도 단말이 죽은 사설 IP에만 매달리는 자가 복구 불가 상태가 만들어진 케이스. OR-1274 본문 후속 과제("Agent UI 재입력 외 원격 매니저 명령으로 복구 절차 필요")의 정확한 케이스다. UI에서 동일 값 재저장 시 CAgentHelper3.command.cpp:260~266의 O == N 가드가 SetServerOverride 호출 자체를 막아 UI는 saved:true로 응답하지만 백엔드는 그대로인 인지 부정합도 해소(O == N 가드 제거 → 다음 사이클에서 ServerOverride 경로로 재등록되고 응답으로 doc.Servers 자연 정리 예정). 본 이슈 변경 이력 추적 자체가 살아남도록 server.log 폭주를 선행 정리 — module/logger.h에 dedup_sink 신설(동일 라인 반복은 메모리 카운트만, 다른 라인 도착 시 [REPEAT N] 자동 출력, 호출 코드는 손대지 않음)해 RegisterToServer2 35회 반복이 2줄로 압축됨을 확인. 단말별 서버 주소 영구 기록(node_server_history)·원격 매니저 reset 명령이 다음 단계.
Baseline 일상 분류 서비스 Sprint 1+1.5
Baseline 일상 분류 서비스 — 5조건 룰 + 다도메인 + LightGBM-ready 스키마 (OR-1438). detect를 시작으로 system / process / nodeinfo까지 확장될 "늘 똑같이 반복되는 일상 노이즈" 자동 분류 독립 서비스. 1순위 요구사항을 "기존 데이터 / 서비스에 영향 0"으로 잡고 원본 컬렉션을 read-only(find만)로 두며 쓰기는 자체 컬렉션(baseline_routine · baseline_overrides · baseline_runs)에만 upsert, 도메인별 독립 systemd unit(baseline-detect.service, baseline-system.service, ...)로 한 도메인 장애가 다른 도메인·기존 서비스에 영향 없도록 격리.
'일상' 판정은 5조건이 모두 만족할 때만 routine. cv_count < 0.15(발생량이 일정) · k ≥ 3(서로 다른 3일 이상 발생) · max_gap ≤ 14일(2주 넘게 끊긴 적 없음) · since ≤ 14일(최근 2주 안에 발생 = 살아있음) · rep ≥ 0.6. 핵심 설계는 반복성 점수 rep = max(reg, freq) — 규칙성 reg = 1 − min(gap_cv, 1)과 빈도 freq = min(1, 2 ÷ mean_gap)를 max로 조화해서 "규칙적으로 반복"과 "자주 반복(들쭉날쭉해도)" 두 형태의 일상을 모두 포착. 다섯 조건 모두 사람이 검산 가능한 단순 통계라 운영자가 결정 근거를 그대로 본다.
운영 서버 detect_node 전체 이력(186,833행 · 206일)로 공식 도출 · 검증. 초기 시도였던 EWMA+CUSUM은 단순 cv 규칙 대비 가치가 입증되지 않아 폐기. 이번 주에는 운영 hang 수정(fork → spawn 전환), baseline settings를 store.code=baseline 패턴으로 통합, multiprocessing 벤치마크(chunksize 50이 기본, 1~10은 IPC 라운드트립 폭증으로 회피, cpu_count // 2 워커가 14 코어 머신의 sweet spot, Pool(workers=1)이 in-line serial보다 100ms 느려서 workers <= 1 분기 의미 있음)까지 Sprint 1.5 검증 마무리. Phase 1 안정기(skip 80% 적중)에 50k 키 compute ~5초, DB 포함 합계 ~1분으로 1시간 주기에 여유 충분, 10x 운영 대비도 안전.
Phase 2 LightGBM 학습 입력 18개 피처(cv_count, k, max_gap, since, mean_gap, gap_cv, B, H_hour, H_dow, reg, freq, rep, mean, std, mx, total_count, active_days, ...)와 baseline_labels 컬렉션 슬롯도 baseline_routine.features 서브도큐먼트에 사전 적재. 라벨 200~500 누적 후 LightGBM 학습 → 피처 중요도·SHAP 해석성 유지하면서 룰 baseline과 직접 비교 → 의미 있게 이기면 앙상블 또는 교체. 그 다음은 Phase 3 Isolation Forest 무라벨 안전망 + 도메인 확장, Phase 4 시퀀스 모델은 시간 의존성 강한 도메인 한정. 룰 → ML 진화 경로에 코드 변경 없이 진입 가능한 스키마가 이번 작업의 핵심 자산이다.
커널 드라이버 파일 이벤트 throttle 본격 적용
파일 삭제·오류 필터링 — 시간 기반 throttle + 512-entry LRU 테이블 (OR-1427). File.Failed.cpp / 삭제 이벤트의 driver→agent notify 횟수가 한 달 동안 17조 회 누적 관측됨(분당 약 39만 건 통과로 agent 큐 폭주 위험). 기존 정책 count % 100 == 1은 동일 (process+status+file) key 마다 매 100회 1회 통과라 한 key가 지속 폭주하면 100건당 1건씩 무한 통과되어 폭주 자체는 막지 못하는 한계가 있었다. 두 가지를 동시에 바꿨다.
첫째, 횟수 단위 → 시간 단위. FILTER_COUNT_NOTIFY_INTERVAL (100) → FILTER_COUNT_NOTIFY_INTERVAL_SEC (600)로 10분당 1회 전환. 시간 source는 KeQueryUnbiasedInterruptTime(100ns 단위, sleep 시간 제외)을 써서 노트북이 절전 상태였던 시간은 카운트하지 않게 했다 — 깨어난 직후 즉시 notify가 폭주하는 패턴을 차단.
둘째, 카운트 테이블을 LRU로. 기존 hash table(open addressing) + ERESOURCE는 포화 시 RtlZeroMemory로 전체 reset하여 모든 key의 LastNotify 손실 → 다음 발생들이 "첫 발생"으로 인식되어 즉시 notify 통과 → throttle 무력화되던 문제와, ERESOURCE(56B)가 read/write 구분 없는 use case에 과한 도구라는 문제를 동시에 안고 있었다. 신규 자료구조는 512-entry linear array(FILTER_COUNT_SLOT[512]) + EX_PUSH_LOCK(8B, 1/7 크기, contention 무 시 atomic CAS만). 포화 시는 가장 오래된 LastNotify entry 1개만 LRU evict해서 다른 entry의 LastNotify를 보존 — throttle 효과가 살아남는다. linear scan 한 번에 기존 key 검색 + oldest 추적 동시 처리.
API도 통합. FilterCountIncrement → FilterCountIncrementAndCheckInterval(Key, IntervalSeconds, &Count) 로 바뀌어 TRUE 반환 시 호출자가 FfNotifyFailed 호출, FALSE면 throttle 차단. 호출 site의 count % N == 1 분기가 사라져 코드 단순화. 효과 비교 — 17조 회 케이스 1,700억 건 notify → 4,320건(약 4천만 배 절감), Lock 크기 56B → 8B, Table 메모리 2048 × 24B = ~48KB → 512 × 24B + 16B = ~12KB.
PE Authenticode 파싱 비용을 회피하는 SIGNER_CACHE(ofilter/File.Signature.h/cpp)도 같이 들어가, ProcessNotify에서 ResolveSignatureLevelCached(pProc, PID, pPathHint) → pPsGetProcessSignatureLevel → SL≥7이면 ProcPath 키로 캐시 등록 흐름으로 매 파일 이벤트마다 디스크 I/O 3회 + PagedPool 최대 16MB이 들어가던 PE 파싱 후보 설계(OR-1423)를 spinlock + atomic CAS 수 마이크로초로 대체. WindowsSystemBinary 화이트리스트도 단일 g_SystemAppsPrefix에서 8개 prefix 배열(SystemApps · WindowsApps · OneDrive · Microsoft x86 · Office 64/32bit · Common Files microsoft shared · Defender Platform) + Office/OneDrive/MsMpEng/WmiPrvSE 등 basename 30여 개로 확장.
Agent > orange.exe > MS 시스템 프로세스 bSystem TRUE 설정 (OR-1434). PROCESS_ENTRY.IsSystemProcess(ofilter.h:242)에 WsbClassifyBinary 결과가 정상 저장되는데도 Y_PROCESS_MESSAGE.bSystem(yagent.define.h:915)이 false 하드코딩이라 svchost · explorer 같은 일반 시스템 프로세스가 PPL 판정 / 파일 속성 판정 단계 전까지 bSystem = false로 전달되던 정합 오류를 수정. ofilter의 CreateProcessMessage 오버로드 1·2 + Process.cpp:422 ProcessStart 콜백 + yagent21 CProcess2.cpp:291 7개 사이트를 손봐 드라이버 경로 기반 판정 결과가 AddProcessCache 진입 시점부터 즉시 Current DB process 테이블 · Summary DB filelist 테이블 · MQTT public/node/filelist 토픽까지 전파되도록 일원화. 3단계 false → true 단방향 승격 구조(① 드라이버 WsbClassifyBinary ② PPL ③ SetSystemFileFlag2 파일 속성)는 그대로 유지.
manager-web 정면 진입
이번 주 manager-web 위젯·CI에 손댔다. React 19 + TypeScript · 실시간 WebSocket · npm 라이브러리 배포가 맞물린 작업이다.
조직관리 위젯 전반 개선 — 커스텀 DnD 3단계 + 사용자 CRUD 즉시 저장 + 색상 일관화 (OR-1447). 조직도 트리 DnD에서 @rc-component/tree 라이브러리 내장 DnD와 커스텀 DOM 이벤트가 혼용되어 초록/파랑 색상 충돌 · 드롭 위치 부정확하던 부분을 커스텀 div 렌더러로 전면 교체. 마우스 Y 위치 기준 before(상위 30%) / into(중간) / after(하위 30%) 3단계 위치 인식 + 최상위 노드 간 이동 차단을 메인 트리·편집 모달 양쪽에 동일 적용. 우클릭 컨텍스트 메뉴(하위 추가/수정/삭제)는 완전 제거하고 우클릭 기본 동작 차단해 "조직도 편집" 버튼 → 편집 모달로 통합.
사용자 추가/수정/삭제는 메인 저장 버튼을 거치지 않고 즉시 API 저장(트리 구조 변경 시에만 메인 저장 사용). 저장 후 window.location.reload() → reset() + organizationDataRefetch() + rebuildUserNodes() 호출로 데이터만 재조회 + 현재 화면 위치 유지. 사용자 추가 폼에 조직 선택 드롭다운 추가 · 필수(*) 표시 · 미입력 시 추가 버튼 비활성화로 입력 검증 강화. AG Grid 색상도 호버 #f3f4f6(연회색) · 선택 #ffedd5(연주황) · 체크박스/포커스 #f97316(주황)으로 일관화, CSS 변수 + !important 이중 오버라이드로 안정 적용. 좌우 패널 하단 높이 미세 불일치 · .ag-root-wrapper 이중 border · footer h-7/h-8 불일치도 정리. 미사용 import(LuFolderX, Menu) · FormIf 필드 9개 · 함수 6개(context menu 관련) · useEffect 4개 · 접근 불가 다이얼로그 2개 · 대형 주석 블록 정리. TypeScript 에러 0건 머지.
FontAwesome Pro 제거 — 무료 버전 마이그레이션 + Menu 리팩토링 + yarn.lock 복구 (OR-1441 / OR-1230). 프로젝트의 FontAwesome Pro 의존성 5종(pro-duotone · pro-light · pro-regular · pro-solid · pro-thin)을 제거하고 @fortawesome/free-regular-svg-icons로 대체. ['far','cancel'] → ['fas','xmark'], ['fal','layer-group'] → ['fas','layer-group'], ['fal','message-exclamation'] → ['fas','message'] 같은 아이콘 치환을 코드 전 영역에 적용. CI/CD .github/workflows/release.yml · ci/develop/Dockerfile.manager · ci/release/Dockerfile에서 Pro 레지스트리 설정 제거 + .yarnrc.yml(nodeLinker: node-modules) 신설. PR #68 진행 중 손상된 yarn.lock도 복구. manager-web의 빌드·CI 영역에 깊이 손댄 작업이다.
고객사 니즈 분석
룰·데이터 분석 R&R에서는 분석 산출물이 코드가 아니므로 커밋 수로 활동을 볼 수 없다.
한 국립대 고객사 니즈 분석 마무리 — 49일 매니저 실측 + 국립대 전산실 통증 매핑 (OR-1420). 한 국립대 고객사가 우리 제품의 어느 면에서 가치를 느낄지 가설을 좁히기 위한 분석을 이번 주 마무리. 49일치 매니저 실측 데이터(TCP 3,493 / DNS 2,574 / 비정상 종료 268 / KillProcess 211 / CPU 점유 129건 등)와 7가지 데이터 축(시스템 · 앱 · 프로세스 · 네트워크 · 세션 · ETW + 메타데이터) 정리. 동시에 국립대 전산실의 6가지 구조적 조건(예산 제약 · 판권/벤더 분산 · 인력 부족 · 인프라 5~6팀 분산 · 감사 부담 등)과 10가지 통증(헬프데스크 본업 잡아먹기 · 장애 원인 책임 회피 · 다운타임 즉시 정치 폭탄 등)을 명확히 분리. "영업의 카운터파트는 기능 매칭이 아니라 통증 매칭이다"라는 결론으로 다음 미팅 전략을 정렬.
이번 주는 매니저 회귀 추적(매니저·에이전트 파트)과 서버 백엔드 폭넓은 전진(서버 백엔드 파트), Baseline 일상 분류 서비스 Sprint 1.5 마무리(분석 서비스 파트), Agent 커널 드라이버 throttle 본격 적용(커널 드라이버 파트), manager-web 정면 진입(프론트엔드 파트), 한 고객사 통증 매핑(룰·데이터 분석 파트)이 동시에 진행된 한 주였다.