프로젝트 개요

  • 주요 기능
    • 반응형 디자인: 웹 페이지의 레이아웃을 태블릿 및 랩탑 화면 크기에 맞게 조정한다.
    • 해와 달, 파동: 웨이브 효과를 통해 자연스럽게 움직이는 파동을 만들고, 해와 달을 동적으로 이동시켜 시각적 효과를 부여한다.
    • 워드 클라우드: 기술 스택을 시각적으로 표현하는 워드 클라우드를 구현한다.
    • 타이핑 효과: 타자기 타이핑 효과를 사용하여 페이지가 로드될 때까지 텍스트가 동적으로 나타나도록 한다.
  • Category
  • Web 프로젝트
  • Period
  • 2023.10.23.~2024.01.21.
  • GitHub
  • https://github.com/Isaac-Seungwon/portfolio.git

HTML

<!-- 별 -->
<div id="space" class="space">
  <div class="star"></div>
</div>

<!-- 해와 달 -->
<div class="sun"></div>
<div class="moon">
  <img src="assets/img/isaac-blog.png" alt="isaac-blog-image" class="moon-image">
  <p class="moon-text">클릭 시 기술 블로그로 이동합니다.</p>
</div>

<!-- 해와 달의 파동 -->
<div id="wave-sun" class="wave"></div>
<div id="wave-moon" class="wave"></div>


CSS

.space {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: -1;
}

.star {
  position: absolute;
  width: 2px;
  height: 2px;
  background-color: #FFFFFF;
  border-radius: 50%;
  animation: twinkling infinite;
}

@keyframes twinkling {
  0% {
    opacity: 0.3;
  }

  50% {
    opacity: 0.8;
  }

  100% {
    opacity: 0.3;
  }
}

.sun {
  position: fixed;
  width: 12rem;
  /* 200px */
  height: 12rem;
  /* 200px */
  background-color: #FFFF00;
  border-radius: 50%;
  top: 80%;
  left: 50%;
  opacity: 0.9;
  transform: translate(-50%, -50%);
  transition: top 0.5s, left 0.5s;
  z-index: 1;
}

.moon {
  position: fixed;
  width: 6rem;
  /* 100px */
  height: 6rem;
  /* 100px */
  background-color: #FFFFFF;
  border-radius: 50%;
  top: 180%;
  left: 50%;
  opacity: 0.9;
  transform: translate(-50%, -50%);
  transition: top 0.5s, left 0.5s;
  z-index: 3;
}

.moon-image {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  z-index: 3;
}

.sun,
.moon {
  cursor: pointer;
  transition: opacity 0.3s ease;
}

.sun:hover {
  opacity: 1;
}

.moon:hover {
  opacity: 1;
}

/* 달 텍스트 스타일 */
.moon-text {
  width: 300px;
  left: -130%;
  opacity: 0;
  transition: opacity 0.5s ease-in-out;
  color: white;
  position: absolute;
  top: 80px;
  pointer-events: none; /* 호버 효과 비활성화 */
  cursor: default;
}

@keyframes fadeInText {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes fadeOutText {
  from {
    opacity: 1;
    transform: translateY(0);
  }
  to {
    opacity: 0;
    transform: translateY(20px);
  }
}

/* 달 호버 효과 */
.moon:hover .moon-text {
  opacity: 1;
  animation: fadeInText 0.6s ease-in-out forwards;
}

/* 달이 호버되지 않은 경우 */
.moon:not(:hover) .moon-text {
  opacity: 0;
  animation: fadeOutText 0.3s ease-in-out forwards;
}

.content {
  padding: 20px;
  margin-top: 200px;
  z-index: 2;
}

section {
  margin-bottom: 25px;
  padding: 20px;
  background-color: #f9f9f9;
  border-radius: 8px;
}

h2 {
  color: #ee0000;
}

p {
  color: #555;
}

/* 태블릿 가로 화면 (화면 너비 769px - 1024px) */
@media (min-width: 769px) and (max-width: 1024px) {
  .sun {
    width: 120px;
    height: 120px;
  }

  .moon {
    width: 60px;
    height: 60px;
  }
}

/* 랩탑 화면 (화면 너비 1025px - 1600px) */
@media (min-width: 1025px) and (max-width: 1600px) {
  .sun {
    width: 150px;
    height: 150px;
  }

  .moon {
    width: 75px;
    height: 75px;
  }
}

/* 파동 효과 */
#wave-sun {
  position: fixed;
  top: 80%;
  left: 50%;
  width: 60rem;
  /* 1000px */
  height: 60rem;
  /* 1000px */
  background: radial-gradient(circle, rgba(200, 200, 200, 0.6) 0%, rgb(0, 0, 0, 0) 70%);
}

#wave-moon {
  position: fixed;
  top: 180%;
  left: 50%;
  width: 50rem;
  /* 1000px */
  height: 50rem;
  /* 1000px */
  background: radial-gradient(circle, rgba(230, 230, 230, 0.2) 0%, rgb(0, 0, 0, 0) 60%);
}

.wave {
  position: absolute;
  transform-origin: center bottom;
  animation: waveAnimation 4s infinite alternate ease-in-out;
  z-index: 0;
  pointer-events: none;
}

@keyframes waveAnimation {
  0% {
    transform: scale(1);
    opacity: 0.8;
  }

  100% {
    transform: scale(1.1);
    opacity: 0;
  }
}


