반응형

vis-timeline을 이용하여 아래와 같이 구현을 해보았습니다.

기능적으로 추가를 더 해야되지만 시간이 없어서 아래와 같이만 구현을 하였습니다.

 

 

아래는 소스 코드 입니다.

 

[React]

import React, { useEffect, useRef } from "react";
import vis from "vis-timeline";
import "vis-timeline/dist/vis-timeline-graph2d.min.css";
import "./index.scss";

const VisTimeLine = () => {
    const timelineRef = useRef(null);
    const today = new Date();
    const count = 2000;

    const SetStartTime = 9;
    const SetEndTime = 18; //24hr, 12hr, 6hr 등등 세팅 가능
    
    // 오늘 날짜를 기준으로 시작과 종료 시간을 설정
    const startDate = new Date(today.getFullYear(), today.getMonth(), today.getDate(), SetStartTime, 0, 0); 
    const endDate = new Date(today.getFullYear(), today.getMonth(), today.getDate(), SetEndTime, 0, 0); 

    // 타임라인 옵션
    const options = {
        locale: "en",
        locales: {
            en: {},
        },        
        //showCurrentTime : false, //현재 시간 표기
        showMajorLabels : false, //최상단 날짜 표기 막기
        //showMinorLabels : false, //시간 나오지 않게
        start : startDate,
        end : endDate,
        preferZoom: true,
        moveable : false, //마우스로 이동하는 것 막기
        // verticalScroll: true,
        orientation: { axis: "top", item: "top" },
        height: "92px",
        // maxHeight: 130,            // 최대 높이
        // minHeight: 130,            // 최소 높이
        // zoomMin: 1000 * 60 * 60,
        // zoomMax: 1000 * 60 * 60 * 24 * 365,
        zoomMin: 1000 * 60 * 60, // 최소 줌 레벨 (10분)
        zoomMax: 1000 * 60 * 60 * 44, // 최대 줌 레벨 (1일)
        // groupHeightMode: "fitItems",
        groupHeightMode: "fixed", // 그룹 높이 고정
        stack: false,     //items이 시간이 겹쳐도 겹치게 볼 수 있게 설정
    };

    // 타임라인의 아이템 생성 함수
    const generateItems = (count) => {
        const intervalSeconds = 30

        return Array.from(Array(count)).map((_, index) => ({
            id: index,            
            className : Math.floor(Math.random() * 4) % 2 == 0 ? 'danger' : 'warning',
            group: Math.floor(Math.random() * 4),            
            start: new Date(
                today.getTime() + (intervalSeconds * 1000 * (index+1))
            ),
            end: new Date(
                today.getTime() + (intervalSeconds * 1000 * (index+2))
            ),
        }));
    };    

    // 타임라인의 그룹 생성 함수
    const generateGroups = () => {
        return [
            {
                id: 0,
                content: `Alarm`,
            },
            {
                id: 1,
                content: `Asystole`,
            },
            {
                id: 2,
                content: `V-FIB`,
            },
            {
                id: 3,
                content: `V-TACH`,
            },
        ];
    };

    const handlePlus = () => {
        alert('plus');
    }

    const handleMinus = () => {
        alert('plus');
    }

    const items = generateItems(count);
    const groups = generateGroups();

    useEffect(() => {
        // 타임라인 초기화
        const timeline = new vis.Timeline(
            timelineRef.current,
            items,
            groups,
            options
        );
        
        return () => {
            timeline.destroy();
        };
    }, [items, groups, options]);

    return (
        <div className="timeline-area">
            <div
                ref={timelineRef}
                id="timeline"
                className="timeline"                
            ></div>     
            <div className="control-timeline">
                <div className="time-area">
                    <span>24 hr</span>
                </div>    
                <div className="button-area">
                    <p onClick={() => handlePlus()}>+</p>
                    <p onClick={() => handleMinus()}>-</p>
                </div>
            </div>       
        </div>
    );
};

export default VisTimeLine;

 

[SCSS]

body {
    background: #000;
    font-family: sans-serif;
    font-weight: 700;
}

