Compare commits

..

2 Commits

4 changed files with 114 additions and 0 deletions

View File

@ -30,6 +30,22 @@ async def account(response: Response, user: UserInfo = Depends(IsValidAccessToke
return await service.account_state(UUID(user.user_id))
class TestPost(BaseModel):
text: str = Field(min_length=1, max_length=500)
@router.post("/test-post")
async def test_post(
req: TestPost, response: Response, user: UserInfo = Depends(IsValidAccessToken)
):
"""연동 확인용 즉시 게시 — 승인 없이 바로 연결된 계정으로 올라간다."""
private_response(response)
try:
return await service.test_post(UUID(user.user_id), req.text)
except SocialError as ex:
raise HTTPException(409, str(ex)) from ex
@router.get("/place/{place_id}")
async def list_posts(
place_id: UUID, response: Response, user: UserInfo = Depends(IsValidAccessToken)

View File

@ -149,6 +149,26 @@ def posting_enabled():
return accounts.configured() and config.get("SOCIAL_POSTING_ENABLED") == "1"
async def test_post(user_id, text, provider=2):
"""사장님이 자기 계정 연결이 실제로 되는지 확인하려고 한 줄 올려 보는 것.
★ 왜 posting_enabled()·승인 절차를 거치지 않는가 — 그 게이트는 승인 기반
"소식 발행" 상품을 계약·해지 안내 정책이 끝나기 전에 열지 않으려는 것이다.
여기서 사장님이 자기 계정에 테스트 한 줄을 올리는 것은 그 상품과 무관하다.
"""
async def run(s):
await accounts.lock_user(s, user_id, provider)
row = await accounts.account(s, user_id, provider)
if not row:
raise SocialError("ACCOUNT_CONNECTION_REQUIRED")
async with httpx.AsyncClient(timeout=20) as client:
token = await accounts.get_usable_token(s, row, client)
return await adapter(provider).publish(text, token, client=client)
return await db.transaction(run)
async def create_draft(user_id, place_id, provider=2):
async def run(s):
place, site, url = await target(s, user_id, place_id)

View File

@ -23,6 +23,11 @@ export function SocialConnectionCard() {
const [state, setState] = useState<SocialState | null>(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
const [testText, setTestText] = useState('');
const [testBusy, setTestBusy] = useState(false);
const [testError, setTestError] = useState('');
const [testPosted, setTestPosted] = useState(false);
const [testPermalink, setTestPermalink] = useState<string | null>(null);
const load = useCallback(async () => {
try {
@ -64,6 +69,25 @@ export function SocialConnectionCard() {
window.location.assign(url);
});
async function testPost() {
setTestBusy(true);
setTestError('');
setTestPosted(false);
setTestPermalink(null);
try {
const result = await socialApi<{id: string; permalink: string | null}>('/test-post', {
text: testText,
});
setTestPosted(true);
setTestPermalink(result.permalink);
setTestText('');
} catch (e) {
setTestError(e instanceof Error ? e.message : '요청을 처리하지 못했습니다.');
} finally {
setTestBusy(false);
}
}
return (
<section className="mb-4 rounded-xl border border-border p-4">
<div className="flex flex-wrap items-center justify-between gap-3">
@ -116,6 +140,58 @@ export function SocialConnectionCard() {
</div>
</div>
{account && !needsReauth && (
<div className="mt-3 border-t border-border pt-3">
<label htmlFor="social-test-text" className="text-xs font-bold">
연동 확인용 테스트 게시
</label>
<div className="mt-1 flex gap-2">
<input
id="social-test-text"
type="text"
value={testText}
onChange={(e) => setTestText(e.target.value)}
placeholder="이 계정으로 바로 올라갑니다"
maxLength={500}
className="min-w-0 flex-1 rounded-md border border-border px-2 py-1 text-xs"
/>
<Button
size="sm"
variant="outline"
disabled={testBusy || !testText.trim()}
onClick={testPost}
>
{testBusy ? <Loader2 className="size-4 animate-spin" /> : null}
<span>테스트 게시</span>
</Button>
</div>
{testPosted && (
<p className="mt-1 text-xs">
{testPermalink ? (
<>
게시됐습니다:{' '}
<a
href={testPermalink}
target="_blank"
rel="noopener noreferrer"
className="font-bold underline underline-offset-4"
>
확인하기 →
</a>
</>
) : (
'게시됐습니다. 잠시 후 계정에서 직접 확인해 주세요.'
)}
</p>
)}
{testError && (
<p role="alert" className="mt-1 text-xs text-destructive">
{testError}
</p>
)}
</div>
)}
{/* 게재가 아직 안 열린 상태를 숨기지 않는다 — 연결만 해 두고 기다리는 것도 사장님의 선택이다. */}
{ready && !state.posting_enabled && (
<p className="mt-3 text-xs text-muted-foreground">

View File

@ -19,6 +19,8 @@ const messages: Record<string, string> = {
PUBLISH_URL_CHANGED: '홈페이지 주소가 변경되었습니다. 기존 승인으로 게재할 수 없습니다.',
APPROVAL_NOT_FOUND: '유효하지 않거나 교체된 승인 링크입니다.',
PLACE_NOT_FOUND: '가게를 찾을 수 없습니다.',
ACCOUNT_NEEDS_REAUTH: '연결이 만료됐습니다. 다시 연결해 주세요.',
TEXT_TOO_LONG: '글이 너무 깁니다.',
};
export async function socialApi<T>(path: string, data?: unknown, publicAccess = false): Promise<T> {
const token = publicAccess ? null : getAccessToken();