photo-validator
Version:
332 lines (288 loc) • 15.7 kB
HTML
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>증명 사진 검증</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- face-api.js 라이브러리 로드 -->
<script defer src="photo-validator.js"></script>
<style>
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeOut {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(-20px); }
}
.modal-enter {
animation: fadeIn 0.3s ease-out forwards;
}
.modal-exit {
animation: fadeOut 0.3s ease-out forwards;
}
.upload-area {
transition: all 0.3s ease;
}
.success-check {
animation: checkmark 0.5s ease-in-out forwards;
}
.error-x {
animation: xmark 0.5s ease-in-out forwards;
}
@keyframes checkmark {
0% { transform: scale(0); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
@keyframes xmark {
0% { transform: scale(0) rotate(0deg); }
50% { transform: scale(1.2) rotate(180deg); }
100% { transform: scale(1) rotate(360deg); }
}
.border-error {
border-color: #ef4444 ;
animation: borderPulse 1s ease-in-out;
}
.border-success {
border-color: #10b981 ;
animation: borderPulse 1s ease-in-out;
}
@keyframes borderPulse {
0% { border-color: #e5e7eb; }
50% { border-color: currentColor; }
100% { border-color: currentColor; }
}
.spinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.loading-content {
text-align: center;
}
.loading-text {
margin-top: 1rem;
color: #4b5563;
font-weight: 500;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<!-- Loading Overlay -->
<div id="loadingOverlay" class="loading-overlay">
<div class="loading-content">
<svg class="w-16 h-16 text-blue-500 spinner mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
<p class="loading-text">모델을 초기화하는 중입니다...</p>
</div>
</div>
<div class="container mx-auto px-4 py-8">
<div class="max-w-2xl mx-auto">
<h1 class="text-3xl font-bold text-center text-gray-800 mb-2">증명 사진 검증</h1>
<p class="text-center text-gray-600 mb-8">업로드한 사진이 증명사진 기준에 맞는지 확인해보세요</p>
<div class="bg-white rounded-xl shadow-lg p-6 upload-area">
<div class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center">
<input type="file" id="photoInput" accept="image/png, image/jpeg" class="hidden">
<label for="photoInput" class="cursor-pointer">
<div class="flex flex-col items-center">
<svg class="w-12 h-12 text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
<p class="text-gray-600">클릭 하여 사진을 업로드하세요</p>
<p class="text-sm text-gray-500 mt-2">PNG 또는 JPG 파일만 가능합니다</p>
</div>
</label>
</div>
</div>
</div>
</div>
<!-- Modal Template -->
<div id="modalTemplate" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden">
<div class="bg-white rounded-lg p-6 max-w-md w-full mx-4 relative" onclick="event.stopPropagation()">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-800"></h3>
<button class="text-gray-500 hover:text-gray-700 transition-colors duration-200">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="text-gray-600"></div>
</div>
</div>
<script>
// 페이지 로드 후 모듈 초기화
window.addEventListener("DOMContentLoaded", async () => {
const photoInput = document.getElementById("photoInput");
const loadingOverlay = document.getElementById("loadingOverlay");
// 초기에는 파일 입력 비활성화
photoInput.disabled = true;
// 사진 검증기 인스턴스 생성 (옵션 설정 가능)
const validator = new PhotoValidator({
debug: true,
modelsPath: './models'
});
try {
// 모델 초기화
await validator.init('photoInput');
// 초기화 완료 후 로딩 오버레이 숨기고 파일 입력 활성화
loadingOverlay.style.display = 'none';
photoInput.disabled = false;
} catch (error) {
console.error("모델 초기화 실패:", error);
loadingOverlay.querySelector('.loading-text').textContent = "모델 초기화에 실패했습니다. 페이지를 새로고침해주세요.";
}
// 모달 요소 참조
const modal = document.getElementById("modalTemplate");
const modalTitle = modal.querySelector("h3");
const modalContent = modal.querySelector("div:last-child");
const modalCloseButton = modal.querySelector("button");
// 모달 닫기 함수
const closeModal = () => {
modal.classList.add("modal-exit");
setTimeout(() => {
modal.classList.add("hidden");
modal.classList.remove("modal-enter", "modal-exit");
}, 300);
};
// 모달 표시 함수
const showModal = (title, message, type = 'error') => {
const icon = type === 'error' ?
`<svg class="w-16 h-16 text-red-500 error-x mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>` :
type === 'loading' ?
`<svg class="w-16 h-16 text-blue-500 spinner mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>` :
`<svg class="w-16 h-16 text-green-500 success-check mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>`;
modalTitle.textContent = title;
modalContent.innerHTML = `
<div class="flex flex-col items-center">
${icon}
<p class="mt-4 text-lg font-semibold text-gray-800">${message}</p>
</div>
`;
modal.classList.remove("hidden");
modal.classList.add("modal-enter");
// 업로드 영역 테두리 색상 변경
const uploadArea = document.querySelector('.border-2');
uploadArea.classList.remove('border-error', 'border-success');
if (type === 'error') {
uploadArea.classList.add('border-error');
} else if (type === 'success') {
uploadArea.classList.add('border-success');
}
};
// ESC 키 이벤트 핸들러
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
}
});
// 모달 이벤트 리스너 등록
modal.addEventListener("click", (e) => {
if (e.target === modal) {
closeModal();
}
});
modalCloseButton.addEventListener("click", closeModal);
// 파일 업로드 핸들러
async function handleImageUpload(event) {
const file = event.target.files[0];
if (!file) return;
const img = validator.getPreviewElement();
const reader = new FileReader();
const errMessages = [];
// 로딩 모달 표시
showModal("검증 중", "이미지를 검증하고 있습니다...", 'loading');
reader.onload = function(e) {
img.src = e.target.result;
img.onload = async () => {
try {
// 파일 형식 검사
if(!validator.isValidFileType(file)) {
errMessages.push("허용되지 않는 파일 형식입니다. PNG 또는 JPG 파일만 업로드 가능합니다.");
showModal("파일 형식 오류", "허용되지 않는 파일 형식입니다. PNG 또는 JPG 파일만 업로드 가능합니다.");
console.error(`파일 형식 오류.. 현재 : ${file.type}`);
}
// 이미지 크기 검사
if(!validator.isImageSizeValid(300, 360)) {
errMessages.push("이미지 크기가 너무 작습니다. 최소 300x360 픽셀 이상의 이미지를 업로드해주세요.");
showModal("이미지 크기 오류", "이미지 크기가 너무 작습니다. 최소 300x360 픽셀 이상의 이미지를 업로드해주세요.");
console.error(`이미지 사이즈 오류.. 가로 x 세로 : ${img.width} x ${img.height}`);
}
// 이미지 비율 검사
if(!validator.isValidAspectRatio()) {
errMessages.push("증명 사진 비율에 맞는 이미지를 등록해주세요.");
showModal("이미지 비율 오류", "증명 사진 비율에 맞는 이미지를 등록해주세요.");
console.error(`이미지 사이즈 오류.. 가로 x 세로 : ${img.width} x ${img.height}`);
}
// 사진 내 여러 사람 얼굴 검출 여부 확인
if(!(await validator.isSingleFace())) {
errMessages.push( "사진에는 반드시 한 사람이 있어야 합니다.");
showModal("얼굴 검출 오류", "사진에는 반드시 한 사람이 있어야 합니다.");
console.error("사진에는 한사람만 등장해야 합니다.");
}
// 머리가 잘리거나 사진 상단과 가까운지 여부
if(!(await validator.isHeadFullyVisible())) {
errMessages.push("머리가 사진 상단과 너무 가깝거나 잘려있습니다.");
showModal("머리 위치 오류", "머리가 사진 상단과 너무 가깝거나 잘려있습니다.");
console.error("머리가 붙어있거나 잘려있는 사진입니다.");
}
// 얼굴 정면 바라봄 여부
if(!(await validator.isFacingForward(3.5))) {
errMessages.push("얼굴이 정면을 향하고 있지 않습니다.");
showModal("얼굴 방향 오류", "얼굴이 정면을 향하고 있지 않습니다.");
console.error("얼굴이 정면을 향하고 있어야합니다.");
}
// 배경 색상 편차 여부
if(!(await validator.isUniformBackground())) {
errMessages.push("배경이 균일하지 않습니다. 단일 색상 배경을 사용하세요.");
showModal("배경 오류", "배경이 균일하지 않습니다. 단일 색상 배경을 사용하세요.");
console.error("배경 색상이 균일하지않습니다.");
}
// 얼굴 중앙 위치 여부
if(!(await validator.isFaceCenteredAndComplete(false))) {
errMessages.push("얼굴이 중앙에 위치하지 않습니다.");
showModal("얼굴 위치 오류", "얼굴이 중앙에 위치하지 않습니다.");
console.error("얼굴이 중앙에 위치하지 않습니다.");
}
if(errMessages.length > 0) {
showModal("검증 실패", errMessages.join("<br/>"), 'error');
} else {
showModal("검증 성공", "증명 사진으로 적합합니다!", 'success');
}
} catch (error) {
console.error("검증 오류:", error);
showModal("검증 오류", error.message || "알 수 없는 오류가 발생했습니다.");
}
};
};
reader.readAsDataURL(file);
}
// 이벤트 리스너 등록
document.getElementById("photoInput").addEventListener("change", handleImageUpload);
});
</script>
</body>
</html>