Vol. 01 · 2026 Web Publisher

PUBLISHING
PORTFOLIO

웹을 한 권의 책처럼
정갈하게 담아냅니다.

서연주

사용자의 시선을 유도하는 퍼블리싱의 정석을 추구합니다.

Profile

이름 : 서연주

나이 : 2001.05.05.

거주지: 김해시 장유동

연락처: 010-8928-6478

" 매체의 특성을 이해하고, 질서 있는 레이아웃으로
정보의 전달력을 극대화하겠습니다.
"

EDUCATION

2025.09 - 2026.03 그린컴퓨터아카데미 UI/UX & 프론트엔드 개발자 양성 수료
2020.03 - 2025.02 경남대학교 심리학과 · 문화콘텐츠학과(복수전공) 졸업
2017.03 - 2020.02 김해 장유고등학교 졸업

EXPERIENCE

2024.04 - 2024.06 경남매일신문 편집기자 인턴 (3개월)

LICENSE

컴퓨터그래픽기능사
GTQ-id (인디자인) 1급
자동차운전면허 2종보통
SKILL

Main Skill

HTML5 시맨틱 마크업을 준수, 웹 표준과 접근성을 고려한 구조를 설계
CSS3 미디어 쿼리를 활용한 반응형 레이아웃 구현 및 애니메이션 최적화
JavaScript add·removeClass 등의 상태 전환, 슬라이드 버튼 등 기능 연결

Design Skill

Adobe 비주얼 리소스 제작, 인쇄 매체의 그리드 시스템을 활용한 레이아웃 기획
Figma 정밀한 위치 정렬 및 타이포그래피, 스타일 라이브러리 설계를 통해 실무 협업에 도움되는 UI 제작

Library Skill

Github 커밋 컨벤션을 준수, 버전 관리와 안정적인 협업 환경 유지
SCSS 공통 변수와 Mixin을 활용, 유지보수가 용이한 스타일 시트 구축
GSAP 타임라인과 스크롤 트리거 활용, 몰입감 있는 인터랙티브 모션 연출
Project
Site01
오리지널 사이트
  • #오리지널
  • #첫작품
  • #1인제작

전통의상과 소품을 판매하는 쇼핑몰 사이트

Site02
멜론 반응형 리디자인
  • #팀 프로젝트
  • #반응형
  • #리뉴얼

별개의 모바일 주소를 사용하는 멜론 사이트를 반응형 사이트로 리뉴얼하는 프로젝트 (비공식)

Site03
동화약품 프로모션 홈페이지
  • #리뉴얼
  • #GSAP
  • #1인제작

자체 리뉴얼 프로젝트

동화약품사 홈페이지의 메인 페이지를 프로모션 형식으로 리뉴얼

keyboard_double_arrow_down scroll
01
Original_site
X

작업기간

9월 28일 ~ 10월 29일 (기획 9일, 코딩 24일)

작업인원

  • 1인
  • |
  • 기획, 디자인, 퍼블리싱 (100%)

사용 기술 및 라이브러리

  • Language: HTML5, SCSS, JavaScript (ES6+)
  • Library: GSAP 3 (Timeline, ScrollTrigger, ScrollToPlugin)
  • Tools: Figma, Adobe Photoshop, VS Code, Git/Github
홈페이지 사용코드
메인 미리보기
메인 비주얼 배너 (CSS only)

[JS 없는 슬라이드]

radio input + label 구조로 JS 없이 배너 전환 구현

[책갈피 인디케이터]

clip-path로 책갈피 모양 인디케이터 제작, :checked 상태로 활성화 표시

메인 미리보기
코디 추천 탭 전환 (CSS only)

[JS 없는 탭 전환]

input:checked + label 구조로 JS 없이 탭 클릭 → 콘텐츠 전환 구현

[상태 연동]

형제 선택자(~)로 체크된 input에 따라 메인 이미지·설명·서브 상품까지 동시 전환

메인 미리보기
타임세일 슬라이더

[외부 버튼 연동]

라이브러리 기본 화살표 대신 커스텀 버튼에 slickPrev/slickNext 직접 연결

