반응형

서버를 만들어서 한번 도메인 네임을 적용 시켜보고 싶었다.

(7) https를 적용시키기 위해서는 도메인 네임이 필요하기 때문에 적용 시킬 필요가 있다.

그냥 만들어 보는 프로젝트 이기때문에 무료 dns를 사용하기로 했다.

https://freedns.afraid.org/

 

FreeDNS - Free DNS - Dynamic DNS - Static DNS subdomain and domain hosting

Free DNS Hosting, Dynamic DNS Hosting, Static DNS Hosting, subdomain and domain hosting. Dynamic update demonstration example (v2 interface) [~] $ curl https://sync.afraid.org/u/CyTXMbtq5cPnLjEg5vKHTPDE/ Updated demo.freshdns.com from 107.170.238.X to 50.2

freedns.afraid.org

 

회원가입을 해서 왼쪽 위 매뉴인 [ Subdomains ] 에 접속하면 

와 같이 나오는것을 볼수가 있다. 이미 만들어 놓아서 add버튼을 눌러서 추가 시켜주면된다.

이렇게 설정해두면 서버 구동 상태에서 해당 dns로 접속을 하게 되면 접속이 가능하다.

dns로 접속한 클라이언트 서버 https적용은 끝마친 상태


 

※ 공유기나 통신사 공유기가 설치되어있는 상태면 포트포워딩은 확실해 해놓아야지 접속이 가능해진다.

※ 통신사 공유기에 개인 공유기(예를들면 iptime)를 물린 경우도 심심찮게 있는데 그런 경우에는 통신사 공유기에 접속해서 설정을 변경이 필요한 경우도 있다.

반응형
반응형

일반적인 로컬 환경에서는 가능한데 각자의 서버에서는 안되는것 중에 하나가 일반적이 각자의 서버(http 환경)로 구등해서 접속해서 움직이면 쿠키를 저장하는것이 불가능하다.

로컬(localhost)에서는 일반적으로 http를 사용해도 백엔드에서 보내주는 세션에 대한 정보를 저장하는것이 가능했는데

그것을 192.168.0.x와 같이 각자의 ip주소로 서버에 접속해서 로그인을 시도, 세션 데이터를 쿠키로 받는 경우에는 https와 같은 통신의 인증과 암호화가 되어야지 쿠키 데이터를 저장하는 것이 가능하다.

그렇기 때문에 서버에 https를 적용해보기로 했다.


https에서 알아보니 https를 적용시키는 방식은

  1. SSL/TLS인증서 획득
  2. Tomcat 설정 하기
  3. Tomcat 재시작

와 같은 방식으로 동작을 하게된다.

SSL/TLS인증서중에 무료로 사용할수 있는 let's encrypt를 사용하기 위해 certbot을 다운받아서 인증서를 획득 해보았다.

linux 서버의 경우

sudo apt-get update
sudo apt-get install certbot

 

를 이용해서 certbot을 인스톨해준뒤 

sudo certbot certonly --standalone

 

을 입력하게되면 몇몇개의 질문을 한 뒤에 인증서를 /etc/letsencrypt/live/your-domain/ 에 저장시켜 준다.

your-domain 은 실제 도메인으로 대체되어야 한다.

그러고 난뒤

아파치 톰캣 폴더 아래에 있는 conf 폴더 안에 있는 server.xml에

<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
           maxThreads="150" SSLEnabled="true">
   <SSLHostConfig>
       <Certificate certificateKeyFile="/etc/letsencrypt/live/your-domain/privkey.pem"
                    certificateFile="/etc/letsencrypt/live/your-domain/fullchain.pem"
                    certificateChainFile="/etc/letsencrypt/live/your-domain/chain.pem" />
   </SSLHostConfig>
</Connector>

와 같은 설정 부분이 추가 한뒤

 

Tomcat을 재 시작한뒤 8443포트에 접속하게 되면 https을 적용할수 있게 된다.


window에서 동작하는 프론트 엔드 서버에서도 똑같은 방식으로 톰캣에 적용시키면 문제없이 동작 가능하다.

 

 

Certbot

Tagline

certbot.eff.org

에 접속해서 

를 입력하면 자세한 설명이 나와있기에 따라 하면 된다.

반응형
반응형

배포 구조


프론트 엔드 서버 구축하기

 

Apache Tomcat® - Welcome!

The Apache Tomcat® software is an open source implementation of the Jakarta Servlet, Jakarta Server Pages, Jakarta Expression Language, Jakarta WebSocket, Jakarta Annotations and Jakarta Authentication specifications. These specifications are part of the

