연결 버튼이 발행 모달 안에 있었다. 그런데 계정은 `user × provider` 하나다(표도 그렇게 생겼다)
— 버튼이 사업장 화면에 있으면 사장님은 **업장마다 연결해야 하는 줄 안다.** 사장님 지적:
"여기 내 사이트에 sns 연동 하나 두고 연동해두고, 사이트 발행 후 게재하는 형식으로".
화면이 데이터 모양을 그대로 말해야 한다 — 연결은 한 번, 게재는 사이트마다다.
- `GET /v1/social/account` 신설: 연결 상태만 준다. 사업장을 고르지 않아도 답할 수 있어야 하는
값인데, 지금까지는 `/place/{id}` 안에만 있어서 사이트를 하나 고르기 전에는 물어볼 수 없었다
- features/social/SocialConnectionCard: '내 사이트' 목록 위의 연동 카드
(@핸들 · [연결] · [다시 연결] · [연결 해제]). 앱 자격증명이 없으면 **아무것도 안 그린다** —
누를 수 없는 버튼을 세우면 사장님에게는 고장난 화면이고 우리에게는 문의가 된다
- SocialPanel(발행 화면)에서 연결·해제 버튼 제거. 대신 계정이 없으면 "막다른 문구"가 아니라
**갈 곳**을 알린다 — [내 사이트] 로 보낸다. 연결 전에도 소개글 복사는 된다
- docs/SOCIAL.md: 사장님 흐름을 '연결(한 번) / 게재(사이트마다)' 로 다시 씀
검증: SNS 테스트 14건 통과 · frontend tsc·eslint 통과. 로컬에서 임시 자격증명으로 카드가
켜지는 것과 인가 URL 조립을 확인하고 값을 되돌렸다(지금은 connection_enabled=false 로 안 뜬다)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
85 lines
2.6 KiB
Python
85 lines
2.6 KiB
Python
from uuid import UUID
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Response
|
|
from pydantic import BaseModel, Field
|
|
from common.models.gmodel import UserInfo
|
|
from router.v1.validator.dependencies import IsValidAccessToken
|
|
from services import social_service as service
|
|
from services.external.social import SocialError
|
|
|
|
router = APIRouter(prefix="/v1/social", tags=["Social"])
|
|
|
|
|
|
class Decision(BaseModel):
|
|
approve: bool
|
|
|
|
|
|
class LinkDecision(Decision):
|
|
t: str = Field(min_length=40, max_length=100)
|
|
|
|
|
|
def private_response(response: Response):
|
|
response.headers["Cache-Control"] = "no-store"
|
|
response.headers["Referrer-Policy"] = "no-referrer"
|
|
response.headers["X-Robots-Tag"] = "noindex, nofollow"
|
|
|
|
|
|
@router.get("/account")
|
|
async def account(response: Response, user: UserInfo = Depends(IsValidAccessToken)):
|
|
"""연결 상태만 준다 — 사업장을 고르지 않아도 답할 수 있어야 하는 값이다."""
|
|
private_response(response)
|
|
return await service.account_state(UUID(user.user_id))
|
|
|
|
|
|
@router.get("/place/{place_id}")
|
|
async def list_posts(
|
|
place_id: UUID, response: Response, user: UserInfo = Depends(IsValidAccessToken)
|
|
):
|
|
private_response(response)
|
|
return await service.list_posts(UUID(user.user_id), place_id)
|
|
|
|
|
|
@router.post("/place/{place_id}/draft")
|
|
async def draft(
|
|
place_id: UUID, response: Response, user: UserInfo = Depends(IsValidAccessToken)
|
|
):
|
|
private_response(response)
|
|
return await service.create_draft(UUID(user.user_id), place_id)
|
|
|
|
|
|
@router.post("/posts/{post_id}/request-approval")
|
|
async def request_approval(
|
|
post_id: UUID, response: Response, user: UserInfo = Depends(IsValidAccessToken)
|
|
):
|
|
private_response(response)
|
|
try:
|
|
return await service.request_approval(UUID(user.user_id), post_id)
|
|
except (SocialError, RuntimeError) as ex:
|
|
raise HTTPException(
|
|
502, "APPROVAL_NOTIFICATION_FAILED_SCREEN_AVAILABLE"
|
|
) from ex
|
|
|
|
|
|
@router.post("/posts/{post_id}/decision")
|
|
async def owner_decision(
|
|
post_id: UUID,
|
|
req: Decision,
|
|
response: Response,
|
|
user: UserInfo = Depends(IsValidAccessToken),
|
|
):
|
|
private_response(response)
|
|
return await service.owner_decision(UUID(user.user_id), post_id, req.approve)
|
|
|
|
|
|
@router.get("/approval/{post_id}")
|
|
async def approval(
|
|
post_id: UUID, response: Response, t: str = Query(min_length=40, max_length=100)
|
|
):
|
|
private_response(response)
|
|
return await service.approval(post_id, t)
|
|
|
|
|
|
@router.post("/approval/{post_id}/decision")
|
|
async def decision(post_id: UUID, req: LinkDecision, response: Response):
|
|
private_response(response)
|
|
return await service.approval(post_id, req.t, approve=req.approve)
|