[자동재생 설정]

autoplaySpeed·autoHover 옵션으로 hover 시 일시정지 구현

메인 미리보기
더보기 / 접기

[UX 이탈 방지]

slice()로 초기 노출을 8개로 제한, 접기 시 fadeOut + scrollTo로 시선을 타이틀로 재고정

[상태 분기]

버튼 텍스트를 상태값으로 활용해 더보기/접기 동작을 단일 핸들러로 처리

메인 미리보기
카테고리 필터링

[구조 분리]

data-filter 값과 상품 클래스명을 일치시켜 JS 조건문 없이 .filter('.' + filterValue)만으로 카테고리 선별

[Graceful Fallback]

상품 미존재 카테고리 클릭 시 빈 화면 대신 "준비 중" 메시지를 fadeIn으로 대체

메인 미리보기
로그인/회원가입 - 사용자 편의성을 고려한 입력 UX

[자동 포커스]

4자리 입력 완료 시 다음 칸으로, 백스페이스 시 이전 칸으로 포커스 이동

[양방향 동기화]

전체동의 ↔ 개별 체크박스가 서로 상태를 반영, every()로 전체 체크 여부 판별

  • Banner_CSS
  • Category_tab
  • Slick_slider
  • More_btn
  • Filter
  • Login
  • // Banner_CSS — HTML <input type="radio" name="main" id="main_banner01">
    <input type="radio" name="main" id="main_banner02" checked>
    <input type="radio" name="main" id="main_banner03">

    <ul class="banner_slide_wrap">
    <li class="slide_img">
    <a href="./Sub_page/index.html"><img src="./img/banner_01.png" alt="메인 배너 1"></a>
    </li>
    <li class="slide_img">
    <a href="./Sub_page/index.html"><img src="./img/banner_02.png" alt="메인 배너 2"></a>
    </li>
    <li class="slide_img">
    <a href="./Sub_page/index.html"><img src="./img/banner_03.png" alt="메인 배너 3"></a>
    </li>
    </ul>

    <div class="btn_main_banner">
    <label for="main_banner01"></label>
    <label for="main_banner02"></label>
    <label for="main_banner03"></label>
    </div>


    // Banner_CSS — CSS #main_banner01,
    #main_banner02,
    #main_banner03 {
    display: none;
    }

    .visual_banner_outer .slide_img {
    display: none;
    }

    #main_banner01:checked ~ .banner_slide_wrap .slide_img:nth-child(1) { display: block; }
    #main_banner02:checked ~ .banner_slide_wrap .slide_img:nth-child(2) { display: block; }
    #main_banner03:checked ~ .banner_slide_wrap .slide_img:nth-child(3) { display: block; }

    #main_banner01:checked ~ .btn_main_banner label[for="main_banner01"],
    #main_banner02:checked ~ .btn_main_banner label[for="main_banner02"],
    #main_banner03:checked ~ .btn_main_banner label[for="main_banner03"] {
    height: 80px;
    background-color: #2C5C6B;
    border: 1px solid #BAD4DD;
    box-sizing: border-box;
    }

    .btn_main_banner label {
    display: inline-block;
    width: 32px;
    height: 64px;
    margin: 0 8px;
    background-color: #ccc;
    cursor: pointer;
    transition: all 0.3s ease-in-out;
    clip-path: polygon(100% 0, 100% 100%, 50% 75%, 0 100%, 0 0);
    }
  • #btn_recommend01,
    #btn_recommend02,
    #btn_recommend03 {
    display: none;
    }

    #btn_recommend01:checked ~ .event_main_left .sheet_01_left,
    #btn_recommend02:checked ~ .event_main_left .sheet_02_left,
    #btn_recommend03:checked ~ .event_main_left .sheet_03_left {
    display: block;
    }

    #btn_recommend01:checked ~ .event_main_left + .event_main_right .sheet_recommend .sheet_01,
    #btn_recommend02:checked ~ .event_main_left + .event_main_right .sheet_recommend .sheet_02,
    #btn_recommend03:checked ~ .event_main_left + .event_main_right .sheet_recommend .sheet_03 {
    display: flex;
    }

    #btn_recommend01:checked ~ .event_main_left + .event_main_right .recommend_right_bottom .sheet_01_all,
    #btn_recommend02:checked ~ .event_main_left + .event_main_right .recommend_right_bottom .sheet_02_all,
    #btn_recommend03:checked ~ .event_main_left + .event_main_right .recommend_right_bottom .sheet_03_all {
    display: flex;
    }

    #btn_recommend01:checked ~ .event_main_left + .event_main_right .label_recommend label[for="btn_recommend01"],
    #btn_recommend02:checked ~ .event_main_left + .event_main_right .label_recommend label[for="btn_recommend02"],
    #btn_recommend03:checked ~ .event_main_left + .event_main_right .label_recommend label[for="btn_recommend03"] {
    background-color: #2C5C6B;
    color: white;
    }
  • $('.autoplay').slick({
    slidesToShow: 4,
    slidesToScroll: 1,
    autoplay: true,
    autoplaySpeed: 1600,
    autoHover: true,
    arrows: false,
    dots: false,
    infinite: true
    });

    $('#prevBtn').on('click', function () {
    $('.autoplay').slick('slickPrev');
    });

    $('#nextBtn').on('click', function () {
    $('.autoplay').slick('slickNext');
    });
  • const initialShowCount = 8;

    $('#Best_product .best_product').slice(initialShowCount).addClass('hide_item');
    $('#New_arrival .new_product').slice(initialShowCount).addClass('hide_item');

    $('.list_bottom_controls .fold_btn').click(function (e) { e.preventDefault();

    const $button = $(this); const $section = $button.closest('#Best_product, #New_arrival');

    const itemClass = $section.attr('id') === 'Best_product' ? '.best_product' : '.new_product'; const $hiddenItems = $section.find(itemClass + '.hide_item');

    const currentText = $button.text();

    if (currentText === '더보기') { $hiddenItems.removeClass('hide_item').hide().fadeIn(400); $button.text('접기');

    } else { const $allItems = $section.find(itemClass); const $itemsToHide = $allItems.slice(initialShowCount);

    $itemsToHide.fadeOut(400, function () { $(this).addClass('hide_item').show(); });

    $button.text('더보기');

    const $sectionTitle = $section.find('.best_title');
    if ($sectionTitle.length) { $(window).scrollTo($sectionTitle, 500, { offset: -200 });
    }
    }
    });
  • $('.sub_nav_item a').click(function (e) {
    e.preventDefault();

    $('.sub_nav_item a').removeClass('active');
    $(this).addClass('active');

    var filterValue = $(this).attr('data-filter');
    var $productCards = $('.product_card');
    var $comingSoonMsg = $('.coming_soon_message');

    $productCards.hide();
    $comingSoonMsg.hide();

    if (filterValue == 'all' || filterValue == 'f_outer'
    || filterValue == 'f_onepiece' || filterValue == 'f_set') {

    if (filterValue == 'all') {
    $productCards.fadeIn(300);
    } else {
    $productCards.filter('.' + filterValue).fadeIn(300);
    }

    } else {
    $comingSoonMsg.fadeIn(300);
    }
    });
  • 1. 전화번호 입력 자동 포커스 이동
    phoneInputs.forEach((input, index) => {
    input.setAttribute('maxlength', '4');

    input.addEventListener('input', function () {
    this.value = this.value.replace(/[^0-9]/g, '');

    if (this.value.length >= 4) {
    const nextInput = phoneInputs[index + 1];
    if (nextInput) nextInput.focus();
    }
    });

    input.addEventListener('keydown', function (e) {
    if (e.key === 'Backspace' && this.value.length === 0) {
    const prevInput = phoneInputs[index - 1];
    if (prevInput) prevInput.focus();
    }
    });
    });

    ==

    2. 전체동의 체크박스 양방향 동기화 checkAll.addEventListener('change', () => {
    checkboxes.forEach(cb => {
    cb.checked = checkAll.checked;
    });
    });

    checkboxes.forEach(cb => {
    cb.addEventListener('change', () => {
    const isAllChecked = Array.from(checkboxes).every(c => c.checked);
    checkAll.checked = isAllChecked;
    });
    });
