Require guest contact for uploads

This commit is contained in:
DESKTOP-KSVGT20\shkim
2026-05-11 22:50:55 +09:00
parent 2a01704b02
commit 7f4e814d94
3 changed files with 96 additions and 0 deletions
+38
View File
@@ -198,6 +198,44 @@ h1 {
margin-bottom: 14px; margin-bottom: 14px;
} }
.guest-fields {
display: grid;
gap: 10px;
margin: 0 0 14px;
text-align: left;
}
.guest-fields label {
display: grid;
gap: 6px;
color: #45615c;
font-family: "Apple SD Gothic Neo", "Malgun Gothic", system-ui, sans-serif;
font-size: 13px;
font-weight: 700;
}
.guest-fields input {
width: 100%;
min-height: 46px;
border: 1px solid rgba(137, 166, 158, 0.38);
border-radius: 7px;
padding: 0 14px;
background: #ffffff;
color: #3f3f3f;
font: inherit;
font-size: 15px;
font-weight: 500;
}
.guest-fields input::placeholder {
color: #9aa4a1;
}
.guest-fields input:focus {
outline: 2px solid rgba(137, 170, 163, 0.6);
outline-offset: 2px;
}
.file-picker input[type="file"] { .file-picker input[type="file"] {
position: absolute; position: absolute;
inset: 0; inset: 0;
+18
View File
@@ -22,6 +22,17 @@
<form class="upload-panel" method="post" action="/r/{{slug}}" enctype="multipart/form-data"> <form class="upload-panel" method="post" action="/r/{{slug}}" enctype="multipart/form-data">
<p class="upload-panel__today">오늘의 순간</p> <p class="upload-panel__today">오늘의 순간</p>
<div class="guest-fields" aria-label="업로드하시는 분 정보">
<label>
<span>성함</span>
<input type="text" name="guestName" autocomplete="name" placeholder="성함을 입력해 주세요." required>
</label>
<label>
<span>연락처</span>
<input type="tel" name="guestPhone" autocomplete="tel" inputmode="tel" placeholder="연락처를 입력해 주세요." required>
</label>
</div>
<button class="upload-button" type="submit" aria-label="업로드" hidden> <button class="upload-button" type="submit" aria-label="업로드" hidden>
<span>업로드하기</span> <span>업로드하기</span>
</button> </button>
@@ -52,6 +63,8 @@
<script> <script>
document.querySelectorAll(".upload-panel").forEach((form) => { document.querySelectorAll(".upload-panel").forEach((form) => {
const input = form.querySelector('input[type="file"][name="files"]'); const input = form.querySelector('input[type="file"][name="files"]');
const guestName = form.querySelector('input[name="guestName"]');
const guestPhone = form.querySelector('input[name="guestPhone"]');
const submit = form.querySelector('button[type="submit"][aria-label="업로드"]'); const submit = form.querySelector('button[type="submit"][aria-label="업로드"]');
const summary = form.querySelector(".file-summary"); const summary = form.querySelector(".file-summary");
const empty = form.querySelector(".file-summary__empty"); const empty = form.querySelector(".file-summary__empty");
@@ -143,10 +156,15 @@
form.addEventListener("submit", async (event) => { form.addEventListener("submit", async (event) => {
event.preventDefault(); event.preventDefault();
if (!form.reportValidity()) return;
if (!input.files || input.files.length === 0) { if (!input.files || input.files.length === 0) {
setStatus("먼저 사진을 선택해 주세요.", "error"); setStatus("먼저 사진을 선택해 주세요.", "error");
return; return;
} }
if (!guestName.value.trim() || !guestPhone.value.trim()) {
setStatus("성함과 연락처를 모두 입력해 주세요.", "error");
return;
}
submit.disabled = true; submit.disabled = true;
form.dataset.uploading = "true"; form.dataset.uploading = "true";
+40
View File
@@ -55,6 +55,15 @@ function sanitizeFileName(value) {
return `${base}${ext}`; return `${base}${ext}`;
} }
function cleanGuestField(value) {
return String(value || "")
.normalize("NFKC")
.replace(/[\r\n\t]/g, " ")
.replace(/\s+/g, " ")
.trim()
.slice(0, 80);
}
function normalizeExts(value) { function normalizeExts(value) {
return String(value || "") return String(value || "")
.split(/[\s,;]+/) .split(/[\s,;]+/)
@@ -204,6 +213,10 @@ async function uploadPage(request) {
<section class="panel"> <section class="panel">
<h1>${values.title}</h1> <h1>${values.title}</h1>
<form method="post" action="/r/${values.slug}" enctype="multipart/form-data"> <form method="post" action="/r/${values.slug}" enctype="multipart/form-data">
<label>성함</label>
<input type="text" name="guestName" autocomplete="name" required>
<label>연락처</label>
<input type="tel" name="guestPhone" autocomplete="tel" required>
<div class="upload-box"> <div class="upload-box">
<label>사진/영상 선택</label> <label>사진/영상 선택</label>
<input type="file" name="files" accept="${values.accept}" multiple required> <input type="file" name="files" accept="${values.accept}" multiple required>
@@ -244,6 +257,20 @@ async function moveUploadedFiles(files, targetDir) {
} }
} }
async function appendUploadMetadata(targetDir, { guestName, guestPhone, files }) {
const entry = {
uploadedAt: new Date().toISOString(),
guestName,
guestPhone,
files: files.map((file) => ({
storedName: file.filename,
originalName: file.originalname,
size: file.size
}))
};
await fsp.appendFile(path.join(targetDir, "upload-metadata.jsonl"), `${JSON.stringify(entry)}\n`, "utf8");
}
app.get("/", (req, res) => res.redirect("/admin")); app.get("/", (req, res) => res.redirect("/admin"));
app.get("/admin/login", (req, res) => { app.get("/admin/login", (req, res) => {
@@ -417,6 +444,18 @@ app.post("/r/:slug", async (req, res) => {
} }
try { try {
const files = req.files || []; const files = req.files || [];
const guestName = cleanGuestField(req.body.guestName);
const guestPhone = cleanGuestField(req.body.guestPhone);
if (!guestName || !guestPhone) {
await removeFiles(files);
const message = "성함과 연락처를 모두 입력해 주세요.";
if (wantsJson(req)) {
res.status(400).json({ ok: false, message });
return;
}
res.status(400).send(uploadResultPage(request, { heading: "업로드 실패", message, isError: true }));
return;
}
const targetDir = path.join(uploadsDir, request.folderName); const targetDir = path.join(uploadsDir, request.folderName);
const existingBytes = await directorySize(targetDir); const existingBytes = await directorySize(targetDir);
const incomingBytes = files.reduce((total, file) => total + file.size, 0); const incomingBytes = files.reduce((total, file) => total + file.size, 0);
@@ -431,6 +470,7 @@ app.post("/r/:slug", async (req, res) => {
return; return;
} }
await moveUploadedFiles(files, targetDir); await moveUploadedFiles(files, targetDir);
await appendUploadMetadata(targetDir, { guestName, guestPhone, files });
if (wantsJson(req)) { if (wantsJson(req)) {
res.json({ ok: true, count: files.length }); res.json({ ok: true, count: files.length });
return; return;