- 달력에 기능 추가하기 2026.04.21
- 제 2차 씨본 컬러 시뮬레이터 보수작업 2026.04.16
- 포폴 웹페이지화-더 이상의 자세한 설명은 생략한다 2026.03.08
- 포폴 웹페이지화-뼈대 잡기 2026.03.07
- 개 얼탱이 없는 작업이 온다 두둥 2026.03.06
- 씨본 팔레트 컬러 시뮬레이터 보수작업 2026.02.22
- 씨본 컬러 파레트 씨뮬레이터 2026.01.30
- 특정 조건을 만족하면 DOM이 나타나게 해 보자 2025.09.17
- 프로그레스 바 만들어보기 2025.09.16
- 드디어 추가되는 파일 불러오기 기능 2025.09.11
- 텍스트 에디터에 뭔가 추가해보자 2025.08.18
- 아주 간단한 텍스트 에디터를 만들어보자 2025.08.08
- 카카오톡 모아보내기를 대충 구현해보자 2025.06.27
- 우리도 그 유리 효과인지 뭔지 해봅시다 거 2025.06.18
- 자바스크립트로 음악 재생기를 만들어보자 2025.06.07
아 EDA 할 생각 없냐고요?
맥북 반납해서 EDA를 못합니다... 리눅스 노트북은 데이터 분석 돌리면 뻗어서 평일에 글쓸때나 자바스크립트 할 때 말고 켜본적 없음...

그새 글꼴이 많이 바뀌긴 했는데... 그래서 이 달력에 뭔 기능을 추가할거임? 바로 오늘 날짜로 되돌아가는 기능과 몇년 몇월로 이동하는 기능이다. 전자는 뭔지 알겠는데 후자는 뭐임? 여러분 이 달력에서 1991년 3월로 어떻게 가는지 아십니까? 1991년 3월 될때까지 이전달을 급나 눌러야됩니다. 일단 햇수로만 36년인데 1년이 12개월이니까 대충 클릭질 몇 번 해야 하는지 견적이 나오시죠?
오늘 날짜로 돌아가는 버튼
let goTodayButton = document.querySelector('#gotoday');
이게 HTML 구조는 간단한데 CSS가… ㅋㅋㅋㅋㅋ 내가 오래전에 한거라 뭘 어떻게 한건지 헷갈림… 여러분은 이런 사태를 미연에 방지하실거면 주석 다십쇼.
아무튼 갖고옵시다.
// 오늘 날짜로 고고싱하는 버튼
goTodayButton.addEventListener('click', (e) => {
// 1. 전역 변수인 'date'를 현재 시점의 날짜로 갱신
date = new Date();
// 2. 갱신된 date 객체를 바탕으로 달력을 다시 그림
renderCalendar();
});
사실 원리는 간단합니다. 오늘 날짜로 객체 생성하고 달력 다시 그리면 돼요. 그러면 일단 오늘 날짜로 복귀는 됐고… 이제 특정 년도 특정 달로 이동하는 기능 만들자.
특정 날짜로 이동하기
// 특정 날짜로 고고싱하는 버튼
goDateButton.addEventListener('click', (e) => {
let goYear = document.querySelector('#goyear').value;
let goMonth = document.querySelector('#gomonth').value;
// 1. 입력값이 있는지 확인 (비어있으면 동작 안 함)
if (goYear && goMonth) {
// 2. 전역 변수인 date 객체의 연도와 월을 업데이트
// 숫자로 형변환(Number)해주고, 월은 반드시 -1을 해줍니다.
date.setFullYear(Number(goYear));
date.setMonth(Number(goMonth) - 1);
// 날짜가 해당 월의 마지막 날을 넘어가는 버그를 방지하기 위해 1일로 세팅하는 것이 안전합니다.
date.setDate(1);
// 3. 갱신된 정보를 바탕으로 달력 다시 그리기
renderCalendar();
} else {
alert("이동할 연도와 월을 모두 입력해주세요!");
}
});
이것도 걍 그 날짜로 달력 다시 그리면 되던데요? 근데 날짜는 왜 1일로 바꿔서 그렸나요? 어차피 우리는 그 특정 해 특정 월로 이동만 할 거잖아요. 그리고 이 달력을 쓰는 당일 날짜가 31일이면 말이이 30일인 달이나 2월로 이동할때 날짜 안 바꾸면 2월 31일은 뭐야!!! 이러고 컴퓨터가 밥상 뒤집어요.

이렇게 하면 일단은 되는'데'… 아직 끝나지 않은 문제가 있다.
콘솔에만 뜨는 메시지 처리하기
여러분들에게는 보이지 않지만, 사실 이 달력을 구동할 때 콘솔창에 보이는 메시지가 하나 있다.

바로 이건데… 저 달력을 만들때 아마 채찍피티가 없었거나 초창기라 디버깅을 못했을거다. 근데 지금은 채찍피티 클로드 에미나이가 다 있잖아요? 그래서 디버깅 해달라고 할거임.

그렇다고 합니다. 그러니까 기존 코드에서는 함수에서 자체적으로 범위를 체크하고 있었는데, 이것때문에 혼선이 와서 저런 메시지가 뜨는 것이다. 그러니까 걍 자체 체크하는걸 없애고 Date 객체가 알아서 보정하게 두면 된다 이거죠.
function dayCalcDisplay(startYear, startMonth, startDay) {
// 1. 넘겨받은 연, 월, 일을 바탕으로 실제 Date 객체 생성
// (월은 0~11이므로 -1, 일은 그대로)
// 예: 2025년 2월의 이전달 날짜로 '31'이 들어와도
// new Date(2025, 1, 31)은 자동으로 2025년 3월 3일로 인식됩니다.
var tempDate = new Date(startYear, startMonth - 1, startDay);
// 2. 보정된 진짜 연/월/일을 다시 추출
var realYear = tempDate.getFullYear();
var realMonth = tempDate.getMonth() + 1;
var realDay = tempDate.getDate();
// 3. (옵션) 윤년 체크 로직은 사실 lunarCalc 내부에서 수행하므로
// 여기서는 범위 체크를 삭제하거나 보정된 값을 사용합니다.
if (realYear < 1900 || realYear > 2040) {
return ""; // 지원 범위 밖이면 빈칸 처리
}
/* 양력/음력 변환 (보정된 진짜 날짜를 넣음) */
var lunar = lunarCalc(realYear, realMonth, realDay, 1);
return (lunar.leapMonth ? "윤" : "") + lunar.month + "." + lunar.day;
}
그래서 함수가 더 짧아졌음. 근데 저거 생각해보니까 2040년 넘어가면 ""준다고? 아니… 그때 나 50살인데? 뭐 그래요… 그때쯤 되면 뭔가 새로운 방법이 나오겠지… 그때 코드를 바꾸던가 합시다…
'Coding > JavaScript' 카테고리의 다른 글
| 제 2차 씨본 컬러 시뮬레이터 보수작업 (0) | 2026.04.16 |
|---|---|
| 포폴 웹페이지화-더 이상의 자세한 설명은 생략한다 (0) | 2026.03.08 |
| 포폴 웹페이지화-뼈대 잡기 (0) | 2026.03.07 |
| 개 얼탱이 없는 작업이 온다 두둥 (0) | 2026.03.06 |
| 씨본 팔레트 컬러 시뮬레이터 보수작업 (0) | 2026.02.22 |
내가 이걸 또 하게 되다니… 싶었는데 쓰다가 또 불편한 게 나와서 에미나이 도움 받아서 기능을 추가했다.

이거 보여요? 기존에는 없었던 저 HEX 코드들. 이게 중간색을 쓰고 싶은데 #rrggbb 코드가 없으니까 내가 개발자도구 열고 들어가서 변환을 해야되는데 이게 증말 번거롭습니다… 그리고 나야 개발자도구의 존재를 안다지만 모든 사람들이 그렇진 않잖아요?
for (let i = 0; i < n; i++) {
const step = i / (n - 1);
const r = Math.round(startRgb[0] + (endRgb[0] - startRgb[0]) * step);
const g = Math.round(startRgb[1] + (endRgb[1] - startRgb[1]) * step);
const b = Math.round(startRgb[2] + (endRgb[2] - startRgb[2]) * step);
const toHex = (c) => c.toString(16).padStart(2, '0');
const hexCode = `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
const chip = document.createElement('div');
chip.classList.add('palette_chip');
chip.style.backgroundColor = `rgb(${r}, ${g}, ${b})`;
chip.style.flex = "1";
// 스타일 및 텍스트 설정
chip.style.display = "flex";
chip.style.alignItems = "center";
chip.style.justifyContent = "center";
chip.style.cursor = "pointer"; // 클릭 가능하다는 표시
chip.style.color = (r * 0.299 + g * 0.587 + b * 0.114) > 186 ? '#000' : '#fff';
chip.innerText = hexCode;
chip.onclick = () => {
navigator.clipboard.writeText(hexCode).then(() => {
// alert 대신 토스트 호출!
showToast(`${hexCode} copied!`);
}).catch(err => {
console.error('복사 실패:', err);
});
};
palette_div.appendChild(chip);
}
저정도 되면 어디까지가 한 블록인지 헷갈려요... const toHex = (c) => c.toString(16).padStart(2, '0');랑 const hexCode = `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase(); 보여요? 저걸로 10개 색상을 HEX코드로 변환한 다음에 이너텍스트 박을거고, 그 네모를 누르면 HEX 코드가 복사되면서 복사됐음! 도 할 거다. 처음에는 alert였는데 alert는 확인버튼을 눌러야되거든요? 그래서 토스트 메시지로 바꿨음.
function showToast(message) {
let toast = document.getElementById('toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'toast';
document.body.appendChild(toast);
}
toast.innerText = message;
toast.classList.add('show');
// 2초 뒤에 사라지게 설정
setTimeout(() => {
toast.classList.remove('show');
}, 2000);
}
그래서 토스트 창(그 있어요 나왔다가 들어가는 창) 생성 함수가 추가됐고
/* 토스트 메시지 기본 스타일 */
#toast {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
opacity: 0;
transition: opacity 0.3s, bottom 0.3s;
z-index: 9999;
pointer-events: none; /* 클릭 방해 금지 */
}
/* 토스트가 보일 때의 상태 */
#toast.show {
opacity: 1;
bottom: 50px;
}
관련 CSS도 추가됐습니다. HTML은 바뀐거 없음.
'Coding > JavaScript' 카테고리의 다른 글
| 달력에 기능 추가하기 (0) | 2026.04.21 |
|---|---|
| 포폴 웹페이지화-더 이상의 자세한 설명은 생략한다 (0) | 2026.03.08 |
| 포폴 웹페이지화-뼈대 잡기 (0) | 2026.03.07 |
| 개 얼탱이 없는 작업이 온다 두둥 (0) | 2026.03.06 |
| 씨본 팔레트 컬러 시뮬레이터 보수작업 (0) | 2026.02.22 |
어제 그 뼈대 올라왔잖아요? 그러고나서 밤에 잡아서 내용 다 채웠음. 농담같죠? 진짜임.
어바웃 페이지