tomcat.apache.org

 

해당 부분으로 아파치 톰캣을 다운 받은 zip을 풀면된다

이러면 아파치 톰캣을 설치가 완료 되었습니다.

 

apache-tomcat-10.1.16 -> bin -> startup.bat을 실행하면 서버가 실행합니다.

프론트엔드 ( 리액트 )를 아파치 톰캣 환경에서 실행하기 위해서는 해당 파일을 빌드를 해야한다.

 

npm run build

 

을 해서 코드들을 빌드 해줄 필요가 있다.

빌드된 파일들을 

 

apache-tomcat-10.1.16 -> webapps에 저장하면 된다.

여기서 폴더 이름을 build에서 프로젝트명으로 변경했습니다.

 

그리고 나서 실행을 하면 localhost:8080/piviewer 을 입력하게 되면 실행이 된다.

 

그런데 기존에는 그냥 localhost:8080으로 실행하던게 piviewer로 실행하는것이 프로젝트명을 입력해줘야지 실행하는 것이 불편하다.

 

해당 부분을 변경할수 있는것은

 

apache-tomcat-10.1.16 -> conf 폴더 아래에 있는

 

server.xml 을 수정해줘야 한다.

      <!-- 수정 전 -->
      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">

      <!-- 수정 후 -->
      <Host name="localhost"  appBase="webapps" unpackWARs="true" autoDeploy="true">
          <Context path="/" docBase="piviewer"  reloadable="false" > </Context>

 

위와 같이 설정을 해주면 localhost:8080을 접속하면 기존과 같이 사용하는 것이 가능하다.

 

web.xml

    <error-page>
        <error-code>404</error-code>
        <location>/index.html</location>
    </error-page>

 

위의 설정은 로그인 버튼을 누르면 CSR로 클라이언트 측에서 렌더를 해서 바로 표시를 하는 구조인데 서버로 출력하기 때문에 html 페이지를 이동하지 않는다. 그런데 아파치 톰캣으로 구동을 하게 되면 일단 서버에서 받기 때문에 login에 대한 처리가 없기 때문에 404 에러를 발생 시킨다. 그것을 index.html로 로케이션을 변경시켜 주면 에러가 발생하지 않고 기존 동작과 같이 동작을 하게 된다.


백 엔드서버 구축하기

Spring을 아파치 톰캣으로 구동하고자 하면 war 파일을 만들어야 한다.

war 파일 만들기 gradle기준

실행이 완료되면

프로젝트 -> build -> libs 파일에 .war파일이 저장된다. 해당 파일을 구동하고자 하는 리눅스 아파치 톰캣 서버에 저장한다.

 

 

WinSCP :: Official Site :: Download

WinSCP 6.1 Download WinSCP 6.1 is a major application update. New features and enhancements include: Local file manager mode (two local panels). Windows 11 flat style graphics. SSH core upgraded to PuTTY 0.78. That includes support for OpenSSH certificates

winscp.net

해당 sftp를 이용하기 위해서 WinSCP를 이용했다.

아파치 톰캣은 미리 다운로드 완료후

apache-tomcat-10.1.4 -> webapps 에 war파일을 옮겨 놓고 난뒤에

ssh로 해당 서버에 접속해서 apache-tomcat-10.1.4 -> bin -> startup.sh 로 실행하면 실행이 가능하다.


DB 서버 구축하기

맥에서 MariaDB 설치

// 마리아 DB 설치
brew install mariadb

// 설치 완료시 입력하면 실행
mariadb

 

properties에 h2 -> mariaDB로 설정을 변경해줘야 합니다.

spring.datasource.url=jdbc:mysql://맥의_IP주소:3306/데이터베이스_이름
spring.datasource.username=사용자명
spring.datasource.password=비밀번호
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver

 

그렇게 한뒤에

맥에서 데이터베이스_이름에 해당하는 database를 생성

create database piviewer

 

그러고 난뒤에 권한을 줘야한다.

grant all privileges on piviewer to '사용자명'@'호스트' identified by '비밀번호' with grant option;

 

권한의 사용자명과 비밀번호는 spring properties에서 설정과 일치해야하고 호스트는 데이터베이스를 사용하는 서버 즉 백엔드 서버에 대한 주소를 입력을 해야한다. 

 

그렇게 한뒤에 flush privileges로 변경사항을 즉시 반영하게 한뒤에

mariaDB를 재시작 해주면 모든 배포할 준비는 모두 갖추게 된다.

