Initial commit: 교육 프로젝트 배포

This commit is contained in:
송대일
2026-07-01 18:32:42 +09:00
commit be6dccd120
1483 changed files with 5082202 additions and 0 deletions
+277
View File
@@ -0,0 +1,277 @@
/**
* 공통 유틸리티 함수 모듈 (Utils.js)
* ========================================
* 자주 쓰는 헬퍼 함수들을 모아둔 정적 클래스입니다.
*
* [초보자용 사용 예]
* Utils.delay(1000) → 1초 대기 후 Promise 반환 (async/await와 함께)
* Utils.debounce(fn, 300) → 입력 등 연속 호출 방지 (마지막 호출만 실행)
* Utils.throttle(fn, 100) → resize 등 빈번한 이벤트 제한 (100ms마다 1번)
* Utils.formatDate(date) → "YYYY-MM-DD" 형식
* Utils.formatNumber(1234) → "1,234" (천단위 콤마)
* Utils.storage.set('key', value) → localStorage 쉽게 사용
*
* @module Utils
*/
class Utils {
/**
* 딜레이 함수
* @param {number} ms - 지연 시간 (밀리초)
* @returns {Promise}
*/
static delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* 디바운스 함수
* @param {Function} func - 실행할 함수
* @param {number} wait - 대기 시간
* @returns {Function}
*/
static debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
/**
* 쓰로틀 함수
* @param {Function} func - 실행할 함수
* @param {number} limit - 제한 시간
* @returns {Function}
*/
static throttle(func, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
/**
* 랜덤 ID 생성
* @param {number} length - ID 길이
* @returns {string}
*/
static generateId(length = 8) {
return Math.random()
.toString(36)
.substring(2, length + 2);
}
/**
* 깊은 복사
* @param {*} obj - 복사할 객체
* @returns {*}
*/
static deepClone(obj) {
if (obj === null || typeof obj !== "object") return obj;
if (obj instanceof Date) return new Date(obj.getTime());
if (obj instanceof Array) return obj.map((item) => this.deepClone(item));
const clonedObj = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = this.deepClone(obj[key]);
}
}
return clonedObj;
}
/**
* 객체 병합
* @param {Object} target - 대상 객체
* @param {Object} source - 소스 객체
* @returns {Object}
*/
static mergeDeep(target, source) {
const output = { ...target };
if (this.isObject(target) && this.isObject(source)) {
Object.keys(source).forEach((key) => {
if (this.isObject(source[key])) {
if (!(key in target)) {
Object.assign(output, { [key]: source[key] });
} else {
output[key] = this.mergeDeep(target[key], source[key]);
}
} else {
Object.assign(output, { [key]: source[key] });
}
});
}
return output;
}
/**
* 객체 확인
* @param {*} item - 확인할 항목
* @returns {boolean}
*/
static isObject(item) {
return item && typeof item === "object" && !Array.isArray(item);
}
/**
* URL 파라미터 파싱
* @param {string} url - URL 문자열
* @returns {Object}
*/
static parseUrlParams(url = window.location.search) {
const params = new URLSearchParams(url);
const result = {};
for (const [key, value] of params) {
result[key] = value;
}
return result;
}
/**
* 퍼센트 계산
* @param {number} current - 현재 값
* @param {number} total - 전체 값
* @param {number} decimals - 소수점 자리수
* @returns {number}
*/
static calculatePercent(current, total, decimals = 0) {
if (total === 0) return 0;
const percent = (current / total) * 100;
return decimals > 0 ? parseFloat(percent.toFixed(decimals)) : Math.round(percent);
}
/**
* 배열 섞기 (Fisher-Yates shuffle)
* @param {Array} array - 섞을 배열
* @returns {Array}
*/
static shuffleArray(array) {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
/**
* 배열 청크 분할
* @param {Array} array - 분할할 배열
* @param {number} size - 청크 크기
* @returns {Array}
*/
static chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
/**
* 로컬 스토리지 관리
*/
static storage = {
set(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (e) {
console.error("Storage set error:", e);
return false;
}
},
get(key, defaultValue = null) {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (e) {
console.error("Storage get error:", e);
return defaultValue;
}
},
remove(key) {
try {
localStorage.removeItem(key);
return true;
} catch (e) {
console.error("Storage remove error:", e);
return false;
}
},
clear() {
try {
localStorage.clear();
return true;
} catch (e) {
console.error("Storage clear error:", e);
return false;
}
},
};
/**
* 날짜 포맷팅
* @param {Date} date - 포맷할 날짜
* @param {string} format - 포맷 문자열 (YYYY-MM-DD, YYYY.MM.DD 등)
* @returns {string}
*/
static formatDate(date, format = "YYYY-MM-DD") {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const hour = String(d.getHours()).padStart(2, "0");
const minute = String(d.getMinutes()).padStart(2, "0");
const second = String(d.getSeconds()).padStart(2, "0");
return format
.replace("YYYY", year)
.replace("MM", month)
.replace("DD", day)
.replace("HH", hour)
.replace("mm", minute)
.replace("ss", second);
}
/**
* 숫자 포맷팅 (천단위 콤마)
* @param {number} num - 포맷할 숫자
* @returns {string}
*/
static formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
/**
* 파일 크기 포맷팅
* @param {number} bytes - 바이트 크기
* @param {number} decimals - 소수점 자리수
* @returns {string}
*/
static formatFileSize(bytes, decimals = 2) {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
}
}
// ES6 모듈 내보내기
if (typeof module !== "undefined" && module.exports) {
module.exports = Utils;
}