뭘 많이 가렸죠? 다 개인정보라 어쩔 수 없음.. 그럼 논문하고 자격증은 왜 안 가렸냐… 논문은 이미 저널에 실린거고 자격증도 내가 땄다고 올렸잖아요. 쟤들은 이미 한번 깐거라 안 가린거임.
원래 생각했던 구성에는 저 카드가 없었습니다. 없었는데 제미나이가 "씁 근데 이거 이렇게만 보면 좀 심심하지 않을까요?라고 하면서 제안한겁니다. 저 선이랑 배경도 지금 조정중인데 선이 굵다고 이쁜건 아니고... 없애봤는데 이것도 아 씁 아닌 것 같고... 그림자는 그래도 배경이나 안이나 톤이 다 연해서 있는 게 나은 것 같긴 하고... 애매함. 그냥 애매함.
/* 개별 오브젝트를 감싸는 카드*/
.card {
width: 95%;
border: 1px solid var(--sub-dark);
border-radius: 10px;
margin: 10px 0;
padding: 10px;
background-color: var(--white);
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
transition: all 0.2s ease;
}
.card:hover {
box-shadow: 0 6px 14px rgba(0,0,0,0.12);
transform: translateY(-2px);
}
이렇게 트랜지션이랑 호버를 주면 마우스를 댔을 때 뜨는데 문제가 하나 있다. 여러분들 그거 아십니까? 모바일에서 호버 안되는거. 당연한 얘기지만, 서마터폰이나 태블릿 PC에서는 마우스를 쓰는 게 아니라 손으로 누르잖아요. 드래그도 손으로 하는데 어쨌든 이게 손으로 뭘 누른 상태에서 움직이고 걍 볼 때는 손을 또 안 대잖아요? 그래서 의미가 없다 이거요.
/*여러분 그 호버도 모바일에서는 끄셔야 하는 거 아십니까 */
@media (hover: hover) {
.card:hover {
box-shadow: 0 6px 14px rgba(0,0,0,0.12);
transform: translateY(-2px);
}
.accordion:hover {
background-color: var(--accent);
}
.link i:hover {
color: var(--accent);
transform: translateY(-1px) scale(1.05);
}
.btn:hover {
color: var(--accent);
}
}
예. 미디어쿼리 주십시오.
근데 메인페이지는 이거 말고는 내용 말고 뭐 없는데 내용을 다 가려버려서 더 서술할게 없음.
스킬

어차피 헤더는 고정이니까 여기만 캡쳐하겠음... 이건 뭐냐면 내가 할 수 있는 것들을 나열한거다. 저 그리드 레이아웃은 나중에 스킬에 뭔가 더 추가되면 바뀔 수도 있는데 지금은 2열 2행이고 저기도 카드 적용되어있고 어이콘 폰트어썸이고... 그게 다임. 여기서 보고 가실건 저 체크입니다. 저게 li태그로 단 건데... 엥? li에 저런거 없는데요?
ul {
margin: 0;
list-style-position: inside;
list-style-type: none;
}
li::before {
content: "\f00c";
/* 7 버전 폰트 패밀리 확인 (보통 'Font Awesome 7 Free' 또는 'Font Awesome 7 Brands') */
font-family: "Font Awesome 7 Free";
font-weight: 900;
color: var(--sub-color);
margin: 0 10px;
}
무늬를 빼고 ::before에 폰트어썸 달았음. 내가 포폴이라 체크표로 달긴 했는데 저거 응용하면 리스트에 이모지 다는 것도 가능합니다.
프로젝트

머여 삼색엔딩도 아니고 왜 색깔이 3개임? 그림을 잘 보시면 저기 초록색으로 된 부분은 아코디언 패널이 열려있죠? 그겁니다. 보라색이 기본이고 파란색은 당신이 마우스를 올리고 있는 패널, 초록색은 열어서 내용을 보고 있는 패널입니다. 근데 이렇게 놓고 보니 얘도 호버를 줄 필요는 없는 것 같아서 저 보고 있는 패널만 액센트로 빼야겠음.
저기는 아코디언 패널인데 지금까지 한 프로젝트들이 들어갑니다. 저기에는 생물쪽 한 거+부트캠프 팀플(아직 안 넣음)+생물학이 아닌 다른 EDA도 같이 들어가는데… 예, 그 캐글 EDA도 들어갑니다… 근데 여기서는 그림 안 넣고, 그냥 프로젝트에 대한 개요(이런거 했다)만 올린 다음 PDF를 올리거나 깃헙으로 유도할 예정임. 그래서 프로젝트 요얄이랑 과정만 있잖아요.
근데 왜 아코디언 패널로 함? 프로젝트 하나당 짧게짧게 한다 쳐도 이게 여러개거든요? 캐글도 선별해서 올리고 있지만 벌써 두개나 올라갔음… 이걸 그냥 보여주는건 좀 아닌 것 같음… 그래서 보고싶은 것만 펴서 보시라고 아코디언 패널로 넣은거다. 그럼 사진은요? 그래프 왜 뺌? 저게 이미지를 보여줄라면 서버에 그 이미지를 같이 올려야되거든요... 일단 그게 귀찮음...
참고로 깃헙이랑 태블로(폰트어썸에 아이콘이 없어서 저걸로 함)는 동작 잘 하는데 파일 링크는 동작을 안 합니다. 왜냐고? 링크를 파일로 안 걸었거든. 아직 파일이 안 올라간 상태입니다. 그리고 캐글 EDA나 캠블 EDA는 깃헙 링크만 있지 PDF도 없습니다. 개별 포폴을 안 만들었거든…
// 프로젝트 아코디언 패널
accordion.forEach((title) => {
title.addEventListener('click', () => {
// 클릭된 제목 바로 다음 요소(accordion_contents)를 타겟팅합니다.
const content = title.nextElementSibling;
const icon = title.querySelector('.icon-toggle');
title.classList.toggle('active');
// 보이고 안 보이고를 토글합니다.
if (content.style.display === 'block') {
content.style.display = 'none';
icon.style.transform = 'rotate(0deg)';
} else {
content.style.display = 'block';
icon.style.transform = 'rotate(180deg)';
}
});
});
기존보다 업그레이드된 아코디언 패널 코드나 보고 가십셔.
1. 저 색깔이 울트라바이올렛이랑 비리디언으로 잡은건데 비리디언이 서브입니다. 서브 먼저 정하고 이색저색 해보다가 울트라바이올렛으로 한건데 얘가 채도가 그렇게 밝은 색이 아님. 좋게 말하자면 눈뽕이 없고 나쁘게 말하자면 그렇게 확 뛰는 색이 아닙니다. 그래서 그것때문에 고민을 많이 했음… 그런데 왜 보라색이죠? 내가 보라색 좋아함. 그 라미 다크라일락 몸통같은 보라색 좋아하는데… 팬톤아… 어떻게 2027년 색깔로 안되겠니…? 되겠냐
2. 액센트가 원래 골드컬러였는데 메인컬러가 보라색+서브컬러가 비리디언이라 액센트가 너무 붕 떠요. 그니까 그 색깔에 문제가 있는 게 아니라 그냥 안 맞는거임. 그래서 액센트를 파란색으로 한겁니다. 저게 비리디언+코발트블루 조합이었으면 아마 액센트로 골드컬러도 맞았을건데...
3. 헤더 오른쪽에 아이콘들 다 눌립니다. 진짜로 링크 걸어놨음.
4. 이걸 어디에 올릴지는 정해지지 않았는데, 이거 올려도 여기에 링크는 안 올릴듯합니다. 일단 저게 포폴이라 내 개인정보를 다 깠어요... 아까도 가리고 올렸잖음. 물론 이 글을 보고 있는 당신이 인사담당자고 내가 당신이 재직중인 회사에 지원하면서 이거 포폴인데 보실래요? 한다거나 나랑 링크드인 팔로워거나 하면 볼 수는 있겠지만 기본적으로 블로그에는 안 올릴 생각입니다. 지금 깃헙에는 올라가있는데 서버에서 내리고 나면 깃헙에서도 내릴까 생각중임.
5. CSS가 뭔가 많아지면서 주석의 소중함을 깨달았음… 주석이 없으니까 뭐가 뭔지 모르겠어요 이거… JS는 생각보다 뭐가 없는데 CSS가 어유 진짜 와… 이거 다 올리면 네이버가 짜름 5000자 넘는다고…
'Coding > JavaScript' 카테고리의 다른 글
| 달력에 기능 추가하기 (0) | 2026.04.21 |
|---|---|
| 제 2차 씨본 컬러 시뮬레이터 보수작업 (0) | 2026.04.16 |
| 포폴 웹페이지화-뼈대 잡기 (0) | 2026.03.07 |
| 개 얼탱이 없는 작업이 온다 두둥 (0) | 2026.03.06 |
| 씨본 팔레트 컬러 시뮬레이터 보수작업 (0) | 2026.02.22 |
내용이요? 이제 생각해야지.

여기까지 해서 뼈대가 잡혔고 탭 메뉴도 잘 돌아간다. 그럼 이제 내용물만 채우면 됩니다…
1. 저 언더 컨스트럭션 란에는 내 이름과 한줄 설명이 들어갈 예정이다. 그래서 일부러 넓게 안 잡았다. 꽉 차보여도 저 분량이 차지하는 건 가로 75%정도… (모바일은 미뎌쿼리 줘서 95%)
2. About에는 본인 약력, 자격증이 들어가고 skills는 뭐 할 수 있냐고 project에 개인적으로 했던 모든 프로젝트들이 들어갈 예정이다.

3. About 단을 하나로 할지 두개로 할 지 생각중임... 한쪽은 넓게 잡아서 내 소개 하고(아직 데이터쪽 약력이 없음...) 한쪽에 좀 좁게 잡아서 자격증이랑 외부 활동 내역같은 거 쓸까...
4. Project에 아코디언 패널 들어갑니다. 아코디언 패널이 뭐냐면 그 클릭하면 접히는? 그런거 있음. 프로젝트별로 패널 하나씩 할당됩니다.
5. HTML 코드는 어마무시하게 긴데 자바스크립트는 아직까진 그렇게 길지 않음… CSS……. (마른세수)
'Coding > JavaScript' 카테고리의 다른 글
| 제 2차 씨본 컬러 시뮬레이터 보수작업 (0) | 2026.04.16 |
|---|---|
| 포폴 웹페이지화-더 이상의 자세한 설명은 생략한다 (0) | 2026.03.08 |
| 개 얼탱이 없는 작업이 온다 두둥 (0) | 2026.03.06 |
| 씨본 팔레트 컬러 시뮬레이터 보수작업 (0) | 2026.02.22 |
| 씨본 컬러 파레트 씨뮬레이터 (0) | 2026.01.30 |

