- 메인 문의 영역을 이메일 주소와 복사 버튼으로 구성하고 복사 결과 안내를 추가 - 문의 폼 페이지를 별도로 보존하고 입력 검증, 파일 첨부 및 완료 안내 동작을 추가 - 제거된 문의 요소를 참조하던 공통 스크립트를 정리해 초기화 오류 방지 - 이메일 복사 버튼의 특수 문자 화살표를 공통 SVG로 통일 - 공유 시 특허증 대신 트론트 로고를 사용하도록 공유 메타 정보 지정 - 공통 스크립트와 포트폴리오 CSS에 버전 URL을 적용해 이전 CDN 캐시 우회 - 프로젝트 및 특허 영역의 자료 기준 안내와 소개서 보기 링크 제거 - 프로젝트 상세 페이지 8개의 상단 썸네일 화살표를 제거하고 마우스 반응 효과 유지 - 특허증 이미지 파일을 갱신하고 문의 구조와 동작 설명을 README에 반영 검증: 공통·이메일·문의 폼 JavaScript 문법 검사 및 변경 공백 검사 통과 확인: 버전이 붙은 운영 사이트 JS와 CSS가 로컬 파일과 동일함 참고: 문의 폼은 서버 전송 없이 화면 동작만 제공하며 이메일 주소는 임시 값 유지
130 lines
5.0 KiB
JavaScript
130 lines
5.0 KiB
JavaScript
/* Local interaction preview: no requests, uploads, or storage. */
|
||
(() => {
|
||
'use strict';
|
||
const form = document.querySelector('.inquiry-form');
|
||
if (!form) return;
|
||
|
||
const message = form.elements.message;
|
||
const counter = form.querySelector('[data-message-count]');
|
||
const input = form.elements.attachments;
|
||
const dropzone = form.querySelector('.inquiry-upload');
|
||
const list = form.querySelector('.inquiry-files');
|
||
const uploadStatus = form.querySelector('.inquiry-upload-status');
|
||
const dialog = document.querySelector('#inquiry-success-dialog');
|
||
const submitButton = form.querySelector('.inquiry-submit');
|
||
submitButton.disabled = false;
|
||
const maxFiles = 5;
|
||
const maxBytes = 20 * 1024 * 1024;
|
||
let files = [];
|
||
let dragDepth = 0;
|
||
|
||
function formatSize(size) {
|
||
return size >= 1024 * 1024 ? `${(size / 1024 / 1024).toFixed(1)} MB` : `${Math.max(1, Math.ceil(size / 1024))} KB`;
|
||
}
|
||
|
||
function showFiles() {
|
||
list.replaceChildren();
|
||
files.forEach((file, index) => {
|
||
const item = document.createElement('li');
|
||
const name = document.createElement('span');
|
||
name.className = 'inquiry-file-name';
|
||
name.textContent = file.name;
|
||
const size = document.createElement('span');
|
||
size.className = 'inquiry-file-size';
|
||
size.textContent = formatSize(file.size);
|
||
const remove = document.createElement('button');
|
||
remove.type = 'button';
|
||
remove.className = 'inquiry-file-remove';
|
||
remove.textContent = '×';
|
||
remove.setAttribute('aria-label', `${file.name} 삭제`);
|
||
remove.addEventListener('click', () => {
|
||
files.splice(index, 1);
|
||
showFiles();
|
||
const next = list.querySelectorAll('button')[Math.min(index, files.length - 1)];
|
||
(next || input).focus();
|
||
});
|
||
item.append(name, size, remove);
|
||
list.append(item);
|
||
});
|
||
uploadStatus.classList.remove('is-error');
|
||
uploadStatus.textContent = files.length ? `${files.length}개 선택 · 총 ${formatSize(files.reduce((sum, file) => sum + file.size, 0))}` : '';
|
||
}
|
||
|
||
function addFiles(selection) {
|
||
const next = [...files];
|
||
for (const file of selection) {
|
||
if (!next.some(existing => existing.name === file.name && existing.size === file.size && existing.lastModified === file.lastModified)) next.push(file);
|
||
}
|
||
let error = '';
|
||
if (next.length > maxFiles) error = '파일은 최대 5개까지 첨부할 수 있습니다.';
|
||
else if (next.reduce((sum, file) => sum + file.size, 0) > maxBytes) error = '첨부파일의 총 용량은 20MB 이하여야 합니다.';
|
||
input.value = '';
|
||
if (error) {
|
||
uploadStatus.textContent = error;
|
||
uploadStatus.classList.add('is-error');
|
||
return;
|
||
}
|
||
files = next;
|
||
showFiles();
|
||
}
|
||
|
||
input.addEventListener('change', () => addFiles([...input.files]));
|
||
dropzone.addEventListener('dragenter', event => {
|
||
event.preventDefault();
|
||
dragDepth++;
|
||
dropzone.classList.add('is-dragging');
|
||
});
|
||
dropzone.addEventListener('dragover', event => {
|
||
event.preventDefault();
|
||
event.dataTransfer.dropEffect = 'copy';
|
||
});
|
||
dropzone.addEventListener('dragleave', () => {
|
||
if (--dragDepth <= 0) dropzone.classList.remove('is-dragging');
|
||
});
|
||
dropzone.addEventListener('drop', event => {
|
||
event.preventDefault();
|
||
dragDepth = 0;
|
||
dropzone.classList.remove('is-dragging');
|
||
addFiles([...event.dataTransfer.files]);
|
||
});
|
||
// Avoid navigating away if a file is dropped just outside the attachment area.
|
||
for (const type of ['dragover', 'drop']) {
|
||
form.addEventListener(type, event => {
|
||
if ([...event.dataTransfer.types].includes('Files')) event.preventDefault();
|
||
});
|
||
}
|
||
|
||
form.addEventListener('input', event => {
|
||
if (event.target.matches('input, textarea')) event.target.setCustomValidity('');
|
||
counter.textContent = message.value.length.toLocaleString('ko-KR');
|
||
});
|
||
dialog.querySelectorAll('[data-close-inquiry]').forEach(button => {
|
||
button.addEventListener('click', () => dialog.close());
|
||
});
|
||
let backdropPressed = false;
|
||
function isBackdrop(event) {
|
||
const bounds = dialog.getBoundingClientRect();
|
||
return event.target === dialog && (event.clientX < bounds.left || event.clientX > bounds.right || event.clientY < bounds.top || event.clientY > bounds.bottom);
|
||
}
|
||
dialog.addEventListener('pointerdown', event => { backdropPressed = isBackdrop(event); });
|
||
dialog.addEventListener('click', event => {
|
||
if (backdropPressed && isBackdrop(event)) dialog.close();
|
||
backdropPressed = false;
|
||
});
|
||
dialog.addEventListener('close', () => {
|
||
document.documentElement.classList.remove('inquiry-success-open');
|
||
submitButton.focus({ preventScroll: true });
|
||
});
|
||
form.addEventListener('submit', event => {
|
||
event.preventDefault();
|
||
for (const field of [form.elements.name, message]) {
|
||
field.setCustomValidity(field.value.trim() ? '' : '공백을 제외한 내용을 입력해 주세요.');
|
||
}
|
||
if (!form.reportValidity()) return;
|
||
if (!dialog.open) {
|
||
dialog.showModal();
|
||
document.documentElement.classList.add('inquiry-success-open');
|
||
}
|
||
});
|
||
})();
|