02
Responsive_site
X
반응형 프로젝트
  • 다양한 기기에서 즐기는
  • Melon
  • 멜론
코드 리뷰 보기 ↓

작업기간

12월 12일 ~ 1월 27일 (기획 25일, 코딩 21일)

작업인원

  • 4인
  • |
  • 기획(메인 페이지 - 추천 플레이 리스트 섹션, 서브 페이지 - 레이아웃 배치), 퍼블리싱(코딩) (35%)

사용 기술 및 라이브러리

  • Language: HTML5, SCSS, JavaScript (ES6+)
  • Library: GSAP Slick.js (Main Banner), Swiper.js (Coverflow Effect)
  • Tools: Figma, Adobe Illustrator, Photoshop, VS Code, Git/Github
홈페이지 사용코드
메인 미리보기
Slick 반응형 배너

Slick 반응형 배너

메인 비주얼 배너를 Slick 라이브러리로 구현하였습니다. PC(1070px 이상)에서는 전체 너비 1장 슬라이드로, 모바일/태블릿에서는 centerMode와 centerPadding으로 좌우 슬라이드가 살짝 보이는 스와이프형으로 자동 전환됩니다. responsive 옵션의 breakpoint 분기를 활용해 단일 초기화 코드로 두 가지 레이아웃을 동시에 처리하였습니다.