.timeline-area {
    display:flex;
    align-items: center;
    margin: 20px;
    border: 1px solid #fff;
    border-radius: 10px;    
    overflow:hidden;

    .timeline{
        width:95%;
        height:92px;    
    }

    .control-timeline{
        width:120px;
        height:92px;   
        display:flex;
        justify-content:space-between;
        align-items: center;
        background-color:#2F2F2F;
        color:#fff;

        .time-area{            
            display:flex;
            justify-content: center;
            align-items: center;
            padding:0 20px;

            span{
                font-size:12px;
                white-space:nowrap;
            }
        }

        .button-area{
            width:50px;            
            height:100%;
            flex-direction: column;
            background:#4F4F4F;

            p{                
                height:50%;
                display:flex;
                align-items: center;
                justify-content: center;
                text-align: center;
                margin:0;
                cursor: pointer;
            }
        }
    }
}

// 아이템의 높이 및 기본 설정
.vis-item {
    top: 0 !important;
    height: 18px !important;
    line-height: 18px !important;

    &.danger {
        color: white;
        background-color: #ff6d87;
        border: none;
    }

    &.warning {
        color: white;
        background-color: #fff500;
        border: none;
    }
}

// 그룹의 높이 고정
.vis-group {
    height: 18px !important; 
}

// 아이템이 그룹의 영역을 초과하지 않도록 제한
.vis-label,
.vis-foreground .vis-item,
.vis-background .vis-item {
    max-height: 18px;
}

// Minor labels 스타일 설정
.vis-time-axis {
    .vis-text {
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 12px;
        font-weight: 700;
        padding: 6px 0;
        height: 20px; 
        line-height: 20px; 
    }
}

// 그룹 레이블 스타일
.vis-labelset {
    .vis-label {
        width: 200px;
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 12px;
        background-color: #2F2F2F;
    }
}

.vis-timeline {
    border: none;

     // 날짜 표시 숨기기
    .vis-date {
        display: none;
    }

    .vis-current-time {
        position: absolute;
        top: 27px !important;
        left: 0;
        width: 3px;
        text-align: center;
        z-index: 1;
        background-color: blue; // 현재 시간 CSS
    }    
}
반응형

React에서 Drag & Drop이 필요하여 아래와 같이 만들었습니다.

만들면서 HTML 구조와 React에서 사용 할 수 있게 작업 하였습니다.

 

자세한 사항은 아래 소스 중간중간에 주석을 남겨 두었으니 참고 바랍니다.

function uploadmodal({...props}) {    
    const [isTxtActive, setTxtActive] = useState(false);
    const [isWavActive, setWavActive] = useState(false);
    
    //Drag & Drop
    const handleDragStart = (type) => type == 'txt' ? setTxtActive(true) : setWavActive(true); //Drag를 시작 할 때 이벤트
    const handleDragEnd = (type) => type == 'txt' ? setTxtActive(false) : setWavActive(false); //Drag가 끝났을 때 이벤트
    const handleDragOver = (event,type) => { //파일을 드래그 하고 놨을 때 (이게 없으면 브라우저에서 실행 되거나 다운로드가 되는 증상이 나타남)
        event.preventDefault();
        event.stopPropagation();        
    };    

    const handelDropFile = (e,type) => { //파일을 놨을 때 이벤트
        e.preventDefault();
        e.stopPropagation();

        const newFiles = e.dataTransfer.files[0]; 
        console.log('[handelDropFile]newFiles :' ,newFiles);
    }    

	//label안에 css인 pointer-events:none를 하지 않으면 내용물 안에 영역으로 들어 갔을 때 DragEnter / DragLeave가 한번씩 타면서 초기화가 되는 증상이 나타남.

    return (
        <label 
            className={`${isTxtActive ? 'bg-[#efeef3] border-[#000]' : ''} w-[50vw] max-w-[1000px] min-w-[400px] flex flex-col items-center justify-center border-2 p-[20px] rounded-lg border-dotted mb-[10px] cursor-pointer`} 
            htmlFor="txt-file"
            onDragEnter={() => handleDragStart('txt')}
            onDragOver={handleDragOver}
            onDragLeave={() => handleDragEnd('txt')}                                        
            onDrop={(e) => handelDropFile(e,'txt')}
        >
            <div className="w-[50px] h-[50px] mb-[20px] pointer-events-none" >
                <img src={txtFileIcon} alt={txtFileIcon} />
            </div>
            <div className="mb-[10px] pointer-events-none" >                                    
                <span className="cursor-pointer text-[14px] bg-[#444] text-white px-[10px] py-[5px] rounded-full">파일 선택</span>                                            
            </div>
            <p className="pointer-events-none">또는</p>
            <p className="pointer-events-none">여기로 <span className="font-bold">텍스트(.txt) 파일</span>을 드래그 하세요.</p>                                        
            <input type="file" accept=".txt" id="txt-file" name="txtfile" style={{display:'none'}}></input>
        </label>   
    );
}