이게 뭐냐고요? 포폴을 웹으로 만들어서 서버에 올리자는 진심 얼탱이없는 작업에 들어갈 예정입니다. 근데 지금 팀플때문에 바빠서 구조 구상하고 색깔만 짜놨음.

대충 그렸음 대충...
1. 내 이름이랑 전번 이메일 깃헙 블로그(티스토리) 링크가 헤더에 들어가고(이부분도 고민 좀 해봐야됨...)
2. 그 밑에 내 이력이랑 스킬(뭐뭐 쓸 수 있나) 프로젝트가 들어가는데 이게 탭메뉴입니다. 그니까 얘를 탭하면 전환이 돼야 하는데 이걸 자바스크립트로 해야 하고…
3. 프로젝트에는 내 포폴에도 올라가는 프로젝트 세 개가 올라가는데 그거에 대한 설명을 개별 프로젝트로 아코디언 메뉴로 하고 PDF파일을 거기다가 올리든가 할겁니다.
링크 관련해서는 이걸 폰트어썸 아이콘만 넣을지(7.x로 올렸더만…) 이름을 병기할지정도 고민중임. 나머지는 뭐… 그렇죠 뭘.
'Coding > JavaScript' 카테고리의 다른 글
| 포폴 웹페이지화-더 이상의 자세한 설명은 생략한다 (0) | 2026.03.08 |
|---|---|
| 포폴 웹페이지화-뼈대 잡기 (0) | 2026.03.07 |
| 씨본 팔레트 컬러 시뮬레이터 보수작업 (0) | 2026.02.22 |
| 씨본 컬러 파레트 씨뮬레이터 (0) | 2026.01.30 |
| 특정 조건을 만족하면 DOM이 나타나게 해 보자 (0) | 2025.09.17 |
https://koreanraichu.tistory.com/853
씨본 컬러 파레트 씨뮬레이터
그 Seaborn에서 컬러 커마 가능한거 아시죠? 근데 이게 뭔 색인지 뭔 파레트가 어떻게 나오는지 모르잖아요. ㅇㅋ? ㅇㅇㅋ. 그래서 색 두 개를 입력받은 다음 한 10칸정도로 띄엄띄엄 칠한거 하나, c
koreanraichu.tistory.com
이거 함 써보고 고칠점 찾음…
입력하는 색상 앞에 #가 들어가면 #을 빼주기
이게 왜 필요함? 하실 수도 있는데 컬러 파레트 만드는데서 복사할때 앞에 #이 붙는 경우가 있고, 아닌 경우가 있습니다. 근데 #이 붙으면 안됐거든요? 이러면 사용자 입장에서는 아니 내가 이거 하나 쓰자고 #을 지워야됨??? 이 되는거죠. 뭔지 아시겠죠?
const color1 = first_color.value.replace(/#/g, '').trim(); // 응 샵 들어가
const color2 = second_color.value.replace(/#/g, '').trim(); // 얘도 데려가
#은 잘 뺐는데 생성이 안돼서 봤더니 함수에 다른걸로 들어가서 그런거였음… 제미나이 이 에미나이 왜 자꾸 딴데서 돌리고 있냐고… 클로드가 찾아줘서 알았음 나도… 아무튼 이거 수정해서 잘됩니다.
그라데이션 종류 추가
그 단색으로 해서 라이트&다크가 있고 diverging이라고 해서 양 끝단에 색 두개+가운데 색 하나 있는 게 있어요. 그걸 만들어야됨.
const cmap_div_light = document.querySelector('.cmap2'); // Light
const cmap_div_dark = document.querySelector('.cmap3'); // Dark
const cmap_div_divl = document.querySelector('.cmap4'); // Diverging(light)
const cmap_div_divd = document.querySelector('.cmap5'); // Diverging(dark)
일단 네개 추가요… diverging도 가운데 색 따라서 라이트 다크 있음.
// 라이트
cmap_div_light.style.backgroundImage = `linear-gradient(to right, ${"#" + color1}, #ffffff)`; // 왼->오
// 다크
cmap_div_dark.style.backgroundImage = `linear-gradient(to right, ${"#" + color1}, #000000)`; // 왼->오
//diverging(light)
cmap_div_divl.style.backgroundImage = `linear-gradient(to right, ${"#" + color1}, #ffffff, ${"#" + color2})`; // 왼->오
//diverging(dark)
cmap_div_divd.style.backgroundImage = `linear-gradient(to right, ${"#" + color1}, #000000, ${"#" + color2})`; // 왼->오
이게 한번에 될라나...

일단 그라데이션은 다 나왔으니까 위치나 잡아봅시다…

.cmap_wrapper > div {
width: 100%;
height: 90px;
margin: 10px 0;
}
일단 하위 div가 늘었는데 그 div들 사양이 다 같아서 이렇게 줬음.

그리고 p태그 문구 바꿨습니다. 저기 개별로 추가하긴 힘들어...
그라데이션은 순서대로 2배색 그라데이션, 단색(밝음), 단색(어두움), diverging(밝은색), diverging(어두운색)입니다. 여기다가 두번째색 단색도 넣으면 될듯?
// 그라데이션
cmap_div.style.backgroundImage = `linear-gradient(to right, ${"#" + color1}, ${"#" + color2})`; // 왼->오
// 라이트
cmap_div_light.style.backgroundImage = `linear-gradient(to right, ${"#" + color1}, #ffffff)`; // 왼->오
// 다크
cmap_div_dark.style.backgroundImage = `linear-gradient(to right, ${"#" + color1}, #000000)`; // 왼->오
// 라이트
cmap_div_light2.style.backgroundImage = `linear-gradient(to right, ${"#" + color2}, #ffffff)`; // 왼->오 (두번째색)
// 다크
cmap_div_dark2.style.backgroundImage = `linear-gradient(to right, ${"#" + color2}, #000000)`; // 왼->오 (두번째색)
//diverging(light)
cmap_div_divl.style.backgroundImage = `linear-gradient(to right, ${"#" + color1}, #ffffff, ${"#" + color2})`; // 왼->오
//diverging(dark)
cmap_div_divd.style.backgroundImage = `linear-gradient(to right, ${"#" + color1}, #000000, ${"#" + color2})`; // 왼->오

한 화면에서 보기 힘들어졌음.. 일단 기능은 잘 됩니다.
근데 한가지 문제가 있다면 씨본에서 diverging 만들때 rgb가 아니라 husl을 입력함… 아 이것도 rgb 해달라고… husl 변환 귀찮다고…
'Coding > JavaScript' 카테고리의 다른 글
| 포폴 웹페이지화-뼈대 잡기 (0) | 2026.03.07 |
|---|---|
| 개 얼탱이 없는 작업이 온다 두둥 (0) | 2026.03.06 |
| 씨본 컬러 파레트 씨뮬레이터 (0) | 2026.01.30 |
| 특정 조건을 만족하면 DOM이 나타나게 해 보자 (0) | 2025.09.17 |
| 프로그레스 바 만들어보기 (0) | 2025.09.16 |
그 Seaborn에서 컬러 커마 가능한거 아시죠? 근데 이게 뭔 색인지 뭔 파레트가 어떻게 나오는지 모르잖아요. ㅇㅋ? ㅇㅇㅋ. 그래서 색 두 개를 입력받은 다음 한 10칸정도로 띄엄띄엄 칠한거 하나, cmap(이어지는거)으로 하나 짜잔 하고 보여주자 이거다.
이거 근데 팀플에서도 써먹으려면 코드펜에도 올려야 할 듯.
<html>
<head>
<title>Seaborn palette simulator</title>
<link href="style.css" rel="stylesheet">
</head>
<body>
<div class="wrapper">
<h1>Seaborn palette simulator</h1>
<p>그... 저기 인풋창에 색깔 두개 입력하시면 파레트랑 cmap이랑 보여줄거긴 합니다. 근데... 아... </p>
<p>세개 이상... 그거는 제 능력 밖이니까 걍 두개씩 돌려보세요... <s>이봐요</s></p>
<div class="color_input"></div>
<div class="palette">
</div>
<div class="cmap"></div>
</div>
</body>
<script src="script.js"></script>
<script src="https://kit.fontawesome.com/dc58858c96.js" crossorigin="anonymous"></script>
</html>
그래요. 여기에도 썼지만 세개 이상은 내 능력 밖이니께 걍 두개씩 보든가 하세요.
cmap_gradation = document.querySelector('.cmap');
palette_gradation = document.querySelector('.palette');
color_button = document.querySelector('#generate'); // Button
일단 파레트는 아래에 div를 10개정도 생성하고 색을 동적으로 줘야 하고, cmap은 걍 그라데이숑 주면 된다. 그라데이션이 쉽죠. 배경만 입히면 되니까. 그럼 걍 그라데이션 하면 안되나 할 수도 있는데 씨본 파레트가 딱딱 끊어지는 구조입니다.
cmap_gradation = document.querySelector('.cmap');
palette_gradation = document.querySelector('.palette');
// Input
first_color = document.querySelector('#first_color');
second_color = document.querySelector('#second_color');
color_button = document.querySelector('#generate'); // Button
// 벗흔이여 일을 하세요
color_button.addEventListener('click',()=>{
console.log(first_color.value, second_color.value)
});
뭔가 가져온 게 늘은 것 같죠? 늘은거 맞음. 인풋창을 갖고와야 색을 제어하죠.
// 벗흔이여 일을 하세요
color_button.addEventListener('click',()=>{
// 칸으로 노나지는 그거
palette_div = document.createElement('div');
palette_div.classList.add('palette_div');
// 그라데이션
cmap_div.style.backgroundImage = `linear-gradient(to right, ${"#" + first_color.value}, ${"#" + second_color.value})`; // 왼->오
});
그라데이션이 간단할거라고 생각하던 시절이 나에게도 있었다. 저거 적용하고 텍스트 달고 하는게 개노가다였지만 그라데이션은 선녀예요. 파레트는 저 전체 크기를 10등분한 걸 생성해서 줄줄이 붙여야됩니다... ㅋㅋㅋㅋㅋㅋ
const hexToRgb = (hex) => {
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return [r, g, b];
};
const startRgb = hexToRgb(first_color.value);
const endRgb = hexToRgb(second_color.value);
const n = 10;
//자 드가자
for (let i = 0; i < n; i++) {
const step = i / (n - 1); // 0부터 1까지의 비율
// R, G, B 각각 보간 계산
const r = Math.round(startRgb[0] + (endRgb[0] - startRgb[0]) * step);
const g = Math.round(startRgb[1] + (endRgb[1] - startRgb[1]) * step);
const b = Math.round(startRgb[2] + (endRgb[2] - startRgb[2]) * step);
// 새 div 생성 및 스타일 적용
const chip = document.createElement('div');
chip.classList.add('palette_chip'); // CSS에서 너비/높이 설정용
chip.style.backgroundColor = `rgb(${r}, ${g}, ${b})`;
chip.style.flex = "1"; // 10칸이 골고루 나눠지도록
// 드디어 등장하는 오타 없는 그 녀석
palette_div.appendChild(chip);
생성되는거 방향 뻑나서 플렉스 줬다... 그럼 여기서 끝인가요? 아뇨. 유효성검사 해야죠.
// 벗흔이여 일을 하세요
color_button.addEventListener('click',()=>{
if (!regex.test(first_color.value) || !regex.test(second_color.value)) {
alert('유효한 색상값이 아닙니다!')
} else {
palette_div.innerHTML = '';
cmap_div.innerHTML = '';
// 칸으로 노나지는 그거
const hexToRgb = (hex) => {
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return [r, g, b];
};
const startRgb = hexToRgb(first_color.value);
const endRgb = hexToRgb(second_color.value);
const n = 10;
//자 드가자
for (let i = 0; i < n; i++) {
const step = i / (n - 1); // 0부터 1까지의 비율
// R, G, B 각각 보간 계산
const r = Math.round(startRgb[0] + (endRgb[0] - startRgb[0]) * step);
const g = Math.round(startRgb[1] + (endRgb[1] - startRgb[1]) * step);
const b = Math.round(startRgb[2] + (endRgb[2] - startRgb[2]) * step);
// 새 div 생성 및 스타일 적용
const chip = document.createElement('div');
chip.classList.add('palette_chip'); // CSS에서 너비/높이 설정용
chip.style.backgroundColor = `rgb(${r}, ${g}, ${b})`;
chip.style.flex = "1"; // 10칸이 골고루 나눠지도록
// 드디어 등장하는 오타 없는 그 녀석
palette_div.appendChild(chip);
}
}
// 그라데이션
cmap_div.style.backgroundImage = `linear-gradient(to right, ${"#" + first_color.value}, ${"#" + second_color.value})`; // 왼->오
});
나 그러고보니 처음에 요소 가져올때 const도 안 붙이고 가져왔네?

마진 좀 조절하고 깃헙에 올리겠음.
See the Pen Seaborn palette simulator by koreanraichu (@koreanraichu) on CodePen.
코드펜에도 올렸음다.
'Coding > JavaScript' 카테고리의 다른 글
| 개 얼탱이 없는 작업이 온다 두둥 (0) | 2026.03.06 |
|---|---|
| 씨본 팔레트 컬러 시뮬레이터 보수작업 (0) | 2026.02.22 |
| 특정 조건을 만족하면 DOM이 나타나게 해 보자 (0) | 2025.09.17 |
| 프로그레스 바 만들어보기 (0) | 2025.09.16 |
| 드디어 추가되는 파일 불러오기 기능 (0) | 2025.09.11 |
요즘은 핸드폰으로 본인인증 많이 하는데, 그 인증창 보면 딸랑 이름 입력하는 란만 보인다. 그리고 이름을 입력하면 주민번호 입력란이, 주민번호 입력란을 다 채우면, 핸드폰 번호 입력란, 그리고 보안문자 입력란(이건 대충 사람입니다 체크하는거라고 보시면 될듯) 다음에 인증번호 입력란이 순차적으로 뜬다. 그러니까 이게 처음부터 떠있는 게 아니라 어떤 조건을 만족하면 뜬다 이거다.
뭐함? 이거 해볼거니까 빨리 에디터 켜요.

입력창 세 개와 버튼 하나가 보인다. 그리고 여기서 어떻게 할 거냐면
1. 이름을 입력하면 생년월일 입력창이 보이고
2. 생년월일 입력창을 다 채우면 연락처 입력하는 창이 보이고
3. 연락처를 입력하면 확인버튼이 보이는
걸 해 볼거다.
#no2 {
display: none;
}
#no3 {
display: none;
}
#no4 {
display: none;
}
그래서 이름 입력란을 제외한 나머지는 다 display를 none으로 해 줬다.
const name = document.querySelector("#no1");
const nameInput = document.querySelector("#name");
const birthday = document.querySelector("#no2");
const birthInput = document.querySelector("#birthday");
const phone = document.querySelector("#no3");
const phoneInput = document.querySelector("#cellphone");
const button = document.querySelector("#no4");
어디가요 제어할라면 가져와야지.
근데 여기서 중요한 게 있다. 우리는 저기에 뭘 '채워 넣었을 때' 다음 창이 보이게 해야 하잖아요? 그러니까 이벤트 리스트너를 쓸 거면 쟤를 클릭할때 되게 하면 안 된다. 그리고 위에 뭐가 길어서 봤더니 입력란 달린 애들은 다 두줄씩 달고 있죠? 보여야 하는 건 div인데 값 갖고와야 하는 건 입력란이라 그렇다. 첫번째줄은 div, 두번째줄은 입력란이라고 생각하면 된다.
근데 애초에 저기에 이벤트 리스트너를 줄 수가 없음… 내가 해봤는데 안됨. 그럼 어떻게 해요? 값 가져와서 if문 때려박아봐야지.
nameInput.addEventListener("input",()=>{
if (nameInput.value.trim() !== "") {
birthday.style.display = "block";
} else {
birthday.style.display = "none";
}
});
아니 쟤 인풋도 있어… 없는게 뭐야 대체… 그래서 이 코드가 뭐임? 말 그대로 입력하면 생년월일이 나오게 하는 코드다. 근데 문제가 하나 있다면 쟤는 입력을 하자마자 밑의 항목이 나오기때문에

