import {useCallback, useEffect, useState} from 'react'; import {Loader2, Link2, Unlink} from 'lucide-react'; import {Button} from '@/components/ui/button'; import {socialApi, type SocialState} from './api'; /** * Threads 계정 연동 — **'내 사이트' 화면에 한 자리**. * * ★ 왜 발행 모달이 아니라 여기인가 * 계정은 `user × provider` 단위다(표도 그렇게 생겼다 — `owner_social_accounts`). * 연결 버튼이 사업장 안에 있으면 사장님은 **업장 수만큼 연결해야 하는 줄 안다.** * 연결은 한 번, 게재는 사이트마다다 — 화면이 그 모양을 그대로 말해야 한다. * 그래서 여기서 미리 연결해 두고, 발행한 뒤 사이트에서 [소개글 쓰기] 를 누른다. * * ★ 준비 전에도 **자리는 보여준다.** 단, 버튼은 죽여 둔다. * 처음에는 `connection_enabled=false` 면 통째로 숨겼는데, 그러면 **기능이 없는 것처럼 보인다** — * 이 기능을 만든 사람조차 "연동 버튼이 안 보인다" 고 했다(2026-09-14). 사장님은 더더욱 못 찾는다. * 숨기는 것과 "아직 준비 중" 은 다른 말이고, 화면은 그 둘을 구별해 말해야 한다. * 앱 자격증명이 없으면 눌러도 409 이므로(`SOCIAL_CONNECTION_DISABLED`) **버튼은 비활성**이다 — * 보이되 눌리지 않고, 왜 아직인지 한 줄로 말한다(docs/SOCIAL.md '연동 준비'). */ export function SocialConnectionCard() { const [state, setState] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(''); const load = useCallback(async () => { try { setState(await socialApi('/account')); } catch { // 연결 상태를 못 읽는 것은 사이트 목록을 못 보여줄 이유가 아니다 — 조용히 접는다. setState(null); } }, []); useEffect(() => { void load(); }, [load]); // 상태를 아직 못 읽었을 때만 접는다(로그인 직후 한순간). '준비 안 됨' 과는 다르다. if (!state) return null; const ready = state.connection_enabled; const account = state.account; const needsReauth = account?.status === 'needs_reauth'; async function run(fn: () => Promise) { setBusy(true); setError(''); try { await fn(); await load(); } catch (e) { setError(e instanceof Error ? e.message : '요청을 처리하지 못했습니다.'); } finally { setBusy(false); } } const connect = () => run(async () => { const {url} = await socialApi<{url: string}>('/oauth/connect', {}); // 인가 화면은 Meta 쪽이다. 돌아오면 `/sites?social=…` 로 되돌아온다. window.location.assign(url); }); return (

SNS 연동 · Threads

{!ready ? '연동 준비 중입니다. 열리면 여기서 계정을 연결합니다.' : account ? needsReauth ? '연결이 만료됐습니다. 다시 연결해야 게재할 수 있습니다.' : '연결되어 있습니다. 사이트를 발행한 뒤 소개글을 써서 올릴 수 있습니다.' : '미리 연결해 두면, 사이트를 발행한 뒤 소개글을 써서 이 계정으로 올립니다.'}

{account && (

@{account.handle}

)}
{account && ( )}
{/* 게재가 아직 안 열린 상태를 숨기지 않는다 — 연결만 해 두고 기다리는 것도 사장님의 선택이다. */} {ready && !state.posting_enabled && (

자동 게재는 준비 중입니다. 지금은 소개글을 만들어 복사하거나 내용을 확인할 수 있습니다.

)} {error && (

{error}

)}
); }