You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
137 lines
4.1 KiB
TypeScript
137 lines
4.1 KiB
TypeScript
// frontend/components/settings/ConnectorList.tsx — 설정의 커넥터 그리드(메일·연결 탭 공유)
|
|
"use client";
|
|
import { Button } from "@/components/Button";
|
|
import { Icon } from "@/components/Icon";
|
|
import type { IconName } from "@/components/icons/paths";
|
|
import { AddAccountMenu } from "@/components/connectors/AddAccountMenu";
|
|
import { ConnectorCard } from "@/components/connectors/ConnectorCard";
|
|
import { connectorsApi } from "@/lib/connectors/api";
|
|
import { useConnectors } from "@/lib/hooks/useConnectors";
|
|
import type { ConnectorStatus } from "@/lib/types";
|
|
import type { ToastFn } from "./SettingsClient";
|
|
|
|
export function ConnectorList({
|
|
title,
|
|
subtitle,
|
|
icon,
|
|
filter,
|
|
toast,
|
|
showSyncAll = false,
|
|
emptyText = "표시할 연결이 없어요.",
|
|
addDomain,
|
|
redirectAfter = "/settings",
|
|
}: {
|
|
title: string;
|
|
subtitle: string;
|
|
icon: IconName;
|
|
filter: (c: ConnectorStatus) => boolean;
|
|
toast: ToastFn;
|
|
showSyncAll?: boolean;
|
|
emptyText?: string;
|
|
addDomain?: string; // 설정 시 "계정 추가"(OAuth) 메뉴 노출
|
|
redirectAfter?: string; // OAuth 콜백 후 복귀 경로
|
|
}) {
|
|
const { data, isLoading, error, refresh } = useConnectors();
|
|
const all = data ?? [];
|
|
const items = all.filter(filter);
|
|
const connected = items.filter((c) => c.on).length;
|
|
|
|
const find = (id: string) => all.find((x) => x.id === id);
|
|
|
|
const onSync = async (id: string) => {
|
|
const c = find(id);
|
|
try {
|
|
const res = await connectorsApi.sync(id);
|
|
await refresh();
|
|
toast(`${c?.name ?? "소스"} · 새 항목 ${res.upserted}건`, c?.tone ?? "blue");
|
|
} catch {
|
|
toast("동기화에 실패했어요", "coral");
|
|
}
|
|
};
|
|
|
|
const onDisconnect = async (id: string) => {
|
|
const c = find(id);
|
|
try {
|
|
await connectorsApi.disconnect(id);
|
|
await refresh();
|
|
toast(`${c?.name ?? "소스"} 연결을 해제했어요`, "faint");
|
|
} catch {
|
|
toast("연결 해제에 실패했어요", "coral");
|
|
}
|
|
};
|
|
|
|
const onImport = async (id: string, file: File) => {
|
|
try {
|
|
const res = await connectorsApi.importFile(id, file);
|
|
await refresh();
|
|
toast(res.detail || `${res.imported}건 가져왔어요`, "green");
|
|
} catch {
|
|
toast("이 소스는 파일 가져오기를 지원하지 않아요", "amber");
|
|
}
|
|
};
|
|
|
|
const onSyncAll = async () => {
|
|
try {
|
|
const res = await connectorsApi.syncAll();
|
|
await refresh();
|
|
const total = res.reduce((n, r) => n + (r.upserted ?? 0), 0);
|
|
toast(`전체 동기화 완료 · 새 항목 ${total}건`, "green");
|
|
} catch {
|
|
toast("전체 동기화에 실패했어요", "coral");
|
|
}
|
|
};
|
|
|
|
return (
|
|
<section className="set-sec" aria-label={title}>
|
|
<header className="set-sechead">
|
|
<h2>{title}</h2>
|
|
<p>{subtitle}</p>
|
|
<div className="sh-side">
|
|
<span className="cn-count">
|
|
{connected}/{items.length} 연결됨
|
|
</span>
|
|
{showSyncAll && items.length > 0 && (
|
|
<Button variant="glass" icon="refresh" onClick={onSyncAll}>
|
|
전체 동기화
|
|
</Button>
|
|
)}
|
|
{addDomain && (
|
|
<AddAccountMenu
|
|
domain={addDomain}
|
|
redirectAfter={redirectAfter}
|
|
onError={(m) => toast(m, "coral")}
|
|
/>
|
|
)}
|
|
</div>
|
|
</header>
|
|
|
|
{error && (
|
|
<div className="set-card" role="alert">
|
|
연결 상태를 불러오지 못했어요
|
|
</div>
|
|
)}
|
|
{isLoading && !data && <div className="set-card">불러오는 중…</div>}
|
|
{!isLoading && items.length === 0 && (
|
|
<div className="set-card set-empty">
|
|
<Icon name={icon} />
|
|
{emptyText}
|
|
</div>
|
|
)}
|
|
|
|
{items.length > 0 && (
|
|
<div className="cn-grid" role="region" tabIndex={0} aria-label={`${title} 목록`}>
|
|
{items.map((c) => (
|
|
<ConnectorCard
|
|
key={c.id}
|
|
connector={c}
|
|
onSync={onSync}
|
|
onDisconnect={onDisconnect}
|
|
onImport={(file) => onImport(c.id, file)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|