JavaScript

/*
 * 별 생성
*/
const numstar = 400; // 별 개수
const space = document.querySelector('.space');

// 별 사이의 간격 계산
const getRandomInterval = () => {
  return Math.random() * 10 + 3;
};

// 별 생성 및 스타일 설정
for (let i = 0; i < numstar; i++) {
  const star = document.createElement('div');
  star.className = 'star';
  star.style.top = `${Math.random() * 100}%`;
  star.style.left = `${Math.random() * 100}%`;

  const starize = Math.random() * 2 + 0.5;
  star.style.width = `${starize}px`;
  star.style.height = `${starize}px`;

  star.style.animationDuration = `${getRandomInterval()}s`;
  space.appendChild(star);
}

// 별 반짝임 효과
const twinklestar = () => {
  const star = document.querySelectorAll('.star');
  star.forEach(star => {
    star.style.animation = 'none';
    star.offsetHeight;
    star.style.animationDuration = `${getRandomInterval()}s`;
    star.style.animation = null;
  });
};

/*
 * 해와 달 생성
*/
let minus = 250;
const sun = document.querySelector('.sun');
const moon = document.querySelector('.moon');
const content = document.querySelector('.content');
const windowHeight = window.innerHeight;
const contentTop = content.offsetTop - (windowHeight + 500 - minus);
const opacityTop = document.getElementById('resume').offsetTop - windowHeight;

const waveSun = document.getElementById('wave-sun');
const waveMoon = document.getElementById('wave-moon');

let moonOpacity = 0.9;
const opacityThreshold = opacityTop + 50;

// 해, 달, 파동 위치 업데이트
const updateWavePosition = () => {
  // 해와 달 위치
  const sunPosition = sun.getBoundingClientRect();
  const moonPosition = moon.getBoundingClientRect();

  // 파동의 위치
  waveSun.style.top = `${sunPosition.top - 350}px`;
  waveSun.style.left = `${sunPosition.left - 400}px`;

  waveMoon.style.top = `${moonPosition.top - 340}px`;
  waveMoon.style.left = `${moonPosition.left - 365}px`;
};

// 페이지 스크롤 시 애니메이션 업데이트
const updateWaveAnimation = () => {

  // 스크린 백분율 계산
  let scrollPercentage = window.scrollY / contentTop;
  let opacityPercentage = (window.scrollY - contentTop - 370 + minus) / (opacityTop - contentTop) * 10;

  scrollPercentage = Math.min(scrollPercentage, 1);
  opacityPercentage = Math.min(opacityPercentage, 1);

  // 각도 계산
  const angle = Math.PI * scrollPercentage;

  // 달 투명도
  moonOpacity = 0.9 - opacityPercentage;
  if (moonOpacity > 0.9) {
    moonOpacity = 0.9;
  }
  moon.style.opacity = moonOpacity;

  // 해와 달 위치 조정
  sun.style.top = `${50 - 40 * Math.cos(angle) + 70}%`;
  sun.style.left = `${50 + 40 * Math.sin(angle)}%`;
  moon.style.top = `${50 + 40 * Math.cos(angle) + 74}%`;
  moon.style.left = `${50 - 40 * Math.sin(angle)}%`;
  waveSun.style.transform = `rotate(${angle}rad) scaleY(${Math.sin(angle)}`;
  waveMoon.style.transform = `rotate(${angle}rad) scaleY(${Math.sin(angle)}`;

  // 달의 파동의 투명도
  let dynamicOpacity = 0.22 * moonOpacity;
  // console.log(moonOpacity + ', ' + dynamicOpacity);

  // 달의 투명도가 0인 경우
  if (moonOpacity > 0) {
    moon.style.pointerEvents = 'auto';
    waveMoon.style.background = `radial-gradient(circle, rgba(230, 230, 230, ${dynamicOpacity}) 0%, rgb(0, 0, 0, 0) 60%)`;

    // 달 클릭 이벤트
    moon.addEventListener('click', (event) => {
      window.open('https://isaac-christian.tistory.com/', '_blank');
    });
  } else {
    moon.style.pointerEvents = 'none';
    moon.style.opacity = 0;
    waveMoon.style.background = '#FFFFFF00';
  }

  updateWavePosition();

  // 페이지 스크롤 백분율에 따른 배경색 및 글자색 변경
  if (scrollPercentage > 0.5) {
    document.body.style.backgroundColor = '#011936';
  } else {
    document.body.style.backgroundColor = '#6395ED';
  }

  requestAnimationFrame(updateWaveAnimation);
};

updateWaveAnimation();

HTML

<!-- 워드 클라우드 -->
<div id="word-cloud-container"></div>


CSS

#word-cloud-container {
  position: absolute;
  width: 100%;
  height: 100vh;
  overflow: auto;
  display: flex;
  justify-content: center;
  align-items: center;
  overflow: hidden;
}

.word-cloud-text {
  cursor: default;
  transition: all 0.3s;
  top: 0;
}

@media (max-width: 1440px) {
  .sun,
  .moon,
  .wave {
    display: none;
  }
  #word-cloud-container {
    height: 113vh;
  }
}

@media (max-width: 768px) {
  .sun,
  .moon,
  .wave {
    display: none;
  }
  #word-cloud-container {
    height: 113vh;
  }
}


JavaScript