반응형
반응형

기본적으로 부트스트랩 프레임워크를 기본으로 사용하면서 조금씩 필요한 부분을 css 조정을 하여서 제작하였습니다.


프로젝트 구조

- project_name // 소스 코드 디렉토리

    - index.html // 리액트 인덱스 html

    - img // 정적 이미지 디렉토리

        - logo.png // 파비콘용 로고

    - src // 소스 코드 디렉토리 

        - components // 페이지 영역 디렉토리

            - Header.js // 헤더 영역 폼

            - LiveStream.js // 생방송 표시 폼

            - Login.js // 로그인 폼

            - MainContents.js // 메인 컨텐츠 폼

            - SearchResults.js // 검색 결과 폼

            - SideBar.js // 사이드바 폼

        - img // 이미지 디렉토리

            - alarm.png // 알람에 사용하고자 하는 이미지

            - logo.png // 임시로 사용하는 이미지

        - style

            - App.css

            - Header.css

            - index.css

            - LiveStream.css

            - MainContents.css

            - SearchResults.css

            - SideBar.css

        - App.js

        - index.js



index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
    <title>PIViewer</title>
    <link rel="icon" href="img/logo.png" type="image/png">
  </head>
  <body>
    <div id="piview" class="h-100"></div>
  </body>
</html>

 

index.js

const root = ReactDOM.createRoot(document.getElementById('piview'));
root.render(
    <React.StrictMode>
        <App/>
    </React.StrictMode>
);

App.js

function App() {
    const [isLoggedIn, setIsLoggedIn] = useState(false);
    const [streramTwitchId, setStreramTwitchId] = useState("");

    const handleLogin = () => {
        setIsLoggedIn(true);
    };

    const handleLogout = () => {
        setIsLoggedIn(false);
    };

    useEffect(() => {
        // Spring 서버로 JSON 데이터를 보내는 함수
        async function sendDataToSpringServer() {
            try {
                const response = await fetch('http://localhost:8080/checkLogin', {
                    method: 'GET',
                    headers: {
                        'Content-Type': 'application/json', // JSON 데이터라는 것을 명시
                    },
                    credentials: 'include',
                });
            
                if (response.ok) {
                    const responseData = await response.json(); // Spring 서버에서의 응답 데이터를 JSON으로 파싱
                    if(responseData === true){
                      // 로그인 확인 함수 실행
                      handleLogin();
                    }
                    console.log('Spring 서버 응답 데이터:', responseData);
                } else {
                    console.error('Spring 서버 응답 에러:', response.status, response.statusText);
                }
            } catch (error) {
                console.error('오류 발생:', error);
            }
        }
        sendDataToSpringServer();
    }, []);

    return (
        <Router>
            <div className="h-100">
                <header>
                    <Header isLoggedIn={isLoggedIn} onLogout={handleLogout} />
                </header>
                <div className="d-flex h-100">
                    <div className="d-flex flex-column h-100 bg-body-tertiary sidebar mt-header">
                        <div className="border-bottom add-follow-button">
                            {isLoggedIn && 
                            <Link className="list-group-item list-group-item-action py-3 lh-sm text-center" to='/search'>
                                <strong className="mb-1">팔로우 추가</strong>
                            </Link>
                            }
                        </div>
                        <div className="text-center p-3 border-bottom fs-5 fw-semibold">팔로우 목록</div>
                        <div className="list-group list-group-flush border-bottom scrollarea">
                            {isLoggedIn  ? (
                                <SideBar setStreramTwitchId={setStreramTwitchId}/>
                            ) : (
                                <div className='text-center'>로그인을 해주세요</div>
                            )}
                        </div>
                    </div>
                    <div className='w-100 mt-header contents'>
                        <Routes>
                            <Route path="/search" element={<SearchResults />} />
                            <Route path="/" element={<MainContents isLoggedIn={isLoggedIn} onLogin={handleLogin}/>} />
                            <Route path="/:customUrl" element={<LiveStream streramTwitchId={streramTwitchId} setStreramTwitchId={setStreramTwitchId}/>} />
                            <Route path="/login" element={<Login isLoggedIn={isLoggedIn} onLogin={handleLogin}/>} />
                        </Routes>
                    </div>
                </div>
            </div>
        </Router>
    );
}

export default App;

Header.js

