// frontend/components/settings/SettingsClient.tsx — 설정 오케스트레이터 (좌측 탭 + 메인 + 토스트) "use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { Icon } from "@/components/Icon"; import type { IconName } from "@/components/icons/paths"; import { AccountTab } from "./AccountTab"; import { MailTab } from "./MailTab"; import { ConnectionsTab } from "./ConnectionsTab"; import { LlmTab } from "./LlmTab"; import { AppearanceTab } from "./AppearanceTab"; import { DataTab } from "./DataTab"; export type TabId = "account" | "mail" | "connect" | "llm" | "appearance" | "data"; type Tab = { id: TabId; label: string; desc: string; icon: IconName }; const TABS: Tab[] = [ { id: "account", label: "계정", desc: "프로필 · 로그인", icon: "user" }, { id: "mail", label: "메일", desc: "메일 계정 연결", icon: "mail" }, { id: "connect", label: "연결", desc: "일정 · 금융 · 건강 · 지식", icon: "link" }, { id: "llm", label: "AI · LLM", desc: "모델 연결 설정", icon: "brain" }, { id: "appearance", label: "외관", desc: "테마", icon: "sun" }, { id: "data", label: "데이터 · 보안", desc: "내보내기 · 토큰 · 시스템", icon: "shield" }, ]; const TAB_IDS = TABS.map((t) => t.id); function isTab(v: string): v is TabId { return (TAB_IDS as string[]).includes(v); } export type ToastFn = (msg: string, tone?: string) => void; export function SettingsClient() { const [tab, setTab] = useState("account"); const [hydrated, setHydrated] = useState(false); const [toasts, setToasts] = useState<{ id: number; msg: string; tone: string }[]>([]); const shellRef = useRef(null); const tid = useRef(0); // localStorage + ?tab= 쿼리 복원 useEffect(() => { if (typeof window === "undefined") return; try { const q = new URLSearchParams(window.location.search).get("tab"); if (q && isTab(q)) { setTab(q); } else { const v = localStorage.getItem("ariS.tab"); if (v && isTab(v)) setTab(v); } } catch { /* ignore */ } setHydrated(true); }, []); useEffect(() => { if (!hydrated || typeof window === "undefined") return; try { localStorage.setItem("ariS.tab", tab); } catch { /* ignore */ } }, [tab, hydrated]); useEffect(() => { const el = shellRef.current; const id = requestAnimationFrame(() => el && el.classList.add("entered")); return () => cancelAnimationFrame(id); }, [tab]); const toast = useCallback((msg, tone = "blue") => { const id = ++tid.current; setToasts((t) => [...t, { id, msg, tone }]); setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 2600); }, []); // OAuth 콜백 복귀(?connect=ok|error) → 토스트 후 쿼리 정리(tab 은 보존) useEffect(() => { if (typeof window === "undefined") return; const params = new URLSearchParams(window.location.search); const connect = params.get("connect"); if (!connect) return; const labels: Record = { gmail: "Gmail", outlook: "Outlook", google_calendar: "Google 캘린더", outlook_calendar: "Outlook 캘린더", }; const name = labels[params.get("provider") ?? ""] ?? "계정"; if (connect === "ok") toast(`${name}을(를) 연결했어요`, "green"); else toast("연결에 실패했어요. 다시 시도해 주세요", "coral"); for (const k of ["connect", "domain", "provider"]) params.delete(k); const qs = params.toString(); window.history.replaceState(null, "", window.location.pathname + (qs ? `?${qs}` : "")); }, [toast]); return (

설정

계정, 연결, AI 모델, 외관까지 한곳에서 관리해요.

{tab === "account" && } {tab === "mail" && } {tab === "connect" && } {tab === "llm" && } {tab === "appearance" && } {tab === "data" && }
{toasts.map((t) => (
{t.msg}
))}
); }