/*
 * 워드 클라우드
*/
const words = [
  { text: "Java", size: 60, color: "white", opacity: 1, tags: ["언어", "Java"] },
  { text: "SQL", size: 55, color: "white", opacity: 1, tags: ["데이터베이스", "SQL"] },
  { text: "R", size: 50, color: "white", opacity: 1, tags: ["언어", "R"] },
  { text: "C", size: 50, color: "white", opacity: 1, tags: ["언어", "C"] },
  { text: "C++", size: 50, color: "white", opacity: 1, tags: ["언어", "C++"] },
  { text: "Eclipse", size: 60, color: "white", opacity: 1, tags: ["도구", "Eclipse"] },
  { text: "Oracle", size: 60, color: "white", opacity: 1, tags: ["데이터베이스", "Oracle"] },
  { text: "VS Code", size: 40, color: "white", opacity: 1, tags: ["도구", "VSCode"] },
  { text: "DBeaver", size: 40, color: "white", opacity: 1, tags: ["도구", "DBeaver"] },
  { text: "Git", size: 55, color: "white", opacity: 1, tags: ["도구", "Git"] },
  { text: "Photoshop", size: 45, color: "white", opacity: 1, tags: ["도구", "Photoshop"] },
  { text: "Illustrator", size: 40, color: "white", opacity: 1, tags: ["도구", "Illustrator"] },
  { text: "Premiere Pro", size: 40, color: "white", opacity: 1, tags: ["도구", "Premiere Pro"] },
  { text: "JSP", size: 50, color: "white", opacity: 1, tags: ["언어", "JSP"] },
  { text: "Servlet", size: 45, color: "white", opacity: 1, tags: ["언어", "Servlet"] },
  { text: "JDBC", size: 45, color: "white", opacity: 1, tags: ["데이터베이스", "JDBC"] },
  { text: "Python", size: 40, color: "white", opacity: 1, tags: ["언어", "Python"] },
  { text: "HTML", size: 50, color: "white", opacity: 1, tags: ["언어", "HTML"] },
  { text: "CSS", size: 45, color: "white", opacity: 1, tags: ["언어", "CSS"] },
  { text: "JavaScript", size: 50, color: "white", opacity: 1, tags: ["언어", "JavaScript"] },
  { text: "Spring", size: 50, color: "white", opacity: 1, tags: ["프레임워크", "Spring"] },
  { text: "MyBatis", size: 45, color: "white", opacity: 1, tags: ["프레임워크", "MyBatis"] },
  { text: "jQuery", size: 45, color: "white", opacity: 1, tags: ["라이브러리", "jQuery"] },
  { text: "SPSS", size: 40, color: "white", opacity: 1, tags: ["통계 소프트웨어", "SPSS"] },
  { text: "AWS", size: 55, color: "white", opacity: 1, tags: ["클라우드", "AWS"] },
  { text: "jSoup", size: 40, color: "white", opacity: 1, tags: ["라이브러리", "jSoup"] },
  { text: "Selenium", size: 40, color: "white", opacity: 1, tags: ["프레임워크", "Selenium"] },
  { text: "SDL", size: 40, color: "white", opacity: 1, tags: ["라이브러리", "SDL"] },
  { text: "STS", size: 50, color: "white", opacity: 1, tags: ["도구", "STS"] },
  { text: "Restful API", size: 45, color: "white", opacity: 1, tags: ["라이브러리", "Restful API"] },
  { text: "Elasticsearch", size: 40, color: "white", opacity: 1, tags: ["라이브러리", "Elasticsearch"] },
  { text: "WSL", size: 40, color: "white", opacity: 1, tags: ["도구", "WSL"] },
];

const colorByTag = {
  "언어": "#f0cf65",
  "데이터베이스": "#bd4f6c",
  "도구": "#93b5c6",
  "프레임워크": "#d7816a",
  "라이브러리": "#ddedaa",
  "통계 소프트웨어": "#dee2e6",
  "클라우드": "#502e2e" // #af8b60 #482626 #643939
};

// 워드 클라우드 원본 색상 객체
const originalWordSizes = {};

