업로드 연락처 입력과 저장 폴더 정리
This commit is contained in:
+9
-18
@@ -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
@@ -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("업로드 중이에요. 화면을 닫지 말아 주세요.");
|
||||
|
||||
+36
-37
@@ -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 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;
|
||||
});
|
||||
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;
|
||||
});
|
||||
return content
|
||||
.split(/\r?\n/)
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user