치지직 앱으로 연동하고, 연동 확인된 ID만 접속할 수 있도록 수정, 개발용으로는 채널ID를 수동으로 접속할 수 있도록 변경.

최초 추천 고정 배치 추가.
This commit is contained in:
kimyu
2026-09-18 04:09:31 +09:00
parent 6f72824598
commit 73520f9f06
26 changed files with 1272 additions and 162 deletions

89
lib/chzzk/oauth.ts Normal file
View File

@@ -0,0 +1,89 @@
import { getChzzkOAuthConfig } from '@/lib/auth/mode';
const OPENAPI = 'https://openapi.chzzk.naver.com';
export function buildChzzkLoginUrl(state: string) {
const { clientId, redirectUri } = getChzzkOAuthConfig();
const url = new URL('https://chzzk.naver.com/account-interlock');
url.searchParams.set('clientId', clientId);
url.searchParams.set('redirectUri', redirectUri);
url.searchParams.set('state', state);
return url.toString();
}
export async function exchangeAuthorizationCode(code: string, state: string) {
const { clientId, clientSecret } = getChzzkOAuthConfig();
const res = await fetch(`${OPENAPI}/auth/v1/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grantType: 'authorization_code',
clientId,
clientSecret,
code,
state,
}),
cache: 'no-store',
});
const json = (await res.json().catch(() => ({}))) as {
content?: {
accessToken?: string;
refreshToken?: string;
tokenType?: string;
expiresIn?: string | number;
};
message?: string;
code?: number;
};
if (!res.ok || !json.content?.accessToken) {
throw new Error(json.message || `토큰 발급 실패 (${res.status})`);
}
return {
accessToken: json.content.accessToken,
refreshToken: json.content.refreshToken ?? null,
};
}
export async function fetchAuthedUser(accessToken: string) {
const res = await fetch(`${OPENAPI}/open/v1/users/me`, {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
cache: 'no-store',
});
const json = (await res.json().catch(() => ({}))) as {
content?: { channelId?: string; channelName?: string };
message?: string;
};
if (!res.ok || !json.content?.channelId) {
throw new Error(json.message || `유저 정보 조회 실패 (${res.status})`);
}
return {
channelId: json.content.channelId.toLowerCase(),
channelName: json.content.channelName ?? null,
};
}
/** 확인 후 치지직 토큰 폐기 (저장하지 않음) */
export async function revokeToken(
token: string,
tokenTypeHint: 'access_token' | 'refresh_token' = 'access_token',
) {
const { clientId, clientSecret } = getChzzkOAuthConfig();
try {
await fetch(`${OPENAPI}/auth/v1/token/revoke`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clientId,
clientSecret,
token,
tokenTypeHint,
}),
cache: 'no-store',
});
} catch {
// 폐기는 best-effort
}
}