성만 써도 이름이 나오는 것을 볼 수 있다. 아니 그럼 이름을 다 썼을 때 나오게 하려면 어떻게 해요?
nameInput.addEventListener("blur",()=>{
if (nameInput.value.trim() !== "") {
birthday.style.display = "block";
} else {
birthday.style.display = "none";
}
});
이벤트 리스트너 안에 있는 input을 blur로 바꿔주면 이름을 다 쓰고 커서를 뗐을 때 생년월일 칸이 나온다.
birthInput.addEventListener('blur',()=>{
if (birthInput.value.trim() !== "") {
phone.style.display = "block";
} else {
phone.style.display = "none";
}
});
같은 input이라 그런가 저 코드가 먹힌다. 그럼 폰 번호도 저렇게 하면 되나요? 일단 저렇게 해도 되긴 되는데, 핸드폰 번호의 경우 절차가 하나 더 필요하다. 뭔 절차요? 저거 유효성검사 하셔야죠.
const phoneRegex = /^01[016789]-?\d{3,4}-?\d{4}$/;
그래서 규식정씨를 불러야 한다. (정규식이라 규식 정)
phoneInput.addEventListener('blur',()=>{
if (phoneRegex.test(phoneInput.value.trim())) {
button.style.display = "block";
} else {
phoneInput.style.borderColor = "#cc0000";
button.style.display = "none";
}
});
근데 저기 else에 들어간 건 뭐예요? 형식 안 맞으면 테두리 색깔 바꾸라는 얘기다.

전화번호는 3자리-4자리-4자리 혹은

11자리 숫자로 걍 쓰면 확인버튼이 보이는데 저 버튼은 눌러도 별 기능 없다.

그리고 형식이 안 맞으면 이렇게 빨간 테두리가 나오는 것.
근데 아까부터 저 if문에 들어가는 trim()은 뭔가요? 저건 문자 양 옆에 있는 공백을 다 떼버리라는 얘기다. 저걸 해주면 이름란에 공백만 입력한 경우에도 다음으로 안 넘어가게 된다. 물론 이름 앞뒤에 공백 들어가면 알아서 떼 주는 역할은 덤.