function Header({ isLoggedIn, onLogout }) {
    const navigate  = useNavigate();

    const Logout = (event) => {
      event.preventDefault();
      async function sendDataToSpringServer() {
          try {
              const response = await fetch('http://localhost:8080/logout', {
                  method: 'GET',
                  headers: {
                      'Content-Type': 'application/json', // JSON 데이터라는 것을 명시
                  },
                  credentials: 'include'
              });
          
              if (response.ok) {
                  const responseData = await response.json(); // Spring 서버에서의 응답 데이터를 JSON으로 파싱
                  onLogout();
                  navigate(`/`);
                  console.log('Spring 서버 응답 데이터:', responseData);
              } else {
                  console.error('Spring 서버 응답 에러:', response.status, response.statusText);
              }
          } catch (error) {
              console.error('오류 발생:', error);
          }
      }
      sendDataToSpringServer();
    }

    return ( isLoggedIn ? (
        <Navbar className="px-3 py-0 bg-primary bg-opacity-50 navbar header">
            <Container fluid>
                <Navbar.Brand>
                    <Link to="/" className='site-logo h2'>PIVeiwer</Link>
                </Navbar.Brand>
                <Navbar id="basic-navbar-nav d-flex justify-content-end">
                    <Nav className='d-flex align-items-center'>
                        <NavDropdown title={<Image src={alarm} className='bg-danger rounded-circle'></Image>} id="basic-nav-dropdown" align="end">
                            <div>
                              <div className="small text-gray-500 text-center">공지판</div>
                            </div>
                            <NavDropdown.Item className='py-3'>
                                <div>
                                  <div className="small text-gray-500">Documentation</div>
                                  Usage instructions and reference
                                </div>
                            </NavDropdown.Item>
                        </NavDropdown>
                        <NavDropdown title={<Image src={logo} className='rounded-circle'></Image>} id="basic-nav-dropdown" align="end">
                            {/* 아직 기능 미구현 */}
                            <NavDropdown.Item href="#action/3.1" >내정보</NavDropdown.Item>
                            <NavDropdown.Divider />
                            <NavDropdown.Item onClick={Logout}>로그 아웃</NavDropdown.Item>
                        </NavDropdown>
                    </Nav>
                  </Navbar>
            </Container>
        </Navbar>
        ) : (
            <Navbar className="px-3 py-0 bg-primary bg-opacity-50 navbar header">
                <Container fluid>
                    <Navbar.Brand>
                        <Link to="/" className='site-logo h2'>PIVeiwer</Link>
                    </Navbar.Brand>
                    <Navbar id="basic-navbar-nav d-flex justify-content-end">
                        <Nav className='d-flex align-items-center'>
                            <Button type="button" href='/login'>login</Button>
                        </Nav>
                    </Navbar>
                </Container>
            </Navbar>
        )
    );
}

export default Header;

 

isLoggedIn 이 true 인 경우( 로그인 상태 )

 

isLoggedin 이 false 인 경우( 로그아웃 상태 ) 혹은 로그아웃 버튼을 눌렀을 경우

 


SideBar.js

