oneted page 원티드 페이지 만들기 도전!!
본문 바로가기

컴퓨터공부/ASAC 웹풀스택

oneted page 원티드 페이지 만들기 도전!!

by Life & study 2023. 12. 22.
반응형

oneted page 원티드 페이지 만들기 도전!!

파일구조를 파악하고 파이어베이스 까지 가자

 

 

파일구조
demo_oneted
├── .idea
├── tutorial-setting-nextjs-main
│   ├── .next
│   ├── node_modules
│   ├── public
│   │   ├── favicon.ico
│   │   └── wantedLogo.JPG
│   ├── src
│   │   ├── app
│   │   │   ├── globals.css
│   │   │   ├── layout.tsx
│   │   │   └── providers.tsx
│   │   ├── components
│   │   │   └── ModalProvider.tsx
│   │   └── pages
│   │       ├── wd
│   │       │   └── index.tsx // 카드 페이지 컴포넌트를 작성합니다.
│   │       └── _app.tsx // 카드 페이지 컴포넌트를 렌더링합니다.
│   └── companies.json // 테스트 데이터를 저장합니다.
└── package.json
@media @media 미디어타입 and (조건) { /* 조건에 맞는 스타일 */ }
테일윈드버전명령어

npm list tailwindcss
C:\demo_oneted\tutorial-setting-nextjs-main>npm list tailwindcss
tutorial-vscode@0.1.0 C:\demo_oneted\tutorial-setting-nextjs-main
`-- tailwindcss@3.3.3
   
<Link>태그이후 import

import Link from 'next/link'; // 이 부분을 추가하세요.
import Link from 'next/link'; // 이 부분을 추가하세요.

    <Link href="/wdlist" className="w-link">채용</Link>​


1
카드
컴포넌트
return (
        <a href={`/company/${id}`} className="Featured_sliderItem__FY8yb" aria-label="featured company card link">
            <header>
                {/* 배경 이미지는 public 폴더의 c1.PNG 파일을 사용합니다. */}
                <img src="/c1.PNG" alt="background" style={{width: '10%'}}/>
            </header>
            <footer>
                {/* 로고 이미지도 public 폴더의 c1.PNG 파일을 사용합니다. 실제로는 각기 다른 이미지 파일을 사용해야 할 것입니다. */}
                <img src="/c1.PNG" alt="logo" style={{width: '50px', height:'50px'}}/>
                <h4>{name}</h4>  {/* 기업 이름 */}
                {/* 포지션 수 정보가 없으므로 임의로 설정합니다. 실제로는 props에서 받아와야 할 것입니다. */}
                <h5>{industry}</h5>  /* 업종 정보 */
            </footer>
        </a>
    );​

1
Next.js
SSG

리액트 SPA의 차이
<Link href="/Toplogo">Go to Toplogo Page</Link>​


     <div style={{marginLeft: '5px'}}> {/* Left margin added */}
                    <Topmenu/>
                </div>​



1
사이드
패딩값



<div className="Hirecom" style={{padding: '20px 800px'}}> {/* padding 스타일을 직접 추가 */}​



1
테일윈드 div로 틈 패딩값 주는 코드

틈패딩 값 div 태그로 만들기

<div className="space-y-2">
        1
      </div>​

1     출력 :     padding: 10px;
   

 

리턴 
삼항연산자
조건&&
import React from 'react';

const MyComponent = () => {
    const myValue = true; // or false, or null, or undefined, etc.

    return (
        <div>
            {myValue && <p>This is a paragraph that will only render if myValue is truthy.</p>}
        </div>
    );
};

export default MyComponent;
<>와 </> 
(즉, 
Fragment)
import React from 'react';

const MyComponent = () => {
    return (
        <>
            <p>This is a paragraph.</p>
            <p>This is another paragraph.</p>
        </>
    );
};

export default MyComponent;
사이트 뒤에 동적 로그만들기

./src/pages/wd/[id].tsx


https://www.wanted.co.kr/wdlist/518?country=kr&job_sort=job.latest_order&years=0&locations=all
사이트 뒤에 동적 로그만들기

- **(필수) 매 기업마다 Dynamic Routing 설정하기** : 더미 회사 JSON
    - 더미 회사 JSON 을 만들어서 API 가 아닌 단순 Import 를 통해 정보를 find / filter 조회
    - 예) http://localhost:3000/wd/10238 접근 시 10238 ID 더미 기업 JSON 정보 사용


import { useRouter } from 'next/router'​

// ./src/pages/wd/[id].tsx
import { useRouter } from 'next/router'
import BottomCard from '../../components/BottomCard'

export default function Company() {
    const router = useRouter()
    const { id } = router.query

    if (!id || Array.isArray(id)) {
        return <div>Not Found</div>
    }

    console.log("ID:", id);  // id 값이 있는 경우에만 로그 출력

    return <BottomCard id={id} />
}​



1
사이트 뒤에 동적 로그만들기

./src/pages/wd/[id].tsx


Next.js의 특징 userRouter 훅을 사용할려면 URL 파라미터 값을 적어야된다.
Next.js의 동적 라우팅을 사용하려면 해당 페이지의 파일명을 [변수명].tsx 형식으로 작성해야 합니다. 예를 들어, /wd/1, /wd/2, ... 등으로 접근할 수 있는 페이지를 만들려면 파일 이름을 [id].tsx로 해야 합니다.

그리고 해당 페이지에서는 Next.js의 useRouter 훅을 이용하여 URL 파라미터 값을 가져올 수 있습니다.


   return <BottomCardinsider id={Number(id)} />​



1
 


1
firebase
명령어
npm install firebase​

1
유투브사이트(참고사이트) https://www.youtube.com/watch?v=uikATllLdRc
파이어베이스 연결강의


파이어베이스1
import { getFirestore } from "firebase/firestore";

//...


// Initialize Firebase
const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);​

1
파이어베이스2
import { collection, addDoc } from "firebase/firestore";​​

1
  19:00
파이어베이스

add
import { collection, addDoc, getFirestore } from "firebase/firestore";​

import React, { useEffect } from 'react';​


1
파이어베이스 3
import { collection, addDoc } from "firebase/firestore";
import { initializeApp } from 'firebase/app';
import { getFirestore } from "firebase/firestore";

const app = initializeApp(firebaseConfig);
const db = getFirestore(app);​

1
파이어베이스4
useEffect
 useEffect(() => {
        const addCompaniesToFirestore = async () => {
            const db = getFirestore(); // Firestore 인스턴스를 가져옵니다.
            const companyCollectionRef = collection(db, 'companies'); // Firestore에서 'companies' 컬렉션의 참조를 가져옵니다.

            for (let company of companies) {
                try {
                    await addDoc(companyCollectionRef , company); // 각 회사 정보를 Firestore에 추가합니다.
                } catch (e) {
                    console.error("Error adding document: ", e);
                }
            }
        };

        addCompaniesToFirestore(); // 위에서 정의한 함수를 호출하여 Firestore에 데이터를 추가합니다.
    }, []);​

1
나의 토큰 
.png?alt=media&token=1e986e74-28af-4aXXXXXXXXXXXXXXXX"
 
https://firebasestorage.googleapis.com/v0/b/{your-bucket-name}/o/{file-path}?alt=media&token={token}​

1
   
   
   
   
image%B1.PNG에서 2F를 사용한이유는? image%2FB1.PNG에서 %2F는 URL 인코딩에서 사용되는 특수 문자입니다. 
즉, image%2FB1.PNG는 실제로 image/B1.PNG를 의미합니다. 파이어베이스 스토리지의 URL에서 파일 경로 부분이 이런 방식으로 인코딩되어 표시됩니다.
위의 규칙은 로그인한 모든 사용자에게 파일에 대한 읽기/쓰기 권한을 부여합니다. 
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if request.auth != null;
    }
  }
}
만약 인증하지 않은 모든 사용자에게 파일에 대한 읽기 권한을 부여하려면
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read: if true;
      allow write: if request.auth != null;
    }
  }
}
   
파일경로의
이해도
import '../../firebase';
C:\demo_oneted\tutorial-setting-nextjs-main\firebase.tsx
..\..\firebase
   
npm install swr
구성 요소/MyComponent.js

export default function MyComponent() {
  return <h1>Hello</h1>
}
페이지/index.js

// Note how `components/MyComponent` 
exists but `Mycomponent` without the capital `c` is imported
import MyComponent from '../components/Mycomponent'
 

   
   
오류 내용
console.warn("crbug/1173575, non-JS module files deprecated.");​​

1
오류 내용 2
1 of 1 unhandled error
Server Error
Error: Ensure bailed, found path does not match ensure type (pages/app)

This error happened while generating the page. 
Any console logs will be displayed in the terminal window.
Call Stack
ensurePageImpl
file:///C:/demo_oneted/tutorial-setting-nextjs-main
/node_modules/next/dist/server/dev/on-demand-entry-handler.js (483:23
 
"Error: Ensure bailed, found path does not match ensure type (pages/app)"
인수 유형 문제
인수 유형 CollectionReference<DocumentData, DocumentData>을(를) 매개변수 유형 
CollectionReference<{name: string, logoUrl: string, 
location: string, location2: string, id: number, description: string}, DocumentData>
에 할당할 수 없습니다 

const companyCollectionRef: CollectionReference<DocumentData, DocumentData>​


import { collection, addDoc, CollectionReference } from "firebase/firestore";
...

const companyCollectionRef = collection(db, 'companies') as CollectionReference<Company>;​


으로 바꿈
 

 

리액트 인라인 스타일코드
로 
Hight 60px 맞춰서 
탑섹션 고정하기


  <div style={{height: '60px'}} /> {/* Adjust the height as needed */}​

제가 제공한 코드 
는 인라인 스타일을 사용한 React JSX 문법입니다.</div style={{height: '60px'}} >
1
@min - max를 적용한
.class name globalcss 만들는
방법
@media (min-width: 992px) and (max-width: 1199px) {
    .custom-header {
        width: 340px;
        border: 1px solid #e1e2e3;
        background-color: #fff;
        padding: 24px 20px;
    }
}
왼쪽 오른쪽 위 아래 패딩값
<Sidefixpage style={{width:'340px', padding: '10px 15px 20px 25px'}}/>
위 코드에서 10px는 위쪽 패딩, 15px는 오른쪽 패딩, 20px는 아래쪽 패딩, 
그리고 25px는 왼쪽 패딩을 나타냅니다.
상하좌우 패딩 조건  padding-top: 10px; 
padding-right :15px ;
padding-bottom :20px ;
padding-left :25px ;
   
라우터 사용법 순서  
npm install react-router-dom  
   
BottomCardinsider.tsx
import { useRouter } from 'next/router';

const BottomCardinsider = () => {
    const router = useRouter();
    const { id } = router.query;
파일구조:
pages
wd
[id].tsx
BottomCard.tsx
 
BottomCard.tsx
import React, { useEffect, useState } from 'react';
import { getStorage, ref, getDownloadURL } from "firebase/storage";
import Link from 'next/link';

const BottomCard = () => {
    const [companies, setCompanies] = useState([]);

    useEffect(() => {
        const fetchLogos = async () => {
            const storage = getStorage();
            const companiesData = [
                {
                    id:1,
                    name:"오너클랜",
                    logoUrl:"image/B1.PNG", // 액세스 토큰 제거
                    description:"고급 개발자(typescript, nodejs, MySQL)",
                    location: "서울",
                    location2: "한국"
                }
                ,


            ];

            for (let company of companiesData) {
                const gsReference = ref(storage, company.logoUrl);
                company.logoUrl = await getDownloadURL(gsReference);
            }

            setCompanies(companiesData);
        };

        fetchLogos();
    }, []);

    return (
        <div className="BottomCard">
            <h3>적극 채용 중인 회사</h3>
            <ul className="company-list" style={{display:'flex', flexWrap:'wrap', padding:'0'}}>
                {companies.map(company => (
                    <>
                    <li key={company.id} className="company-card" style={{width:'23%', listStyleType:'none', margin:'1%'}}>
                        <Link href={`/wd/${company.id}`}>
                        <img src={company.logoUrl} alt={`${company.name} logo`} />
                        <div style={{height: '10px'}} />
                        <div className="BottomCardHead">{company.description}</div>
                        <h4>{company.name}</h4>

                        <span className="addressDot">{company.location}</span>
                        <span className="addressDot">.</span>
                        <span className="addressDot">{company.location2}</span>
                        <span>{company.reward}</span>
                        </Link>
                    </li>
                    </>
                ))}
            </ul>
        </div>
    );
};

export default BottomCard;
[id].tsx
import { useRouter } from 'next/router';
import BottomCardinsider from '../../components/BottomCardinsider';

export default function Company() {
    const router = useRouter();
    const { id } = router.query;

    return <BottomCardinsider id={id} />;
}
   
이해 <OnerClan/> 컴포넌트가 렌더링되는 위치는 코드에서 정의된 순서에 따릅니다. 현재 코드에서 <OnerClan/> 컴포넌트는 두 번 호출되고 있습니다.

첫 번째 호출은 {company && <ComponentToRender company={company} />} 부분에서 이루어지며, 여기서 ComponentToRender는 components[id]를 통해 결정됩니다. 만약 URL의 id가 1이라면, ComponentToRender는 OnerClan이 되고, 따라서 <OnerClan company={company} />가 렌더링됩니다.

두 번째 호출은 코드 하단의 <Tag/><OnerClan/> 부분에서 이루어집니다. 이 부분은 조건문 없이 항상 <OnerClan/>를 렌더링합니다.

따라서 만약 URL의 id가 1일 경우, <OnerClan/> 컴포넌트가 두 번 렌더링되므로 페이지 상단과 하단에 각각 나타나게 됩니다.

만약 하단에 있는 <OnerClan/> 컴포넌트를 제거하려면 해당 줄을 주석 처리하거나 삭제하면 됩니다:

jsx
<div>
    <Tag/>
    {/*<OnerClan/>*/}
</div>
또한 동적으로 선택된 컴포넌트(ComponentToRender)와 이미지 및 기타 정보 사이에 공간을 추가하려면 적절한 스타일(예: 마진)을 적용하거나 추가적인 HTML 요소(예: 개행(<br />) 등)를 사용할 수 있습니다.
   
이해2 import React from 'react';
import { useRouter } from 'next/router';
import TopSection from "./TopSection";
import ImageComponent from './ImageComponent';
import Tag from './Tag';
import OnerClan from "../pages/OnerClan";
// import Nulldy from '../pages/Nulldy'; // 예시: 만약 Nulldy라는 컴포넌트가 있다면
import Sidefixpage from "./Sidefixpage";


// 가능한 모든 JobDetailX 컴포넌트를 포함하는 객체
const components = {
    1: OnerClan,
    // 2: Nulldy, // 예시: 만약 id가 2인 회사의 상세 페이지로 Nulldy라는 컴포넌트를 사용하려면
};

const JobDetail = ({company}) => {
    const router = useRouter();
    const { id } = router.query; // URL에서 id 값을 추출

    // id에 해당하는 컴포넌트 선택
    const ComponentToRender = components[id];

    return (
        <div className="Hirecom" style={{padding: '0px 80px 50px 80px'}}>
            {/* 패딩: 위 80px, 오른쪽 80px, 아래 50px, 왼쪽 20px */}
            <TopSection/>
            <div style={{height: '60px'}} />
            <div style={{display: 'flex'}}>
                <Sidefixpage style={{width:'340px'}}/>
            </div>
                <div>
                    {/* ComponentToRender 가 존재할 경우만 렌더링 */}
                    {company && ComponentToRender && <ComponentToRender company={company} />}

                    {/* 기타 내용... */}

                </div>

        </div>
    );
};

export default JobDetail;
   
회사의 추가에 대한 개념 만약 각각의 회사에 대해 별도의 컴포넌트(OnerClan, Nulldy, 등)를 가지고 있고, 이러한 컴포넌트들이 각기 다른 레이아웃과 스타일을 가지고 있다면, URL에서 추출한 id에 따라 해당 컴포넌트를 동적으로 렌더링하려면 아래와 같은 방식을 사용해야 합니다:

jsx
import OnerClan from "../pages/OnerClan";
import Nulldy from '../pages/Nulldy';

const components = {
    1: OnerClan,
    2: Nulldy,
};

const JobDetail = ({company}) => {
    const router = useRouter();
    const { id } = router.query;

    const ComponentToRender = components[id];

    return (
        <div className="Hirecom">
            {/* ... */}
            {company && ComponentToRender && <ComponentToRender company={company} />}
            {/* ... */}
        </div>
    );
};
위의 코드는 URL의 id 값에 따라 다른 컴포넌트를 렌더링합니다. 즉, /wd/1 URL은 OnerClan 컴포넌트를, /wd/2 URL은 Nulldy 컴포넌트를 보여줍니다. 새로운 회사가 추가될 때마다 해당 회사의 상세 정보 페이지용 새로운 컴포넌트를 생성하고 이를 components 객체에 추가해야 합니다.

그러나 대부분의 경우, 모든 회사가 공유하는 공통된 레이아웃과 스타일을 가진 단일 JobDetail 컴포넌트가 있고, 그 안에서 동적으로 변경되는 부분만 데이터(회사 정보)로 처리합니다. 이 경우에는 각각의 회사 데이터만 다르게 전달하면 되므로 새로운 페이지나 별도의 파일을 만들 필요가 없습니다.
회사의 추가 기본틀 const JobDetail = ({company}) => {
    // ... 기타 코드

    return (
        <div className="Hirecom" style={{padding: '0px 80px 50px 80px'}}>
            {/* 패딩: 위 80px, 오른쪽 80px, 아래 50px, 왼쪽 20px */}
            <TopSection/>
            {/* ... 기타 코드 */}
            
            <div>
                {company && (
                    <>
                        <ImageComponent imageName={company.logoUrl}/>
                        <h3>{company.description}</h3>
                        <p>{company.name}</p>
                        {/* ... 기타 정보 */}
                    </>
                )}
            </div>

        </div>
    );
};

 

제목1

 

규칙에서 allow read: if true; 부분은 모든 사용자가 모든 문서를 읽을 수 있음을 나타내고, allow write: if request.auth != null; 부분은 인증된 사용자만 모든 문서를 쓸 수 있음을 나타냅니다.
Firebase Storage 규칙: Firebase Storage는 개발자가 파일(예: 이미지, 비디오 등)을 업로드하고 다운로드할 수 있는 클라우드 기반 스토리지 서비스입니다. 이러한 파일에 대한 접근을 제어하기 위해 사용되는 것이 바로 Firebase Storage 규칙입니다.
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read: if true;
      allow write: if request.auth != null;
    }
  }
}
   
규칙에서 allow read; 부분은 모든 사용자가 모든 파일을 읽을 수 있음을 나타내고, allow write: if request.auth != null; 부분은 인증된 사용자만 모든 파일에 쓸 수 있음(즉, 업로드 및 삭제 가능)를 나타냅니다.
결국 두 코드 조각은 각기 다른 서비스(Cloud Firestore와 Firebase Storage)에 대해 어떤 유형의 요청(read/write 등)이 허용될 것인지를 결정하는 보안 설정입니다.
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read;
      allow write: if request.auth != null;
    }
  }
}
   
firebase 스토리 이미지파일을
가져와서 로드하는
코드
import { getStorage, ref, getDownloadURL } from "firebase/storage";

const storage = getStorage();
const gsReference = ref(storage, 'image/B1.PNG');

getDownloadURL(gsReference)
  .then((url) => {
    console.log(url);
  })
  .catch((error) => {
    console.error(error);
  });​

1
.Tag 코드 




.Tag {
    display: inline-block;
    margin-right: 6px;
    margin-bottom: 10px;
    padding: 9px 14px;
    font-size: 12px;
    font-weight: 500;
    line-height: 1;
    color: #333;
    background-color: #9ba7aa;
    border-radius: 25px;
}​

 <a href="/tag_search?tag=%25ED%2587%25B4%25EC%2582
 %25AC%25EC%259C%25A85%2525%25EC%259D%25B4%25ED%2595%2598"
                   className="Tag">#퇴사율5%이하</a>​



1
Top section hight60px;
높이설정하는방법
  <div style={{height: '60px'}} /> {/* Adjust the height as needed */}​

1
   
   
   
.jobdetailboder 

상 하 좌 우
페이지 css
적용하는방법

.jobdetailboder {
    padding: 0 20px; /* 상 and 하 padding: 0, 왼쪽 and 오른쪽 padding: 20px */
    background: #69d0b3;
    border: 1px solid #8dee64; /* or your preferred color/thickness */
    min-height: 200px; /* 최소 위높이 */
    width: auto; /* width auto */
    max-width: 300px /*300너비고정*/
}​

1
동작의 원리 1.
BottomCard.tsx

2.
BottomCardinsider.tsx

3.

JobDetail.tsx

   

구글 인증하는방법

Authentication

새 제공업체 추가

구글 이메일 등록

사이트
Google Cloud Console에 접속한다.

API API 및 서비스






1 구글 인증하는방법






※ 구글 api 

사용자 인증 정보

API 키

Browser key (auto created by Firebase) 2023. 9. 16. 없음

OAuth 2.0 클라이언트 ID

Web client (auto created by Google Service) 2023. 9. 23. 웹 애플리케이션

서비스 계정

fir-oneted@appspot.gserviceaccount.com App Engine default service account
firebase-adminsdk-dkhwn@fir-oneted.iam.gserviceaccount.comfirebase-adminsdk
1
   
탑메뉴 css
.TopMenu {
    display: -ms-flexbox;
    display: flex;
    padding-left: 64px;
    height: 100%;
    -ms-flex-align: stretch;
    align-items: stretch;
}

.TopMenu ul {
    list-style-type: none;
    margin: 0;
    padding: 0;
}

.TopMenu ul li {
    display:inline-block;
    margin-right :10px;
}​

1
.Topfilter1{
    margin-bottom: 25px;
    padding-top: 10px;
    width: 100%;
    padding-bottom: 10px;
}

.Topfilter2 {
    position: fixed;
    left: 0;
    right: 0;
    top: 0;
    border-bottom: 1px solid #36f;
    background-color: #fff;
    z-index: 2;
    padding-top :20px;
    padding-bottom :25px !important;
}



탑 필터를 주므로서 탑은 고정된 상태에서 탑상단은 고정되고
밑에 채용정보의 글의 내용과는 겹치지않고 자연스럽게 보여집니다.


- z-index:2; : z-index 값을 '2'로 설정하여 다른 겹치는요 소보다 위에 위치하도록 합니다(z-index 값이 클수록 위).
-padding-top and -bottom with !important keyword to ensure these styles take precedence over other potentially conflicting styles

1
   
양옆 패딩값 조절하는
div 

    <div className="HiringCompanies" 
    style={{paddingTop:'200px', paddingLeft: '80px', paddingRight: '80px'}}>
모달창에 대한 이해



import {useState} from "react";

const Top2 = () => {

    const [modalVisible, setModalVisible] = useState(false);

    const handleOpenModal = () => {
        setModalVisible(true);
    };

    const handleCloseModal = () => {
        setModalVisible(false);
    };


    return (
        <div className="Top2">
            <div className="buttonContainer" style={{ display: 'flex', alignItems: 'center' }}>
                <button
                    className=""
                    type="button"
                    aria-label="popup navigation button"
                    onClick={handleOpenModal}
                >​

    const [modalVisible, setModalVisible] = useState(false);

    const handleOpenModal = () => {
        setModalVisible(true);
    };

    const handleCloseModal = () => {
        setModalVisible(false);
    };



                    onClick={handleOpenModal}

으로 작동하게된다.
1
모달창
왼쪽
오른쪽 
이동


모달창
전체크기
만드는
코드



.Top2side2{
    position: absolute;
    left: 50px;
    right: 50px;
    bottom: 0;
    top: 100px;
    overflow-y: hidden;
    max-width: 220px;
    height: 70vh;
    background-color: #fff;
    border: 1px solid #e1e2e3;
    -webkit-box-shadow: 0 4px 8px rgba(0,0,0,.15);
    box-shadow: 0 4px 8px rgba(0,0,0,.15);
    border-radius: 5px;
    z-index: 99;
    padding: 15px 0;
}

.Top2side1{
    font-size: 16px;
    padding: 10px 0 10px 25px;
    display: -ms-flexbox;
    display: flex;
    -ms-flex-align: center;
    align-items: center;
    color: #333;
}​

1
모달창
다른곳
누르면
닫히게
하는
코드

import {useEffect, useState} from "react";

const Top2 = () => {
    const [modalVisible, setModalVisible] = useState(false);

    const handleOpenModal = (e) => {
        e.stopPropagation(); // 추가: 부모 요소로의 이벤트 전파를 막음
        setModalVisible(true);
    };

    const handleCloseModal = (e) => {
        if (!e.target.closest('.Top2side2')) { 
        // 추가: 
        클릭한 요소가 .Top2side2 내부에 있는 경우는 제외
            setModalVisible(false);
        }
    };

    useEffect(() => {
        if (modalVisible) {
            window.addEventListener('click', handleCloseModal);
        }

        return () => {
            window.removeEventListener('click', handleCloseModal);
        };

    }, [modalVisible]);


    return (
        <div className="Top2">
            <div className="buttonContainer" style={{ display: 'flex', alignItems: 'center' }}>
                <button
                    className=""
                    type="button"
                    aria-label="popup navigation button"
                    onClick={handleOpenModal}
                >
                    <span>개발</span>
                </button>
                <div className="Top2svg">
\<svg xmlns="https://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 12 12"
\ className="">
\ <path fill="#767676" fill-rule="nonzero"
 d="M2.28 3.22a.75.75 
 0 0 0-1.06 1.06l4.25 4.25a.75.75 0 0 0 1.06 0l4.25-
 4.25a.75.75 0 0 0-1.06-1.06L6 6.94 2.28 3.22z"></path>
                </svg>
            </div>
                {modalVisible &&
                <section role="navigation" class="Top2side2 isKR">
<a href="/wdlist" class="Top2side1" data-attribute-id="jobCategory__click" 
data-job-category-id="all" data-job-category="all">전체</a>
                    {/* ... */}
                    {/* 이곳에 나머지 항목들을 추가하세요 */}
                    {/* ... */}
                    <button onClick={handleCloseModal}>Close</button>
                </section>}

            </div>

1
   
   
   

 

햄버거 모달창
코드


// pages/ToptreeMenu.tsx

import React, { useState } from 'react';

const NavBar = () => {
    const [isOpen, setIsOpen] = useState(false);

    // 메뉴 항목 리스트
    const menuItems = [
        "직군 전체",
        "개발",
        "경영·비즈니스",
        "마케팅·광고",
        "디자인",
        "영업",
        "고객서비스·리테일",
        "미디어",
        "엔지니어링·설계",
        "게임 제작",
        "HR",
        "금융",
        "제조·생산",
        "물류·무역",
        "교육"
    ];

    return (
        <div>
            <button
                type="button"
                aria-label="직군/직무 목록 열기"
                aria-controls="navbar_overlay_explore"
                aria-expanded={isOpen}
                class="Menu_MenuExpandableButton__vPEIA"
                onClick={() => setIsOpen(!isOpen)}
            >
              <span>
                  <span class="SvgIcon_SvgIcon__root__8vwon">
               <img src="../../Toptreemenu.PNG" alt='Toptreemenu'/>
                  </span>
              </span>
            </button>

            {isOpen && (
                <div role='navigation' className='Top2side2 isKR'>
                    {/* map 함수를 이용하여 메뉴 항목들을 렌더링합니다 */}
                    {menuItems.map((item, index) => (
                        <a key={index} className='Top2side1' href={`/${item}`}>{item}</a>
                    ))}
                </div>
            )}
        </div>
    );
};

export default NavBar;

//코딩베낌방지 설리번 코드​

1

 

스타일 컴포넌트 설치 명령어 npm install --save styled-components
   
   

 

 

 

 

 

 

반응형

'컴퓨터공부 > ASAC 웹풀스택' 카테고리의 다른 글

Next.js 공부 정리 (1)  (0) 2023.09.25
git 사용법 정리  (0) 2023.09.19
ASAC 웹 풀스택 리액트 복습  (0) 2023.09.11
Next.js 기본세팅  (0) 2023.09.08

댓글