업로드 연락처 입력과 저장 폴더 정리

This commit is contained in:
DESKTOP-KSVGT20\shkim
2026-05-13 23:03:44 +09:00
parent 648974960e
commit 735c05ed1e
3 changed files with 50 additions and 76 deletions
+9 -18
View File
@@ -64,18 +64,6 @@ body {
object-position: center top;
}
.poster__plate {
position: absolute;
z-index: 5;
left: 8.5%;
right: 8.5%;
bottom: 10.5%;
padding: clamp(16px, 3.5vw, 25px) 12px clamp(13px, 2.6vw, 20px);
background: rgba(255, 255, 255, 0.9);
border: 0;
backdrop-filter: none;
}
h1 {
margin: 0;
color: #565656;
@@ -160,6 +148,15 @@ h1 {
transform: scaleX(-1);
}
.upload-panel__notice {
margin: -6px auto 16px;
color: #5f6f6b;
font-family: "Apple SD Gothic Neo", "Malgun Gothic", system-ui, sans-serif;
font-size: 12px;
line-height: 1.55;
word-break: keep-all;
}
.select-button,
.upload-button,
.upload-again {
@@ -485,12 +482,6 @@ h1 {
content: none;
}
.poster__plate {
left: 8%;
right: 8%;
bottom: 10.5%;
}
.upload-panel {
width: min(calc(100% - 48px), 340px);
margin-top: -54px;
+3 -19
View File
@@ -8,28 +8,19 @@
</head>
<body>
<main class="upload-page">
<section class="wedding-card" aria-labelledby="archive-title">
<section class="wedding-card" aria-label="{{title}}">
<div class="poster">
<img class="poster__art" src="/assets/wedding-illustration.png?v=single-illustration-1" alt="">
<div class="poster__plate">
<h1 id="archive-title">{{title}}</h1>
<p class="poster__date">2026.05.24 SUN</p>
<p class="poster__subtitle">OUR WEDDING ARCHIVE</p>
</div>
</div>
<form class="upload-panel" method="post" action="/r/{{slug}}" enctype="multipart/form-data">
<p class="upload-panel__today">오늘의 순간</p>
<p class="upload-panel__notice">※ 입력해주신 연락처는 이벤트 상품 전달 용도로만 사용되며,<br>행사 종료 후 삭제됩니다.</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>
<input type="tel" name="guestPhone" autocomplete="tel" inputmode="tel" placeholder="연락처를 입력해 주세요.">
</label>
</div>
@@ -63,8 +54,6 @@
<script>
document.querySelectorAll(".upload-panel").forEach((form) => {
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 summary = form.querySelector(".file-summary");
const empty = form.querySelector(".file-summary__empty");
@@ -161,11 +150,6 @@
setStatus("먼저 사진을 선택해 주세요.", "error");
return;
}
if (!guestName.value.trim() || !guestPhone.value.trim()) {
setStatus("성함과 연락처를 모두 입력해 주세요.", "error");
return;
}
submit.disabled = true;
form.dataset.uploading = "true";
setStatus("업로드 중이에요. 화면을 닫지 말아 주세요.");
+38 -39
View File
@@ -64,6 +64,11 @@ function cleanGuestField(value) {
.slice(0, 80);
}
function submissionFolderName(guestPhone) {
const phone = cleanGuestField(guestPhone).replace(/[^\d]/g, "");
return phone || "익명";
}
function normalizeExts(value) {
return String(value || "")
.split(/[\s,;]+/)
@@ -171,7 +176,6 @@ function wantsJson(req) {
}
function uploadResultPage(request, { heading, message, isError = false }) {
const title = escapeHtml(request.title);
const slug = escapeHtml(request.slug);
return `<!doctype html>
<html lang="ko">
@@ -183,14 +187,9 @@ function uploadResultPage(request, { heading, message, isError = false }) {
</head>
<body>
<main class="upload-page">
<section class="wedding-card" aria-labelledby="archive-title">
<section class="wedding-card" aria-label="${escapeHtml(request.title)}">
<div class="poster">
<img class="poster__art" src="/assets/wedding-illustration.png?v=single-illustration-1" alt="">
<div class="poster__plate">
<h1 id="archive-title">${title}</h1>
<p class="poster__date">2026.05.24 SUN</p>
<p class="poster__subtitle">OUR WEDDING ARCHIVE</p>
</div>
</div>
<section class="thank-you${isError ? " thank-you--error" : ""}">
<p class="thank-you__eyebrow">${isError ? "PLEASE TRY AGAIN" : "THANK YOU"}</p>
@@ -221,10 +220,8 @@ async function uploadPage(request) {
<section class="panel">
<h1>${values.title}</h1>
<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>
<input type="tel" name="guestPhone" autocomplete="tel">
<div class="upload-box">
<label>사진/영상 선택</label>
<input type="file" name="files" accept="${values.accept}" multiple required>
@@ -265,11 +262,11 @@ async function moveUploadedFiles(files, targetDir) {
}
}
async function appendUploadMetadata(targetDir, { guestName, guestPhone, files }) {
async function appendUploadMetadata(targetDir, { guestPhone, folderName, files }) {
const entry = {
uploadedAt: new Date().toISOString(),
guestName,
guestPhone,
folderName,
files: files.map((file) => ({
storedName: file.filename,
originalName: Buffer.from(file.originalname, "latin1").toString("utf8"),
@@ -280,22 +277,33 @@ async function appendUploadMetadata(targetDir, { guestName, guestPhone, files })
}
async function readUploadMetadata(folderName) {
const metadataPath = path.join(uploadsDir, folderName, "upload-metadata.jsonl");
const content = await fsp.readFile(metadataPath, "utf8").catch((error) => {
if (error.code === "ENOENT") return "";
const requestDir = path.join(uploadsDir, folderName);
const paths = [path.join(requestDir, "upload-metadata.jsonl")];
const entries = await fsp.readdir(requestDir, { withFileTypes: true }).catch((error) => {
if (error.code === "ENOENT") return [];
throw error;
});
return content
.split(/\r?\n/)
.filter(Boolean)
.map((line) => {
for (const entry of entries) {
if (entry.isDirectory()) {
paths.push(path.join(requestDir, entry.name, "upload-metadata.jsonl"));
}
}
const submissions = [];
for (const metadataPath of paths) {
const content = await fsp.readFile(metadataPath, "utf8").catch((error) => {
if (error.code === "ENOENT") return "";
throw error;
});
for (const line of content.split(/\r?\n/).filter(Boolean)) {
try {
return JSON.parse(line);
submissions.push(JSON.parse(line));
} catch {
return null;
// Ignore malformed legacy lines so one bad record does not hide the list.
}
})
.filter(Boolean);
}
}
return submissions.sort((a, b) => String(a.uploadedAt || "").localeCompare(String(b.uploadedAt || "")));
}
function formatDateTime(value) {
@@ -414,8 +422,8 @@ app.get("/admin/requests/:slug/submissions", requireAdmin, async (req, res) => {
return `<article class="submission">
<h3>제출 ${index + 1}</h3>
<dl>
<dt>성함</dt><dd>${escapeHtml(submission.guestName || "-")}</dd>
<dt>연락처</dt><dd>${escapeHtml(submission.guestPhone || "-")}</dd>
<dt>저장 폴더</dt><dd>${escapeHtml(submission.folderName || "-")}</dd>
<dt>제출 시간</dt><dd>${escapeHtml(formatDateTime(submission.uploadedAt))}</dd>
<dt>파일 수</dt><dd>${files.length}개</dd>
</dl>
@@ -431,7 +439,7 @@ app.get("/admin/requests/:slug/submissions", requireAdmin, async (req, res) => {
</div>
<h1>${escapeHtml(item.title)} 제출 내역</h1>
<p class="muted">저장 위치: ${escapeHtml(path.join(uploadsDir, item.folderName))}</p>
<p class="muted">제출 정보 파일: ${escapeHtml(path.join(uploadsDir, item.folderName, "upload-metadata.jsonl"))}</p>
<p class="muted">제출 정보 파일: 각 연락처/익명 폴더의 upload-metadata.jsonl</p>
${rows || `<div class="notice">아직 이 링크로 제출된 내역이 없어. 예전 업로드는 메타데이터가 없으면 여기에는 표시되지 않을 수 있어.</div>`}
</section>
`));
@@ -530,20 +538,11 @@ app.post("/r/:slug", async (req, res) => {
}
try {
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 existingBytes = await directorySize(targetDir);
const requestDir = path.join(uploadsDir, request.folderName);
const folderName = submissionFolderName(guestPhone);
const targetDir = path.join(requestDir, folderName);
const existingBytes = await directorySize(requestDir);
const incomingBytes = files.reduce((total, file) => total + file.size, 0);
if (existingBytes + incomingBytes > request.maxBytes) {
await removeFiles(files);
@@ -556,7 +555,7 @@ app.post("/r/:slug", async (req, res) => {
return;
}
await moveUploadedFiles(files, targetDir);
await appendUploadMetadata(targetDir, { guestName, guestPhone, files });
await appendUploadMetadata(targetDir, { guestPhone, folderName, files });
if (wantsJson(req)) {
res.json({ ok: true, count: files.length });
return;