function SideBar({setStreramTwitchId}) {
    const [followes, setFollowes] = useState([]);
    const navigate = useNavigate();

    // Link를 클릭하면 URL 경로와 함께 쿼리를 추가하여 다른 페이지로 이동합니다
    const handleLinkClick = (e, custom_url, video_id) => {
        e.preventDefault(); // 기본 동작 중지
        getLiveStreamTwitchChannelId(custom_url);
        const query = '?v=' + video_id;
        const newPath = '/'+ custom_url + query;
        navigate(newPath);
    }

    async function getFollowChannels() {
        try {
            const response = await fetch('http://localhost:8080/getFollowChannels', {
                method: 'GET',
                headers: {
                'Content-Type': 'application/json',
                },
                credentials: 'include',
            });
                if (response.ok) {
                    const responseData = await response.json();
                    setFollowes(responseData || []);
                    console.log('Spring 서버 응답 데이터:', responseData);
                } else {
                    console.error('Spring 서버 응답 에러:', response.status, response.statusText);
                }
            } catch (error) {
            console.error('오류 발생:', error);
        }
    }

    async function getLiveStreamTwitchChannelId(custom_url) {
        try {
            const response = await fetch('http://localhost:8080/getLiveStreamTwitchChannelId?custumUrl=' + custom_url, {
                method: 'GET',
                headers: {
                'Content-Type': 'application/json',
                },
                credentials: 'include',
            });
                if (response.ok) {
                    const responseData = await response.text();
                    setStreramTwitchId(responseData);
                    console.log('Spring 서버 응답 데이터:', responseData);
                    return responseData;
                } else {
                    console.error('Spring 서버 응답 에러:', response.status, response.statusText);
                }
            } catch (error) {
            console.error('오류 발생');
        }
    }

    useEffect(() => {
        // 초기 로딩 시 한 번 데이터 가져오기
        getFollowChannels();

        // 3초마다 데이터 업데이트
        const intervalId = setInterval(() => {
            getFollowChannels();
        }, 30000);

        return () => {
            // 컴포넌트가 언마운트될 때 clearInterval을 사용하여 인터벌 제거
            clearInterval(intervalId);
        };
    }, []);

    return (
        <div>
            <h4 className='m-0'>생방송</h4>
        {followes.map((item, index) => (
        <div key={index}>
            { item.is_live === "Live" && (
            <Link className="list-group-item list-group-item-action py-3 lh-sm" aria-current="true" onClick={(e) => handleLinkClick(e, item.custom_url, item.video_id)}>
            <div className='row'>
                <div className='sidebar_img'>
                    <img src={item.thumbnails_url} alt="" className="col sidebar_img"/>
                </div>
                <div className='col mx-2'>
                    <div className='overflow-text'><strong >{item.name}</strong></div>
                    <div><small className='live-text'>{item.is_live}</small></div>
                </div>
            </div>
            </Link>
            )}
        </div>
        ))}
        <h4 className='m-0'>오프라인</h4>
        {followes.map((item, index) => (
        <div key={index}>
            { item.is_live === "" && (
            <Link className="list-group-item list-group-item-action py-3 lh-sm" aria-current="true">
            <div className='row'>
                <div className='sidebar_img'>
                    <img src={item.thumbnails_url} alt="" className="col sidebar_img"/>
                </div>
                <div className='col mx-2'>
                    <div className='overflow-text'><strong >{item.name}</strong></div>
                    <div><small>{item.is_live}</small></div>
                </div>
            </div>
            </Link>
            )}
        </div>
        ))}
        </div>
    );
}


export default SideBar;

 

 

 


로그아웃 상태

로그인 상태(생방송 중, 비방상태)

SideBar.js에서 팔로우 추가를 입력했을때

Search.Results.js

