[fix] solution/frontend: 연동 카드를 준비 전에도 보여준다 — 숨기면 기능이 없는 것처럼 보인다

앱 자격증명(THREADS_*)이 없으면 카드를 통째로 숨겼다. 근거는 "누를 수 없는 버튼을 세우지
않는다" 였는데, **이 기능을 만든 사람조차 "연동 버튼이 아예 안 보인다" 고 했다**(2026-09-14).
만든 사람이 못 찾으면 사장님은 더더욱 못 찾는다 — 숨기는 것과 "아직 준비 중" 은 다른 말이고,
화면은 그 둘을 구별해 말해야 한다.

- 자리는 늘 보이고 **버튼만 비활성**이다. "연동 준비 중입니다. 열리면 여기서 계정을 연결합니다."
- 통째로 접는 경우는 하나만 남겼다 — 상태를 아직 못 읽었을 때(로그인 직후 한순간).
  그건 '준비 안 됨' 이 아니라 '모름' 이라 다르게 다뤄야 한다

검증: frontend tsc·eslint 통과. 번들에 새 문구가 실린 것과 `connection_enabled=false` 에서
비활성 상태로 그려지는 것을 확인

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hbyang 2026-09-14 16:29:31 +09:00
parent c8dc68536e
commit a517439c7d

View File

@ -12,10 +12,12 @@ import {socialApi, type SocialState} from './api';
* 연결은 한 번, 게재는 사이트마다다 — 화면이 그 모양을 그대로 말해야 한다. * 연결은 한 번, 게재는 사이트마다다 — 화면이 그 모양을 그대로 말해야 한다.
* 그래서 여기서 미리 연결해 두고, 발행한 뒤 사이트에서 [소개글 쓰기] 를 누른다. * 그래서 여기서 미리 연결해 두고, 발행한 뒤 사이트에서 [소개글 쓰기] 를 누른다.
* *
* ★ 연결이 꺼져 있으면 **아무것도 그리지 않는다.** * ★ 준비 전에도 **자리는 보여준다.** 단, 버튼은 죽여 둔다.
* `THREADS_APP_ID` 같은 앱 자격증명이 없으면 눌러도 409 다(`SOCIAL_CONNECTION_DISABLED`). * 처음에는 `connection_enabled=false` 면 통째로 숨겼는데, 그러면 **기능이 없는 것처럼 보인다** —
* 누를 수 없는 버튼을 세워 두면 사장님에게는 고장난 화면이고, 우리에게는 문의가 된다. * 이 기능을 만든 사람조차 "연동 버튼이 안 보인다" 고 했다(2026-09-14). 사장님은 더더욱 못 찾는다.
* 준비되면 서버가 `connection_enabled` 로 알려 준다(docs/SOCIAL.md '연동 준비'). * 숨기는 것과 "아직 준비 중" 은 다른 말이고, 화면은 그 둘을 구별해 말해야 한다.
* 앱 자격증명이 없으면 눌러도 409 이므로(`SOCIAL_CONNECTION_DISABLED`) **버튼은 비활성**이다 —
* 보이되 눌리지 않고, 왜 아직인지 한 줄로 말한다(docs/SOCIAL.md '연동 준비').
*/ */
export function SocialConnectionCard() { export function SocialConnectionCard() {
const [state, setState] = useState<SocialState | null>(null); const [state, setState] = useState<SocialState | null>(null);
@ -35,8 +37,10 @@ export function SocialConnectionCard() {
void load(); void load();
}, [load]); }, [load]);
if (!state?.connection_enabled) return null; // 상태를 아직 못 읽었을 때만 접는다(로그인 직후 한순간). '준비 안 됨' 과는 다르다.
if (!state) return null;
const ready = state.connection_enabled;
const account = state.account; const account = state.account;
const needsReauth = account?.status === 'needs_reauth'; const needsReauth = account?.status === 'needs_reauth';
@ -66,11 +70,13 @@ export function SocialConnectionCard() {
<div className="min-w-0"> <div className="min-w-0">
<h2 className="text-sm font-bold">SNS 연동 · Threads</h2> <h2 className="text-sm font-bold">SNS 연동 · Threads</h2>
<p className="mt-1 text-xs text-muted-foreground"> <p className="mt-1 text-xs text-muted-foreground">
{account {!ready
? needsReauth ? '연동 준비 중입니다. 열리면 여기서 계정을 연결합니다.'
? '연결이 만료됐습니다. 다시 연결해야 게재할 수 있습니다.' : account
: '연결되어 있습니다. 사이트를 발행한 뒤 소개글을 써서 올릴 수 있습니다.' ? needsReauth
: '미리 연결해 두면, 사이트를 발행한 뒤 소개글을 써서 이 계정으로 올립니다.'} ? '연결이 만료됐습니다. 다시 연결해야 게재할 수 있습니다.'
: '연결되어 있습니다. 사이트를 발행한 뒤 소개글을 써서 올릴 수 있습니다.'
: '미리 연결해 두면, 사이트를 발행한 뒤 소개글을 써서 이 계정으로 올립니다.'}
</p> </p>
{account && ( {account && (
<p className="mt-1 text-xs"> <p className="mt-1 text-xs">
@ -87,7 +93,12 @@ export function SocialConnectionCard() {
</div> </div>
<div className="flex shrink-0 flex-wrap gap-2"> <div className="flex shrink-0 flex-wrap gap-2">
<Button size="sm" variant={account && !needsReauth ? 'outline' : 'primary'} disabled={busy} onClick={connect}> <Button
size="sm"
variant={account && !needsReauth ? 'outline' : 'primary'}
disabled={busy || !ready}
onClick={connect}
>
{busy ? <Loader2 className="size-4 animate-spin" /> : <Link2 className="size-4" />} {busy ? <Loader2 className="size-4 animate-spin" /> : <Link2 className="size-4" />}
<span>{account ? '다시 연결' : 'Threads 계정 연결'}</span> <span>{account ? '다시 연결' : 'Threads 계정 연결'}</span>
</Button> </Button>
@ -106,7 +117,7 @@ export function SocialConnectionCard() {
</div> </div>
{/* 게재가 아직 안 열린 상태를 숨기지 않는다 — 연결만 해 두고 기다리는 것도 사장님의 선택이다. */} {/* 게재가 아직 안 열린 상태를 숨기지 않는다 — 연결만 해 두고 기다리는 것도 사장님의 선택이다. */}
{!state.posting_enabled && ( {ready && !state.posting_enabled && (
<p className="mt-3 text-xs text-muted-foreground"> <p className="mt-3 text-xs text-muted-foreground">
자동 게재는 준비 중입니다. 지금은 소개글을 만들어 복사하거나 내용을 확인할 수 있습니다. 자동 게재는 준비 중입니다. 지금은 소개글을 만들어 복사하거나 내용을 확인할 수 있습니다.
</p> </p>