Files
johngreen 04cea72f33 fix(ai-modules): JSONB ::text 응답 자동 파싱 + workspace 카드 깨짐 수정
백엔드:
- 6개 AI Service (group/apiKey/provider/conversation/agent/scheduler) 가 응답 메서드에서
  `parseJsonField` 헬퍼로 JSONB(::text) 컬럼 (connectors / config / permissions /
  metadata / tool_calls / notification / tools) 을 String → Object 자동 변환.
- 모범 패턴 (`AuditLogService.processChanges`, `BusinessRuleService.parseJsonField`,
  `DataflowDiagramService.parseJsonbFields`) 동일하게 적용.
- model 의 String getter 는 그대로 유지 — `MultiAgentExecutionEngine` 등
  내부 LLM 호출 chain 영향 없음 (`getEntityById` 분리).
- 컨트롤러 시그니처 generic 만 변경 (return type Map).

프론트엔드:
- `safeArray<T>` / `safeObject<T>` 헬퍼 (`lib/utils.ts`) — 백엔드가 미파싱 String 으로
  올 때 graceful fallback. 빈 배열/객체 반환.
- `workspace/page.tsx` 멤버 카드:
  - `safeArray(member.connectors)` 적용 → `.map()` 폭발 차단.
  - 좁은 viewport 에서 한글 텍스트 한 글자씩 세로로 깨지던 문제 해결
    (`flex-wrap` + `truncate` + `whitespace-nowrap` + `max-w` + `title`).

그렘린 1000마리 폭격 + architect 자문으로 발견. workspace `Application error`,
`memberConnectors.map is not a function` 모두 해결.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:11:50 +09:00

51 lines
1.5 KiB
TypeScript

import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
/**
* 백엔드 JSONB(::text) 응답이 String 으로 올 때 안전하게 array 로 변환.
* INVYONE 백엔드의 모든 ::text AS jsonb 컬럼이 가끔 미파싱 String 으로 옵니다.
*/
export const safeArray = <T = unknown>(v: unknown): T[] => {
if (Array.isArray(v)) return v as T[];
if (typeof v === "string") {
try {
const p = JSON.parse(v);
return Array.isArray(p) ? (p as T[]) : [];
} catch {
return [];
}
}
return [];
};
/**
* JSONB string → object 안전 변환. array/null/undefined → 빈 객체.
*/
export const safeObject = <T extends Record<string, unknown> = Record<string, unknown>>(v: unknown): T => {
if (v && typeof v === "object" && !Array.isArray(v)) return v as T;
if (typeof v === "string") {
try {
const p = JSON.parse(v);
return p && typeof p === "object" && !Array.isArray(p) ? (p as T) : ({} as T);
} catch {
return {} as T;
}
}
return {} as T;
};
/**
* 파일 크기를 사람이 읽기 쉬운 형태로 포맷팅
*/
export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}