미리보기
최신음악 국내/해외 필터

최신음악 국내/해외 필터

메인 페이지의 최신 음악 섹션에서 탭 클릭 시 전체 / 국내 / 해외 카테고리로 목록을 필터링하도록 구현하였습니다. 각 곡 항목에 data-category 속성을 부여하고, 선택된 탭의 data-target 값과 비교하여 해당 항목만 노출하는 방식으로, HTML 구조 변경 없이 JS만으로 필터 전환이 가능하도록 설계하였습니다.

메인 미리보기
검색 풀다운 & 최근 검색어

검색 풀다운 & 최근 검색어

PC에서 검색창 클릭 시 풀다운이 노출되고, 검색 실행 시 최근 검색어 목록 상단에 동적으로 추가됩니다. 최대 10개 초과 시 가장 오래된 항목을 자동 삭제하며, 외부 영역 클릭 시 풀다운이 닫히도록 document 이벤트로 처리하였습니다. 모바일에서는 검색 아이콘 클릭 시 별도 전체화면 검색 페이지로 전환됩니다.

메인 미리보기
매거진 필터 정렬

매거진 필터 정렬

매거진 목록 상단의 필터(날짜순 / 좋아요순 / 북마크순) 클릭 시 allData 배열을 sort()로 재정렬한 뒤 renderList()를 다시 호출하도록 구현하였습니다. 정렬 변경 시 표시 상태(displayedCount, currentPage)를 초기화하여 항상 첫 화면부터 새로운 순서로 출력됩니다.

메인 미리보기
서브(매거진) - 반응형 목록 렌더링 (더보기 ↔ 페이지네이션)

반응형 더보기 ↔ 페이지네이션

