+60
-39
@@ -177,7 +177,7 @@
|
||||
}
|
||||
|
||||
async function requestJson(path, options) {
|
||||
const sameOriginProxy = !config.apiBaseUrl && path === config.createFeedbackPath;
|
||||
const sameOriginProxy = !config.apiBaseUrl && (path === config.createFeedbackPath || /^\/api\/feedbacks\/[^/]+\/comments$/.test(path));
|
||||
if (!config.apiBaseUrl && !sameOriginProxy) return { local: true };
|
||||
const target = config.apiBaseUrl ? config.apiBaseUrl.replace(/\/$/, '') + path : path;
|
||||
const response = await fetch(target, Object.assign({ credentials: 'include' }, options));
|
||||
@@ -189,7 +189,8 @@
|
||||
}
|
||||
|
||||
async function postToApi(path, payload) {
|
||||
return requestJson(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
|
||||
const isFormData = typeof FormData !== 'undefined' && payload instanceof FormData;
|
||||
return requestJson(path, isFormData ? { method: 'POST', body: payload } : { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
|
||||
}
|
||||
|
||||
function makeUuid() {
|
||||
@@ -215,40 +216,17 @@
|
||||
return Array.prototype.map.call(new Uint8Array(digest), function (byte) { return ('00' + byte.toString(16)).slice(-2); }).join('');
|
||||
}
|
||||
|
||||
async function uploadFiles(files, feedbackId, purpose) {
|
||||
async function buildFeedbackSubmission(post, feedbackId, files) {
|
||||
files = Array.prototype.slice.call(files || []);
|
||||
if (!files.length) return [];
|
||||
const descriptors = await Promise.all(files.map(async function (file) {
|
||||
return { clientId: makeUuid(), originalFileName: file.name, mimeType: file.type || 'application/octet-stream', fileSize: file.size, checksumSha256: await sha256(file) };
|
||||
return { id: makeUuid(), originalFileName: file.name, mimeType: file.type || 'application/octet-stream', fileSize: file.size, checksumSha256: await sha256(file), purpose: 'FEEDBACK_ATTACHMENT' };
|
||||
}));
|
||||
if (!config.apiBaseUrl) {
|
||||
return descriptors.map(function (item) { return Object.assign(item, { id: makeUuid(), storageBucket: config.storageBucket, storageKey: 'local-preview/' + feedbackId + '/' + item.clientId + '-' + item.originalFileName, purpose: purpose }); });
|
||||
}
|
||||
const presign = await postToApi(config.presignPath, { feedbackId: feedbackId, workspaceId: config.workspaceId, workspaceCode: config.workspaceCode, bucket: config.storageBucket, purpose: purpose, files: descriptors });
|
||||
const uploads = presign.uploads || presign.items || [];
|
||||
if (uploads.length !== descriptors.length) throw new Error('첨부파일 업로드 URL을 모두 받지 못했습니다.');
|
||||
return Promise.all(uploads.map(async function (upload, index) {
|
||||
const file = files[index];
|
||||
const headers = Object.assign({}, upload.headers || {});
|
||||
if (!headers['Content-Type'] && file.type) headers['Content-Type'] = file.type;
|
||||
const response = await fetch(upload.uploadUrl, { method: upload.method || 'PUT', headers: headers, body: file });
|
||||
if (!response.ok) throw new Error(file.name + ' 업로드에 실패했습니다.');
|
||||
if (!upload.storageKey) throw new Error(file.name + '의 storageKey를 받지 못했습니다.');
|
||||
return Object.assign(descriptors[index], upload.metadata || {}, {
|
||||
id: upload.id || makeUuid(), storageBucket: upload.storageBucket || config.storageBucket,
|
||||
storageKey: upload.storageKey, purpose: purpose
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
function buildFeedbackPayload(post, user, feedbackId, attachments) {
|
||||
const categoryCode = CATEGORY_CODES[post.category];
|
||||
const imageMetadata = attachments.filter(function (item) { return item.purpose === 'FEEDBACK_ATTACHMENT'; }).map(function (item) {
|
||||
return { id: item.id, original_file_name: item.originalFileName, storage_bucket: item.storageBucket, storage_key: item.storageKey, mime_type: item.mimeType, file_size: item.fileSize, checksum_sha256: item.checksumSha256 };
|
||||
});
|
||||
const payload = { feedbackId: feedbackId, title: post.title, contents: post.content, Category: categoryCode, is_secret: post.secret ? 1 : 0 };
|
||||
if (imageMetadata.length) payload.images = imageMetadata;
|
||||
return payload;
|
||||
const payload = { feedbackId: feedbackId, title: post.title, contents: post.content, Category: CATEGORY_CODES[post.category], is_secret: post.secret ? 'true' : 'false' };
|
||||
if (!files.length) return { body: payload, attachments: descriptors };
|
||||
const formData = new FormData();
|
||||
Object.keys(payload).forEach(function (key) { formData.append(key, payload[key]); });
|
||||
files.forEach(function (file) { formData.append('images', file, file.name); });
|
||||
return { body: formData, attachments: descriptors };
|
||||
}
|
||||
|
||||
function initList() {
|
||||
@@ -325,12 +303,12 @@
|
||||
submitButton.disabled = true;
|
||||
submitButton.textContent = '등록 중...';
|
||||
try {
|
||||
const attachments = await uploadFiles(qs('#attachment').files, feedbackId, 'FEEDBACK_ATTACHMENT');
|
||||
const payload = buildFeedbackPayload(post, currentUser, feedbackId, attachments);
|
||||
const submission = await buildFeedbackSubmission(post, feedbackId, qs('#attachment').files);
|
||||
console.info('[QA] sending feedback to Worker:', config.createFeedbackPath);
|
||||
const result = await postToApi(config.createFeedbackPath, payload);
|
||||
const result = await postToApi(config.createFeedbackPath, submission.body);
|
||||
console.info('[QA] Worker feedback response:', result);
|
||||
post.id = result.id || post.id;
|
||||
post.attachments = submission.attachments;
|
||||
savePost(post);
|
||||
showToast(result.local ? '테스트 글로 저장했습니다. UUID가 생성되었습니다.' : '문의가 등록되었습니다.');
|
||||
window.setTimeout(function () { window.location.href = 'detail.html?id=' + encodeURIComponent(post.id); }, 500);
|
||||
@@ -363,9 +341,52 @@
|
||||
qs('[data-detail-department]').textContent = post.department;
|
||||
qs('[data-detail-content]').textContent = post.content;
|
||||
qs('[data-detail-secret]').hidden = !post.secret;
|
||||
const comments = getComments()[post.id] || [];
|
||||
qs('#commentCount').textContent = comments.length;
|
||||
qs('#comments').innerHTML = comments.length ? comments.map(function (comment) { return '<div class="comment"><div class="comment-meta"><strong>' + escapeHtml(comment.author) + '</strong><span>' + escapeHtml(comment.date) + '</span></div><div class="comment-content">' + escapeHtml(comment.content) + '</div></div>'; }).join('') : '<div class="comment-empty">등록된 답변이 없습니다.</div>';
|
||||
const attachmentBox = qs('[data-detail-attachments]');
|
||||
const attachments = Array.isArray(post.attachments) ? post.attachments : [];
|
||||
if (attachmentBox) attachmentBox.innerHTML = attachments.length ? attachments.map(function (attachment) {
|
||||
return '<div class="attachment-item">📎 ' + escapeHtml(attachment.originalFileName || '첨부파일') + '</div>';
|
||||
}).join('') : '<span class="help-text">첨부된 파일이 없습니다.</span>';
|
||||
function normalizeComment(comment) {
|
||||
comment = comment || {};
|
||||
return { id: comment.id || '', author: comment.author_name || comment.authorName || comment.author || comment.author?.name || '작성자', date: comment.created_at || comment.createdAt || comment.updated_at || '', content: comment.content || comment.body || '' };
|
||||
}
|
||||
function renderComments(comments) {
|
||||
comments = (comments || []).map(normalizeComment).filter(function (comment) { return comment.content; });
|
||||
qs('#commentCount').textContent = comments.length;
|
||||
qs('#comments').innerHTML = comments.length ? comments.map(function (comment) { return '<div class="comment"><div class="comment-meta"><strong>' + escapeHtml(comment.author) + '</strong><span>' + escapeHtml(comment.date) + '</span></div><div class="comment-content">' + escapeHtml(comment.content) + '</div></div>'; }).join('') : '<div class="comment-empty">등록된 답변이 없습니다.</div>';
|
||||
return comments;
|
||||
}
|
||||
let comments = renderComments(getComments()[post.id] || []);
|
||||
const commentsPath = '/api/feedbacks/' + encodeURIComponent(post.id) + '/comments';
|
||||
requestJson(commentsPath, { method: 'GET' }).then(function (result) {
|
||||
comments = renderComments(result.comments || result.items || []);
|
||||
}).catch(function (error) {
|
||||
console.warn('[QA] comments load failed:', error.message);
|
||||
});
|
||||
const commentForm = qs('#commentForm');
|
||||
if (commentForm) commentForm.addEventListener('submit', async function (event) {
|
||||
event.preventDefault();
|
||||
const contentInput = qs('#commentContent');
|
||||
const errorBox = qs('#commentError');
|
||||
const submitButton = qs('#commentSubmit');
|
||||
const content = contentInput.value.trim();
|
||||
errorBox.hidden = true;
|
||||
if (!content) { errorBox.textContent = '댓글 내용을 입력해주세요.'; errorBox.hidden = false; return; }
|
||||
submitButton.disabled = true;
|
||||
submitButton.textContent = '등록 중...';
|
||||
try {
|
||||
const result = await postToApi(commentsPath, { content: content });
|
||||
comments.push(normalizeComment(result.comment || { id: result.id, content: content, author: (getAuthUser() || {}).name || '작성자', createdAt: new Date().toISOString() }));
|
||||
renderComments(comments);
|
||||
contentInput.value = '';
|
||||
} catch (error) {
|
||||
errorBox.textContent = error.message || '댓글 등록에 실패했습니다.';
|
||||
errorBox.hidden = false;
|
||||
} finally {
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = '댓글 등록';
|
||||
}
|
||||
});
|
||||
qs('#backButton').addEventListener('click', function () { window.location.href = 'index.html'; });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user