관리페이지 데이터 전송 구현
Deploy EG-BIM QA Gateway / deploy (push) Successful in 2m4s

This commit is contained in:
root
2026-09-21 14:46:36 +09:00
parent 42516b4f8b
commit 3c10478482
54 changed files with 16055 additions and 45 deletions
+98 -1
View File
@@ -10,6 +10,7 @@ export default {
if (url.pathname === '/auth/callback') return finishLogin(request, env);
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 (request.method !== 'GET' && request.method !== 'HEAD') {
return json({ error: 'method_not_allowed' }, 405);
@@ -141,6 +142,21 @@ function randomString(size = 32) {
return base64url(randomBytes(size));
}
function uuidv7() {
const bytes = randomBytes(16);
const timestamp = Date.now();
bytes[0] = Math.floor(timestamp / 0x10000000000) & 0xff;
bytes[1] = Math.floor(timestamp / 0x100000000) & 0xff;
bytes[2] = Math.floor(timestamp / 0x1000000) & 0xff;
bytes[3] = Math.floor(timestamp / 0x10000) & 0xff;
bytes[4] = Math.floor(timestamp / 0x100) & 0xff;
bytes[5] = timestamp & 0xff;
bytes[6] = (bytes[6] & 0x0f) | 0x70;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
function getCookie(request, name) {
const header = request.headers.get('cookie') || '';
const match = header.split(';').map((part) => part.trim()).find((part) => part.startsWith(name + '='));
@@ -166,6 +182,8 @@ function requiredAuthConfig(env) {
function normalizeClaims(claims) {
const profile = claims.profile && typeof claims.profile === 'object' ? claims.profile : {};
const custom = claims.customAttributes || claims.custom_attributes || {};
const profilePhones = Array.isArray(profile.phones) ? profile.phones : [];
const profilePhone = profile.phone || profile.phone_number || profilePhones[0]?.number || profilePhones[0]?.value || profilePhones[0] || '';
const tenants = claims.tenants || claims.tenantIds || custom.tenants || [];
const tenantList = Array.isArray(tenants) ? tenants : Object.keys(tenants).map((key) => ({ id: key, ...(tenants[key] || {}) }));
const tenantId = claims.tenant_id || claims.tenantId || custom.tenant_id || custom.tenantId || tenantList[0]?.id || tenantList[0]?.tenantId || '';
@@ -184,7 +202,7 @@ function normalizeClaims(claims) {
loginId: claims.email || claims.loginId || claims.preferred_username || profile.email || '',
email: claims.email || profile.email || '',
name: claims.name || profile.name || claims.display_name || '',
phone: claims.phone || claims.phone_number || '',
phone: claims.phone || claims.phone_number || profilePhone,
company: custom.company || claims.company || claims.organization || '',
familyCompany: custom.familyCompany || custom.family_company || claims.familyCompany || '',
department: custom.team || custom.department || claims.department || ''
@@ -267,6 +285,85 @@ async function sessionResponse(request, env) {
return json({ authenticated: Boolean(user), user }, 200, { 'access-control-allow-credentials': 'true' });
}
async function createFeedback(request, env) {
if (request.method !== 'POST') return json({ error: 'method_not_allowed' }, 405, { allow: '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);
let input;
try {
input = await request.json();
} catch (error) {
return json({ error: 'invalid_json' }, 400);
}
if (!input || typeof input !== 'object' || Array.isArray(input)) return json({ error: 'invalid_json' }, 400);
const title = String(input.title || '').trim();
const contents = String(input.contents || '').trim();
const category = String(input.Category || '').trim();
if (!title || !contents || !category) return json({ error: 'title_contents_category_required' }, 400);
if (!['ERROR_QNA', 'IMPROVEMENT_QNA', 'GENERAL_QNA'].includes(category)) return json({ error: 'invalid_category' }, 400);
const feedbackId = isUuidv7(input.feedbackId) ? input.feedbackId : uuidv7();
const requesterId = user.requesterId || user.ssoSubject || user.userUuid;
const requesterTenantId = user.requesterTenantId || user.tenantId;
if (!requesterId || !requesterTenantId) return json({ error: 'requester_identity_missing' }, 422);
if (!user.phone) return json({ error: 'requester_phone_missing' }, 422);
const upstreamPayload = {
title,
contents,
Category: category,
requester_id: requesterId,
requester_tenant_id: requesterTenantId,
requester_email: user.email || '',
requester_name: user.name || '',
requester_department: user.department || '',
requester_phone_number: user.phone,
is_secret: input.is_secret ? 1 : 0
};
if (Array.isArray(input.images) && input.images.length) upstreamPayload.images = input.images;
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`;
const sourceNamespace = env.ABC_SOURCE_NAMESPACE || 'EGBIM_QA';
const idempotencyConsumer = env.ABC_IDEMPOTENCY_CONSUMER || sourceNamespace;
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)
});
} catch (error) {
console.error('feedback_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) {
console.error('feedback_api_rejected', response.status);
return json({ error: 'feedback_api_rejected', status: response.status }, response.status >= 500 ? 502 : response.status);
}
const id = result.id || result.data?.id || result.feedback?.id;
if (!id) return json({ error: 'feedback_id_missing' }, 502);
return json({ id });
}
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);
}
function logout(request, env) {
const url = new URL(request.url);
const target = safeReturnUrl(env.AUTH_POST_LOGOUT_REDIRECT_URI || '/', url.origin);