export default uploadmodal;
반응형

div 2개만 이용하고 Percent를 변경하여 사용하고 싶어서 아래와 같이 만들었습니다.

 

 

HTML (React)

uploadPercent는 useState로 변경 하는 로직을 사용 하였습니다.

<div className="progress-bg">
    <div className="progress-bar" style={{width:`${uploadPercent}%`}}></div>
</div>

 

CSS (SASS)

/* 프로그레스바 배경 */
.progress-bg {
    width: 100%;
    height: 10px;
    background-color: #aeb5c4; /* 배경색 */
    border-radius: 15px;
    overflow: hidden;

    /* 진행 부분 */
    .progress-bar {
        height: 100%;
        width: 0%; /* 진행 상태: 0% ~ 100% */
        background-color: #193263; /* 진행바 색상 */
        opacity: 0.9;
        border-radius: 15px 0 0 15px;
        transition: width 0.3s ease;
    }
}

 

반응형

기존 JavaScript로 된 페이징 버전 함수는 만들어두어서 그대로 사용하면 되는데 React 버전은 따로 만들어 둔게 없어서 아래와 같이 컴포넌트로 만들었습니다.

 

부모 컴포넌트

const [fileList, setFileList] = useState([]) //전체 리스트 데이터
const [onPageChange, setOnPageChange] = useState(1) //현재 페이징 번호
const [onDataPerPage, setOnDataPerPage] = useState(1) //페이지 별 보여지는 리스트 수
const [pageList, setPageList] = useState([]) //현재 페이징에 보여져야 될 리스트

//현재 Page에 맞는 리스트가 보여지게 끔 세팅
const handlePageView = () => {        
    const indexOfLastData = onPageChange * onDataPerPage;
    const indexOfFirstData = indexOfLastData - onDataPerPage;
    const currentData = fileList.slice(indexOfFirstData, indexOfLastData);

    setPageList(currentData);
}

//파일 리스트 최초 검색, 페이징 변경 되면 세팅 시기기
useEffect(() => {
    handlePageView()
}, [fileList,onPageChange])

return (
	<>
    	<Pagination totalData={fileList ? fileList.length : 0} dataPerPage={onDataPerPage} pageCount={5} currentPage={onPageChange} onPageChange={setOnPageChange}/>
    </>
)

 

자식 컴포넌트

mostback : '<<'

left : '<'

right : '>'

mostfront : '>>'

function pagination ({totalData,dataPerPage,pageCount,currentPage,onPageChange}){
    const [pagination, setPagination] = useState({
        firstPage: 1,
        lastPage: 1,
        nextPage: 1,
        prevPage: 1,
    });

    useEffect(() => {
        handlePaging(totalData, dataPerPage, pageCount, currentPage);
    }, [totalData, dataPerPage, pageCount, currentPage]);

    const handlePaging = (totalData, dataPerPage, pageCount, currentPage) => {
        let totalPage = Math.ceil(totalData / dataPerPage); // 총 페이지 수
        let pageGroup = Math.ceil(currentPage / pageCount); // 페이지 그룹

        let lastPage = pageGroup * pageCount; // 화면에 보여질 마지막 페이지 번호
        if (lastPage > totalPage) lastPage = totalPage;

        let firstPage = lastPage - (pageCount - 1); // 화면에 보여질 첫 번째 페이지 번호
        if (firstPage < 1) firstPage = 1;

        let prevPage;
        if (currentPage - 1 < 1) {
            prevPage = 1;
        } else {
            prevPage = currentPage - 1;
        }

        let nextPage;
        if (lastPage <= totalPage) {
            if((currentPage + 1) > totalPage){
                nextPage = lastPage;
            }else{
                nextPage = currentPage + 1;
            }
        } else {
            nextPage = lastPage;
        }

        setPagination({ firstPage, lastPage, nextPage, prevPage });
    };

    const handleClick = (page) => {
        if (onPageChange) {
            onPageChange(page);
        }
    };

    const { firstPage, lastPage, nextPage, prevPage } = pagination;

    return (
        <div className="pagination">
            <span className="first_btn" onClick={() => handleClick(1)}>
                <img src={mostback} alt="First" />
            </span>
            <span className="prev_btn" onClick={() => handleClick(prevPage)}>
                <img src={left} alt="Previous" />
            </span>

            {/* 페이지 번호들 */}
            {[...Array(lastPage - firstPage + 1).keys()].map((i) => (
                <span
                    key={i + firstPage}
                    className={currentPage === i + firstPage ? "current" : ""}
                    onClick={() => handleClick(i + firstPage)}
                >
                    {i + firstPage}
                </span>
            ))}

            <span className="next_btn" onClick={() => handleClick(nextPage)}>
                <img src={right} alt="Next" />
            </span>
            <span
                className="last_btn"
                onClick={() => handleClick(Math.ceil(totalData / dataPerPage))}
            >
                <img src={mostfront} alt="Last" />
            </span>
        </div>
    );
};