function resizeWordCloud() {
  // 브라우저 창 너비, 높이
  const svgContainer = d3.select("#word-cloud-container");
  const width = window.innerWidth - 100;
  const height = window.innerHeight - 250;

  // 요소 초기화
  svgContainer.selectAll("*").remove();

  const svg = svgContainer
    .append("svg")
    .attr("width", width)
    .attr("height", height)
    .style("position", "absolute")
    .style("left", "50%")
    .style("top", "50%")
    .style("transform", "translate(-50%, -60%)")
    .attr("text-anchor", "middle")
    .append("g")
    .attr("transform", `translate(${width / 2},${height / 2})`);

  const layout = d3.layout.cloud()
    .size([width, height])
    .words(words)
    .padding(5)
    .rotate(function () { return ~~(Math.random() * 2); })
    .font("'SBAggroB', Impact")
    .fontSize(function (d) {
      const originalSize = originalWordSizes[d.text];
      const newSize = (originalSize / 60) * (width / 20);
      const maxSize = originalSize;
      return Math.max(Math.min(newSize, maxSize), 30); // 최소 폰트 크기 30, 최대 폰트 크기 기존 크기
    })
    .on("end", draw);

  layout.start();

  function draw(words) {
    svg
      .selectAll("text")
      .data(words)
      .enter()
      .append("text")
      .attr("class", "word-cloud-text")
      .style("font-size", function (d) { return d.size + "px"; })
      .style("font-family", "'SBAggroB', 'Impact'")
      .style("fill", "transparent")
      .style("stroke", "white")
      .style("stroke-width", "0.2px")
      .style("opacity", function (d) { return d.opacity; })
      .attr("text-anchor", "middle")
      .attr("transform", function (d) {
        return `translate(${d.x},${d.y})rotate(${d.rotate})`;
      })
      .text(function (d) { return d.text; })
      .on("mouseover", function () {
        if (!isOriginalColors) return;
        d3.select(this)
          .style("fill", function (d) { return colorByTag[d.tags[0]] || "white"; })
          .style("stroke", "transparent");
      })
      .on("mouseout", function () {
        if (!isOriginalColors) return;
        d3.select(this)
          .style("fill", "transparent")
          .style("stroke", "white");
      });
  }

  // 해 클릭 이벤트
  let isOriginalColors = true;

  sun.addEventListener("click", () => {
    const wordCloudTextElements = svg.selectAll(".word-cloud-text");
    wordCloudTextElements.each(function (d, i) {
      const wordCloudText = d3.select(this);
      setTimeout(function () {
        if (isOriginalColors) {
          wordCloudText
            .style("fill", "transparent")
            .style("stroke", "white");
        } else {
          wordCloudText
            .style("fill", function (d) { return colorByTag[d.tags[0]] || "white"; })
            .style("stroke", "transparent");
        }
      }, i * 20);
    });
    isOriginalColors = !isOriginalColors;
  });
}

// 초기 워드 클라우드 생성
if (Object.keys(originalWordSizes).length === 0) {

  // 각 단어의 원래 크기를 저장
  words.forEach(word => {
    originalWordSizes[word.text] = word.size;
  });

  setTimeout(resizeWordCloud, 300);
}

HTML

<section id="hero">
    <div class="hero-container">
      <h1>Seungwon Lee</h1>
      <h2>
        <div id="typewriter-text"></div>
      </h2>
      <a href="#about" class="btn-scroll scrollto" title="Scroll Down"><i class="bx bx-chevron-down"></i></a>
    </div>
  </section>


CSS

#typewriter-text {
  margin-top: 20px;
  margin-bottom: 10px;
  position: absolute;
  transform: translate(-50%, -50%);
}


JavaScript

/**
 * 타자기 타이핑 효과
 */
const typewriterText = document.getElementById('typewriter-text');
const texts = [
  'Developer Portfolio          ',
  'I\'m Passionate Developer          ',
  'I Enjoy Programming!          '
];

let textIndex = 0;
let textLength = 0;
let isDeleting = false;
let animationStarted = false;
let typingSpeed = 85; // 출력 속도
let deletingSpeed = 55; // 삭제 속도
let pauseDuration = 100; // 일시 정지

