Compare commits
2 Commits
13cc8cc830
...
176a77a231
| Author | SHA1 | Date | |
|---|---|---|---|
| 176a77a231 | |||
| 921c85df25 |
@ -30,6 +30,22 @@ async def account(response: Response, user: UserInfo = Depends(IsValidAccessToke
|
|||||||
return await service.account_state(UUID(user.user_id))
|
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}")
|
@router.get("/place/{place_id}")
|
||||||
async def list_posts(
|
async def list_posts(
|
||||||
place_id: UUID, response: Response, user: UserInfo = Depends(IsValidAccessToken)
|
place_id: UUID, response: Response, user: UserInfo = Depends(IsValidAccessToken)
|
||||||
|
|||||||
@ -149,6 +149,26 @@ def posting_enabled():
|
|||||||
return accounts.configured() and config.get("SOCIAL_POSTING_ENABLED") == "1"
|
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 create_draft(user_id, place_id, provider=2):
|
||||||
async def run(s):
|
async def run(s):
|
||||||
place, site, url = await target(s, user_id, place_id)
|
place, site, url = await target(s, user_id, place_id)
|
||||||
|
|||||||
@ -23,6 +23,11 @@ export function SocialConnectionCard() {
|
|||||||
const [state, setState] = useState<SocialState | null>(null);
|
const [state, setState] = useState<SocialState | null>(null);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState('');
|
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 () => {
|
const load = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@ -64,6 +69,25 @@ export function SocialConnectionCard() {
|
|||||||
window.location.assign(url);
|
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 (
|
return (
|
||||||
<section className="mb-4 rounded-xl border border-border p-4">
|
<section className="mb-4 rounded-xl border border-border p-4">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
@ -116,6 +140,58 @@ export function SocialConnectionCard() {
|
|||||||
</div>
|
</div>
|
||||||
</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 && (
|
{ready && !state.posting_enabled && (
|
||||||
<p className="mt-3 text-xs text-muted-foreground">
|
<p className="mt-3 text-xs text-muted-foreground">
|
||||||
|
|||||||
@ -19,6 +19,8 @@ const messages: Record<string, string> = {
|
|||||||
PUBLISH_URL_CHANGED: '홈페이지 주소가 변경되었습니다. 기존 승인으로 게재할 수 없습니다.',
|
PUBLISH_URL_CHANGED: '홈페이지 주소가 변경되었습니다. 기존 승인으로 게재할 수 없습니다.',
|
||||||
APPROVAL_NOT_FOUND: '유효하지 않거나 교체된 승인 링크입니다.',
|
APPROVAL_NOT_FOUND: '유효하지 않거나 교체된 승인 링크입니다.',
|
||||||
PLACE_NOT_FOUND: '가게를 찾을 수 없습니다.',
|
PLACE_NOT_FOUND: '가게를 찾을 수 없습니다.',
|
||||||
|
ACCOUNT_NEEDS_REAUTH: '연결이 만료됐습니다. 다시 연결해 주세요.',
|
||||||
|
TEXT_TOO_LONG: '글이 너무 깁니다.',
|
||||||
};
|
};
|
||||||
export async function socialApi<T>(path: string, data?: unknown, publicAccess = false): Promise<T> {
|
export async function socialApi<T>(path: string, data?: unknown, publicAccess = false): Promise<T> {
|
||||||
const token = publicAccess ? null : getAccessToken();
|
const token = publicAccess ? null : getAccessToken();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user