| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387 |
- 'use client';
- import './style.scss';
- import { useState, useEffect } from 'react';
- import { useSearchParams } from 'next/navigation';
- import Image from 'next/image';
- import { fetchApi } from '@/lib/utils/client';
- import { Checkbox } from '@/components/ui/checkbox';
- import { Label } from '@/components/ui/label';
- import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
- import {
- faLink,
- faAlignLeft,
- faCalendarDays,
- faUsers,
- faVideo,
- faEye,
- faGlobe,
- } from '@fortawesome/free-solid-svg-icons';
- import type { StudioSettingsResponse } from '@/types/response/studio/settings';
- import YouTubeSignBoard from '@/public/resources/YouTube-signboard.svg';
- export default function StudioSettingsPage()
- {
- const searchParams = useSearchParams();
- const [data, setData] = useState<StudioSettingsResponse|null>(null);
- const [loading, setLoading] = useState(true);
- const [connecting, setConnecting] = useState(false);
- const [agreedTerms, setAgreedTerms] = useState(false);
- const [agreedYouTube, setAgreedYouTube] = useState(false);
- const [showDisconnect, setShowDisconnect] = useState(false);
- const [agreedDisconnect, setAgreedDisconnect] = useState(false);
- const [disconnecting, setDisconnecting] = useState(false);
- const connected = searchParams.get('connected') === 'true';
- const errorParam = searchParams.get('error');
- useEffect(() => {
- fetchApi<StudioSettingsResponse>('/api/studio/settings')
- .then(res => {
- if (res.data) setData(res.data);
- })
- .catch(() => {
- setData({
- isYouTubeConnected: false,
- channelName: null,
- handle: null,
- description: null,
- channelUrl: null,
- thumbnailUrl: null,
- bannerUrl: null,
- subscriberCount: 0,
- videoCount: 0,
- viewCount: 0,
- youTubePublishedAt: null
- });
- })
- .finally(() => setLoading(false));
- }, []);
- const handleConnect = async () => {
- if (!agreedTerms || !agreedYouTube) {
- return;
- }
- setConnecting(true);
- try {
- const redirectUri = `${window.location.origin}/api/auth/youtube/callback`;
- const res = await fetchApi<string>(`/api/studio/youtube-connect-url?redirectUri=${encodeURIComponent(redirectUri)}`);
- window.location.href = res.data!;
- } catch (err) {
- alert(err instanceof Error ? err.message : 'OAuth URL을 가져오지 못했습니다.');
- setConnecting(false);
- }
- };
- const handleDisconnectClick = () => {
- if (!window.confirm('YouTube 연동을 해지하시겠습니까?\n해지 시 후원 수신, 라이브 채팅 등의 서비스를 이용할 수 없습니다.')) {
- return;
- }
- setShowDisconnect(true);
- };
- const handleDisconnect = async () => {
- if (!agreedDisconnect) return;
- setDisconnecting(true);
- try {
- await fetchApi('/api/studio/youtube-disconnect', { method: 'POST' });
- alert('YouTube 연동이 해지되었습니다.');
- window.location.replace('/studio/settings');
- } catch (err) {
- alert(err instanceof Error ? err.message : '연동 해지에 실패했습니다.');
- setDisconnecting(false);
- }
- };
- const handleDisconnectCancel = () => {
- setShowDisconnect(false);
- setAgreedDisconnect(false);
- };
- const handleAgreedDisconnectChange = (v: boolean|'indeterminate') => {
- setAgreedDisconnect(v === true);
- };
- const isConnected = data?.isYouTubeConnected;
- const canConnect = agreedTerms && agreedYouTube;
- return (
- <div className="studio-page">
- {/* 페이지 헤더 */}
- <div className="settings-header">
- <h1 className="studio-page__title">
- {!loading && isConnected ? '채널 정보' : '채널 설정'}
- </h1>
- </div>
- {loading && <p className="settings-loading">불러오는 중...</p>}
- {!loading && !isConnected && (
- <>
- {errorParam && (
- <div className="settings-alert settings-alert--error">
- {errorParam === 'cancelled' ? '연결이 취소되었습니다.' : decodeURIComponent(errorParam)}
- </div>
- )}
- <div className="settings-connect-card">
- <div className="settings-connect-card__icon">
- <YouTubeSignBoard />
- </div>
- <div className="settings-connect-card__body">
- <p className="settings-connect-card__title">YouTube 채널 연결이 필요합니다.</p>
- <p className="settings-connect-card__desc">
- YouTube 채널을 연결하면 채널 이름, 설명, 시청자 링크 등 채널 정보를 자동으로 가져와 스트리밍과 후원 기능을 원활하게 사용할 수 있습니다.
- </p>
- <ul className="settings-connect-card__scope-list">
- <li>채널 정보 (이름, 설명, 썸네일)</li>
- <li>라이브 채팅 읽기/쓰기</li>
- <li>채널 멤버십 확인</li>
- </ul>
- <div className="settings-connect-card__terms">
- <div className="settings-connect-card__terms-item">
- <Checkbox
- id="terms"
- checked={agreedTerms}
- onCheckedChange={v => setAgreedTerms(v === true)}
- />
- <Label htmlFor="terms" className="settings-connect-card__terms-label">
- <span className="settings-connect-card__terms-required">[필수]</span>
- <a
- href="/docs/terms"
- target="_blank"
- rel="noopener noreferrer"
- className="settings-connect-card__terms-link"
- onClick={e => e.stopPropagation()}
- >서비스 이용약관</a>에 동의합니다.
- </Label>
- </div>
- <div className="settings-connect-card__terms-item">
- <Checkbox
- id="youtube"
- checked={agreedYouTube}
- onCheckedChange={v => setAgreedYouTube(v === true)}
- />
- <Label htmlFor="youtube" className="settings-connect-card__terms-label">
- <span className="settings-connect-card__terms-required">[필수]</span>
- <a
- href="https://www.youtube.com/t/terms"
- target="_blank"
- rel="noopener noreferrer"
- className="settings-connect-card__terms-link"
- onClick={e => e.stopPropagation()}
- >YouTube 서비스 약관</a> 및 데이터 수집에 동의합니다.
- </Label>
- </div>
- </div>
- <button
- type="button"
- className="settings-connect-card__btn"
- disabled={!canConnect || connecting}
- onClick={handleConnect}
- >
- <YouTubeIcon />
- {connecting ? '연결 중...' : 'YouTube로 연결하기'}
- </button>
- </div>
- </div>
- </>
- )}
- {!loading && isConnected && (
- <div className="settings-channel">
- {connected && (
- <div className="settings-alert settings-alert--success">
- YouTube 채널이 성공적으로 연결되었습니다.
- </div>
- )}
- {/* 배너 */}
- {data?.bannerUrl && (
- <div className="settings-channel__banner">
- <Image
- src={`${data.bannerUrl}=w1138`}
- alt="채널 배너"
- width={1138}
- height={190}
- className="settings-channel__banner-img"
- priority
- />
- </div>
- )}
- {/* 프로필 영역 */}
- <div className="settings-channel__profile">
- {data?.thumbnailUrl ? (
- <Image
- src={data.thumbnailUrl}
- alt={data?.channelName ?? '채널'}
- width={80}
- height={80}
- className="settings-channel__avatar"
- />
- ) : (
- <div className="settings-channel__avatar-placeholder" />
- )}
- <div className="settings-channel__identity">
- <h2 className="settings-channel__name">{data?.channelName}</h2>
- <div className="settings-channel__meta">
- {data?.handle && <span>{data.handle}</span>}
- <span>구독자 {data?.subscriberCount.toLocaleString()}명</span>
- <span>동영상 {data?.videoCount.toLocaleString()}개</span>
- </div>
- </div>
- </div>
- {/* 상세 정보 */}
- <div className="settings-channel__details">
- {data?.description && (
- <div className="settings-channel__row">
- <FontAwesomeIcon icon={faAlignLeft} className="settings-channel__icon" />
- <div className="settings-channel__value">
- <span className="settings-channel__label">설명</span>
- <p className="settings-channel__desc">{data.description}</p>
- </div>
- </div>
- )}
- {data?.channelUrl && (
- <div className="settings-channel__row">
- <FontAwesomeIcon icon={faLink} className="settings-channel__icon" />
- <div className="settings-channel__value">
- <span className="settings-channel__label">채널 주소</span>
- <a
- href={data.channelUrl}
- target="_blank"
- rel="noopener noreferrer"
- className="settings-channel__link"
- >{data.channelUrl}</a>
- </div>
- </div>
- )}
- <div className="settings-channel__row">
- <FontAwesomeIcon icon={faGlobe} className="settings-channel__icon" />
- <div className="settings-channel__value">
- <span className="settings-channel__label">조회수</span>
- <span>조회수 {data?.viewCount.toLocaleString()}회</span>
- </div>
- </div>
- {data?.youTubePublishedAt && (
- <div className="settings-channel__row">
- <FontAwesomeIcon icon={faCalendarDays} className="settings-channel__icon" />
- <div className="settings-channel__value">
- <span className="settings-channel__label">가입일</span>
- <span>{new Date(data.youTubePublishedAt).toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric' })}</span>
- </div>
- </div>
- )}
- <div className="settings-channel__stats">
- <div className="settings-channel__stat">
- <FontAwesomeIcon icon={faUsers} className="settings-channel__stat-icon" />
- <span className="settings-channel__stat-value">{data?.subscriberCount.toLocaleString()}</span>
- <span className="settings-channel__stat-label">구독자</span>
- </div>
- <div className="settings-channel__stat">
- <FontAwesomeIcon icon={faVideo} className="settings-channel__stat-icon" />
- <span className="settings-channel__stat-value">{data?.videoCount.toLocaleString()}</span>
- <span className="settings-channel__stat-label">동영상</span>
- </div>
- <div className="settings-channel__stat">
- <FontAwesomeIcon icon={faEye} className="settings-channel__stat-icon" />
- <span className="settings-channel__stat-value">{data?.viewCount.toLocaleString()}</span>
- <span className="settings-channel__stat-label">조회수</span>
- </div>
- </div>
- </div>
- {/* 연동 해지 */}
- {!showDisconnect ? (
- <div className="settings-disconnect">
- <div className='settings-disconnect__logo'>
- <YouTubeSignBoard />
- </div>
- <p className="settings-disconnect__hint">
- YouTube 연동을 해지하면 후원 수신, 라이브 채팅 등의 서비스를 이용할 수 없습니다. <br/>
- <button
- type="button"
- className="settings-disconnect__trigger"
- onClick={handleDisconnectClick}
- >
- 연동 해지하기
- </button>
- </p>
- </div>
- ) : (
- <div className="settings-disconnect__card">
- <p className="settings-disconnect__card-title">YouTube 연동 해지</p>
- <p className="settings-disconnect__card-desc">
- 연동을 해지하면 아래 서비스를 더 이상 이용할 수 없습니다.
- </p>
- <ul className="settings-disconnect__impact-list">
- <li>후원 수신 (시청자로부터 후원을 받을 수 없습니다)</li>
- <li>라이브 채팅 읽기/쓰기</li>
- <li>채널 멤버십 확인</li>
- <li>채널 정보 자동 동기화</li>
- </ul>
- <div className="settings-disconnect__terms">
- <div className="settings-disconnect__terms-item">
- <Checkbox
- id="disconnect"
- checked={agreedDisconnect}
- onCheckedChange={handleAgreedDisconnectChange}
- />
- <Label htmlFor="disconnect" className="settings-disconnect__terms-label">
- <span className="settings-disconnect__terms-required">[필수]</span>
- 위 사항을 이해하고 YouTube 연동을 해지합니다.
- </Label>
- </div>
- </div>
- <div className="settings-disconnect__actions">
- <button
- type="button"
- className="settings-disconnect__btn settings-disconnect__btn--cancel"
- onClick={handleDisconnectCancel}
- >
- 취소
- </button>
- <button
- type="button"
- className="settings-disconnect__btn settings-disconnect__btn--danger"
- disabled={!agreedDisconnect || disconnecting}
- onClick={handleDisconnect}
- >
- {disconnecting ? '해지 중...' : '연동 해지'}
- </button>
- </div>
- </div>
- )}
- </div>
- )}
- </div>
- );
- }
- function YouTubeIcon() {
- return (
- <svg viewBox="0 0 24 24" fill="currentColor" width={18} height={18}>
- <path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/>
- </svg>
- );
- }
|