async function startTypewriter() {
  const currentText = texts[textIndex];

  // 타이핑
  while (!isDeleting && textLength <= currentText.length) {
    typewriterText.textContent = currentText.substring(0, textLength);
    textLength++;
    await sleep(typingSpeed);
  }

  // 일시정지
  await sleep(pauseDuration);

  // 삭제
  isDeleting = true;
  while (isDeleting && textLength >= 0) {
    typewriterText.textContent = currentText.substring(0, textLength);
    textLength--;
    await sleep(deletingSpeed);
  }

  // 다음 문장으로 이동
  if (textLength < 0) {
    textIndex = (textIndex + 1) % texts.length; // 텍스트 인덱스 증가
    textLength = 0;
    isDeleting = false;
    await sleep(pauseDuration); // 일시정지
  }

  // 다음 텍스트 반복
  startTypewriter();
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function isInViewport(element) {
  const rect = element.getBoundingClientRect();
  return (
    rect.top >= 0 &&
    rect.left >= 0 &&
    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
    rect.right <= (window.innerWidth || document.documentElement.clientWidth)
  );
}

function handleScroll() {
  if (!animationStarted && isInViewport(typewriterText)) {
    animationStarted = true;
    startTypewriter();
    window.removeEventListener('scroll', handleScroll);
  }
}

window.addEventListener('scroll', handleScroll);
handleScroll();

HTML

<div class="col-lg-6">
  <ul>
    <li><i class="bi bi-chevron-right"></i> <strong>생년월일</strong>2000.02.03 (<span id="age"></span>세)
    </li>
    <li><i class="bi bi-chevron-right"></i> <strong>연락처</strong> <span>010-5213-0157</span></li>
    <li><i class="bi bi-chevron-right"></i> <strong>이메일</strong> <span>zhzkdkrak@naver.com</span>
    </li>
  </ul>
</div>


JavaScript

/**
 * 나이 계산
 */
function calculateAge() {
  const birthDate = new Date('2000-02-03'); // 생년월일
  const currentDate = new Date(); // 현재 날짜
  const ageInMilliseconds = currentDate - birthDate; // 밀리초 단위로 나이 차 계산
  const millisecondsPerYear = 3.15576e10; // 1년의 평균 밀리초
  const age = Math.floor(ageInMilliseconds / millisecondsPerYear); // 나이 계산

  return age;
}

// 나이 표시
const ageElement = document.getElementById('age');
if (ageElement) {
  const age = calculateAge();
  ageElement.textContent = age.toString();
}

HTML

<li onmousemove="showTooltip(event, 'tooltip1')" onmouseout="hideTooltip('tooltip1')">
  <a href="https://magazine.hankyung.com/business/article/202102226965b" target="_blank">
    게임 개발 동아리 Games 연구(창업) 동아리장
  </a>
  <div class="arrow_box" id="tooltip1">[하이틴 잡앤조이 1618] 게임? 저희는 직접 만들어서 해요! 게임 개발의 매력 속으로 Go Go~! (매거진한경)
  </div>
</li>


JavaScript

/*
 * 링크 말풍선
*/
function showTooltip(event, tooltipId) {
  const tooltip = document.getElementById(tooltipId);
  tooltip.style.display = 'block';
  tooltip.style.left = `${event.clientX + 10}px`;
  tooltip.style.top = `${event.clientY + 10}px`;
}

function hideTooltip(tooltipId) {
  const tooltip = document.getElementById(tooltipId);
  tooltip.style.display = 'none';
}

HTML

<div class="col-lg-4 col-md-6 portfolio-item filter-web" data-portfolio-number="12">
  <div class="portfolio-img"><img src="assets/img/portfolio/portfolio-12/dd-land/dd-land 21.png" class="img-fluid" alt=""></div>
  <div class="portfolio-info">
    <h4>놀이동산 [DD-Land]</h4>
    <div class="portfolio-description">
      <p>기존의 DD-Studio 프로젝트를 Spring으로 구현하여 기능을 발전시킨 놀이동산 웹 사이트 프로젝트입니다.</p>
      <table class="detail-table">
        <tr>
          <th><li><strong>Category</strong></li></th>
          <td>Spring 프로젝트</td>
        </tr>
        <tr>
          <th><li><strong>Platform</strong></li></th>
          <td><high>Windows 11</high> <high>Mac OS</high> <high>WSL</high></td>
        </tr>
        <tr>
          <th><li><strong>Front-End</strong></li></th>
          <td><high>HTML</high> <high>CSS</high> <high>JavaScript</high> <high>jQuery</high></td>
        </tr>
        <tr>
          <th><li><strong>Back-End</strong></li></th>
          <td><high>Java</high> <high>JSP</high> <high>JSTL</high> <high>Apache Tomcat</high> <high>MyBatis</high> <high>HikariCP</high></td>
        </tr>
        <tr>
          <th><li><strong>Database</strong></li></th>
          <td><high>Oracle Database 11g</high></td>
        </tr>
        <tr>
          <th><li><strong>Deployment</strong></li></th>
          <td><high>AWS (EC2)</high></td>
        </tr>
        <tr>
          <th><li><strong>Tool</strong></li></th>
          <td><high>STS-3</high> <high>SQL Developer</high> <high>DBeaver</high> <high>Git</high> <high>eXERD</high> <high>Draw.io</high> <high>Sourcetree</high> <high>Google Drive</high></td>
        </tr>
        <tr>
          <th><li><strong>Skill</strong></li></th>
          <td><high>Spring MVC Pattern</high> <high>Spring Security</high> <high>Socket</high> <high>Restful API</high> <high>Tiles</high> <high>Ajax</high> <high>Elasticsearch</high> <high>RegEx</high></td>
        </tr>
        <tr>
          <th><li><strong>API</strong></li></th>
          <td><high>Kakao 지도 API</high> <high>Daum 주소 API</high></td>
        </tr>
        <tr>
          <th><li><strong>Period</strong></li></th>
          <td>2023.12.18.~2023.12.27.</td>
        </tr>
      </table>
    </div>
    <a href="assets/img/portfolio/portfolio-12/dd-land/dd-land 21.png" data-gallery="portfolioGallery"
      class="portfolio-lightbox preview-link" title="놀이동산 [DD-Land]"><i class="fa-solid fa-expand"></i></a>
    <a href="portfolio-details12.html" class="details-link" title="More Details"><i class="fa-solid fa-up-right-from-square"></i></a>
  </div>
</div>


CSS

/*--------------------------------------------------------------
# My Portfolio
--------------------------------------------------------------*/
.portfolio #portfolio-flters {
  list-style: none;
  margin-bottom: 20px;
}

.portfolio #portfolio-flters li {
  cursor: pointer;
  display: inline-block;
  margin: 0 10px 10px 10px;
  font-size: 16px;
  font-weight: 600;
  line-height: 1;
  padding: 7px 18px 11px 17px;
  text-transform: uppercase;
  color: #444444;
  transition: all 0.2s ease-in-out;
  border: 2px solid #f4f4f4;
  border-radius: 2px;
}

.portfolio .portfolio-container {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
}

.portfolio #portfolio-flters li:hover,
.portfolio #portfolio-flters li.filter-active {
  color: #4A70B5;
  border-color: #4A70B5;
  cursor: pointer;
}

.portfolio .portfolio-item {
  margin: 0px 10px 30px 10px;
  box-shadow: 0 10px 29px 0 rgba(68, 88, 144, 0.1);
  padding: 20px;
  text-align: center;
}

.portfolio .portfolio-item {
  overflow: hidden;
}

.portfolio .portfolio-item .portfolio-img img {
  transition: all 0.5s ease-in-out;
}