아까 그 빨간 테두리를 전체 영역으로 확장했다.
이벤트 리스트너의 blur는 이름을 입력하고 나서 커서를 입력란 말고 다른데다 둬야 이벤트를 실행하는데, 이조차 번거롭다! 하면 다른 방법도 있다.
nameInput.addEventListener("keydown",(e)=>{
if (e.key === "Enter" && nameInput.value.trim() !== "") {
nameInput.style.borderColor = "#000000";
birthday.style.display = "block";
} else {
nameInput.style.borderColor = "#cc0000";
birthday.style.display = "none";
}
});
이벤트 리스트너의 blur를 키다운으로 바꾸고 if문에 1) 이름이 입력된 상태에서 2) 엔터키를 눌렀을 때 다음 입력란을 볼 수 있게 하면 된다. 근데 쟤는 위에 썼던 코드랑 달리 트리거 옆에 e가 있네요? 님 원래 저거 생략하지 않았음? 저걸 생략한 게 익명함수 김람다씨인데 김람다씨가 아무때나 다 쓸 수 있는 게 아니다. 마치 우리가 인터넷에서는 닉네임으로 활동하지만 송금할때는 본명이 필요하듯이 말이다. 김람다인 이유는 그냥 김씨가 흔해서다 그럼 저 e가 없으면 어떻게 되는데요? 컴퓨터가 타겟을 못 찾아서 뭔 엔터를 뭘 어쩌라는거냐면서 밥상을 뒤집습니다. 물론 진짜 밥상을 뒤집지는 않겠지만 아마 의도한대로 작동은 안 할거다.
아니 근데 하는 김에 하나만 더 합시다. 이거 커서 다음 입력란으로 알아서 못 넘겨요?
nameInput.addEventListener("keydown",(e)=>{
if (e.key === "Enter" && nameInput.value.trim() !== "") {
nameInput.style.borderColor = "#000000";
birthday.style.display = "block";
birthInput.focus();
} else {
nameInput.style.borderColor = "#cc0000";
birthday.style.display = "none";
}
});
커서를 넘길 DOM.focus() 해 주면 알아서 다음으로 넘어간다.
See the Pen 조건부 DOM by koreanraichu (@koreanraichu) on CodePen.
버튼은 장식이니까 굳이 눌러보지는 말자. 진짜로 장식이다.
'Coding > JavaScript' 카테고리의 다른 글
| 씨본 팔레트 컬러 시뮬레이터 보수작업 (0) | 2026.02.22 |
|---|---|
| 씨본 컬러 파레트 씨뮬레이터 (0) | 2026.01.30 |
| 프로그레스 바 만들어보기 (0) | 2025.09.16 |
| 드디어 추가되는 파일 불러오기 기능 (0) | 2025.09.11 |
| 텍스트 에디터에 뭔가 추가해보자 (0) | 2025.08.18 |
여기서 만들 건 나우 로딩하면 나오는 프로그레스 바가 아니라, 스크롤 프로그레스 바다. 티스토리 블로그 중에 가끔 글을 읽을 때 화면 상단에 무슨 막대기같은 게 자라는 게 있는데, 그걸 해 볼거다.
Reference
https://doooodle932.tistory.com/177
[JS] 스크롤 프로그레스 바 만들기
완성본 HTML CSS body { margin: 0; padding: 0; height: 1000px; /* 스크롤을 위해 임시로 추가 */ } .progressWrap { position: fixed; top: 0; left: 0; width: 100%; height: 3px; background-color: #fff; } .bar { width: 0%; height: inherit; position:
doooodle932.tistory.com
https://sunshineyellow.tistory.com/88#clientHeight
[코딩애플] 요소의 높이와 위치값 구하기 (clientHeight, offsetHeight, scrollHeight, getBoundingClientRect())
clientHeight console.log(element.clientHeight); clientHeight는 패딩을 포함한 요소의 높이를 가져온다. offsetHeight console.log(element.offsetHeight); clientHeight는 패딩, 테두리, 가로스크롤바(있는 경우)을 포함한 요소
sunshineyellow.tistory.com

일단 이건 스크롤을 해야 하는 거기 때문에 분량을 급나 길게 늘려야 한다. 그래서 폰트 크기에 줄 높이 주고 로렘입숨을 겁나 때려박아서 기이이이이이이이이이일게 만들었다.
const progressBar = document.querySelector('#progress');
만들었으면 가져오십쇼.
#progress {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 5px;
background-color: #f7cac9;
}
깜빡하고 CSS 생략할 뻔 했는데, 보통 프로그레스 바는 화면 상단에 있다. 그러니까 위치를 화면 상단으로 고정해야 한다.
이 스크롤바 역시 이 블로그에 단골로 등장하는 이벤트 리스트너를 이용할건데... 그럼 여기서 짬 좀 있으신 분들은 감이 좀 오실 것이다. 아니 이거 스크롤 이벤트도 커버가 돼요? 아니 글쎄 그것까지 커버가 되더라고요? 얘 대체 안되는게 뭐임?
document.addEventListener('scroll',()=>{
let scrollNum = window.scrollY;
console.log(scrollNum);
})
이런 식으로 스크롤 높이를 가져온 다음 거기에 맞춰서 작대기 크기를 늘였다 줄였다 하면 된다고 보시면 된다. window.scrollY 말고 document.documentElement.scrollTop도 쓴다.
document.addEventListener('scroll',()=>{
let scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
let progressPercent = window.scrollY / scrollHeight
console.log(progressPercent)
})
뭐임? 왜 갑자기 많아짐? 을 이제 하나씩 풀어보자...
document.documentElement.scrollHeight은 본문에 로렘 입숨을 급나 때려박아서 만든 높이이다. 그러니까 캡쳐된 부분 말고 그 밑에 더 있는 본문까지 해서 나오는 높이를 말한다. 그리고 document.documentElement.clientHeight은 나도 뭔지 모르겠음… 패딩을 포함한 요소의 높이라는데 이건 좀 더 알아봐야겠음.
그래서 이게 뭐 하는건데요? 저 작대기 길이 정할라면 계산을 해야 할 거 아닙니까. 프로그레스 바 길이 정할라고 계산하는거다.
document.addEventListener('scroll',()=>{
let scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight; //뒤에꺼 뭔지 모르겠음...
let progressPercent = (window.scrollY / scrollHeight) * 100;
progressBar.style.width = progressPercent + "%";
})
그리고 길이 계산해서 프로그레스 바 길이 바꾸게 하면 땡이다.

티스토리 블로그에 적용중인 프로그레스 바. 이거 괴담수사대에도 적용중인데, 모바일 버전에서는 껐다. (미디어쿼리로 끌 수 있음)

코드펜에 적용한 프로그레스 바. 단색 뿐 아니라 그라데이션도 가능하다. 티스토리 블로그에 적용한 것도 그라데이션이다.
See the Pen progress bar(JS) by koreanraichu (@koreanraichu) on CodePen.
가서 직접 휠을 굴려보자.
'Coding > JavaScript' 카테고리의 다른 글
| 씨본 컬러 파레트 씨뮬레이터 (0) | 2026.01.30 |
|---|---|
| 특정 조건을 만족하면 DOM이 나타나게 해 보자 (0) | 2025.09.17 |
| 드디어 추가되는 파일 불러오기 기능 (0) | 2025.09.11 |
| 텍스트 에디터에 뭔가 추가해보자 (0) | 2025.08.18 |
| 아주 간단한 텍스트 에디터를 만들어보자 (0) | 2025.08.08 |
https://koreanraichu.tistory.com/718
텍스트 에디터에 뭔가 추가해보자
https://koreanraichu.tistory.com/711 아주 간단한 텍스트 에디터를 만들어보자카테고리를 보시면 아시겠지만, 자바스크립트로 할 거다. 근데 일단 구현할 기능이 쓴 걸 저장하는 것 말고 없음… ㅋㅋㅋ
koreanraichu.tistory.com
여기서 이어진다. 텍스트 에디터를 만들면서 처음에 얘기했던 '텍스트 파일을 불러오면 제목에 파일명, 이름에 내용을 띄우는'걸 해 보자. 이건 일단 input이랑 FileReader API가 필요하다고 한다.

저기 셀렉트박스 오른쪽에 보면 버튼 하나가 있다. 저건 사실 인풋 파일+라벨 붙여놓고 라벨에 CSS 준 거임. 아무튼 그럼.
const fileOpen = document.querySelector('#textuploader');
늘 그렇듯이 주무르려면 일단 갖고와야 한다.
fileOpen.addEventListener('click',(e)=>{
const file = e.target.files;
console.log(file[0])
})
쟤 file만 해서 콘솔 띄워보니까 FileList라고 뜨더라… 이런건 바로바로 불러오려면 인덱싱을 해야 한다. 사실 GPT가 [0] 썼을때부터 아 저놈도 인덱싱 해야 하는구나 싶긴 했음.
fileOpen.addEventListener('click',(e)=>{
const file = e.target.files[0]; //너도 배열이냐?
if (!file) return;
textTitle.value = file.name.replace(/\.txt$/, '');
const reader = new FileReader();
reader.onload = function(event) {
textArea.value = event.target.result;
count.innerText = textArea.value.length;
};
reader.readAsText(file, 'utf-8');
})
이게 로직은 문제가 없음. 불러와서 파일명을 제목에 띄우고, 내용을 내용란에 띄워라. 이게 다임. 근데 안되는거임. 그래서 왜 안됨? 했더니 정말 뜻밖의 문제점을 지적해줬다.
fileOpen.addEventListener('change',(e)=>{
const file = e.target.files[0]; //너도 배열이냐?
if (!file) return;
textTitle.value = file.name.replace(/\.txt$/, '');
const reader = new FileReader();
reader.onload = function(event) {
textArea.value = event.target.result;
count.innerText = textArea.value.length;
};
reader.readAsText(file, 'utf-8');
})
무슨 차이인지 모르겠다고? 안 되는 코드는 이벤트 리스트너에 click이 들어가 있고, 되는 코드는 이벤트 리스트너에 change가 들어가 있다. 아니 지피티야 이거 왜이럼?
클릭은 쟤를 클릭하면 이벤트가 시작된다. 그러니까 파일을 첨부하기 '전에' 이벤트가 발동되는데, 컴퓨터 입장에서는 뭐여 왜 없는 파일 제목을 띄우래? 가 되는거다. 마치 내 남친은 질량이 0인데 남자친구를 데려오라는 것과 같다. 그리고 체인지는 파일이 올라왔을 때 발동되는거라 아 올라왔으니 띄워줌이 되는 것이다. 이해가 어렵다면 change는 '(파일의 업로드) 상태가 바꼈을 때'라고 이해하면 된다.

