diff --git a/app.js b/app.js index 139ffcf..2a7c8da 100644 --- a/app.js +++ b/app.js @@ -192,7 +192,7 @@ const response = await fetch(target, Object.assign({ credentials: 'include' }, options)); if (!response.ok) { const errorBody = await response.json().catch(function () { return {}; }); - throw new Error(errorBody.message || 'API 요청에 실패했습니다. (' + response.status + ')'); + throw new Error(errorBody.message || errorBody.error || 'API 요청에 실패했습니다. (' + response.status + ')'); } return response.json().catch(function () { return {}; }); } diff --git a/docs/qa-feedback-api-integration-task.md b/docs/qa-feedback-api-integration-task.md index 1ef450c..3bd86a8 100644 --- a/docs/qa-feedback-api-integration-task.md +++ b/docs/qa-feedback-api-integration-task.md @@ -34,6 +34,7 @@ - [x] 프로젝트·채널 UUID 확인 - [x] 채널 필드 확인: `title`, `contents`, `Category`, `images` +- [x] 서버 requester 메타데이터 계약 확인 - [x] SSO 세션 저장 및 `/auth/session` 확인 - [x] Worker에 `POST /api/feedbacks` 라우트 추가 - [x] Worker에서 `baron_qa_session` 검증 @@ -77,7 +78,7 @@ - [ ] 로그인하지 않은 사용자는 `401` 응답을 받음 - [ ] 로그인한 사용자가 제목·내용·카테고리를 입력하면 ABC에 1건 저장됨 - [ ] 저장된 데이터에 제목·내용·Category가 정확히 들어감 -- [ ] 저장된 데이터에 requester ID·tenant·이름·이메일·부서·전화번호가 들어감 +- [ ] 저장된 데이터에 requester ID·UUID·tenant·이름·이메일·부서·전화번호·소속·직책·등급이 들어감 - [ ] 동일한 `Idempotency-Key` 재요청 시 중복 저장되지 않음 - [ ] ABC 응답의 `id`로 상세 페이지 이동 - [ ] API Key가 브라우저 Network 탭에 노출되지 않음 @@ -101,4 +102,4 @@ npx wrangler secret put ABC_API_KEY --name baron-qa-gateway-test - ABC 문서에는 현재 `/api/projects/...` 경로가 실제 시나리오로 기재되어 있다. - `/api/v1/...`는 패키징 권장 경로로 문서화되어 있으므로 1차 구현은 현재 운영 시나리오인 `/api/...`를 사용한다. - UUID v7 정책에 맞춰 현재 브라우저의 UUID v4 생성 로직을 교체한다. -- `requester_phone_number`는 관리페이지 문서의 SSO 매핑 및 알림 정책에서 필수 requester 메타데이터로 정의되어 있다. ABC 채널의 동적 필드로 직접 저장 가능한지 여부는 API 응답에 맞춰 Worker에서 검증한다. +- `requester_id`, `requester_tenant_id`, `requester_email`, `requester_name`, `requester_department`, `requester_phone_number`, `requester_uuid`, `requester_affiliation`, `requester_position`, `requester_grade`, `is_secret`는 ABC 서버가 별도 requester 메타데이터로 처리한다. 채널 `fields` 등록 여부와 무관하게 payload에 포함한다. diff --git a/src/index.js b/src/index.js index 9fb0863..d9c028c 100644 --- a/src/index.js +++ b/src/index.js @@ -189,6 +189,7 @@ function normalizeClaims(claims) { const tenantId = claims.tenant_id || claims.tenantId || custom.tenant_id || custom.tenantId || tenantList[0]?.id || tenantList[0]?.tenantId || ''; const tenantIds = tenantList.map((tenant) => typeof tenant === 'string' ? tenant : (tenant.id || tenant.tenantId || tenant.key || '')).filter(Boolean); if (tenantId && !tenantIds.includes(tenantId)) tenantIds.unshift(tenantId); + const primaryTenant = tenantList.find((tenant) => typeof tenant !== 'string' && (tenant.id || tenant.tenantId) === tenantId) || tenantList[0] || {}; const subject = claims.sub || claims.subject || claims.userId || claims.user_id || ''; return { userUuid: claims.userId || claims.user_id || subject, @@ -203,9 +204,11 @@ function normalizeClaims(claims) { email: claims.email || profile.email || '', name: claims.name || profile.name || claims.display_name || '', 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 || '' + company: custom.company || claims.company || claims.organization || profile.company || '', + familyCompany: custom.familyCompany || custom.family_company || claims.familyCompany || claims.affiliation || '', + department: custom.team || custom.department || claims.department || primaryTenant.name || '', + position: custom.position || claims.position || profile.position || primaryTenant.position || primaryTenant.title || '', + grade: custom.grade || claims.grade || profile.grade || primaryTenant.grade || '' }; } @@ -322,6 +325,10 @@ async function createFeedback(request, env) { requester_name: user.name || '', requester_department: user.department || '', requester_phone_number: user.phone, + requester_uuid: user.userUuid || requesterId, + requester_affiliation: user.familyCompany || user.company || '', + requester_position: user.position || '', + requester_grade: user.grade || '', is_secret: input.is_secret ? 1 : 0 }; if (Array.isArray(input.images) && input.images.length) upstreamPayload.images = input.images; @@ -353,7 +360,8 @@ async function createFeedback(request, env) { 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 upstreamMessage = String(result.message || result.error || result.code || '').slice(0, 240); + return json({ error: 'feedback_api_rejected', message: upstreamMessage || 'ABC API가 문의 저장을 거부했습니다.', 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);