# 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<T>` / `safeObject<T>` 헬퍼 추가)
- `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` 에 `<include refid="common.companyCodeFilter"/>` 적용 + `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<String, Object> getById(long id) {
    Entity e = mapper.getById(id);
    if (e == null) return null;
    Map<String, Object> 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<String, Object> 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<Item>(row.connectors);
items.map(...)  // 안전

// JSONB object 필드 사용
const cfg = safeObject<Config>(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` 또는 "그램린" 키워드로 자동 호출.
