o2o-site-AEO/solution/frontend/src/features/onboarding/useChannelLinks.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

91 lines
3.6 KiB
TypeScript

/**
* 채널 링크 — 목록 · 직접 등록 · 확정.
*
* ★ 수집 흐름(useCollectFlow)에서 떼어낸 이유: 이건 **단계 전이가 없는** 일이다.
* 등록하고 확정하는 것뿐이고, 수집이 돌고 있든 아니든 똑같이 동작한다.
* 한 훅에 섞여 있으면 "지금 어느 단계라 이 버튼이 되는가"를 매번 따져야 한다.
*
* ★ 확정된 링크만 크롤링 대상이 된다 — 이 훅이 수집 게이트의 앞단이다.
*/
import {useCallback, useState} from 'react';
import {LinkChannel, SourceType} from '@o2o/shared';
import type {LinkChannel as LinkChannelCode, LinkData} from '@/api';
import {confirmLink, createLink, getListLinksQueryKey, useListLinks} from '@/api';
import {notify, notifyApiError} from '@/lib/notify';
import {queryClient} from '@/lib/query-client';
export function useChannelLinks(placeId: string | null) {
const [confirmingId, setConfirmingId] = useState<string | null>(null);
const [isAdding, setIsAdding] = useState(false);
/** 서버가 네이버 플레이스를 찾지 못했는지 여부. 수집 결과가 알려주고, 직접 등록하면 내린다. */
const [naverPlaceMissing, setNaverPlaceMissing] = useState(false);
const enabled = Boolean(placeId);
const linksQuery = useListLinks(placeId ?? '', undefined, {query: {enabled}});
const links: LinkData[] = linksQuery.data?.links ?? [];
const confirmedCount = linksQuery.data?.confirmed ?? 0;
/**
* 사장님이 직접 붙여넣은 채널 URL 을 등록하고 **동시에 확정**한다.
*
* ★ 발견된 URL 과 달리 확인 절차를 한 번 더 두지 않는다. 사장님이 자기 가게 주소를
* 직접 가져온 것이므로, 그 붙여넣기 자체가 확인이다. 여기서 또 [내 채널 맞아요]를
* 요구하면 같은 판단을 두 번 시키는 셈이다.
*/
const addLink = useCallback(
async (url: string, channel: LinkChannelCode, title: string) => {
if (!placeId) return;
setIsAdding(true);
try {
const created = await createLink(placeId, {
channel, url, title, discovered_by: SourceType.OWNER,
});
const linkId = created.link?.link_id;
if (linkId) await confirmLink(placeId, linkId);
// 사장님이 직접 주소를 넣었으면 "자동으로 못 찾았습니다" 안내는 할 일을 다 했다 — 내린다.
if (channel === LinkChannel.NAVER_PLACE) setNaverPlaceMissing(false);
await queryClient
.invalidateQueries({queryKey: getListLinksQueryKey(placeId)})
.catch(() => undefined);
notify.success('채널을 등록했습니다', `${title} · 바로 수집 대상이 됩니다.`);
} catch (error) {
notifyApiError(error, '채널을 등록하지 못했습니다.');
} finally {
setIsAdding(false);
}
},
[placeId],
);
/** 사람이 "내 채널 맞다"고 고른 URL 을 확정한다. ★ 확정된 것만 크롤링 대상이 된다. */
const confirm = useCallback(
async (linkId: string) => {
if (!placeId) return;
setConfirmingId(linkId);
try {
await confirmLink(placeId, linkId);
await queryClient
.invalidateQueries({queryKey: getListLinksQueryKey(placeId)})
.catch(() => undefined);
} catch (error) {
notifyApiError(error, '채널을 확정하지 못했습니다.');
} finally {
setConfirmingId(null);
}
},
[placeId],
);
return {
links,
confirmedCount,
confirmingId,
isAdding,
naverPlaceMissing,
setNaverPlaceMissing,
isLinksLoading: enabled && linksQuery.isPending,
addLink,
confirm,
};
}