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

// 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>
);
}