diff --git a/README.md b/README.md index 4a2c025..e3fdc45 100644 --- a/README.md +++ b/README.md @@ -39,5 +39,5 @@ | [문소희](https://github.com/ccconac) | ✅ | ✅ | ✅ | ✅ | | ✅ | ✅ | ✅ | ✅ | | ✅ | | ✅ | | | | [고세종](https://github.com/SebellKo) | ✅ | | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | ✅ | | | | | [김용호](https://github.com/KKYHH) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | ✅ | ✅ | | ✅ | | ✅ | ✅ | ✅ | -| [강채연](https://github.com/rkdcodus) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | ✅ | +| [강채연](https://github.com/rkdcodus) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | [최동현](https://github.com/saetakki) | ✅ | | | | | | | | | | | | | | diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..016199e --- /dev/null +++ b/docs/README.md @@ -0,0 +1,15 @@ +# Q&A + +- - 버튼을 클릭 시 토글이 열리며 - 버튼으로 바뀐다. 질문에 대한 답을 보여준다. +- -버튼을 클릭하면 +버튼으로 바뀌며 토글이 닫힌다 + +# 구현 전 기능 목록 + +- [ ] UI 디자인 구현 + - 토글 박스들은 중앙에 세로 정렬한다. + - 타이틀은 토글 박스 위에 위치한다. + - 토글 박스는 그림자 효과가 있다. +- [ ] 토글 기능 + - 토글 박스는 아래로 열린다. +- [ ] 토글 기능에 따라 버튼 디자인 변환하기 + - hover 시 오른쪽으로 90도 회전한다. diff --git a/week14/docs/[rkdcodus]README.md b/week14/docs/[rkdcodus]README.md new file mode 100644 index 0000000..ec2787e --- /dev/null +++ b/week14/docs/[rkdcodus]README.md @@ -0,0 +1,234 @@ +# Grocery Bud + +- CRUD 기능이 있는 장바구니 리스트이다. +- 장바구니 리스트에 아이템을 추가하고 삭제할 수 있다. +- 아이템명을 수정할 수 있다. +- 리스트에 담긴 아이템들을 한번에 삭제할 수 있다. + +
+ +## ✅ 구현 전 기능 목록 + +- [x] UI 구현 +- [x] 아이템 create 기능 구현 +- [x] 아이템 read 기능 구현 +- [x] 아이템 update 기능 구현 +- [x] 아이템 delete 기능 구현 + - [x] 아이템 삭제 버튼 + - [x] 아이템 전체 clear 버튼 + +
+ +## 📷 화면 캡처 +![2024-07-10 22;54;40](https://github.com/CHZZK-Study/Vanilla-JS-Study/assets/97906653/9925a820-98c4-4a09-8753-0d595b3d8d8e) + +## 🔎 개념 정리 + +### 1️⃣ insertAdjacentElement()와 insertAdjacentHTML() 차이 + +전에는 이 둘의 차이를 잘 모르고 같은 거라 생각하고 있었다. 결론적으로 말하자면 특정 요소에 새로운 내용을 삽입하는 기능은 동일하지만 받는 인수의 형태가 다르다. + +
+ +#### insertAdjacentElement(position, element) + +이 메서드에서 인수로 받는 element는 `document.createElement` 로 생성된 DOM 요소를 말한다. + +```jsx +// 예시 +const newElement = document.createElement("div"); +newElement.textContent = "새로운 요소"; +document.querySelector("#target").insertAdjacentElement("beforebegin", newElement); +``` + +
+ +#### insertAdjacentHTML(position, html) + +이 메서드는 html 문자열을 인수로 받는다. + +```jsx +// 예시 +const htmlString = "
새로운 HTML 요소
"; +document.querySelector("#target").insertAdjacentHTML("beforebegin", htmlString); +``` + +
+ +### 2️⃣ delete 와 splice()의 차이 + +자바스크립트 배열에서 특정 인덱스의 요소를 삭제하고 싶을 땐 2가지 방법이 있다. + +`delete`: 삭제된 요소는 'undefined'로 저장, 배열의 길이 유지
+`splice()`: 요소를 배열에서 완전히 제거, 배열의 길이 바뀜 + +splice()는 배열을 재구성하여 빈 공간이 생기지 않는다. 삭제함과 동시에 삭제한 위치에 바로 새 요소를 삽입할 수도 있어 유연하게 배열을 사용할 수 있다. + +
+ +## 📝 구현 설명 + +### itemList 데이터 구조 + +```jsx +let itemList = []; +``` + +장바구니에 추가된 아이템들은 itemList 배열에 저장된다. 배열의 인덱스 번호가 아이템의 고유 id가 된다. + +
+ +### 장바구니에 아이템 추가하기 + +```jsx +const submit = () => { + itemList.push(input.value); + printItem(); +}; + +submitButton.addEventListener("click", submit); +``` + +input 창과 관련된 버튼은 총 2 종류가 있다. submit 버튼과 edit 버튼. + +submit 버튼은 새로운 아이템을 생성하고 edit 버튼은 아이템을 수정한다. edit 버튼은 아래에서 설명할 예정! + +생각해보니 input창에 빈 값인 채로 제출 버튼을 누르면 비어잇는 아이템 창이 생성된다. 이를 막기 위해 빈 문자열이 들어왔다면 무시하도록 개선하였다. + +```jsx +const submit = () => { + if (input.value === "") return; + itemList.push(input.value); + printItem(); +}; +``` + +
+ +### itemList 화면에 반영하기 + +```jsx +// itemList 출력 +const printItem = () => { + clear(); + + itemList.forEach((item, index) => { + const newItem = createHTML(item, index); + list.insertAdjacentHTML("beforeend", newItem); + }); + + if (!clearButton.classList.contains(SHOW)) { + clearButton.classList.add(SHOW); + } + + input.value = ""; +}; +``` + +가장 먼저 전에 반영된 아이템들을 모두 삭제한다. 그렇지 않으면 아이템이 중복되어 화면에 반영된다. 그 후 itemList 를 순회하여 아이템마다 li태그를 생성해 추가한다. 아이템이 하나 이상이므로 clear 버튼을 활성화한다. 그리고 새로운 값을 받을 수 있도록 input 창을 비워준다. + +
+ +```jsx +// li 태그 생성 +const createHTML = (text, id) => { + const newItem = ` +
  • + ${text} + + +
  • + `; + + return newItem; +}; +``` + +li 태그 생성 함수는 아이템 text와 id(itemList 인덱스 번호)를 파라미터로 받도록 했다. 아이템들은 서로 구분해야하기 때문에 각자의 고유번호가 필요하다. + +
    + +```jsx +// li 태그 전부 삭제 +const clear = () => { + while (list.firstChild) { + list.removeChild(list.firstChild); + } +}; +``` + +clear() 함수는 화면에 반영된 li태그들을 전부 삭제한다. list는 li태그를 감싸는 부모 요소, ul 태그를 가리킨다. ul 태그의 자식요소를 한번에 삭제하는 메서드는 없는 것 같다. 대신에 firstChild로 자식 요소를 하나씩 선택해 removeChild 메서드로 제거해주는 방법을 사용했다. + +
    + +### 아이템 삭제하거나 수정하기 + +먼저 삭제 버튼을 눌렀는지 수정 버튼을 눌렀는지 확인해야한다. + +```jsx +// 아이템 option 버튼 판단 +const clickItemOption = (e) => { + const { DELETE, EDIT } = OPTIONS; + const option = e.target.id; + const selectedItem = e.target.parentNode; + + if (option === DELETE) deleteItem(selectedItem); + else if (option === EDIT) editItem(selectedItem); +}; + +list.addEventListener("click", clickItemOption); +``` + +버튼의 id를 통해 삭제 버튼인지 수정 버튼인지 확인한다. 하지만 선택된 e.target은 li태그의 자식요소인 button 태그를 가리키고 있다. li태그를 선택하기 위해 .parentNode를 사용했다. + +
    + +#### 아이템 삭제 기능 + +삭제일 경우 deleteItem 함수를 호출한다. + +```jsx +// 아이템 삭제 +const deleteItem = (target) => { + itemList.splice(target.id, 1); + printItem(); + + if (!itemList.length) { + clearButton.classList.remove(SHOW); + } +}; +``` + +해당 item은 itemList에서 제거해준다. +splice() 메서드를 사용할 때는 index와 제거할 요소의 개수를 인수로 받는다. +itemList가 변경되었지만 아직 화면에 반영된 li태그의 id는 과거의 인덱스이다. 맞춰주기 위해 printItem()으로 업데이트된 itemList로 화면에 반영해준다. + +#### 아이템 수정 기능 + +```jsx +// 아이템 수정시 input 창 변경 +const editItem = (target) => { + const text = itemList[target.id]; + + editId = target.id; + input.value = text; + submitButton.classList.add(HIDE); + editButton.classList.add(SHOW); +}; +``` + +아이템 수정 버튼을 누르면 input창에서 수정할 수 있다. 수정하기 전에 input창과 버튼을 바꿔줘야한다. +또한 수정할 아이템의 id를 전역변수로 저장해 edit 버튼을 눌렀을 때 업데이트할 아이템을 찾아 업데이트 한다. + +```jsx +// 아이템 수정 +const editComplete = () => { + itemList[editId] = input.value; + submitButton.classList.remove(HIDE); + editButton.classList.remove(SHOW); + + printItem(); +}; +``` + +edit 버튼을 눌렀을 때 호출되는 함수이다. 수정된 text로 업데이트 해주고 제출버튼을 다시 바꿔준다. 그리고 새로 바뀐 itemList를 다시 화면에 반영해준다. diff --git a/week14/rkdcodus/app.js b/week14/rkdcodus/app.js new file mode 100644 index 0000000..4502b5b --- /dev/null +++ b/week14/rkdcodus/app.js @@ -0,0 +1,109 @@ +const input = document.getElementById("input"); +const submitButton = document.getElementById("submitButton"); +const editButton = document.getElementById("editButton"); +const clearButton = document.getElementById("clearButton"); +const list = document.getElementById("list"); + +const SHOW = "show"; +const HIDE = "hide"; +const OPTIONS = { + DELETE: "delete", + EDIT: "edit", +}; + +let itemList = []; +let editId = -1; + +// li 태그 생성 +const createHTML = (text, id) => { + const newItem = ` +
  • + ${text} + + +
  • + `; + + return newItem; +}; + +// li 태그 전부 삭제 +const clear = () => { + while (list.firstChild) { + list.removeChild(list.firstChild); + } +}; + +// itemList 출력 +const printItem = () => { + clear(); + + itemList.forEach((item, index) => { + const newItem = createHTML(item, index); + list.insertAdjacentHTML("beforeend", newItem); + }); + + if (!clearButton.classList.contains(SHOW)) { + clearButton.classList.add(SHOW); + } + + input.value = ""; +}; + +// 아이템 추가 +const submit = () => { + if (input.value === "") return; + itemList.push(input.value); + printItem(); +}; + +// 아이템 수정 +const editComplete = () => { + itemList[editId] = input.value; + submitButton.classList.remove(HIDE); + editButton.classList.remove(SHOW); + + printItem(); +}; + +// 아이템 삭제 +const deleteItem = (target) => { + itemList.splice(target.id, 1); + printItem(); + + if (!itemList.length) { + clearButton.classList.remove(SHOW); + } +}; + +// 아이템 수정시 input 창 변경 +const editItem = (target) => { + const text = itemList[target.id]; + + editId = target.id; + input.value = text; + submitButton.classList.add(HIDE); + editButton.classList.add(SHOW); +}; + +// 아이템 option 버튼 판단 +const clickItemOption = (e) => { + const { DELETE, EDIT } = OPTIONS; + const option = e.target.id; + const selectedItem = e.target.parentNode; + + if (option === DELETE) deleteItem(selectedItem); + else if (option === EDIT) editItem(selectedItem); +}; + +// 아이템 전체 삭제 +const clearItem = () => { + clear(); + itemList = []; + clearButton.classList.remove(SHOW); +}; + +submitButton.addEventListener("click", submit); +editButton.addEventListener("click", editComplete); +list.addEventListener("click", clickItemOption); +clearButton.addEventListener("click", clearItem); diff --git a/week14/rkdcodus/icon/delete.svg b/week14/rkdcodus/icon/delete.svg new file mode 100644 index 0000000..2ea2dc9 --- /dev/null +++ b/week14/rkdcodus/icon/delete.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/week14/rkdcodus/icon/edit.svg b/week14/rkdcodus/icon/edit.svg new file mode 100644 index 0000000..9e718ea --- /dev/null +++ b/week14/rkdcodus/icon/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/week14/rkdcodus/index.html b/week14/rkdcodus/index.html new file mode 100644 index 0000000..24ed62d --- /dev/null +++ b/week14/rkdcodus/index.html @@ -0,0 +1,24 @@ + + + + + + Grocery Bud + + + +
    +
    Grocery Bud
    +
    + + + +
    + +
    + +
    +
    + + + diff --git a/week14/rkdcodus/style.css b/week14/rkdcodus/style.css new file mode 100644 index 0000000..8473b5f --- /dev/null +++ b/week14/rkdcodus/style.css @@ -0,0 +1,133 @@ +* { + margin: 0; + padding: 0; +} + +li { + list-style: none; +} + +.clearBtn-container { + margin-top: 50px; + text-align: center; +} + +.clearBtn { + display: block; + margin: 0 auto; + border: none; + background: none; + cursor: pointer; + transition: 0.5s; +} + +.clearBtn:hover { + color: red; +} + +section { + position: absolute; + top: 40%; + left: 50%; + + width: 400px; + padding: 80px 40px; + border-radius: 15px; + transform: translate(-50%, -50%); +} + +.title { + font-size: 32px; + font-weight: bold; + text-align: center; +} + +.input-container { + position: relative; + display: flex; + justify-content: center; + margin: 30px auto; +} + +input { + width: 100%; + height: 30px; + margin: 0; + padding: 0 20px; + border: none; + border: 1px solid #b7b7b7; + border-radius: 10px; + + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +input:focus { + outline: none; +} + +input::-ms-clear { + display: none; +} + +.inputBtn { + position: absolute; + right: 0; + height: 100%; + margin: 0; + padding: 0 10px; + + border: none; + border-left: 1px solid #b7b7b7; + border-top-right-radius: 10px; + border-bottom-right-radius: 10px; + + background-color: cornflowerblue; + color: black; + cursor: pointer; + transition: 0.5s; +} + +.inputBtn:hover { + color: white; +} + +#list li { + display: flex; + justify-content: space-between; + align-items: center; + margin: 5px 0; + border-bottom: 1px solid #111; +} + +span#itemText { + flex-grow: 1; + text-align: left; + padding: 15px 20px; +} +.itemBtn { + margin-left: 8px; +} + +.edit { + filter: invert(76%) sepia(42%) saturate(755%) hue-rotate(39deg) brightness(119%) contrast(107%); + width: 20px; + height: 20px; + cursor: pointer; +} + +.delete { + filter: invert(26%) sepia(96%) saturate(7499%) hue-rotate(360deg) brightness(109%) contrast(122%); + width: 20px; + height: 20px; + cursor: pointer; +} + +.hide { + display: none; +} + +.show { + display: inline-block; +} diff --git a/week15/app.js b/week15/app.js new file mode 100644 index 0000000..dfd8722 --- /dev/null +++ b/week15/app.js @@ -0,0 +1,72 @@ +const datas = [ + { + id: 0, + url: "https://wallpapercave.com/wp/aWaXW72.jpg", + }, + { + id: 1, + url: "https://wallpapercave.com/wp/wp3181476.jpg", + }, + { + id: 2, + url: "https://wallpapercave.com/wp/wp3181484.jpg", + }, + { + id: 3, + url: "https://wallpapercave.com/wp/wp2123889.jpg", + }, + { + id: 4, + url: "https://wallpapercave.com/wp/wp2209011.jpg", + }, +]; +const image = document.querySelector(".image_wrap"); +const screen = document.getElementById("screen-size"); +const next = document.getElementById("next"); +const prev = document.getElementById("prev"); + +let center = 0; + +const nextSlide = () => { + center += 1; + if (center > datas.length - 1) { + center = datas.length - 1; + } + image.style.transform = `translate(-${center * 100}vw)`; +}; + +const prevSlide = () => { + center -= 1; + if (center < 0) { + center = 0; + } + image.style.transform = `translate(-${center * 100}vw)`; +}; + +const createImage = () => { + datas.map((data) => { + image.insertAdjacentHTML("beforeend", ``); + }); +}; + +const toggleFullScreen = () => { + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen(); + screen.replaceChildren(); + screen.insertAdjacentHTML( + "beforeend", + '' + ); + } else { + if (document.exitFullscreen) { + document.exitFullscreen(); + screen.replaceChildren(); + screen.insertAdjacentHTML("beforeend", ''); + } + } +}; + +next.addEventListener("click", nextSlide); +prev.addEventListener("click", prevSlide); +screen.addEventListener("click", toggleFullScreen); +window.addEventListener("DOMContentLoaded", createImage); diff --git a/week15/docs/[rkdcodus]README.md b/week15/docs/[rkdcodus]README.md index f68bed6..2f4b9dc 100644 --- a/week15/docs/[rkdcodus]README.md +++ b/week15/docs/[rkdcodus]README.md @@ -12,6 +12,142 @@ #### 확장 기능 - [x] 전체화면 기능 -- [ ] 전체화면 후 마우스가 움직이지 않으면 버튼이 잠시 보이지 않도록 구현해보기 -- [ ] 마우스 드래그 앤 드롭 이벤트로 슬라이딩 기능 구현해보기 -- [ ] 마우스가 사진 위에 있지 않았을 경우, 일정 시간마다 자동으로 이미지 슬라이딩되는 기능 구현해보기 +- [x] 전체화면 후 마우스가 움직이지 않으면 버튼이 잠시 보이지 않도록 구현해보기 +- [x] mouse 이벤트로 드래그 슬라이딩 기능 구현해보기 +- [x] 마우스가 사라지면, 일정 시간마다 자동으로 이미지 슬라이딩되는 기능 구현해보기 + +#### 슬라이딩 기능 + +![2024-07-13 00;54;42](https://github.com/user-attachments/assets/13fc9d4b-dd3f-4c9f-8278-4a1a5fce8eb4) + +#### 전체화면 기능 + +![2024-07-13 00;59;26](https://github.com/user-attachments/assets/889db57f-7ad2-4b74-bde9-4e846a437013) + +#### 개선할 점 + +- 이미지 넘겨질 때 눈이 불편하여 이미지 슬라이딩 효과를 바꿔봐야할 것 같다. + +## 구현 설명 + +### ✨마우스 감지하여 버튼 투명도 조절하기 + +#### 시현 영상 + +![2024-07-13 00;49;05](https://github.com/user-attachments/assets/1ba0eb99-d57d-4dee-bbbd-4bbc657c3830) + +
    + +### 설명 + +```jsx +const buttons = document.querySelector(".button"); +let mouse = { x: null, y: null }; +``` + +먼저 사라지게할 요소를 querySelector로 선택한다. 마우스가 멈추면 버튼들이 사라지도록 할 것이기 때문에 button 부모 요소를 선택했다. +그리고 마우스의 위치 값을 저장할 mouse 변수도 선언해주었다. + +
    + +```jsx +// 이벤트 등록 +image.addEventListener("mousemove", detectMouseMove); + +// 이벤트 삭제 +image.removeEventListener("mousemove", detectMouseMove); +``` + +그리고 마우스를 감지할 배경 요소인 image에 mousemove 이벤트를 걸어주었다. +이 기능은 전체화면일 때에만 적용시킬 것이기 때문에 전체 화면 기능이 종료되면 이벤트를 제거해주어야한다. + +
    + +```jsx +const detectMouseMove = (e) => { + mouse.x = e.screenX; + mouse.y = e.screenY; + buttons.classList.remove("hide"); + image.style.cursor = "default"; + + setTimeout(() => { + if (mouse.x === e.screenX && mouse.y === e.screenY) { + buttons.classList.add("hide"); + image.style.cursor = "none"; + } + }, 2000); +}; +``` + +mousemove로 마우스의 움직임이 감지되었을 때 실행되는 함수이다. +마우스 위치의 값을 mouse 변수에 저장하고 마우스가 움직이고 4초 후 마우스의 위치가 같은지 확인한다. +위치가 같다면 button의 투명도를 낮추는 애니메이션이 걸린 class를 달아준다. +다시 마우스가 움직인다면 버튼이 나타나야하기때문에 함수가 실행될 때마다 hide class를 지워주도록 해주었다. +추가로 마우스도 버튼과 함께 사라지도록 image 위에 있는 마우스는 `cursor: none` css style을 주었다. + +
    + +```css +.hide { + pointer-events: none; + animation-name: fadeOut; + animation-duration: 2s; + animation-fill-mode: forwards; +} + +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} +``` + +여기서 중요한 것은 `animation-fill-mode`이다. `forwards`를 적어주면 애니메이션이 종료된 상태 그대로를 유지한다. +없다면 버튼이 사라지는 애니메이션 후 처음으로 돌아와 버튼이 다시 보인다. + +### 문제 발견 + +1. esc로 전체화면을 종료할 경우 버튼아이콘이 변경되지 않는다. (버튼을 눌러 종료했을 경우엔 변경됨) +2. 전체화면 종료 후에 몇 초 후 버튼이 사라진다. + +이 2가지 문제가 있었다. +1번 문제를 해결하기 위해서 esc로 전체화면을 종료했을 때 화면이 변경되었음을 감지하여 버튼의 모양을 변경해주어야한다. +`fullscreenchange`이벤트를 사용하면 전체화면이 변경된 걸 감지할 수 있다. +버튼 모양 변경과 동시에 mousemove 이벤트를 제거하고 버튼에 hide class를 제거하여 버튼과 마우스가 바로 보일 수 있도록 했다. + +```jsx +document.addEventListener("fullscreenchange", () => { + if (!document.fullscreenElement) { + image.removeEventListener("mousemove", detectMouseMove); // mousemove 이벤트 제거 + screen.replaceChildren(); // 화면 축소 아이콘 태그 삭제 + screen.insertAdjacentHTML("beforeend", ''); // 확대 아이콘 태그로 변경 + buttons.classList.remove("hide"); // 버튼, 마우스 바로 보이도록 함. + image.style.cursor = "default"; + } +}); +``` + +2번 문제가 발생한 이유는 전체화면이 종료된 후에 setTimeOut에 설정한 2초가 이제서야 끝나 setTimeOut 내 함수가 실행되었기 때문이다. 때문에 setTimeOut 내부 함수에 전체화면일 때에만 버튼과 마우스가 사라지도록 조건을 걸어주었다. + +```jsx +setTimeout(() => { + if (mouse.x === e.screenX && mouse.y === e.screenY && document.fullscreenElement) { + buttons.classList.add("hide"); + image.style.cursor = "none"; + } +}, 2000); +``` + +
    + +### ✨ 마우스 드래그로 이미지 슬라이드 기능 구현하기 + +이 기능에 대한 설명은 블로그에 작성하였다.
    +[mouse 이벤트로 드래그로 이미지 슬라이드 효과내기](https://codus43.tistory.com/18) + +### ✨ 자동 슬라이드 기능 구현하기 + +[자동 슬라이드 기능 구현하기](https://codus43.tistory.com/19) diff --git a/week15/index.html b/week15/index.html new file mode 100644 index 0000000..bcbb88e --- /dev/null +++ b/week15/index.html @@ -0,0 +1,22 @@ + + + + + + + + Image Slider + + +
    +
    +
    +
    + + + +
    + + + + diff --git a/week15/rkdcodus/app.js b/week15/rkdcodus/app.js index dfd8722..85cda63 100644 --- a/week15/rkdcodus/app.js +++ b/week15/rkdcodus/app.js @@ -24,31 +24,75 @@ const image = document.querySelector(".image_wrap"); const screen = document.getElementById("screen-size"); const next = document.getElementById("next"); const prev = document.getElementById("prev"); +const buttons = document.querySelector(".button"); +let mouse = { x: null, y: null }; +let translateValue = 0; let center = 0; +let autoDireaction = true; +let autoSlide = false; const nextSlide = () => { - center += 1; - if (center > datas.length - 1) { - center = datas.length - 1; - } - image.style.transform = `translate(-${center * 100}vw)`; + center -= 1; + if (-center > datas.length - 1) center = -(datas.length - 1); + translateValue = center * 100; + image.style.transform = `translate(${translateValue}vw)`; }; const prevSlide = () => { - center -= 1; - if (center < 0) { - center = 0; - } - image.style.transform = `translate(-${center * 100}vw)`; + center += 1; + if (center > 0) center = 0; + translateValue = center * 100; + image.style.transform = `translate(${translateValue}vw)`; }; const createImage = () => { datas.map((data) => { - image.insertAdjacentHTML("beforeend", ``); + image.insertAdjacentHTML( + "beforeend", + `` + ); }); }; +window.addEventListener("DOMContentLoaded", createImage); +next.addEventListener("click", nextSlide); +prev.addEventListener("click", prevSlide); + +// 자동 슬라이드 기능 구현 +setInterval(() => { + if (document.fullscreenElement && autoSlide) { + if (center === 0) { + autoDireaction = true; + } else if (-center === datas.length - 1) { + autoDireaction = false; + } + + if (autoDireaction) { + nextSlide(); + } else { + prevSlide(); + } + } +}, 7000); + +// 전체화면 기능 + +const detectMouseMove = (e) => { + mouse.x = e.screenX; + mouse.y = e.screenY; + buttons.classList.remove("hide"); + image.style.cursor = "default"; + + setTimeout(() => { + if (mouse.x === e.screenX && mouse.y === e.screenY && document.fullscreenElement) { + buttons.classList.add("hide"); + image.style.cursor = "none"; + autoSlide = true; + } + }, 2000); +}; + const toggleFullScreen = () => { if (!document.fullscreenElement) { document.documentElement.requestFullscreen(); @@ -57,16 +101,54 @@ const toggleFullScreen = () => { "beforeend", '' ); + image.addEventListener("mousemove", detectMouseMove); } else { - if (document.exitFullscreen) { - document.exitFullscreen(); - screen.replaceChildren(); - screen.insertAdjacentHTML("beforeend", ''); - } + document.exitFullscreen(); } }; -next.addEventListener("click", nextSlide); -prev.addEventListener("click", prevSlide); screen.addEventListener("click", toggleFullScreen); -window.addEventListener("DOMContentLoaded", createImage); + +document.addEventListener("fullscreenchange", () => { + if (!document.fullscreenElement) { + image.removeEventListener("mousemove", detectMouseMove); + screen.replaceChildren(); + screen.insertAdjacentHTML("beforeend", ''); + buttons.classList.remove("hide"); + image.style.cursor = "default"; + autoSlide = false; + } +}); + +// 마우스 슬라이드 기능 + +let startX = 0; +let prevX = 0; + +const dragImage = (e) => { + translateValue += e.offsetX - prevX; + prevX = e.offsetX; + image.style.transform = `translate(${translateValue}vw)`; +}; + +image.addEventListener("mousedown", (e) => { + startX = e.offsetX; + prevX = e.offsetX; + image.addEventListener("mousemove", dragImage); +}); + +image.addEventListener("mouseup", (e) => { + image.removeEventListener("mousemove", dragImage); + const movingX = startX - e.offsetX; // 움직인 거리 + + if (Math.abs(movingX) < 50) { + image.style.transform = `translate(${center * 100}vw)`; + return; + } + + if (movingX > 0) { + nextSlide(); + } else { + prevSlide(); + } +}); diff --git a/week15/rkdcodus/style.css b/week15/rkdcodus/style.css index 36de69e..548319e 100644 --- a/week15/rkdcodus/style.css +++ b/week15/rkdcodus/style.css @@ -21,7 +21,8 @@ display: flex; justify-content: center; align-items: center; - transition: 1s; + transition: 3s; + transition-timing-function: ease-in-out; } .image { @@ -45,9 +46,24 @@ button { font-size: larger; cursor: pointer; color: #e2e2e2; - transition: 0.3s; } button:hover { background-color: rgba(0, 0, 0, 0.5); } + +.hide { + pointer-events: none; + animation-name: fadeOut; + animation-duration: 2s; + animation-fill-mode: forwards; +} + +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} diff --git a/week15/style.css b/week15/style.css new file mode 100644 index 0000000..36de69e --- /dev/null +++ b/week15/style.css @@ -0,0 +1,53 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +.slider { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + width: 100vw; + height: 100vh; + + overflow: hidden; +} + +.image_wrap { + position: absolute; + display: flex; + justify-content: center; + align-items: center; + transition: 1s; +} + +.image { + width: 100vw; + height: 100vh; + object-fit: cover; +} + +.button { + position: absolute; + top: 85%; + left: 50%; + transform: translate(-50%, -50%); +} + +button { + width: 30px; + height: 30px; + background-color: rgba(0, 0, 0, 0.2); + border: none; + font-size: larger; + cursor: pointer; + color: #e2e2e2; + transition: 0.3s; +} + +button:hover { + background-color: rgba(0, 0, 0, 0.5); +}