불러온 후
참고로 원래 기능(텍스트 내용 복사, 텍스트 저장)역시 파일을 불러온 후로도 제대로 작동한다.
'Coding > JavaScript' 카테고리의 다른 글
| 특정 조건을 만족하면 DOM이 나타나게 해 보자 (0) | 2025.09.17 |
|---|---|
| 프로그레스 바 만들어보기 (0) | 2025.09.16 |
| 텍스트 에디터에 뭔가 추가해보자 (0) | 2025.08.18 |
| 아주 간단한 텍스트 에디터를 만들어보자 (0) | 2025.08.08 |
| 카카오톡 모아보내기를 대충 구현해보자 (0) | 2025.06.27 |
https://koreanraichu.tistory.com/711
아주 간단한 텍스트 에디터를 만들어보자
카테고리를 보시면 아시겠지만, 자바스크립트로 할 거다. 근데 일단 구현할 기능이 쓴 걸 저장하는 것 말고 없음… ㅋㅋㅋㅋㅋㅋ 여기서 더 들어가면 지피티 불러야됩니다…기본적인 기능(저장
koreanraichu.tistory.com
여기서 이어진다.
원래 추가하려던 건 파일 불러서 파일 이름을 제목으로 하는거였는데… 씁 그건 지피티 있어야되고… 오늘 추가해볼 건 그거다. 에디터 글꼴 변경하는 거. 페이지 전체 글꼴이 아니라, 글 작성하는 부분 글꼴(인풋이랑 텍스트에리어) 말이다.
우선 이걸 하려면 먼저 할 일이 있다. 물론 HTML에 추가하는 것도 할 일인 건 맞는데, CSS단에서 바꾸려면 폰트 URL을 import해야 한다. 뭐 거창한 건 아니고

이거 몇 개 복사해서 CSS파일에 붙여넣기 하자.
<select id="fonts">
<option value="CHOGOONCHICKENSCRATCHV7">조군 개발새발 7</option>
<option value="nanumgothic">나눔고딕</option>
<option value="Pretendard">프리텐다드</option>
<option value="ridibatang">리디바탕</option>
<option value="dosiyagi">도스이야기</option>
<option value="mabinogiclassic">마비옛체</option>
</select>
그리고 HTML단에서는 select태그를 줬다. 저 폰트 전부 CSS에 import 된 상태여야 적용이 된다. 참고로 기본 글씨체는 온글잎 콘콘이다.
const fontSelection = document.querySelector('#font');
어디가요 가져와야지.

이거 반복문 되나…? 아니 그 전에 이거 리스트가 아닌 거 아냐?

아, 얘도 .value 써서 가져오는 모양이다.

selectedIndex도 있는데 걍 이걸로 하자. 폰트명 너무 길어… ㅡㅡ

아니 내가 이걸 생각한 건 맞는데… 이게 왜 됨????? 진짜 왜 되는거임?

이게 기본폰트(온글잎 콘콘)다.

그리고 마비옛체로 바꾼 결과가 이거. 아니 근데 뭘 생각하셨길래요?
fontSelection.addEventListener('click',()=>{
if (fontSelection.selectedIndex == 0) {
fontSelection.style.fontFamily = 'Ownglyph_corncorn-Rg';
textTitle.style.fontFamily = 'Ownglyph_corncorn-Rg';
textArea.style.fontFamily = 'Ownglyph_corncorn-Rg';
} else if (fontSelection.selectedIndex == 1) {
fontSelection.style.fontFamily = 'CHOGOONCHICKENSCRATCHV7';
textTitle.style.fontFamily = 'CHOGOONCHICKENSCRATCHV7';
textArea.style.fontFamily = 'CHOGOONCHICKENSCRATCHV7';
} else if (fontSelection.selectedIndex == 2) {
fontSelection.style.fontFamily = 'nanumgothic';
textTitle.style.fontFamily = 'nanumgothic';
textArea.style.fontFamily = 'nanumgothic';
} else if (fontSelection.selectedIndex == 3) {
fontSelection.style.fontFamily = 'Pretendard-Regular';
textTitle.style.fontFamily = 'Pretendard-Regular';
textArea.style.fontFamily = 'Pretendard-Regular';
} else if (fontSelection.selectedIndex == 4) {
fontSelection.style.fontFamily = 'RIDIBatang';
textTitle.style.fontFamily = 'RIDIBatang';
textArea.style.fontFamily = 'RIDIBatang';
} else if (fontSelection.selectedIndex == 5) {
fontSelection.style.fontFamily = 'DOSIyagiMedium';
textTitle.style.fontFamily = 'DOSIyagiMedium';
textArea.style.fontFamily = 'DOSIyagiMedium';
} else if (fontSelection.selectedIndex == 6) {
fontSelection.style.fontFamily = 'MabinogiClassicR';
textTitle.style.fontFamily = 'MabinogiClassicR';
textArea.style.fontFamily = 'MabinogiClassicR';
}
});
이거요. forEach 돌릴까 하다가 걍 if 돌려버렸음.

온글잎 콘콘

조군 개발새발 V7

나눔고딕

프리텐다드.. 솔직히 고딕체는 딱딱해서 별로 안 좋아하지만 가장 깔끔한 것도 고딕체이긴 하다. 깔끔한건 인정. 근데 내가 고딕체를 별로 안 좋아하는 이유도 특유의 딱딱함때문임...

리디바탕. 바탕체중에서 개인적으로 제일 좋아하는 바탕체이다.

도스이야기. 왜 초딩때 했던 한컴타자가 생각나는거냐...

마비옛체
참고로 이 글꼴들을 고를 때 본인 기준으로 한자 지원 안 하는 폰트는 다 빠진다. 이게 단순히 지원만 안 하는거면 구글 웹폰트에서 일본 폰트 긁어와서 쓰면 되는데 한자 지원을 안하면서 한자를 공백으로 표기하면 골치아파짐… 특히나 괴담수사대에는 제목이나 본문에 한자가 들어가는 경우도 있어서 그런 폰트는 기피된다.
'Coding > JavaScript' 카테고리의 다른 글
| 프로그레스 바 만들어보기 (0) | 2025.09.16 |
|---|---|
| 드디어 추가되는 파일 불러오기 기능 (0) | 2025.09.11 |
| 아주 간단한 텍스트 에디터를 만들어보자 (0) | 2025.08.08 |
| 카카오톡 모아보내기를 대충 구현해보자 (0) | 2025.06.27 |
| 우리도 그 유리 효과인지 뭔지 해봅시다 거 (0) | 2025.06.18 |
카테고리를 보시면 아시겠지만, 자바스크립트로 할 거다. 근데 일단 구현할 기능이 쓴 걸 저장하는 것 말고 없음… ㅋㅋㅋㅋㅋㅋ 여기서 더 들어가면 지피티 불러야됩니다…
기본적인 기능(저장하기)

그니까 뭘 할거냐면, 여기서 글씨를 적고 하단의 저 버튼을 누르면 저 텍스트에리어 안의 글이 저장된다. 오케이?
const textArea = document.querySelector('#textarea');
const textSave = Document.querySelector('#save');
일단 자바스크립트로 뭘 할라면 가져와야된다.
textSave.addEventListener('click',()=>{
console.log(textArea.value);
})
이 블로그를 많이 봐오신 분들은 다 알겠지만, 여기까지는 쉽다. 여기까지는.
const textArea = document.querySelector('#textarea');
const textSave = document.querySelector('#save');
textSave.addEventListener('click',()=>{
const blob = new Blob([textArea.value], {type:'text/plain'});
const url = window.URL.createObjectURL(blob);
//블롭블롭
const link = document.createElement('a');
link.style.display = 'none';
link.href = url;
link.download = 'file';
document.body.appendChild(link);
link.click();
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
})
롸? 이 코드가 아님?
const textArea = document.querySelector('#textarea');
const textSave = document.querySelector('#save');
textSave.addEventListener('click',()=>{
const blob = new Blob([textArea.value], {type:'text/plain'});
const url = window.URL.createObjectURL(blob);
//블롭블롭
const link = document.createElement('a');
link.style.display = 'none';
link.href = url;
link.download = 'file';
document.body.appendChild(link);
link.click();
setTimeout(() => {
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}, 100);
})
바꾼건 setTimeout함수 안에 있는 a를 link로 바꾼 것 뿐인데 갑자기 잘 된다. 많이 당황스럽다.
그럼 이제 여기서 뭘 할거냐… 지금은 텍스트파일을 저장하면 file로 저장되는데 이걸 이름을 입력받을거고, 이거 글쓰기 할 때 쓸거라서 글자수 세는 기능도 넣을거다.
제목을 파일명으로 저장하기
일단 파일명을 어떻게 할 거냐고? 이 스크린샷을 보자.

저 위에 인풋창을 새로 만들었는데, 쟤가 제목 겸 파일명이다. 그럼 자바스크립트에서 뭘 해야 하냐, 크게 1) input의 값이 있는지 확인하고 있으면 그걸 파일명으로 쓰되 2) 없을때에 대한 처리도 해야 한다.
const textTitle = document.querySelector('#title');
어디가요 갖고와야 뭘 하지.
link.download = 'file';
그리고 이 부분을
if (textTitle.value.trim().length == 0) {
link.download = 'Untitled';
}
else {
link.download = textTitle.value;
}
이렇게 수정할거다.

?? 왜 됨?
글자수 세 주는 기능을 넣어보자
이건 전에 했던거 있어서 그거 걍 갖다 붙이면 된다.
https://koreanraichu.tistory.com/618
글자 수 카운터를 만들어보자
우리가 해 볼 게 위 그림 두 장에 나와있다. 머스크가 인수해서 똥을 싸제끼고 있는 트위터나 마스토돈, 블루스카이에는 한 번에 올릴 수 있는 내용에 글자수 제한이 있다. 트
koreanraichu.tistory.com
여기를 참고합시다.
Coming up...
1. 파일 불러오기(파일명을 제목으로 불러오고 내용을 내용으로 불러옴)
'Coding > JavaScript' 카테고리의 다른 글
| 드디어 추가되는 파일 불러오기 기능 (0) | 2025.09.11 |
|---|---|
| 텍스트 에디터에 뭔가 추가해보자 (0) | 2025.08.18 |
| 카카오톡 모아보내기를 대충 구현해보자 (0) | 2025.06.27 |
| 우리도 그 유리 효과인지 뭔지 해봅시다 거 (0) | 2025.06.18 |
| 자바스크립트로 음악 재생기를 만들어보자 (0) | 2025.06.07 |
여러분들 카톡할때 사진 모아서 보내기 하시죠? 그걸 구현해볼건데, 이게 사진 장 수에 따라서 구성이 어떻게 되냐면
3장: 3*1
4장: 2*2
5장: 3*1+2*2
10장: 3*2+2*2
이렇게 된다. 10은 3*3+1로 해도 되지 않아요? 그렇게 해도 되는데 카톡 모아서 보내기는 사진 한장만 보낼 때 빼고는 밑에 한장만 띡 안 띄우고 3배수+2배수로 한다.
아, 근데 사진을 매번 만들기도 힘들고 해서 일단은 div를 동적으로 만드는걸로 나 자신이랑 합의 보기로 했음. 오늘은 감도 안 잡혀서 걍 GPT한테 해달라고 했다. 고수 코더 여러분들은 따라하지 마세여...
const photoContainer = document.querySelector('.photocontainer');
const photoCount = document.querySelector('#photocount');
const photoButton = document.querySelector('#photobutton');
photoButton.addEventListener('click',(e)=>{
let countNum = photoCount.value;
console.log(countNum)
});
일단 동적으로 생성할 div의 수를 입력받아보자. HTML? CSS? 그게 지금 중요한 게 아녀… 이 코드의 코어이자 빡셈을 담당하는 자바스크립트를 봐야 한다 이거요.
사진이 한 장일때
일단 사진이 한 장일때를 빼면 무조건 3n+2n으로 들어가는 구조인데, 여기에 맞춰서 div를 우리가 동적으로 생성해야 한다. 이거 어디서 비슷한 문제 봤다고요? 예에에에에에에에전에 그리디 알고리즘 풀 때 봤던 것 같죠? 거스름돈 640원을 가장 적은 수의 동전으로 지불하려면 어떻게 해야 할 지. 뭐 그리디 알고리즘이 전체적으로 최적일거라는 보장은 없지만, 사진 올린거 배치하는데 전체적으로 최적이고 자시고가 어디 있음.
if (photoRemain == 1) {
const photoRow = document.createElement('div');
photoRow.className = 'grid-row';
photoContainer.appendChild(photoRow);
}; //사진이 한 장만 있을 때
일단 사진이 한 장일때를 따로 처리해야 한다. 사진이 하나밖에 없으면 3n+2n으로 못 하거든… 3n+2n에 0을 때려박지 않는 이상 1이 더 작아요. 아무튼 그래서 예외처리 한다 보시면 됩니다.