function SearchResults() {
    const [customUrl, setCustomUrl] = useState('');
    const [searchResult, setSearchResult] = useState();
    const [show, setShow] = useState(false);

    const handleShow = () => {
        setShow(true);
    };

    const handleClose = () => setShow(false);

    async function searchChannel(customUrl) {
        try {
            const response = await fetch('http://localhost:8080/searchChannel?search=' + customUrl, {
                method: 'GET',
                headers: {
                'Content-Type': 'application/json',
                },
                credentials: 'include',
            });
                if (response.ok) {
                    const responseData = await response.json();
                    if(responseData.searchResult)
                        setSearchResult(responseData.liveConfig);
                    else
                        setSearchResult(null);
                    console.log('Spring 서버 응답 데이터:', responseData);
                } else {
                    console.error('Spring 서버 응답 에러:', response.status, response.statusText);
                }
            } catch (error) {
            console.error('오류 발생:', error);
        }
    }

    async function sendFollowData() {
        try {
            const response = await fetch('http://localhost:8080/follow?customUrl='+ customUrl, {
                method: 'GET',
                headers: {
                'Content-Type': 'application/json',
                },
                credentials: 'include',
            });
                if (response.ok) {
                } else {
                    console.error('Spring 서버 응답 에러:', response.status, response.statusText);
                }
            } catch (error) {
            console.error('오류 발생:', error);
        }
    }

    const addFollow = () => {
        sendFollowData();
        handleClose();
    }

    return (
        <div className='h-100 p-5 w-50'>
            <h1>팔로우 추가</h1>
            <form onSubmit={(event) => {
                event.preventDefault();
                searchChannel(customUrl);
                }} className='pt-3 py-2'>
                <div className="row">
                    <div className="row w-100">
                        <label htmlFor="firstName" className="form-label">채널 검색</label>
                        <div className='col'>
                            <input type="text" className="form-control" placeholder="검색할 채널의 커스텀url을 정확히 입력해주세요" value={customUrl} onChange={(e) => setCustomUrl(e.target.value)}/>
                        </div>
                        <Button variant="primary" onClick={() => handleShow()} className='col addfollowbutton'>추가하기</Button>
                    </div>
                </div>
            </form>
            <div className="d-md-flex flex-md-equal w-100">
                <div className="text-bg-dark pt-3 w-100 overflow-hidden">
                    <div className="my-3 py-3">
                        <h2 className="display-5 text-center">검색결과</h2>
                    </div>
                    <div className="bg-body-tertiary shadow-sm mx-auto my-3 searchResult">
                        <div className="text-black searchResultField" id="youtubeResult" >
                            {searchResult != null ? (
                                <Link className="list-group-item py-3 px-2 lh-sm searchResultElement" aria-current="true">
                                    <div className='row'>
                                        <img src={searchResult.thumbnails_url} alt="Thumbnail" className='thumbnailSize'></img>
                                        <div className="w-100 align-items-center col">
                                            <div>
                                                <strong className="mb-1">{searchResult.custom_url}</strong>
                                            </div>
                                            <div className="d-flex justify-content-center align-items-center channel-des-area">{searchResult.description}</div>
                                        </div>
                                    </div>
                                </Link>
                            ) : (
                                <div className="list-group-item py-3 px-2 lh-sm searchResultElement">
                                    <div className='row'>
                                        <div className="w-100 align-items-center col">
                                            <div>
                                                <strong className="mb-1">검색결과가 없습니다.</strong>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                            )}
                        </div>
                    </div>
                </div>
            </div>
            <Modal show={show} onHide={handleClose} className='disable-drag'>
                <Modal.Header closeButton>
                <Modal.Title>확인</Modal.Title>
                </Modal.Header>
                <Modal.Body>
                    <div className="row">
                        <div className="col">
                            <div className="mb-3">
                                <div>
                                    <div className='row'>
                                        <div>
                                        {searchResult != null ? (
                                            <Link className="list-group-item py-3 px-2 lh-sm searchResultElement" aria-current="true">
                                                <div className='row'>
                                                    <img src={searchResult.thumbnails_url} alt="Thumbnail" className='thumbnailSize'></img>
                                                    <div className="w-100 align-items-center col">
                                                        <div>
                                                            <strong className="mb-1">{searchResult.custom_url}</strong>
                                                        </div>
                                                        <div className="d-flex justify-content-center align-items-center channel-des-area">{searchResult.description}</div>
                                                    </div>
                                                </div>
                                            </Link>
                                        ) : (
                                            <div>검색을 진행한 뒤 진행해 주세요</div>
                                        )
                                        }
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </Modal.Body>
                <Modal.Footer>
                    <div className='d-flex align-items-center justify-content-between'>
                        확인이 끝났다면 추가하기 버튼을 눌러주세요. &nbsp;&nbsp;&nbsp;
                        <Button variant="primary" onClick={addFollow}>
                            추가하기
                        </Button>
                    </div>
                </Modal.Footer>
            </Modal>
        </div>
    );
}


export default SearchResults;

 

초기화면

 

검색전
검색후 예) 슈카 코믹스
추가하기 버튼 입력 ( 검색전 )

 

추가하기 버튼 입력 ( 검색후 )


MainContents.js

function InitForm() {
    return (
        <div className='col p-5'>
            로그인 초기 화면 입니다.
        </div>
    );
}

function NoticeBoard() {
    return (
      <div>
          <h1>공지판 입니다.</h1>
      </div>
    );
  }

function MainContents({ isLoggedIn, onLogin }) {
    return (
        isLoggedIn ? (
            <InitForm />
        ) : (
            <NoticeBoard />
        )
    );
}

export default MainContents;

로그인 전
로그인 후


생방송 중인 채널을 클릭시

초기 화면(예) 슈카월드 코믹스)

LiveStream.js