.portfolio .portfolio-item .portfolio-img img {
  width: 100%;
  height: auto;
  border-radius: 5px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

.portfolio .portfolio-item .portfolio-info {
  background: rgba(252, 252, 252, 0);
  transition: all ease-in-out 0.1s;
  padding: 15px 0 0 0;
  text-align: center;
}

.portfolio .portfolio-item:hover .portfolio-info {
  background: rgb(250, 250, 250);
}

.portfolio .portfolio-item .portfolio-info h4 {
  font-size: 20px;
  color: #333;
  font-weight: 600;
  color: #333;
  margin-bottom: 0px;
}

.portfolio .portfolio-item .portfolio-info p {
  color: #333;
  margin: 10px 0 10px 0;
  font-size: 18px;
}

.fa-expand {
  font-size: 22px;
}

.fa-up-right-from-square {
  font-size: 21px;
}

.portfolio .portfolio-item .portfolio-info .preview-link,
.portfolio .portfolio-item .portfolio-info .details-link {
  display: inline-flex;
  font-size: 24px;
  color: #dcdcdc;
  transition: 0.3s;
}

.portfolio .portfolio-item .portfolio-info .details-link {
  margin-left: 17px;
}

.portfolio .portfolio-item .portfolio-info .preview-link:hover,
.portfolio .portfolio-item .portfolio-info .details-link:hover {
  transform: scale(1.2);
  color: #4284FF;
}

.portfolio-button {
  margin-top: -10px;
}

/* 
.portfolio .portfolio-item .portfolio-info .preview-link:hover {
  rotate: 30deg;
}

.portfolio .portfolio-item .portfolio-info .details-link:hover {
  rotate: 30deg;
}
*/

.portfolio .portfolio-item .portfolio-info .details-link {
  right: 10px;
}

.portfolio .portfolio-item:hover .portfolio-info {
  opacity: 0.9;
}

.portfolio-description {
  text-align: center;
  margin: -5px 0px 20px 0px;
}

.portfolio-description p {
  font-size: 14px;
  color: #333;
}

.portfolio-description::before {
  display: block;
  content: '';
  width: 98%;
  height: 1px;
  text-align: center;
  margin: 15px 0px 10px 0px;
  opacity: 0.3;
  background-color: #d1d0d0; /* e5e5e5 */
}

.portfolio .col-lg-4 {
  display: flex;
  flex-direction: column;
  width: 100%;
}

.portfolio .col-lg-4.portfolio-item {
  border: 2px solid transparent;
  transition: border 0.3s, cursor 0.3s;
  cursor: pointer;
}

.portfolio .col-lg-4.portfolio-item:hover {
  border: 2px solid #4A70B5;
}

#portfolio .col-lg-4 {
  width: 47%;
  /* width: 100%; */
  /* max-width: 400px; */
}

@media (max-width: 576px) {
  .portfolio .col-lg-4.portfolio-item {
    cursor: default;
  }

  .portfolio .col-lg-4.portfolio-item:hover {
    border: 2px solid transparent;
  }
}

@media (max-width: 1200px) {
  #portfolio .col-lg-4 {
    width: 100%;
  }
}

div.portfolio-info > div > table > tbody > tr:nth-child(1) > th {
  width: 25%;
}

/*--------------------------------------------------------------
# Portfolio Details
--------------------------------------------------------------*/
.portfolio-details {
  padding-top: 40px;
  padding-right: 0;
}

.portfolio-details .portfolio-details-slider img {
  width: 100%;
}

.portfolio-details .portfolio-details-slider .swiper-pagination {
  margin-top: 20px;
  position: relative;
}

.portfolio-details .portfolio-details-slider .swiper-pagination .swiper-pagination-bullet {
  width: 12px;
  height: 12px;
  background-color: #fff;
  opacity: 1;
  border: 1px solid #4A70B5;
}

.portfolio-details .portfolio-details-slider .swiper-pagination .swiper-pagination-bullet-active {
  background-color: #4A70B5;
}

.portfolio-details .portfolio-info {
  padding: 30px;
  padding-bottom: 1px;
  box-shadow: 0px 0 30px rgba(59, 67, 74, 0.08);
}

.portfolio-details .portfolio-info h3 {
  font-size: 22px;
  font-weight: 700;
  margin-bottom: 20px;
  padding-bottom: 20px;
  border-bottom: 1px solid #eee;
  text-align: center;
}

.portfolio-details .portfolio-info ul {
  list-style: none;
  padding: 0;
  font-size: 18px;
}

.portfolio-details .portfolio-info ul li+li {
  margin-top: 10px;
}

.portfolio-details .portfolio-description {
  padding-top: 30px;
}

.portfolio-details .portfolio-description h2 {
  font-size: 26px;
  font-weight: 700;
  margin-bottom: 20px;
}

.portfolio-details .portfolio-description p {
  padding: 0;
}

.portfolio-details-content h2 {
  color: rgb(1, 25, 54);
  font-size: 30px;
  font-weight: bold;
}

.portfolio-details-content h2 #sub-title {
  color: #5186e7;
  font-size: 29px;
  letter-spacing: 1px;
  /* text-decoration: underline; */
}

@media (min-width: 300px) and (max-width: 1024px) {
  .material-symbols-outlined {
    opacity: .9;
    vertical-align: middle;
    padding-right: 5px;
  }

  .portfolio-details-content h2 {
    margin-top: 20px;
  }
}