이건 나중에 디스플레이 플렉스 줘서 잡으면 되니까 안심하십시오.
사진이 두 장 이상일때
왜 두장부터냐고요? 3n+2n에서 3n이 0이면 두장 할 수 있잖아.
while (photoRemain > 0) {
let photosInrow;
if (photoRemain >= 3) {
photosInrow = 3;
} else if (photoRemain == 2) {
photosInrow = 2;
} else {
console.log('어? 뭔가 이상한데?');
break;
}
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
for (let i = 0;i < photosInrow;i++) {
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
}
photoContainer.appendChild(photoInbox);
photoRemain -= photosInrow;
}
자바스크립트에도 while문이 있냐고? 있다. 잘 안 쓸 뿐이지. 아무튼 사진이 2장 이상이면 3x+2y꼴로 나타내라는 얘기인데... 이게 문제가 있어요.

일단 이게 첫번째 문제다. 최대 3장씩 가로로 나열돼야 맞는데 저기 보시면 세로로 줄줄이 있죠? 이거는 JS가 아니라 CSS를 건드려서 해결 봐야 하는 문제라서 디스플레이를 flex로 주면 된다. 근데 저게 첫 번째 문제면, 다른 문제가 또 있다는건가요? 그렇다.

사진은 13장인데 한장이 짤렸다. 내가 지피티한테 3n+2n으로 나와야 한다고 했는데… 얘 더워먹었나…
if (photoRemain == 1) {
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
photoContainer.appendChild(photoInbox);
return;
}; //사진이 한 장만 있을 때
지피티는 가끔 말을 해줘도 씹어먹을 때가 있어서 매번 리마인드를 해 줘야 한다는 단점이 있다. 얘 그림 그릴때도 A 수정하면 B 망치고 B 수정하면 다시 A 망치고 반복함… 실화예요. 이것때문에 이미지 리사이징 포기하고 구글 whisk에게 그려달라고 했다니까.
그래서 뭘 어떻게 바꾼건데요? 일단 사진 한장짜리는 크게 바뀐 거 없다. 인박스 안에 사진이 들어가는 구조긴 한데 그게 다거든. 그래서 크게 바꿀 건 2장 이상 말고는 없다.
while (photoRemain > 0) {
let photosInrow;
if (photoRemain % 3 === 1 && photoRemain >= 4) {
photosInRow = 2; // 1장 남을 미래 방지
} else if (photoRemain >= 3) {
photosInRow = 3;
} else if (photoRemain == 2) {
photosInRow = 2;
} else {
console.log('어? 뭔가 이상한데?');
break;
}
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
for (let i = 0; i < photosInRow; i++) {
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
}
photoContainer.appendChild(photoInbox);
photoRemain -= photosInRow;
}
얘는 대체 뭔 미래를 방지한다는건지 모르겠음... 시키는거나 똑띠 했으면 좋겠구만.
일단 while문의 if문 맨 위를 보자. 저 문제가 터진게 4, 7, 10인데 1을 제외하면 3n+1이라는 공통점이 있다. 근데 내가 지피티한테 예시로 설명까지 들어가면서 설명했던 배치는 '2장 이상이면 무조건 3x+2y 형태로 배치가 되어야 한다'였고, 저대로라면 4장은 2+2, 7장은 3+2+2, 10장은 3+3+2+2가 되어야 하는데 내 요청을 또 무시하고 3n+1로 만들어버린 것. if문 맨 위는 사진 장 수를 3으로 나누기했을 때 1이 남았거나(3n+1) 4장이면 사진을 한 줄에 두 장 넣으라는 얘기이다. 7장에서 두장 빼면 5장은 3n+2니까 기존 로직으로 해결이 된다 이거지.
그럼 이걸로 끝인가요? 아뇨.

내가 표현식을 3x+2y로 쓴 건 3장짜리가 먼저 올라오기 때문이다. 그니까 사진이 7장이면 3+2+2, 10장이면 3+3+2+2, 13장이면 3+3+3+2+2 이런 식으로. 근데 지금 보시면 7장이 2+3+2로 올라오잖음? 근데 이것까지는 내가 지시를 안 했으니 그럴 수 있다 치자.

let photoRemain = countNum;
let photoIndex = 1;
if (photoRemain == 1) {
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
photoContainer.appendChild(photoInbox);
return;
}; //사진이 한 장만 있을 때
if (photoRemain === 4) {
for (let i = 0; i < 2; i++) {
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
for (let j = 0; j < 2; j++) {
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
}
photoContainer.appendChild(photoInbox);
}
return;
}//4장은 2+2
// 그 외 케이스
let threeRows = Math.floor(photoRemain / 3);
let extra = photoRemain % 3;
// 3장씩 먼저 배치
for (let i = 0; i < threeRows; i++) {
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
for (let j = 0; j < 3; j++) {
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
}
photoContainer.appendChild(photoInbox);
}
// 3n + 1 → 2 + 2 배치
if (extra === 1) {
for (let i = 0; i < 2; i++) {
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
for (let j = 0; j < 2; j++) {
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
}
photoContainer.appendChild(photoInbox);
}
}
// 3n + 2 → 2장 배치
else if (extra === 2) {
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
for (let j = 0; j < 2; j++) {
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
}
photoContainer.appendChild(photoInbox);
}
저게 다 뜨는 게 아니고 3n+1… 1, 7, 10에서 뜬 걸 확인했다. 4는 원래 4장이라 패스. 내가 저거 실행해보고 하도 얼척없어서 지피티한테 이게 맞냐고 물었음.
사실 코드가 길고 복잡해보이지만 전체적으로 보면 사진 장수를 3장씩 먼저 올리고 남은걸 두 장씩 올리면 되는 매우 간단한 코드이다. 근데 뭐가 문제임? 이게 3n+2(5, 8, 11)장일때는 3장을 n줄씩 올리고 밑에 두 장이 들어가면 되는데, 3n+1이 되니까 나머지를 계산하는 과정에서 문제가 생긴 것이다.
let threeRows = Math.floor(photoRemain / 3);
let extra = photoRemain % 3;
이게 7장일때, 8장일때는 똑같이 threeRows가 2지만 extra가 각각 1, 2다. 그러니까 6+1, 6+2인건데
// 3n + 1 → 2 + 2 배치
if (extra === 1) {
for (let i = 0; i < 2; i++) {
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
for (let j = 0; j < 2; j++) {
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
}
photoContainer.appendChild(photoInbox);
}
}
extra가 1일때 저 코드를 돌게 된다. 아니 그럼 애초에 3n+1이 3n+4로 배치되는거니까 걍 4장 남을때까지 3씩 빼면 안되는거임…? 그러면 개적화 되나?
코드_최종.js
const photoContainer = document.querySelector('.photocontainer');
const photoCount = document.querySelector('#photocount');
const photoButton = document.querySelector('#photobutton');
photoButton.addEventListener('click',(e)=>{
let countNum = photoCount.value;
console.log(typeof(countNum));
photoContainer.innerHTML = ''; //아무것도 없지만 일단 비운다
if(isNaN(countNum) || countNum <= 0) {
alert ('유효한 숫자를 입력해주세요!');
};
let photoRemain = countNum;
let photoIndex = 1;
if (photoRemain == 1) {
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
photoContainer.appendChild(photoInbox);
return;
}
if (photoRemain == 4) {
// 4장은 2 + 2 고정
for (let i = 0; i < 2; i++) {
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
for (let j = 0; j < 2; j++) {
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
}
photoContainer.appendChild(photoInbox);
}
return;
}
// 여기부터는 일반 배치
let threeRows = 0;
let twoRows = 0;
if (photoRemain % 3 === 1 && photoRemain !== 4) {
// 3n + 1 → 마지막 2 + 2 필요
threeRows = Math.floor((photoRemain - 4) / 3);
twoRows = 2;
} else if (photoRemain % 3 === 2) {
// 3n + 2 → 마지막 2 필요
threeRows = Math.floor((photoRemain - 2) / 3);
twoRows = 1;
} else {
// 3n → 다 3장 줄로 채움
threeRows = Math.floor(photoRemain / 3);
twoRows = 0;
}
// 3장 줄 배치
for (let i = 0; i < threeRows; i++) {
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
for (let j = 0; j < 3; j++) {
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
}
photoContainer.appendChild(photoInbox);
}
// 2장 줄 배치
for (let i = 0; i < twoRows; i++) {
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
for (let j = 0; j < 2; j++) {
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
}
photoContainer.appendChild(photoInbox);
}
});
일단 이것도 바로 나온 게 아니라 문제가 하나 더 있었다. 이번에는 한 장 올렸는데 사진이 4장 올라가서
if (photoRemain === 1) {
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
photoContainer.appendChild(photoInbox);
return;
}
if (photoRemain === 4) {
// 4장은 2 + 2 고정
for (let i = 0; i < 2; i++) {
const photoInbox = document.createElement('div');
photoInbox.className = 'grid-row';
for (let j = 0; j < 2; j++) {
const photosIndiv = document.createElement('div');
photosIndiv.className = 'photo-row';
photosIndiv.textContent = photoIndex++;
photoInbox.appendChild(photosIndiv);
}
photoContainer.appendChild(photoInbox);
}
return;
}
여기서 ===을 ==로 바꿨다. 그래서 됐어요? 예.
저게 왜 문제냐면 ===로 하면 타입까지 비교하게 되는데, 막 input을 통해 입력받은 숫자는 숫자가 아니라 string… 그니까 문자다. ===를 쓰게 되면 컴퓨터 입장에서는 얘는 문자고 쟤는 숫자니까 다른거지만, ==를 쓰면 어쨌든 1이니까 아 ㅇㅋㅇㅋ 하게 되는 것. 이 사태를 예방하려면 아예 let countNum = photoCount.value;에서 등호 오른쪽을 parseInt로 감싸는 방법도 있다.
.grid-row {
background-color: var(--sub-color);
display: flex;
gap: 10px;
margin-bottom: 10px;
transition: .5s;
}
.photo-row {
flex: 1;
height: 150px;
border-radius: 5px;
background-color: #603F83;
display: flex;
align-items: center;
justify-content: center;
transition: .5s;
color: var(--sub-color);
}
지금까지는 flex를 주는 요소들에 크기 속성이 따로 있었지만, 이번에는 3장일때, 2장일때, 한장일때 크기가 다 다르기때문에 크기가 상황에 따라 변해야 한다. 이걸 근데 JS단에서 만지나요? 아뇨, 그냥 flex: 1;주시면 됩니다.

한장

3n+1(10)

3n+2(14)

3n(9)