매거진 기사 목록을 화면 너비에 따라 PC(1070px 이상)에서는 페이지네이션, 모바일/태블릿에서는 더보기 방식으로 각각 다르게 렌더링하도록 구현하였습니다. renderList() 함수 내에서 window.innerWidth로 분기하며, resize 이벤트에 연결하여 창 크기가 바뀌어도 자동으로 전환됩니다. 더보기 모드에서는 5개씩 추가 노출되고, 전체 노출 시 버튼이 "접기"로 전환됩니다.

  • Slick_banner
  • Filter
  • Search
  • Sort
  • Pagenation
  • $('.visual_box').slick({
      slidesToShow: 1,
      centerMode: false,
      centerPadding: '0',
      arrows: true,
      dots: true,
      infinite: true,
      autoplay: true,
      autoplaySpeed: 2000,
      pauseOnHover: true,

      responsive: [
        {
          breakpoint: 1070,
          settings: {
            centerMode: true,
            centerPadding: '16%',
            arrows: false
          }
        }
      ]
    });
  • $('.filter_item').click(function () {

      $('.filter_item').removeClass('on_newlist');
      $(this).addClass('on_newlist');

      let target = $(this).attr('data-target');

      if (target === 'all') {
        $('.new_list li').show();
      } else {
        $('.new_list li').hide();
        $('.new_list li[data-category="' + target + '"]').show();
      }
    });
  • function addRecentSearch() {
      const $input = isPC() ? $('.search_input_wrapper input') : $('#searchInput');
      const keyword = $input.val().trim();
      if (keyword === '') return;

      const now = new Date();
      const dateStr = `${now.getMonth() + 1}.${now.getDate()}.`;
      const newItem = `<li>
        <span class="keyword">${keyword}</span>
        <div class="info">
          <span class="date">${dateStr}</span>
          <button class="btn_del">✕</button>
        </div>
      </li>`;

      $('.recent_list').prepend(newItem);
      $input.val('');

      $('.recent_list').each(function () {
        while ($(this).find('li').length > 10) {
          $(this).find('li').last().remove();
        }
      });
    }
  • filterItems.forEach(filter => {
      filter.addEventListener('click', (e) => {
        filterItems.forEach(li => li.classList.remove('on'));
        e.target.classList.add('on');

        const filterType = e.target.innerText;

        if (filterType === '좋아요 순') {
          allData.sort((a, b) => b.likes - a.likes);
        } else if (filterType === '북마크 순') {
          allData.sort((a, b) => b.bookmarks - a.bookmarks);
        } else {
          allData.sort((a, b) =>
            new Date(b.date.replace(/\./g, '-')) - new Date(a.date.replace(/\./g, '-'))
          );
        }

        displayedCount = DEFAULT_COUNT;
        currentPage = 1;
        renderList();
      });
    });
  • function renderList() {
      const isTabletOrMobile = window.innerWidth < 1070;
      let dataToRender = [];

      if (isTabletOrMobile) {
        dataToRender = allData.slice(0, displayedCount);
        if (paginationContainer) paginationContainer.innerHTML = '';
      } else {
        const start = (currentPage - 1) * itemsPerPage;
        dataToRender = allData.slice(start, start + itemsPerPage);
        renderPagination();
      }

      listContainer.innerHTML = dataToRender.map(item => `...`).join('');
      moreBtn.innerText = displayedCount >= allData.length ? "접기" : "더보기";
    }

    moreBtn.addEventListener('click', () => {
      if (displayedCount >= allData.length) {
        displayedCount = DEFAULT_COUNT;
        window.scrollTo({ top: document.querySelector('.filter').offsetTop - 20, behavior: 'smooth' });
      } else {
        displayedCount += 5;
      }
      renderList();
    });

    window.addEventListener('resize', renderList);
03
Promotion_site
X
프로모션 사이트
  • 129년의 역사를 웅장하게
  • DongWha
  • 동화약품
코드 리뷰 보기 ↓

작업기간

1월 28일 ~ 3월 5일 (기획 7일, 코딩 28일)

작업인원

  • 1인
  • |
  • 기획, 디자인, 퍼블리싱 (100%)

사용 기술 및 라이브러리

  • Language: HTML5, SCSS, JavaScript (ES6+)
  • Library: GSAP 3 (Timeline, ScrollTrigger, ScrollToPlugin)
  • Tools: Figma, Adobe Illustrator, Photoshop, VS Code, Git/Github
홈페이지 사용코드
미리보기
메인페이지 - 설립연도 카운팅 애니메이션

[시각적 긴장감]

2020년 이전 구간은 10단위로 빠르게 훑고, 이후 1씩 올라오도록 onUpdate 콜백 안에서 분기 처리

[순차 연출]

Timeline으로 이미지 확장 → 연도 등장 → 카운팅 → 설명 텍스트를 하나의 흐름으로 제어

미리보기
GSAP 인트로 순차 애니메이션

[Hybrid Animation]

Timeline의 onStart 콜백에서 .active 클래스를 추가해 GSAP 타이밍에 맞춰 CSS transition 트리거

[연출 흐름 설계]