/* 랩탑 화면 (화면 너비 1025px - 1600px) */
@media (min-width: 769px) and (max-width: 1600px) {
  .material-symbols-outlined {
    opacity: .9;
    vertical-align: top;
    padding-top: 10px;
    padding-right: 5px;
  }
}

.portfolio-info ul li strong {
  display: inline-block;
  width: 100px;
  font-weight: bold;
  /* margin-right: 5px; */
  margin-top: 10px;
}

.portfolio-info ul li:first-child strong {
  margin-top: 0;
}

.portfolio-info ul li strong::before {
  content: "•";
  margin-right: 5px;
}

.swiper-button-prev,
.swiper-button-next {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  font-size: 24px;
  color: #4A70B5;
  cursor: pointer;
  z-index: 2;
  opacity: 0.7;
  transition: all 0.5s;
}

.swiper-button-prev:hover,
.swiper-button-next:hover {
  opacity: 1;
}

.swiper-button-prev:hover {
  left: 15px;
}

.swiper-button-next:hover {
  right: 15px;
}

.swiper-button-prev {
  left: 20px;
}

.swiper-button-next {
  right: 20px;
}


JavaScript

/*
 * portfolio detail 이동
*/
document.addEventListener('DOMContentLoaded', function () {
  const portfolioItems = document.querySelectorAll('.portfolio .col-lg-4.portfolio-item');

  portfolioItems.forEach((item) => {
    if (window.innerWidth >= 576) {
      item.addEventListener('click', function (event) {
        // 기본 링크 동작 방지
        event.preventDefault();

        // 클릭한 포트폴리오 아이템에서 포트폴리오 번호 추출
        const portfolioNumber = item.getAttribute('data-portfolio-number');

        // 포트폴리오 번호를 사용하여 포트폴리오 상세 페이지 URL 생성
        const portfolioDetailURL = `portfolio-details${portfolioNumber}.html`;

        // URL로 이동
        window.location.href = portfolioDetailURL;
      });
    }

    // bx-plus 아이콘 클릭 이벤트 리스너를 추가
    const plusIcons = item.querySelectorAll('.portfolio-info .preview-link');
    plusIcons.forEach((icon) => {
      icon.addEventListener('click', function (event) {
        // 상위로 버블링 방지
        event.stopPropagation();
      });
    });
  });
});

/**
 * 반응형 디자인
 */
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
  // 모바일 기기
  //console.log("현재 기기: 모바일");

  var prevButton = document.querySelector('.swiper-button-prev');
  var nextButton = document.querySelector('.swiper-button-next');

  if (prevButton && nextButton) {
    prevButton.style.display = 'none';
    nextButton.style.display = 'none';
  }

} else {
  // 데스크탑 기기
  //console.log("현재 기기: 데스크탑");
  window.addEventListener("resize", resizeWordCloud);
}

/**
 * Porfolio isotope and filter
 */
window.addEventListener('load', () => {
  let portfolioContainer = select('.portfolio-container');
  if (portfolioContainer) {
    let portfolioIsotope = new Isotope(portfolioContainer, {
      itemSelector: '.portfolio-item',
      layoutMode: 'masonry', // Masonry 레이아웃 모드 적용
      masonry: {
        columnWidth: '.portfolio-item',
        horizontalOrder: false,
        gutter: 25 // 간격 설정
      }
    });

    let portfolioFilters = select('#portfolio-flters li', true);

    on('click', '#portfolio-flters li', function (e) {
      e.preventDefault();
      portfolioFilters.forEach(function (el) {
        el.classList.remove('filter-active');
      });
      this.classList.add('filter-active');

      portfolioIsotope.arrange({
        filter: this.getAttribute('data-filter')
      });
    }, true);
  }
});

/**
 * Initiate portfolio lightbox 
 */
const portfolioLightbox = GLightbox({
  selector: '.portfolio-lightbox'
});

/**
 * Portfolio details slider
 */
new Swiper('.portfolio-details-slider', {
  speed: 400,
  loop: true,
  autoplay: {
    delay: 10000,
    disableOnInteraction: false
  },
  pagination: {
    el: '.swiper-pagination',
    type: 'bullets',
    clickable: true
  },
  navigation: {
    nextEl: '.swiper-button-next',
    prevEl: '.swiper-button-prev'
  }
});

HTML