4장
'Coding > JavaScript' 카테고리의 다른 글
| 텍스트 에디터에 뭔가 추가해보자 (0) | 2025.08.18 |
|---|---|
| 아주 간단한 텍스트 에디터를 만들어보자 (0) | 2025.08.08 |
| 우리도 그 유리 효과인지 뭔지 해봅시다 거 (0) | 2025.06.18 |
| 자바스크립트로 음악 재생기를 만들어보자 (0) | 2025.06.07 |
| 체크박스를 버튼처럼 만들어보자 (0) | 2025.05.19 |
이번에 애플이 iOS 26을 발표하면서 리퀴드 글래스인가 뭐시기인가를 도입한다고 했는데… 우리 쿡이는 19 20 21 22 23 24 25를 못 세나요? 왜 갑자기 26으로 건너뛰었음? 우리 쿡쪽이는 수학공부를 안 했나?
각설하고 우리도 그 리퀴드 글래스인지 뭔지 하는 그 효과 좀 내 보자. 근데 애플처럼 깔쌈하게 나올거라는 장담은 못 드림. 잊지 마십시오. 이 블로그 주인장은 뉴비입니다. 애플처럼 진짜 깔쌈한 유리 효과를 내고 싶으시면 여기 말고 다른 블로그를 가십시오.
일단 유리 하면 투명하다. 아무것도 안 묻은 새삥 유리는 투명한데… 문제가 하나 있다. 이걸 그대로 CSS에 도입하게 되면, 배경에 뭐가 있을 경우 콘텐츠가 안 보일 위험이 있다. 정확히는 사용자가 콘텐츠에 집중을 못 할 위험이 크고, 내용을 알아먹는것도 빡세다. 그래서 보통은 뒤에 블러를 준다. 왜 예전에 모달창 설명 할때도 모달창을 열 동안 배경에 블러를 줬죠? 그거 비슷합니다.
일단 오늘도 참고한 사이트가 있는데
https://www.vibeaz.co.kr/content/apple-liquid-glass-css-effect/
순수 CSS로 애플 Liquid Glass 효과 구현 방법
순수 CSS로 애플의 Liquid Glass 효과를 재현하는 방법. backdrop-filter, rgba, ::after 활용한 유리질감 디자인. 웹 개발자 및 UI/UX 디자이너를 위한 트렌디한 기법.
www.vibeaz.co.kr
여기다.
See the Pen Untitled by koreanraichu (@koreanraichu) on CodePen.
그리고 참고한 결과가 이거. 코드가 생각보다 간단하기 때문에 놓칠 염려도 없다.
.glass {
width: 290px;
height: 240px;
padding: 5px;
color: #ffffff;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(2px);
box-shadow: inset 0 4px 20px rgba(255, 255, 255, 0.3);
border: rgba(255, 255, 255, 0.8);
}
여기서 위에 네 줄은 무시해도 된다. 아래 네 줄이 리퀴드 글래스 효과를 낼 때 필요한 요소들이다. 엥? 블러는 뭔가요? 일단 저 코드펜은 배경이 그라데이션이라 체감이 안 되겠지만 배경에 사진이 있다면 어떻게 될까?

이게 블러 한거고

이게 블러 뺀 거다. 배경을 일부러 어두운 톤으로 골라서 오른쪽 박스만 영향권에 걸쳐있긴 한데, 아래보다 위쪽 사진의 내용이 좀 더 확 들어온다. 그죠? 그래서 블러를 준 게 아닐까 생각한다. 배경은 내가 직접 만든거니까 저작권 걱정 ㄴㄴ요.
그럼 이 리퀴드 글래스가 겹치면 어떻게 되냐고?

투명도가 적용되어 있는거긴 한데 알파가 0이 아니니까 겹치면 그만큼 하얀색이 진해지겠죠.

glass 클래스랑 애프터만 지정해두고 HTML 요소에 클래스만 주면 적용하는 건 쉽다. 저건 textarea다.

당연한 얘기지만 색도 바꿀 수 있다. 근데 좀 미미하긴 하다.

스크롤바도 원한다면 바꿀 수는 있는데 그렇게 되면 스크롤바가 잘 안 보여서 권하지는 않는다.
'Coding > JavaScript' 카테고리의 다른 글
| 아주 간단한 텍스트 에디터를 만들어보자 (0) | 2025.08.08 |
|---|---|
| 카카오톡 모아보내기를 대충 구현해보자 (0) | 2025.06.27 |
| 자바스크립트로 음악 재생기를 만들어보자 (0) | 2025.06.07 |
| 체크박스를 버튼처럼 만들어보자 (0) | 2025.05.19 |
| 토스트 창을 만들어보자 (0) | 2025.05.17 |
오늘 그래서 뭘 해볼거냐... 두가지를 해볼건데
1. 당신 컴퓨터에 있는 음악 파일(예: *.mp3)을 열어서
2. 재생할거다.
끝이다. 나한테 많은 걸 기대하지 말라.
파일 첨부를 할 수 있게끔 만들자

컴퓨터에 있는 파일을 불러오려면 input type="file"을 써야 한다. 그리고 이 인풋을 통해 사용자가 음악 파일만 입력하도록 해야 하는데
<input type="file" id="uploader" accept=".mp3, .wav, .wma, .flac">
내가 아는 음악파일이 이거밖에 없으니 걍 이것만 함.

그리고 저 파일 첨부하는 거 그대로 올라와있는 거 뵈기 싫어서 라벨 연결하고 숨겼다.
본격적으로 자바스크립트에 들어가자
올 것이 왔다. 아주 크나큰 난관이 예상되지만 아무튼...
const musicUpload = document.querySelector('#uploader');
var audio = new Audio(musicUpload.file);
audio.load();
audio.volume = 1;
audio.play();
원래 이런건 한번에 되는 게 이상한거긴 한데... 왜 안되는지 모르겠음. audio.play()에서 Uncaught (in promise) NotAllowedError: play() failed because the user didn't interact with the document first. 라는 오류가 나고 있다.
const musicUpload = document.querySelector('#uploader');
const playMusic = document.querySelector('#playmusic');
playMusic.addEventListener('click', ()=>{
var audio = new Audio(musicUpload.file);
audio.load();
audio.volume = 1;
audio.play();
})
저게 브라우저에서 자동 재생을 막아서 그렇다는겨. 그래서 버튼을 만들어주고, 파일을 올린 다음 재생하게끔 했는데 저것도 재생이 안되는거다. 그래서 지피티한테 물어봤지.
- .file이 아니라 **.files[0]**을 사용해야 합니다.
- input[type="file"] 요소에서 사용자가 업로드한 파일을 가져오려면 .files[0]으로 첫 번째 파일을 접근해야 합니다.
- 또한 Audio() 생성자에 Blob URL을 전달해야 브라우저가 로컬 파일을 재생할 수 있어요.
예? 뭘 하라고요?
왜 files[0]일까?
Input[type="file"] 파일 정보 갖고 오기
파일의 정보를 잘 확인할 수 있음을 알수있다.
velog.io
우연히 찾았던 이 사이트에서 그 답을 찾았다. result 코드에 FileList라고 나오길래 아 얘도 쿼리셀렉터올처럼 인덱싱이 필요하구나… 하고 이해했음. 블롭 URL은 내가 올린 음악 파일을 처리하기 위해 임시 URL을 만들어주는 거라고 생각하면 된다.

참고로 파일을 불러온 상태에서 console.log로 files를 가져오면 이렇게 뜬다.
playMusic.addEventListener('click', () => {
const file = uploader.files[0];
if (!file) {
alert("오디오 파일을 업로드해주세요.");
return;
}
const audio = new Audio(URL.createObjectURL(file));
audio.volume = 1;
audio.play();
});
아무튼 그래서 이렇게만 하면 일단 재생은 된다. 재생은.
오디오 컨트롤러 넣기
일단 저 상태에서 열고 재생해도 되는데, 문제가 뭐냐면 이건 걍 bgm이다. 그래서 재생버튼을 누르면 오디오 콘트롤러가 뿅 하고 튀어나오게 만들건데... 이것도 지피티가 짜줌. 헤헤.
const playerContainer = document.querySelector('#playercontainer');
일단 기존 HTML의 버튼 밑에 새로운 div를 만든다. 여기다가 audio 태그를 붙일거다.
playMusic.addEventListener('click', () => {
const file = uploader.files[0];
if (!file) {
alert("오디오 파일을 업로드해주세요.");
return;
}
playerContainer.innerHTML = '';
const audioElement = document.createElement('audio');
audioElement.controls = true;
audioElement.src = URL.createObjectURL(file);
playerContainer.appendChild(audioElement);
});
그리고 이렇게만 하면 된다. 이렇게 하고 음악 파일을 열고 버튼을 누르면 audio태그가 뿅 하고 나온다. 이제 버튼 위치 잡아야지… ㅡㅡ

아, 참고로 오디오 태그가 추가된 버전은 자동재생이 안되기때문에 재생버튼을 눌러야된다.
오디오태그는 커마가 안되나요?
결론부터 말하자면 길이 외에는 커마가 안 된다. 저게 길이 늘린 버전이고, 직접 본인이 만들거나 라이브러리를 받아와야 한다…

지피티의 힘으로 커마 완료함. 반복재생이요?

그것도 커마 했다.

여기까지 지피티의 힘을 빌려서 커마 끝… OTL 이게 하나 하면 또 추가하고싶고 이거 추가하면 또 추가하고싶고 해서 해달라는게 점점 늘어요… 얘는 과정은 따로 서술 안 하고 깃헙에 올리기만 하겠습니다. 참고로 글꼴은 변경했고 생각보다 한글 글꼴중에 한자 지원 안 하는 글꼴이 많아서 구글 웹폰트에서 일본어 폰트 따로 추가했습니다.
현재 지원되는 기능은
1. 플레이리스트(파일 여러개 여는거)
2. 반복재생(전체반복, 한곡반복, 반복없음)
3. 셔플(사진에는 ON되어있음)
4. 앨범 아트, 아티스트, 제목 불러와서 표시하기
5. 이전곡 일시정지(재생) 다음곡
이정도…
오늘의 결론: 지피티와 함께라면 뭐든지 만들 수 있다. 남자친구 빼고
'Coding > JavaScript' 카테고리의 다른 글
| 카카오톡 모아보내기를 대충 구현해보자 (0) | 2025.06.27 |
|---|---|
| 우리도 그 유리 효과인지 뭔지 해봅시다 거 (0) | 2025.06.18 |
| 체크박스를 버튼처럼 만들어보자 (0) | 2025.05.19 |
| 토스트 창을 만들어보자 (0) | 2025.05.17 |
| 아이디 생성기에 복사버튼을 달아보자 (0) | 2025.05.09 |