o2o-site-AEO/solution/frontend/src/features/publish/siteTemplate.ts
Mina Choi c85c577349 이름: solution/front → solution/frontend
`backend` 옆에 `front` 가 있을 이유가 없었다. negosium 의 negodata/front 를 그대로
베꼈고 그게 왜 front 인지는 따져보지 않았다 — 근거 없이 들여온 이름이라 바로잡는다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uYhHQdssRubirPirrdJJC
2026-08-31 15:27:16 +09:00

55 lines
2.9 KiB
TypeScript

import {ApiError} from '@/api';
import {notifyApiError} from '@/lib/notify';
import {callSiteApi, type ResultEnvelope} from './siteApi';
/**
* 위저드에서 고른 템플릿을 서버에 저장한다(POST /v1/place/{id}/site/template).
*
* ★ 왜 저장하나: 고른 templateId 가 브라우저 상태로만 남으면 발행 잡(backend services/site_payload)이
* 읽을 곳이 없어 업종 기본 템플릿으로 굽는다 — 사장님이 고른 디자인과 실제 발행본이 갈린다.
* ★ 주소(slug)와 달리 발행 뒤에도 저장된다. 서버가 잠그지 않는다 —
* 디자인이 바뀌어도 URL 은 그대로라 색인이 깨지지 않기 때문이다(대신 재빌드 대상으로 표시된다).
* ★ orval 을 쓰지 않는 이유는 siteApi.ts 참조.
*/
export async function saveSiteTemplate(placeId: string, templateId: string): Promise<void> {
// placeId 가 없으면(데모 경로) 저장할 사이트가 없다 — 네트워크를 한 번도 타지 않고 조용히 통과한다.
if (!placeId) return;
const res = await callSiteApi<ResultEnvelope>(`/v1/place/${placeId}/site/template`, {
method: 'POST',
body: JSON.stringify({template_id: templateId}),
});
// ★ 거절도 200 으로 온다. 여기서 throw 하지 않으면 저장 안 된 템플릿이 저장된 것처럼 넘어가고,
// 호출측은 알릴 근거를 잃는다. notifyApiError 가 읽는 자리(error.data)에 응답을 그대로 싣는다.
if (res.result?.success === false) throw new ApiError(200, 'SITE_TEMPLATE_REJECTED', res);
}
/**
* 보낸 순서대로 흘리는 저장 대기줄.
*
* ★ 병렬로 쏘면 먼저 누른 템플릿의 요청이 늦게 도착해 마지막 선택을 덮을 수 있다 —
* 화면은 새 템플릿, 서버는 옛 템플릿이 되고 그 차이는 발행하고 나서야 드러난다.
*/
let saveChain: Promise<void> = Promise.resolve();
/** 마지막 요청만 실패를 알린다 — 뒤엎힌 옛 선택의 실패까지 토스트로 쌓지 않는다. */
let saveSeq = 0;
/**
* 고른 템플릿을 저장하되 **화면을 기다리게 하지 않는다**.
*
* ★ 저장 실패가 위저드 진행을 막으면 안 된다. 템플릿은 주소와 달리 발행 뒤에도 바꿀 수 있는 값이라
* 여기서 사장님을 세울 이유가 없다 — 알리기만 하고 다음 단계로 보낸다.
*/
export function queueSiteTemplateSave(placeId: string | null, templateId: string) {
if (!placeId || !templateId) return;
saveSeq += 1;
const seq = saveSeq;
saveChain = saveChain.then(async () => {
try {
await saveSiteTemplate(placeId, templateId);
} catch (error) {
if (seq !== saveSeq) return; // 이미 다른 선택이 올라탔다 — 그쪽 결과만 알린다.
notifyApiError(error, '템플릿 선택을 저장하지 못했습니다. 발행 전에 다시 골라 주세요.');
}
});
}