function LiveStream({streramTwitchId, setStreramTwitchId}) {
    const { customUrl } = useParams();
    const [twitchSearchResult, setTwitchSearchResult] = useState([]);
    const [show, setShow] = useState(false);
    const location = useLocation();
    const searchParams = new URLSearchParams(location.search);
    const videoId = searchParams.get('v');
    
    const handleShow = () => setShow(true);
    const handleClose = () => setShow(false);

    useEffect(() => {
        ytplayerResize();
        twitchChatIframeResize();
        window.addEventListener("ytplayer", ytplayerResize);
        window.addEventListener("twitchChatIframe", twitchChatIframeResize);

        // 컴포넌트가 언마운트될 때 이벤트 리스너 정리
        return () => {
            window.removeEventListener("ytplayer", ytplayerResize);
            window.removeEventListener("twitchChatIframe", twitchChatIframeResize);
        };
    }, []);

    async function setLiveStreamTwitchChannelId(e) {
        e.preventDefault();

        const twitchChannelId = document.getElementById('twitchId').value;
        try {
            const response = await fetch('http://localhost:8080/setLiveStreamTwitchChannelId?custumUrl=' + customUrl + "&twitchChannelId=" + twitchChannelId, {
                method: 'GET',
                headers: {
                'Content-Type': 'application/json',
                },
                credentials: 'include',
            });
                if (response.ok) {
                    const responseData = await response.text();
                    setStreramTwitchId(twitchChannelId);
                    console.log('Spring 서버 응답 데이터:', responseData);
                    return responseData;
                } else {
                    console.error('Spring 서버 응답 에러:', response.status, response.statusText);
                }
            } catch (error) {
            console.error('오류 발생');
        }
    }

    async function searchTwitchChannelId(search) {
        try {
            const response = await fetch('http://localhost:8080/twitchSearchChannel?search=' + search, {
                method: 'GET',
                headers: {
                'Content-Type': 'application/json',
                },
                credentials: 'include',
            });
                if (response.ok) {
                    const responseData = await response.json();
                    setTwitchSearchResult(responseData.data || []);
                    console.log('Spring 서버 응답 데이터:', responseData);
                } else {
                    console.error('Spring 서버 응답 에러:', response.status, response.statusText);
                }
            } catch (error) {
            console.error('오류 발생:', error);
        }
    }

    function ytplayerResize() {
        const iframe = document.getElementById("ytplayer");
        // 필요한 경우 헤더와 푸터의 높이를 뺄 수 있습니다
        const headerHeight = 100; // 헤더 높이로 대체
        const footerHeight = 0; // 푸터 높이로 대체

        const sideBarWidth = 250;
        const chattingWidth = 350;

        iframe.width = window.innerWidth - sideBarWidth - chattingWidth;
        iframe.height = window.innerHeight - headerHeight - footerHeight;
    }

    function twitchChatIframeResize() {
        const iframe = document.getElementById("twitchChatIframe");
        // 필요한 경우 헤더와 푸터의 높이를 뺄 수 있습니다
        const headerHeight = 100; // 헤더 높이로 대체

        // iframe.width = window.innerWidth - sideBarWidth - chattingWidth;
        iframe.height = window.innerHeight - headerHeight;
    }

    return (
        <div className='w-100 h-100 row'>
            <iframe 
                id="ytplayer" 
                title="liveStream"
                src={"https://www.youtube.com/embed/" + videoId + "?autoplay=1"}
                frameborder="0"
                allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
                allowfullscreen
                className="col-md-8"
                >
            </iframe>
            {/* <iframe width="560" height="315" src="https://www.youtube.com/embed/DgpEmYsT9hE?si=NJmaumU3EIGVDXzi" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe> */}
            <div className='col-md-3'>
                <div className='streamIdBx'>
                    <button onClick={() => handleShow()}>Twitch id 찾기</button>
                </div>
                <iframe
                    src={"https://www.twitch.tv/embed/"+ streramTwitchId +"/chat?parent=localhost"}
                    title="twitchChatIframe"
                    width="350"
                    id="twitchChatIframe"
                    theme="black"
                >
                </iframe>
            </div>
            <Modal show={show} onHide={handleClose} className='disable-drag'>
                <Modal.Header closeButton>
                <Modal.Title>twitch id 찾기</Modal.Title>
                </Modal.Header>
                <Modal.Body>
                    <label>직접 입력하거나 검색해서 찾아주세요</label>
                    <input type="text" className="form-control" id="twitchId" placeholder="twitch id"></input>
                    <div id="searchResult"></div>
                    <div className='pt-4'>
                        <label>검색</label>
                        <form onSubmit={(e) => {
                            e.preventDefault();
                            searchTwitchChannelId(e.target.elements.searchInput.value);}}>
                            <input type="text" className="form-control" placeholder="Search..." name="searchInput"/>
                        </form>
                        <div>검색 결과</div>
                        <div className='scrollarea search-result'>
                        {twitchSearchResult.map((item, index) => (
                            <Link key={index} className="list-group-item py-3 px-2 lh-sm searchResultElement" aria-current="true" onClick={(e) => {e.preventDefault(); document.getElementById("twitchId").value = item.broadcaster_login}}>
                                <div className='row scrollba'>
                                    <img src={item.thumbnail_url} alt={item.thumbnail_url ? "Thumbnail" : ""} className="thumbnailSize" />
                                    <div className="w-100 align-items-center col">
                                        <div>
                                            <strong className="mb-1">{item.display_name}</strong>
                                        </div>
                                    </div>
                                </div>
                            </Link>
                        ))}
                        </div>
                    </div>
                </Modal.Body>
                <Modal.Footer>
                    <Button variant="primary" onClick={setLiveStreamTwitchChannelId}>
                        추가하기
                    </Button>
                </Modal.Footer>
            </Modal>
        </div>
    );
}