<!-- ======= My Archives Section ======= -->
<section id="archives" class="archives">
  <div class="container">

    <div class="section-title">
      <h2>My Archives</h2>
    </div>

    <div class="row">
      <!-- GitHub -->
      <div class="col-md-6 col-lg-3 d-flex align-items-stretch mb-5 mb-lg-0">
        <a href="https://github.com/Isaac-Seungwon" target="_blank" class="icon-box">
          <div class="icon"><i class="fa-brands fa-github" style="font-size: 40px; margin-top: -3px;"></i></div>
          <h4 class="title">GitHub</h4>
          <p class="description"><b>소스 코드 저장소</b></p>
          <ul>
            <li>프로젝트 및 프로그램 저장</li>
            <li>코딩 연습용 소스코드</li>
          </ul>
        </a>
      </div>

      <!-- Tistory Blog -->
      <div class="col-md-6 col-lg-3 d-flex align-items-stretch mb-5 mb-lg-0">
        <a href="https://isaac-christian.tistory.com/" target="_blank" class="icon-box">
          <div class="icon"><i class="fa-solid fa-book-bookmark"></i></div>
          <h4 class="title">Tistory Blog</h4>
          <p class="description"><b>블로그</b></p>
          <ul>
            <li>학업 기록 및 정보 공유 블로그</li>
          </ul>
        </a>
      </div>

      <!-- Baekjoon Online Judge -->
      <div class="col-md-6 col-lg-3 d-flex align-items-stretch mb-5 mb-lg-0">
        <a href="https://www.acmicpc.net/user/isaac_christian" target="_blank" class="icon-box">
          <div class="icon"><i class="fa-solid fa-laptop-code"></i></div>
          <h4 class="title">Baekjoon Online Judge</h4>
          <p class="description"><b>코딩 스터디</b></p>
          <ul>
            <li>문제 해결 능력을 키우기 위한 알고리즘 공부</li>
          </ul>
        </a>
      </div>
    </div>
  </div>
</section>
<!-- End My Archives Section -->


CSS

/*--------------------------------------------------------------
# My Archives
--------------------------------------------------------------*/
.archives .icon-box {
  padding: 30px;
  width: 100%;
  position: relative;
  overflow: hidden;
  background: #fff;
  box-shadow: 0 10px 29px 0 rgba(68, 88, 144, 0.1);
  transition: all 0.3s ease-in-out;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  border: 2px solid #fff;
}

.archives {
  height: 100%;
}

.icon-box {
  height: 310px;
}

.archives .icon-box:hover {
  border-color: #4A70B5;
  box-shadow: 0 5px 20px 0 rgba(68, 88, 144, 0.1);
  z-index: 100;
}

.archives .row {
  display: flex;
  justify-content: center;
  align-items: center;
}

.archives .icon {
  margin: 0 auto 20px auto;
  padding-top: 17px;
  display: inline-block;
  text-align: center;
  border-radius: 50%;
  width: 70px;
  height: 70px;
  background: #dfe7f1;
}

@media (min-width: 976px) and (max-width: 1300px) {
  .icon-box {
    height: 370px;
  }
}

.archives .icon i {
  font-size: 35px;
  padding: 1px;
  line-height: 1;
  color: #4A70B5;
  transition: all 0.3s ease;
}

.archives .icon-box::before,
.archives .icon-box::after {
  content: '';
  position: absolute;
  top: 22%;
  left: 50%;
  transform: translate(-50%, -50%) scale(1);
  border-radius: 50%;
  width: 70px;
  height: 70px;
  opacity: 10;
  z-index: -1;
}

.archives .icon-box:hover::before {
  opacity: 1;
  transform: translate(-50%, -50%) scale(4);
  transition: all 0.5s ease-in-out;
}

.archives .icon-box:hover::after {
  opacity: 1;
  transform: translate(-50%, -50%) scale(4.24);
  transition: all 1s ease-in-out;
}

.archives .icon-box::before {
  background: rgba(116, 157, 255, 0.075);
  box-shadow: 0 0 6px 6px rgba(116, 157, 255, 0.075);
}

.archives .icon-box::after {
  background: #ffffff;
  box-shadow: 0 0 8px 8px #ffffff;
}

.archives .title {
  font-weight: 700;
  font-size: 20px;
  border-radius: 1px;
  border-bottom: 2px solid transparent;
  position: relative;
}

.archives .title:after {
  content: "";
  position: absolute;
  font-size: 10px;
  bottom: -10px;
  left: 50%;
  transform: translateX(-50%);
}

/* 
.archives .icon-box:hover * {
  color: #fff;
}

.archives .icon-box:hover .icon {
  background: rgba(1, 25, 54, 0.8);
}

.archives .title:after {
  content: "";
  position: absolute;
  width: 5px;
  height: 5px;
  background-color: #fff; 
  bottom: -12px;
  left: 50%;
  transform: translateX(-50%) scaleY(0);
  border-radius: 50%;
  box-shadow: 0 0 10px rgba(114, 146, 205, 0.8), 0 0 5px rgba(255, 255, 255, 0.9);
  opacity: 0;
  transition: transform 0.3s ease, height 0.3s ease, opacity 0.3s ease;
}

.archives .icon-box:hover .title:after {
  transform: translateX(-50%) scaleY(1);
  opacity: 1;
}
*/

/*
.archives .title:after {
  content: "";
  position: absolute;
  width: 100%;
  height: 2px;
  bottom: 0;
  left: 0;
  background-color: #7292cd;
  transform: scaleX(0);
  transform-origin: bottom right;
  transition: transform 0.3s ease;
}

.archives .icon-box:hover .title:after {
  transform: scaleX(1);
  transform-origin: bottom left;
}

.archives .icon-box:hover .icon i {
  color: cornflowerblue;
}
*/

.archives .title a {
  color: #111;
  transition: 0.3s;
}

.archives .description {
  max-width: 300px;
  font-size: 18px;
  line-height: 28px;
  margin-bottom: 0;
  align-content: center;
  color: #111;
}

.archives ul {
  padding: 10px;
  margin-bottom: 0;
  color: var(--bs-body-color);
  text-align: left;
}

.archives .description b {
  display: inline-block;
  margin-bottom: 10px;
}

.archives .icon-box:hover .title a {
  color: #4A70B5;
}