문의 영역 개편과 공유 미리보기 및 포트폴리오 표시 개선

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

검증: 공통·이메일·문의 폼 JavaScript 문법 검사 및 변경 공백 검사 통과
확인: 버전이 붙은 운영 사이트 JS와 CSS가 로컬 파일과 동일함
참고: 문의 폼은 서버 전송 없이 화면 동작만 제공하며 이메일 주소는 임시 값 유지
This commit is contained in:
2026-10-06 15:59:27 +09:00
parent ecc3807cfa
commit 7b4badf5a4
19 changed files with 791 additions and 75 deletions

129
contact-form.js Normal file
View File

@@ -0,0 +1,129 @@
/* 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');
}
});
})();