UI는 굳이 이쁘게 만들 생각이 없었고
원하던 기능만 말해보자면..
사진 갯수에 제한 안두고 사진을 보여주고 싶음 -> 사실 몇개가 될지 모름
사진이동
1.1. 기본은 사진이 자동으로 넘어가게 할건데
1.2. 원하면 사진을 직접 넘길수도 있어야 한다탭 이동하면 지정해둔 사진 바뀌어야 한다
별거 아니라고 생각했는데 마지막께 처음을 바라보고 있어야 하며
내가 구현한거는 양 옆에 사진 두장도 짜잔하고 보여주고 있어야 하므로
구현해두고도 이게 이렇게 복잡한 게 맞나 싶은 느낌이다.
구현 기술과 관련된 설명은 참고 블로그 원 에서 정말 상세하게 잘 다뤄주고 있기 때문에
나는 생략하도록 하겠다. 😇
사진은 /public/store_image 에 폴더를 나누어서 저장해두었고.
네이밍이 이것밖에 안되는 이유는 ...더보기 우끼끼 🐵
대충 src 내의 경로는 이런 식으로 되어 있다.
레츠고
css 몇개랑 slide 에서 사용할 아이콘 좀 저장해두려고 한다.
1. main.js
/*[main.css]*/
@import "./reset.css";
html,body{
width: 100%;
height: 100%;
}
body{
font-size: 14px;
color: #333333;
}
*{
box-sizing: border-box;
padding: 0;
margin: 0;
}
*::before,*::after{
box-sizing: border-box;
}
a{
text-decoration: none;
color: inherit;
}
button{
background-color: transparent;
border: none;
}
:root{
--color_main: #3366FF;
}
2. reset.css
/*[reset.css]*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
img{
max-width: 100%;
max-height: 100%;
}
button,a{
cursor: pointer;
}
3. icons 폴더
그냥 이 폴더는 코드 쭉쭉쭉 나열할게요..
링크로 아이콘 가져오는 부분일 뿐이니까...
ic_alert.svg
<svg xmlns="https://www.w3.org/2000/svg" xmlns:xlink="https://www.w3.org/1999/xlink" width="18" height="18" viewBox="0 0 18 18"><defs><path id="bpnpn3yn0a" d="M7.554 14.813h3.183a1.689 1.689 0 01-3.183 0zm1.592 2.25a2.813 2.813 0 002.812-2.813.563.563 0 00-.562-.563h-7.5c-.31 0-.541-.014-.699-.04.018-.036.04-.077.066-.123.036-.065.354-.605.46-.8.477-.875.735-1.676.735-2.599V6.75c0-2.656 2.057-4.688 4.688-4.688 2.63 0 4.687 2.032 4.687 4.688v3.375c0 .923.258 1.724.736 2.6.106.194.424.734.46.799.026.046.047.087.065.123-.157.026-.389.04-.698.04a.564.564 0 000 1.126c1.263 0 1.896-.221 1.896-1.002 0-.26-.092-.494-.28-.833-.045-.083-.361-.619-.456-.792-.395-.724-.598-1.355-.598-2.061V6.75c0-3.28-2.563-5.813-5.812-5.813S3.333 3.47 3.333 6.75v3.375c0 .706-.203 1.337-.598 2.06-.094.174-.41.71-.456.793-.188.339-.279.572-.279.833 0 .78.632 1.002 1.896 1.002H6.39a2.813 2.813 0 002.756 2.25z"></path></defs><g fill="none" fill-rule="evenodd"><g transform="translate(-1079 -16) translate(224 7) translate(855 9)"><mask id="1dencd96ob" fill="#fff"><use xlink:href="#bpnpn3yn0a"></use></mask><use fill-rule="nonzero" stroke="currentColor" stroke-width=".3" xlink:href="#bpnpn3yn0a"></use><g fill="currentColor" mask="url(#1dencd96ob)"><path d="M0 0H18V18H0z"></path></g></g></g></svg>
ic_arrow.svg
<svg class="SvgIcon_SvgIcon__root__svg__DKYBi" viewBox="0 0 18 18"><path d="m11.955 9-5.978 5.977a.563.563 0 0 0 .796.796l6.375-6.375a.563.563 0 0 0 0-.796L6.773 2.227a.562.562 0 1 0-.796.796L11.955 9z"></path></svg>
ic_new.svg
<svg class="" width="5" height="5" viewBox="0 0 6 6" style=""><g fill="#fff" fill-rule="nonzero"><path d="M6.647 11L6.647 7.259 6.688 7.259 9.158 11 11 11 11 5 9.353 5 9.353 8.357 9.322 8.357 7.089 5 5 5 5 11z" transform="translate(-123 -375) translate(20 365) translate(98 5)" style=""></path></g></svg>
ic_search.svg
<svg xmlns="https://www.w3.org/2000/svg" xmlns:xlink="https://www.w3.org/1999/xlink" width="18" height="18" viewBox="0 0 18 18"><defs><path id="qt2dnsql4a" d="M15.727 17.273a.563.563 0 10.796-.796l-4.875-4.875-.19-.165a.563.563 0 00-.764.028 5.063 5.063 0 111.261-2.068.562.562 0 101.073.338 6.188 6.188 0 10-1.943 2.894l4.642 4.644z"></path></defs><g fill="current" fill-rule="evenodd"><use fill="#333" fill-rule="nonzero" stroke="#333" stroke-width=".3" xlink:href="#qt2dnsql4a"></use></g></svg>
1. SlideButton.js
슬라이드 버튼 만들어주는 부분
import { ReactComponent as ArrowIcon } from '../../assets/icons/ic_arrow.svg'
export default function SlideButton({ direction, onClick }) {
return (
<button onClick={onClick} className={`btn-slide-control btn-${direction}`}>
<ArrowIcon width="16" height="16" fill="#333" />
</button>
);
}
2. Slider.css
슬라이드 관련 css 입혀주는 부분
.slider-area{
position: relative;
overflow: hidden;
height: auto;
}
.slider{
position: relative;
display: block;
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-ms-touch-action: pan-y;
touch-action: pan-y;
-webkit-tap-highlight-color: transparent;
}
.slider-list{
position: relative;
overflow: hidden;
display: block;
margin: 0;
}
.slider-track{
position: relative;
position: relative;
left: 50%;
top: 0;
display: flex;
flex-direction: row;
text-align: left;
width: fit-content;
/* transition: -webkit-transform 500ms ease 0s; */
/* transition: transform 500ms ease 0s; */
}
.slider-auto{
}
.slider-item{
position: relative;
height: 100%;
padding: 0 12px;
float: left;
-webkit-filter: brightness(50%);
filter: brightness(50%);
}
.btn-slide-control{
position: absolute;
top: calc(50% - 30px);
padding: 20px 4px;
z-index: 1;
background-color: white;
width: 30px;
height: 60px;
opacity: .5;
border-radius: 15px;
}
.btn-prev{
transform: rotate(180deg);
left : calc((100% - 1200px) / 2)
}
.btn-next{
right: calc((100% - 1200px) / 2);
}
.slider-item div{
display: flex;
flex-direction: column;
align-items: center;
height: 300px;
color: white;
justify-content: center;
font-size: 60px;
font-weight: bold;
}
.slider-item span{
font-size: 18px;
margin-bottom: 1rem ;
}
.current-slide{
-webkit-filter: none;
filter: none;
}
3. Slider.js
내 코드에서는 왜 Slider1 Slider2 로 나뉘어져 있냐면..
사진을 다르게 뿌려주는 방법을 아직 고민 안하고
해당 탭 클릭하면 바로 해당 slider가 바라보는 쪽으로 이동하도록 만들었기 때문
그냥 Slider 기능만 이해해 주세요
import './Slider.css';
import './SliderItem.css';
import React, { useLayoutEffect, useRef, useEffect, useState } from "react";
import SlideButton from './SlideButton'
function useWindowSize() {
const [size, setSize] = useState([0, 0]);
useLayoutEffect(() => {
function updateSize() {
setSize([window.innerWidth, window.innerHeight]);
}
window.addEventListener('resize', updateSize);
updateSize();
return () => window.removeEventListener('resize', updateSize);
}, []);
return size;
}
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
function Slider() {
const [windowWidth, windowHeight] = useWindowSize();
const items = ['/store_image/WC01/작업지시서1.jpg', '/store_image/WC01/작업지시서2.jpg', '/store_image/WC01/작업지시서3.jpg',]
const itemSize = items.length;
const sliderPadding = 40;
const sliderPaddingStyle = `0 ${sliderPadding}px`;
const newItemWidth = getNewItemWidth();
const transitionTime = 500;
const transitionStyle = `transform ${transitionTime}ms ease 0s`;
const 양끝에_추가될_데이터수 = 2;
const [currentIndex, setCurrentIndex] = useState(양끝에_추가될_데이터수)
const [slideTransition, setTransition] = useState(transitionStyle);
const [isSwiping, setIsSwiping] = useState(false);
const [slideX, setSlideX] = useState(null);
const [prevSlideX, setPrevSlideX] = useState(false);
let isResizing = useRef(false);
let slides = setSlides();
function setSlides() {
let addedFront = [];
let addedLast = [];
var index = 0;
while (index < 양끝에_추가될_데이터수) {
addedLast.push(items[index % items.length])
addedFront.unshift(items[items.length - 1 - index % items.length])
index++;
}
return [...addedFront, ...items, ...addedLast];
}
function getNewItemWidth() {
let itemWidth = windowWidth * 0.9 - (sliderPadding * 2)
itemWidth = itemWidth > 1060 ? 1060 : itemWidth;
return itemWidth;
}
useEffect(() => {
isResizing.current = true;
setIsSwiping(true);
setTransition('')
setTimeout(() => {
isResizing.current = false;
if (!isResizing.current)
setIsSwiping(false)
}, 1000);
}, [windowWidth])
useInterval(() => {
handleSlide(currentIndex + 1)
}, !isSwiping && !prevSlideX ? 2000 : null)
function replaceSlide(index) {
setTimeout(() => {
setTransition('');
setCurrentIndex(index);
}, transitionTime)
}
function handleSlide(index) {
setCurrentIndex(index);
if (index - 양끝에_추가될_데이터수 < 0) {
index += itemSize;
replaceSlide(index)
}
else if (index - 양끝에_추가될_데이터수 >= itemSize) {
index -= itemSize;
replaceSlide(index)
}
setTransition(transitionStyle);
}
function handleSwipe(direction) {
setIsSwiping(true);
handleSlide(currentIndex + direction)
}
function getItemIndex(index) {
index -= 양끝에_추가될_데이터수;
if (index < 0) {
index += itemSize;
}
else if (index >= itemSize) {
index -= itemSize;
}
return index;
}
function getClientX(event) {
return event._reactName == "onTouchStart" ? event.touches[0].clientX :
event._reactName == "onTouchMove" || event._reactName == "onTouchEnd" ? event.changedTouches[0].clientX : event.clientX;
}
function handleTouchStart(e) {
setPrevSlideX(prevSlideX => getClientX(e))
}
function handleTouchMove(e) {
if (prevSlideX) {
setSlideX(slideX => getClientX(e) - prevSlideX);
}
}
function handleMouseSwipe(e) {
if (slideX) {
const currentTouchX = getClientX(e);
if (prevSlideX > currentTouchX + 100) {
handleSlide(currentIndex + 1)
}
else if (prevSlideX < currentTouchX - 100) {
handleSlide(currentIndex - 1)
}
setSlideX(slideX => null)
}
setPrevSlideX(prevSlideX => null)
}
return (
<div className="slider-area">
<div className="slider">
<SlideButton direction="prev" onClick={() => handleSwipe(-1)} />
<SlideButton direction="next" onClick={() => handleSwipe(1)} />
<div className="slider-list" style={{ padding: sliderPaddingStyle }}>
<div className="slider-track"
onMouseOver={() => setIsSwiping(true)}
onMouseOut={() => setIsSwiping(false)}
style={{
transform: `translateX(calc(${(-100 / slides.length) * (0.5 + currentIndex)}% + ${slideX || 0}px))`,
transition: slideTransition
}}>
{
slides.map((slide, slideIndex) => {
const itemIndex = getItemIndex(slideIndex);
return (
<div key={slideIndex} className={`slider-item ${currentIndex === slideIndex ? 'current-slide' : ''}`}
style={{ width: newItemWidth || 'auto', height:'auto' }}
onMouseDown={handleTouchStart}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onMouseMove={handleTouchMove}
onMouseUp={handleMouseSwipe}
onTouchEnd={handleMouseSwipe}
onMouseLeave={handleMouseSwipe}
>
<a >
<img src={items[itemIndex]} alt={`banner${itemIndex}`} />
</a>
</div>
)
})
}
</div>
</div>
</div >
</div >
);
}
export default Slider;
4. SliderItem.css
.slider-item{
display: inline-block;
}
.slider-item img{
width: 100%;
height: 100%;
border-radius: 4px;
object-fit: cover;
max-height: 300px;
-webkit-user-drag: none;
-khtml-user-drag: none;
-moz-user-drag: none;
-o-user-drag: none;
user-select: none;
}
이렇게 하고서
내가 사진 carousel 시키고 싶은 곳에서 임포트 받아서
써주면 됨...
그러면 워크센터1 을 선택했을 때는
썸네일 사진처럼 등록한 작업지시서 사진들이 주루룩 뜨게 되고
워크센터2를 선택하면
이렇게 동그리 사진이 뜨게 된다 !
🐱
여기서 보완해야할 점은 분명 더 있지만
일단 여기서 잠시 스탑하고...
정확한 요구사항이 들어오면 기능 추가하면서 포스팅을 이어나가보도록 하겠다.
예상치도 못했는데
빡세네 이거... 🐵