This commit is contained in:
+70
-4
@@ -12,6 +12,7 @@ export default {
|
||||
if (url.pathname === '/auth/session') return sessionResponse(request, env);
|
||||
if (url.pathname === '/api/feedbacks') return createFeedback(request, env);
|
||||
if (/^\/api\/feedbacks\/[^/]+\/comments$/.test(url.pathname)) return feedbackComments(request, env, url);
|
||||
if (/^\/api\/feedbacks\/[^/]+$/.test(url.pathname)) return feedbackDetail(request, env, url);
|
||||
|
||||
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
||||
return json({ error: 'method_not_allowed' }, 405);
|
||||
@@ -432,22 +433,40 @@ async function feedbackComments(request, env, url) {
|
||||
};
|
||||
let body;
|
||||
let commentId;
|
||||
const uploadedAttachments = [];
|
||||
if (request.method === 'GET') {
|
||||
const query = new URLSearchParams({ includeInternal: 'false' });
|
||||
endpoint = endpoint + '?' + query.toString();
|
||||
} else {
|
||||
let input;
|
||||
try { input = await request.json(); } catch (error) { return json({ error: 'invalid_json' }, 400); }
|
||||
const requestContentType = request.headers.get('content-type') || '';
|
||||
try {
|
||||
if (requestContentType.toLowerCase().includes('multipart/form-data')) {
|
||||
const formData = await request.formData();
|
||||
input = {};
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (key === 'attachments' && typeof value !== 'string') uploadedAttachments.push(value);
|
||||
else if (typeof value === 'string' && !(key in input)) input[key] = value;
|
||||
}
|
||||
} else {
|
||||
input = await request.json();
|
||||
}
|
||||
} catch (error) {
|
||||
return json({ error: requestContentType.toLowerCase().includes('multipart/form-data') ? 'invalid_multipart' : 'invalid_json' }, 400);
|
||||
}
|
||||
const content = String(input && input.content || '').trim();
|
||||
if (!content) return json({ error: 'comment_content_required' }, 400);
|
||||
if (content.length > 5000) return json({ error: 'comment_content_too_long' }, 400);
|
||||
const invalidAttachment = uploadedAttachments.find((file) => !String(file.type || '').toLowerCase().startsWith('image/'));
|
||||
if (invalidAttachment) return json({ error: 'comment_image_required', message: '댓글 첨부는 이미지 파일만 등록할 수 있습니다.' }, 415);
|
||||
const oversizedAttachment = uploadedAttachments.find((file) => Number(file.size || 0) > 30 * 1024 * 1024);
|
||||
if (oversizedAttachment) return json({ error: 'comment_attachment_too_large', message: '댓글 이미지는 파일당 30MB 이하만 등록할 수 있습니다.' }, 413);
|
||||
commentId = uuidv7();
|
||||
headers['content-type'] = 'application/json';
|
||||
headers['x-source-namespace'] = sourceNamespace;
|
||||
headers['x-source-record-id'] = commentId;
|
||||
headers['x-idempotency-consumer'] = sourceNamespace;
|
||||
headers['idempotency-key'] = commentId;
|
||||
body = JSON.stringify({
|
||||
const commentPayload = {
|
||||
author_id: requesterId,
|
||||
author_tenant_id: requesterTenantId,
|
||||
author_name: String(user.name || user.email || ''),
|
||||
@@ -455,7 +474,16 @@ async function feedbackComments(request, env, url) {
|
||||
is_internal: false,
|
||||
actor_type: 'USER',
|
||||
comment_type: 'COMMENT'
|
||||
});
|
||||
};
|
||||
if (uploadedAttachments.length) {
|
||||
const multipart = new FormData();
|
||||
Object.entries(commentPayload).forEach(([key, value]) => multipart.append(key, String(value)));
|
||||
uploadedAttachments.forEach((file) => multipart.append('attachments', file, file.name || 'comment-image'));
|
||||
body = multipart;
|
||||
} else {
|
||||
headers['content-type'] = 'application/json';
|
||||
body = JSON.stringify(commentPayload);
|
||||
}
|
||||
}
|
||||
|
||||
let response;
|
||||
@@ -477,6 +505,44 @@ async function feedbackComments(request, env, url) {
|
||||
return json({ id: result.id || result.data?.id || result.comment?.id || commentId, comment: result.comment || result.data || result });
|
||||
}
|
||||
|
||||
async function feedbackDetail(request, env, url) {
|
||||
if (request.method !== 'GET') return json({ error: 'method_not_allowed' }, 405, { allow: 'GET' });
|
||||
const user = await getSession(request, env);
|
||||
if (!user) return json({ error: 'unauthenticated' }, 401);
|
||||
if (!env.ABC_API_KEY) return json({ error: 'feedback_api_not_configured' }, 503);
|
||||
if (!env.ABC_PROJECT_ID || !env.ABC_CHANNEL_ID) return json({ error: 'feedback_target_not_configured' }, 503);
|
||||
|
||||
const feedbackId = decodeURIComponent(url.pathname.split('/')[3] || '');
|
||||
if (!isUuidv7(feedbackId)) return json({ error: 'invalid_feedback_id' }, 400);
|
||||
|
||||
const requesterId = String(user.requesterId || user.ssoSubject || user.userUuid || '');
|
||||
const requesterTenantId = String(user.requesterTenantId || user.tenantId || '');
|
||||
if (!requesterId || !requesterTenantId) return json({ error: 'requester_identity_missing' }, 422);
|
||||
const apiBaseUrl = String(env.ABC_API_BASE_URL || 'https://feedback.hmac.kr').replace(/\/$/, '');
|
||||
const endpoint = `${apiBaseUrl}/api/projects/${encodeURIComponent(env.ABC_PROJECT_ID)}/channels/${encodeURIComponent(env.ABC_CHANNEL_ID)}/feedbacks/${encodeURIComponent(feedbackId)}`;
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(endpoint, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'x-api-key': env.ABC_API_KEY,
|
||||
'x-requester-id': requesterId,
|
||||
'x-requester-tenant-id': requesterTenantId
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('feedback_detail_api_request_failed', error instanceof Error ? error.message : 'unknown');
|
||||
return json({ error: 'feedback_api_unreachable' }, 502);
|
||||
}
|
||||
const result = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
const upstreamMessage = String(result.message || result.error || result.code || '').slice(0, 240);
|
||||
return json({ error: 'feedback_api_rejected', message: upstreamMessage || '문의 상세 조회에 실패했습니다.', status: response.status }, response.status >= 500 ? 502 : response.status);
|
||||
}
|
||||
return json({ feedback: result.feedback || result.data || result });
|
||||
}
|
||||
|
||||
function isUuidv7(value) {
|
||||
return typeof value === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user