4단계 애니메이션을 단일 Timeline으로 묶어 "읽히는 순서"와 "보이는 순서"를 일치시킴

미리보기
핀 네비게이션 연동

[스크롤 동기화]

ScrollTrigger.create()를 content_item마다 등록, onToggle로 현재 섹션에 맞게 핀 active 상태 자동 갱신

[커스텀 스크롤]

핀 클릭 시 targetST.start 값을 역산해 gsap.to()로 ease 적용된 부드러운 이동 구현

미리보기
제품 슬라이더

[커스텀 슬라이드]

Slick 없이 GSAP x값 보간으로 슬라이드 이동 구현, 인덱스 순환 처리 포함

[리스트 연동]

리스트 패널에서 항목 클릭 시 해당 인덱스로 직접 이동 후 패널 자동 닫힘

미리보기
브랜드 스크롤

[무한 루프]

동일 요소 3벌 복제 후 scrollWidth / 3 만큼 이동, repeat: -1로 끊김 없는 자동 흐름 구현

[ease: none]

일정한 속도 유지를 위해 ease 없이 linear 이동 적용

  • Counting
  • Timeline
  • Pin_nav
  • Product_slider
  • Brand_line
  • let countObj = { val: 1897 };

    tl.to(countObj, {
      val: 2026,
      duration: 1,
      ease: "power2.out",
      onUpdate: function () {
        let current = Math.floor(countObj.val);
        if (current < 2020) current = Math.floor(current / 10) * 10;
        $(".company_years").text(current);
      }
    })
  • const tl = gsap.timeline({
      scrollTrigger: {
        trigger: "#page01",
        start: "top 20%",
        toggleActions: "play none none none"
      }
    });

    tl.to(".img_page01", { width: "80%", duration: 1, ease: "power3.inOut" })
      .to(".company_years", { opacity: 1, duration: 0.5 })
      .to(countObj, { val: 2026, duration: 1, /* ... */ })
      .to(".desc_txt", {
        opacity: 1, y: -10, duration: 0.6,
        onStart: function () {
          $(".desc_txt").addClass("active");
        }
      }, "-=0.3")
      .to(".txt_page01", { opacity: 1, y: 0, duration: 0.8 }, "-=0.4");
  • gsap.utils.toArray(".content_item").forEach((item, i) => {
      ScrollTrigger.create({
        trigger: item,
        start: "top center",
        end: "bottom center",
        onToggle: self => {
          if (self.isActive) {
            $(".pin_item").removeClass("active");
            $(".pin_item").eq(i).addClass("active");
          }
        }
      });
    });

    pinItems.forEach((item, index) => {
      item.addEventListener('click', () => {
        const targetId = `content0${index + 1}`;
        const targetST = ScrollTrigger.getAll()
          .find(st => st.trigger && st.trigger.id === targetId);
        if (targetST) {
          let scrollObj = { y: window.scrollY };
          gsap.to(scrollObj, {
            y: targetST.start + 280,
            duration: 1.2,
            ease: "power2.inOut",
            onUpdate: () => window.scrollTo(0, scrollObj.y)
          });
        }
      });
    });
  • function move() {
      const slideWidth = products[0].offsetWidth;
      const moveDistance = (slideWidth + 40) * currentIndex;
      gsap.to(sliderWrapper, { x: -moveDistance, duration: 0.6, ease: "power2.out" });
    }

    nextBtn.addEventListener('click', () => {
      currentIndex = currentIndex < products.length - 1 ? currentIndex + 1 : 0;
      move();
    });

    listItems.forEach((item) => {
      item.addEventListener('click', () => {
        currentIndex = Number(item.getAttribute('data-target'));
        move();
        productList.classList.remove('on');
      });
    });
  • const topUl = document.querySelector('.brand_line_top');

    gsap.to(topUl, {
      x: () => -(topUl.scrollWidth / 3),
      duration: 30,
      repeat: -1,
      ease: "none"
    });

Vol. 01 · 2025

Thanks
For
Reading

읽어주셔서 감사합니다.

Contact
마무리 사진
ⓒ 2025 · Publisher Portfolio