export default pagination;
반응형

framer-motion이라는 라이브러리를 이용하여 간단한 애니메이션을 구현 할 수 있다.

아래는 framer-motion을 설치하는 방법의 관련된 URL 입니다.

 

https://www.npmjs.com/package/framer-motion

 

framer-motion

A simple and powerful JavaScript animation library. Latest version: 11.0.5, last published: 7 days ago. Start using framer-motion in your project by running `npm i framer-motion`. There are 3777 other projects in the npm registry using framer-motion.

www.npmjs.com

 

framer-motion을 사용하면 초기시점 , 애니메이션 , 끝났을 때(사라질 때)를 선택하여 애니메이션을 만들 수 있습니다.

아래와 같이 지정하면 아코디언을 만들 수 있습니다.

 

주의 사항

꼭 변동이 되는 부분의 <AnimatePresence>로 감싸주어야지만 사라질 때 (exit) 동작 기능이 됩니다.

'IT > React' 카테고리의 다른 글

[React] Drag & Drop 파일 업로드  (1) 2024.09.26
[React] 페이징 Paging  (0) 2024.09.24
[React] Debounce 정리  (0) 2023.08.23
[React] uncontrolled input이라는 에러 해결  (0) 2023.08.08
[React] 커스텀 Hooks 정리  (0) 2023.08.08
반응형

Webpack을 사용하여 이미지를 경로에 넣으려면 webpack.config.js를 수정하지 않으면 넣기가 어렵습니다.

 

아래와 같이 2가지를 추가해주고 소스 상에서는 alias로 되어있는 부분으로 이미지를 가져와서 사용하면 됩니다.

 

webpack.config.js

 

아래는 사용 하는 방법 입니다.

 

'IT > Webpack' 카테고리의 다른 글

[WebPack] Debug 크롬 개발자 도구 Setting  (0) 2024.02.19
[Webpack] scss 를 사용하기 위한 세팅  (0) 2024.02.19
반응형

※ Debounce이란?

- 이벤트를 호출 시 연속적으로 여러번 호출 되는 것이 아닌 일정한 시간 동안 지연 시간을 주어 반복 횟수를 제어하는 것 입니다.

 

import { useRef, useState } from 'react';

export const useDebounce = (value) => {
	const [DebouncedVal, setDebouncedVal] = useState(value);
	const eventBlocker = useRef(null);

	//Timeout을 초기화 시켜줍니다.
	clearTimeout(eventBlocker.current);

	//일정 시간 동안 지연을 시켜줍니다.
	eventBlocker.current = setTimeout(() => {
		setDebouncedVal(value);
	}, 500);

	return DebouncedVal;
};

 

 

 

반응형

- Next에서는 Autoplay, Pagination, Navigation 기능을 활성화하기 위해 SwiperCore.use 사용

SwiperCore.use([Autoplay]);

<Swiper
    className={clsx(styles.swiper)}
    modules={[Autoplay]}
    autoplay={{ delay: 2000, disableOnInteraction: true }}
    loop={true}
    grabCursor={true}
    slidesPerView={1}
    spaceBetween={100}
    centeredSlides={true}
    breakpoints={{
        1200: {
            slidesPerView: 3,
            spaceBetween: 50,
        },
    }}
>

+ Recent posts