export default LiveStream;

 

동영상 영역

채팅 영역



트위치 방송 아이디가 없는 경우


트위치 방송 아이디를 입력했는 경우

 

Twitch Id 찾기 버튼을 누른경우

 

다이어로그로 표현 ( 트위치 아이디 검색전 )
다이어로그로 표현 ( 트위치 아이디 검색후 )


헤더의 Login 버튼을 클릭 했을 경우

로그인 화면

Login.js

function SignupForm(){
    const Signup = () => {
        const data = {
            username: document.getElementById('username').value,
            password: document.getElementById('password').value,
            youtubeChannelId: document.getElementById('youtubeChannelId').value,
        };
        // Spring 서버로 JSON 데이터를 보내는 함수
        async function sendDataToSpringServer() {
        try {
            const response = await fetch('http://localhost:8080/signup', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json', // JSON 데이터라는 것을 명시
                },
                body: JSON.stringify(data), // JSON 데이터를 문자열로 변환해서 요청 본문에 담음
            });
        
            if (response.ok) {
                const responseData = await response.json(); // Spring 서버에서의 응답 데이터를 JSON으로 파싱
                console.log('Spring 서버 응답 데이터:', responseData);
            } else {
                console.error('Spring 서버 응답 에러:', response.status, response.statusText);
            }
        } catch (error) {
            console.error('오류 발생:', error);
        }
      }
      sendDataToSpringServer();
    }

    return (
        <div className='form-signin col p-5 mx-auto'>
            <div className='text-center h3 p-3'>회원 가입</div>
            <EmailForm/>
            <PasswordForm/>
            <div className="form-floating mt-2">
                <input type="text" className="form-control" id="youtubeChannelId" placeholder=""></input>
                <label htmlFor="youtubeChannelid">YoutubeChannelid</label>
            </div>
            <button onClick={Signup} className="btn btn-primary w-100 py-2 mt-3">Signin</button>
        </div>
    );
}

function EmailForm(){
    return (
        <div className="form-floating">
            <input type="text" className="form-control email-signin" id="username" placeholder=""></input>
            <label htmlFor="username">Email address</label>
        </div>
    );
}

function PasswordForm(){
    return (
        <div className="form-floating">
            <input type="password" className="form-control password-signin" id="password" placeholder=""></input>
            <label htmlFor="password">password</label>
        </div>
    );
}

function Login({ onLogin }) {
    const [isSignUp, setIsSignUp] = useState(true);
    const navigate  = useNavigate();

    const GoSignup = () => {
        setIsSignUp(false);
    };

    const LoginSubmit = () =>{
        const data = {
            username: document.getElementById('username').value,
            password: document.getElementById('password').value
        };
        // Spring 서버로 JSON 데이터를 보내는 함수
        async function sendDataToSpringServer() {
            try {
                const response = await fetch('http://localhost:8080/login', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json', // JSON 데이터라는 것을 명시
                    },
                    body: JSON.stringify(data), // JSON 데이터를 문자열로 변환해서 요청 본문에 담음
                    credentials: 'include',
                });
            
                if (response.ok) {
                    const responseData = await response.json(); // Spring 서버에서의 응답 데이터를 JSON으로 파싱
                    // 로그인 확인 함수 실행
                    onLogin();
                    navigate(`/`);
                    console.log('Spring 서버 응답 데이터:', responseData);
                } else {
                    console.error('Spring 서버 응답 에러:', response.status, response.statusText);
                }
            } catch (error) {
                console.error('오류 발생:', error);
            }
        }
        sendDataToSpringServer();
    }
    return (
        isSignUp ? (
            <React.StrictMode>
                <div className='form-signin col p-5 mx-auto'>
                    <div className='text-center h3 p-3'>로그인</div>
                    <EmailForm />
                    <PasswordForm />
                    <button onClick={LoginSubmit} className="btn btn-primary w-100 py-2 mt-3">Login</button>
                    <button onClick={GoSignup} className="btn btn-primary w-100 py-2 mt-3">Signin</button>
                </div>
            </React.StrictMode>
        ) : (
            <React.StrictMode>
                <SignupForm />
            </React.StrictMode>
        )
    );
}

export default Login;

 

Signin 버튼을 누른 경우

회원가입 폼

 

반응형

+ Recent posts