style(rolesList): 다른 메뉴 톤에 맞춰 사이즈/글씨 축소 #12

Merged
hjjeong merged 22 commits from hjjeong into main 2026-05-13 08:23:51 +00:00
Showing only changes of commit 0c9e22a679 - Show all commits
@@ -18,6 +18,10 @@ import {
ConditionalEditor,
emptyConditionalConfig,
} from "@/components/admin/batch/ConditionalEditor";
import {
ExternalRestApiConnectionAPI,
type ExternalRestApiConnection,
} from "@/lib/api/externalRestApiConnection";
// 타입 정의
type BatchType = "db-to-restapi" | "restapi-to-db" | "restapi-to-restapi";
@@ -127,6 +131,15 @@ export default function BatchManagementNewPage() {
const [fromApiData, setFromApiData] = useState<any[]>([]);
const [fromApiFields, setFromApiFields] = useState<string[]>([]);
// 등록된 REST API 연결 (외부 커넥션 관리에서 등록한 연결 선택)
// - 선택 시 폼(URL/엔드포인트/메서드/Body/인증) 자동 채움
// - 자동으로 API 호출하여 응답 필드 추출 → 매핑 드롭다운 즉시 활성화
const [registeredRestApis, setRegisteredRestApis] = useState<ExternalRestApiConnection[]>([]);
const [selectedRestApiId, setSelectedRestApiId] = useState<string>("manual"); // "manual" = 직접 입력
const [rawResponse, setRawResponse] = useState<unknown>(null);
const [rawResponseLoading, setRawResponseLoading] = useState(false);
const [rawResponseError, setRawResponseError] = useState<string>("");
// 통합 매핑 리스트
const [mappingList, setMappingList] = useState<MappingItem[]>([]);
@@ -155,8 +168,100 @@ export default function BatchManagementNewPage() {
useEffect(() => {
loadConnections();
loadAuthServiceNames();
loadRegisteredRestApis();
}, []);
// 등록된 REST API 연결 목록 로드
const loadRegisteredRestApis = async () => {
try {
const list = await ExternalRestApiConnectionAPI.getConnections();
setRegisteredRestApis(Array.isArray(list) ? list : []);
} catch (e) {
console.error("등록된 REST API 연결 목록 로드 실패:", e);
}
};
// 등록된 연결 선택 시 폼 자동 채우기 + API 호출 + 응답 필드 추출 (자동 매핑 준비).
// vexplor_rps 의 applyRegisteredRestApi 에서 회사 전용 프리셋(Amaranth) 분기는 의도적으로 제외.
const applyRegisteredRestApi = async (id: string) => {
setSelectedRestApiId(id);
if (id === "manual") return;
const conn = registeredRestApis.find((c) => String(c.id) === id);
if (!conn) return;
// 폼 자동 채움
setFromApiUrl(conn.base_url || "");
setFromEndpoint(conn.endpoint_path || "");
setFromApiMethod((conn.default_method as "GET" | "POST" | "PUT" | "DELETE") || "GET");
setFromApiBody(conn.default_body || "");
// 인증 토큰 자동 채움 (직접 입력 모드)
setAuthTokenMode("direct");
setAuthServiceName("");
if (conn.auth_type === "bearer" && conn.auth_config?.token) {
setFromApiKey(`Bearer ${conn.auth_config.token}`);
} else if (conn.auth_type === "api-key" && conn.auth_config?.keyValue) {
setFromApiKey(conn.auth_config.keyValue);
} else {
// wehago 등 백엔드 자동 서명 타입은 토큰 입력 불필요 — 비워둠
setFromApiKey("");
}
// 자동으로 API 호출 → 응답 본문 + 필드 추출하여 매핑 드롭다운 즉시 활성화
setRawResponseError("");
setRawResponseLoading(true);
setRawResponse(null);
try {
const result = await ExternalRestApiConnectionAPI.testConnectionById(
Number(id),
conn.endpoint_path || undefined,
);
if (result.success) {
setRawResponse(result.response_data);
// 응답 안에서 배열을 자동 탐색 (dataArrayPath 가 아직 안 박혀도 동작)
const findArr = (o: unknown, depth = 0): unknown[] | null => {
if (Array.isArray(o)) return o;
if (depth >= 4 || typeof o !== "object" || o === null) return null;
for (const v of Object.values(o)) {
const a = findArr(v, depth + 1);
if (a) return a;
}
return null;
};
const arr = findArr(result.response_data);
if (arr && arr.length > 0 && typeof arr[0] === "object" && arr[0] !== null) {
const fields = Object.keys(arr[0] as Record<string, unknown>);
setFromApiFields(fields);
setFromApiData(arr as Record<string, unknown>[]);
toast.success(
`'${conn.connection_name}' API 호출 완료 — 배열 ${arr.length}건 / 필드 ${fields.length}개 추출`,
);
} else if (
result.response_data &&
typeof result.response_data === "object" &&
!Array.isArray(result.response_data)
) {
const fields = Object.keys(result.response_data as Record<string, unknown>);
setFromApiFields(fields);
setFromApiData([result.response_data as Record<string, unknown>]);
toast.success(`'${conn.connection_name}' API 호출 완료 — 필드 ${fields.length}개 추출`);
} else {
toast.success(`'${conn.connection_name}' API 호출 완료 — 응답을 받았어요`);
}
} else {
const msg = result.message || result.error_details || "API 호출 실패";
setRawResponseError(msg);
toast.error(`'${conn.connection_name}' API 호출 실패: ${msg}`);
}
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
setRawResponseError(msg);
toast.error(`API 호출 중 오류: ${msg}`);
} finally {
setRawResponseLoading(false);
}
};
// 인증 서비스명 목록 로드
const loadAuthServiceNames = async () => {
try {
@@ -699,6 +804,40 @@ export default function BatchManagementNewPage() {
{/* REST API 설정 (REST API → DB) */}
{batchType === "restapi-to-db" && (
<div className="space-y-4">
{/* 등록된 연결 선택 — 외부 커넥션 관리에 등록한 REST API 연결을 골라 자동 호출 */}
<div>
<Label htmlFor="registeredRestApi" className="flex items-center gap-1.5">
<span>🔗 </span>
{rawResponseLoading && (
<RefreshCw className="h-3 w-3 animate-spin text-muted-foreground" />
)}
</Label>
<Select
value={selectedRestApiId}
onValueChange={applyRegisteredRestApi}
>
<SelectTrigger id="registeredRestApi">
<SelectValue placeholder="직접 입력 (등록된 연결 사용 안 함)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="manual"> ( )</SelectItem>
{registeredRestApis.map((c) => (
<SelectItem key={c.id} value={String(c.id)}>
<div className="flex items-center gap-2">
<span>{c.connection_name}</span>
<span className="text-[10px] text-muted-foreground">
[{c.auth_type || "none"}]
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
{rawResponseError && (
<p className="mt-1 text-[11px] text-destructive">{rawResponseError}</p>
)}
</div>
{/* API 서버 URL */}
<div>
<Label htmlFor="fromApiUrl">API URL *</Label>