Files
tront.noname.company/contact-form.js
zenn 7b4badf5a4 문의 영역 개편과 공유 미리보기 및 포트폴리오 표시 개선
- 메인 문의 영역을 이메일 주소와 복사 버튼으로 구성하고 복사 결과 안내를 추가
- 문의 폼 페이지를 별도로 보존하고 입력 검증, 파일 첨부 및 완료 안내 동작을 추가
- 제거된 문의 요소를 참조하던 공통 스크립트를 정리해 초기화 오류 방지
- 이메일 복사 버튼의 특수 문자 화살표를 공통 SVG로 통일
- 공유 시 특허증 대신 트론트 로고를 사용하도록 공유 메타 정보 지정
- 공통 스크립트와 포트폴리오 CSS에 버전 URL을 적용해 이전 CDN 캐시 우회
- 프로젝트 및 특허 영역의 자료 기준 안내와 소개서 보기 링크 제거
- 프로젝트 상세 페이지 8개의 상단 썸네일 화살표를 제거하고 마우스 반응 효과 유지
- 특허증 이미지 파일을 갱신하고 문의 구조와 동작 설명을 README에 반영

검증: 공통·이메일·문의 폼 JavaScript 문법 검사 및 변경 공백 검사 통과
확인: 버전이 붙은 운영 사이트 JS와 CSS가 로컬 파일과 동일함
참고: 문의 폼은 서버 전송 없이 화면 동작만 제공하며 이메일 주소는 임시 값 유지
2026-10-06 15:59:27 +09:00

130 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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');
}
});
})();