+118
-12
@@ -11,6 +11,7 @@ export default {
|
||||
if (url.pathname === '/auth/logout') return logout(request, env);
|
||||
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 (request.method !== 'GET' && request.method !== 'HEAD') {
|
||||
return json({ error: 'method_not_allowed' }, 405);
|
||||
@@ -302,10 +303,24 @@ async function createFeedback(request, env) {
|
||||
if (!env.ABC_PROJECT_ID || !env.ABC_CHANNEL_ID) return json({ error: 'feedback_target_not_configured' }, 503);
|
||||
|
||||
let input;
|
||||
const uploadedImages = [];
|
||||
const contentType = request.headers.get('content-type') || '';
|
||||
try {
|
||||
input = await request.json();
|
||||
if (contentType.toLowerCase().includes('multipart/form-data')) {
|
||||
const formData = await request.formData();
|
||||
input = {};
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (key === 'images' && typeof value !== 'string') {
|
||||
uploadedImages.push(value);
|
||||
} else if (typeof value === 'string' && !(key in input)) {
|
||||
input[key] = value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
input = await request.json();
|
||||
}
|
||||
} catch (error) {
|
||||
return json({ error: 'invalid_json' }, 400);
|
||||
return json({ error: contentType.toLowerCase().includes('multipart/form-data') ? 'invalid_multipart' : 'invalid_json' }, 400);
|
||||
}
|
||||
if (!input || typeof input !== 'object' || Array.isArray(input)) return json({ error: 'invalid_json' }, 400);
|
||||
|
||||
@@ -321,6 +336,8 @@ async function createFeedback(request, env) {
|
||||
if (!requesterId || !requesterTenantId) return json({ error: 'requester_identity_missing' }, 422);
|
||||
const requesterPhone = String(user.phone || '');
|
||||
if (!requesterPhone) return json({ error: 'requester_phone_missing' }, 422);
|
||||
const oversizedImage = uploadedImages.find((file) => Number(file.size || 0) > 30 * 1024 * 1024);
|
||||
if (oversizedImage) return json({ error: 'attachment_too_large', message: '첨부파일은 파일당 30MB 이하만 등록할 수 있습니다.' }, 413);
|
||||
|
||||
const upstreamPayload = {
|
||||
title,
|
||||
@@ -340,20 +357,39 @@ async function createFeedback(request, env) {
|
||||
const endpoint = `${apiBaseUrl}/api/projects/${encodeURIComponent(env.ABC_PROJECT_ID)}/channels/${encodeURIComponent(env.ABC_CHANNEL_ID)}/feedbacks`;
|
||||
const sourceNamespace = env.ABC_SOURCE_NAMESPACE || 'EGBIM_QA';
|
||||
const idempotencyConsumer = env.ABC_IDEMPOTENCY_CONSUMER || sourceNamespace;
|
||||
const upstreamHeaders = {
|
||||
accept: 'application/json',
|
||||
'x-api-key': env.ABC_API_KEY,
|
||||
'x-source-namespace': sourceNamespace,
|
||||
'x-source-record-id': feedbackId,
|
||||
'x-idempotency-consumer': idempotencyConsumer,
|
||||
'idempotency-key': feedbackId
|
||||
};
|
||||
let upstreamBody;
|
||||
if (uploadedImages.length) {
|
||||
const multipart = new FormData();
|
||||
multipart.append('title', title);
|
||||
multipart.append('contents', contents);
|
||||
multipart.append('Category', category);
|
||||
multipart.append('requester_id', requesterId);
|
||||
multipart.append('requester_tenant_id', requesterTenantId);
|
||||
multipart.append('requester_email', String(user.email || ''));
|
||||
multipart.append('requester_name', String(user.name || ''));
|
||||
multipart.append('requester_department', String(user.department || ''));
|
||||
multipart.append('requester_phone_number', requesterPhone);
|
||||
multipart.append('is_secret', upstreamPayload.is_secret);
|
||||
uploadedImages.forEach((file) => multipart.append('images', file, file.name || 'attachment'));
|
||||
upstreamBody = multipart;
|
||||
} else {
|
||||
upstreamHeaders['content-type'] = 'application/json';
|
||||
upstreamBody = JSON.stringify(upstreamPayload);
|
||||
}
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'content-type': 'application/json',
|
||||
'x-api-key': env.ABC_API_KEY,
|
||||
'x-source-namespace': sourceNamespace,
|
||||
'x-source-record-id': feedbackId,
|
||||
'x-idempotency-consumer': idempotencyConsumer,
|
||||
'idempotency-key': feedbackId
|
||||
},
|
||||
body: JSON.stringify(upstreamPayload)
|
||||
headers: upstreamHeaders,
|
||||
body: upstreamBody
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('feedback_api_request_failed', error instanceof Error ? error.message : 'unknown');
|
||||
@@ -371,6 +407,76 @@ async function createFeedback(request, env) {
|
||||
return json({ id });
|
||||
}
|
||||
|
||||
async function feedbackComments(request, env, url) {
|
||||
if (request.method !== 'GET' && request.method !== 'POST') return json({ error: 'method_not_allowed' }, 405, { allow: 'GET, POST' });
|
||||
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 apiBaseUrl = String(env.ABC_API_BASE_URL || 'https://feedback.hmac.kr').replace(/\/$/, '');
|
||||
let endpoint = `${apiBaseUrl}/api/projects/${encodeURIComponent(env.ABC_PROJECT_ID)}/channels/${encodeURIComponent(env.ABC_CHANNEL_ID)}/feedbacks/${encodeURIComponent(feedbackId)}/comments`;
|
||||
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 sourceNamespace = env.ABC_SOURCE_NAMESPACE || 'EGBIM_QA';
|
||||
const headers = {
|
||||
accept: 'application/json',
|
||||
'x-api-key': env.ABC_API_KEY,
|
||||
'x-requester-id': requesterId,
|
||||
'x-requester-tenant-id': requesterTenantId
|
||||
};
|
||||
let body;
|
||||
let commentId;
|
||||
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 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);
|
||||
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({
|
||||
author_id: requesterId,
|
||||
author_tenant_id: requesterTenantId,
|
||||
author_name: String(user.name || user.email || ''),
|
||||
content,
|
||||
is_internal: false,
|
||||
actor_type: 'USER',
|
||||
comment_type: 'COMMENT'
|
||||
});
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(endpoint, { method: request.method, headers, body });
|
||||
} catch (error) {
|
||||
console.error('feedback_comments_api_request_failed', error instanceof Error ? error.message : 'unknown');
|
||||
return json({ error: 'feedback_comments_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_comments_api_rejected', message: upstreamMessage || '댓글 저장 또는 조회에 실패했습니다.', status: response.status }, response.status >= 500 ? 502 : response.status);
|
||||
}
|
||||
if (request.method === 'GET') {
|
||||
const comments = Array.isArray(result) ? result : (result.items || result.comments || result.data?.items || result.data?.comments || result.data || []);
|
||||
return json({ comments: Array.isArray(comments) ? comments : [] });
|
||||
}
|
||||
return json({ id: result.id || result.data?.id || result.comment?.id || commentId, comment: result.comment || 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