04cea72f33
백엔드:
- 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>
51 lines
1.5 KiB
TypeScript
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];
|
|
}
|