문의 문의 제목
문의접수담당자가 처리를 완료했습니다. 내용을 확인하셨다면 완료 확인을 눌러주세요.
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index aa9304e..556f676 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -13,6 +13,14 @@ jobs: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} SESSION_SECRET: ${{ secrets.SESSION_SECRET }} ABC_API_KEY: ${{ secrets.ABC_API_KEY }} + ORG_CONTEXT_ENABLED: ${{ vars.STAGING_ORG_CONTEXT_ENABLED }} + ORG_CONTEXT_API_URL: ${{ vars.STAGING_ORG_CONTEXT_API_URL }} + ORG_CONTEXT_SCOPE_MODE: ${{ vars.STAGING_ORG_CONTEXT_SCOPE_MODE }} + ORG_CONTEXT_EXPOSURE: ${{ vars.STAGING_ORG_CONTEXT_EXPOSURE }} + ORG_CONTEXT_TENANT_IDS: ${{ vars.STAGING_ORG_CONTEXT_TENANT_IDS }} + ORG_CONTEXT_CACHE_TTL_SECONDS: ${{ vars.STAGING_ORG_CONTEXT_CACHE_TTL_SECONDS }} + ORG_CONTEXT_KEY_ID: ${{ secrets.STAGING_ORG_CONTEXT_KEY_ID }} + ORG_CONTEXT_KEY_SECRET: ${{ secrets.STAGING_ORG_CONTEXT_KEY_SECRET }} steps: - name: Checkout @@ -35,7 +43,10 @@ jobs: run: npm install --no-fund --no-audit - name: Check source - run: npm run check + run: npm run check && npm test + + - name: Verify organization integration settings + run: npm run org:verify - name: Register Worker session secret shell: bash @@ -53,4 +64,4 @@ jobs: run: npm run r2:upload - name: Deploy Worker - run: npx wrangler deploy --name baron-qa-gateway-test + run: node scripts/deploy-worker.mjs diff --git a/README.md b/README.md index 1f1a327..b0e2488 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Worker는 OAuth Authorization Code + PKCE를 사용하고, callback에서 검증 ## Gitea Actions 등록값 -저장소 Settings → Actions → Secrets에 아래 2개를 등록합니다. +저장소 Settings → Actions → Secrets에 아래 기본 Secret 3개를 등록합니다. | 이름 | 종류 | 값 | |---|---|---| @@ -92,3 +92,39 @@ Cloudflare API Token에는 최소한 다음 권한이 필요합니다. `.gitea/workflows/deploy.yml`은 `main` push 또는 수동 실행 시 세션 secret을 Worker에 등록하고 R2 업로드 후 `baron-qa-gateway-test`를 배포합니다. 실제 secret 값은 로그에 출력하지 않습니다. 현재 화면은 API 설정 전에도 QA 흐름을 확인할 수 있도록 샘플 글과 로컬 테스트 저장을 포함합니다. 운영 배포 시에는 Gitea Secret `ABC_API_KEY`가 Worker에 등록되어야 하며, 작성페이지는 Worker 프록시를 통해 ABC API를 호출합니다. + + +## 회사·부서 조직 연동 (스테이징) + +Gitea의 스테이징 설정을 `.gitea/workflows/deploy.yml`에서 Worker 런타임 이름으로 전달합니다. + +| Gitea | 위치 | Worker 런타임 | +|---|---|---| +| `STAGING_ORG_CONTEXT_KEY_ID` | Secrets | `ORG_CONTEXT_KEY_ID` (Secret) | +| `STAGING_ORG_CONTEXT_KEY_SECRET` | Secrets | `ORG_CONTEXT_KEY_SECRET` (Secret) | +| `STAGING_ORG_CONTEXT_ENABLED` | Variables | `ORG_CONTEXT_ENABLED` | +| `STAGING_ORG_CONTEXT_API_URL` | Variables | `ORG_CONTEXT_API_URL` | +| `STAGING_ORG_CONTEXT_SCOPE_MODE` | Variables | `ORG_CONTEXT_SCOPE_MODE` | +| `STAGING_ORG_CONTEXT_EXPOSURE` | Variables | `ORG_CONTEXT_EXPOSURE` | +| `STAGING_ORG_CONTEXT_TENANT_IDS` | Variables | `ORG_CONTEXT_TENANT_IDS` | +| `STAGING_ORG_CONTEXT_CACHE_TTL_SECONDS` | Variables, 선택 | `ORG_CONTEXT_CACHE_TTL_SECONDS` (기본 300) | + +조회는 `SCOPED`, `identifiers`를 사용합니다. 기준 테넌트는 UUID JSON 배열로 등록하며 UUID v4를 허용합니다. 각 테넌트를 최대 3개씩 동시 조회하고, 완전한 응답만 캐시합니다. 조회 타임아웃은 요청당 5초이며 일부 테넌트 실패·범위 불일치·구성원 누락은 검증 실패로 처리합니다. + +기본 `ORG_CONTEXT_ENABLED=false`에서는 조직 API를 호출하지 않습니다. `true`로 배포하면 `npm run org:verify`가 실제 키로 조직 API를 먼저 검증하며, 실패 시 새 Worker 배포를 진행하지 않습니다. 성공 시에는 조직·사용자·소속 건수만 로그에 출력합니다. 전체 응답·전화번호·키 원문은 출력하지 않습니다. + +Worker는 서명된 세션의 사용자 UUID로 소속을 찾습니다. 조회 조직 컨텍스트·대표 소속을 선택하고, 기존 requester ID·tenant ID·권한은 유지합니다. 새 문의는 `requester_affiliation`에 회사, `requester_department`에 부서를 저장합니다. JSON과 첨부 multipart에 같은 값을 사용합니다. 목록·상세는 글에 저장된 정보를 사용하며 조회 시점의 조직으로 과거 글을 바꾸지 않습니다. + +조직 조회가 실패하면 기존 세션·글쓰기 흐름과 확인된 SSO 정보를 유지합니다. 누락되거나 모호한 사용자는 `orgContextStatus`로 구분하고 다른 작성자의 정보를 대신 쓰지 않습니다. + +로컬 검증: + +```sh +npm run check +npm test +WRANGLER_LOG_PATH=/tmp/qa-worker-build.log node scripts/deploy-worker.mjs --dry-run +``` + +마지막 명령은 Secret 등록이나 배포를 실행하지 않고 번들만 확인합니다. 실제 키를 로컬로 준비했다면 런타임 환경변수 이름으로 주입한 뒤 `npm run org:verify`를 실행할 수 있습니다. 키 값을 커맨드라인 인자나 소스에 넣지 않습니다. + +기존 마이그레이션 글의 보정은 `baron_qa/scripts/qa_org_metadata.py` 도구에서 별도로 수행합니다. 개인정보가 포함된 엑셀·보정 보고서를 이 저장소의 정적 파일이나 Worker 번들에 넣지 않습니다. diff --git a/egbim/app.js b/egbim/app.js index 918d312..29b2c42 100644 --- a/egbim/app.js +++ b/egbim/app.js @@ -118,7 +118,7 @@ requesterTenantId: value(["requester_tenant_id", "requesterTenantId", "tenant_id", "tenantId"]), requesterEmail: requesterEmail, category: normalizeCategory(value(["Category", "category"])), - company: getCompanyNameFromEmail(requesterEmail) || value(["requester_company", "company", "organization", "tenant_name", "tenantName", "requester_affiliation"]), + company: value(["requester_affiliation", "requester_company", "company", "organization", "tenant_name", "tenantName"]) || getCompanyNameFromEmail(requesterEmail), department: value(["requester_department", "department", "team"]) || "-", author: value(["requester_name", "author_name", "authorName", "name", "author"]) || "작성자", title: value(["title", "subject"]) || "제목 없음", @@ -271,11 +271,11 @@ function getPostCompanyLabel(post, user) { post = post || {}; const raw = post.raw || {}; + const savedCompany = raw.requester_affiliation || (raw.extra_fields || {}).requester_affiliation; + if (savedCompany) return savedCompany; + if (post.company && post.company !== "외부 사용자") return post.company; const emailCompany = getCompanyNameFromEmail(post.requesterEmail || raw.requester_email || raw.requesterEmail); if (emailCompany) return emailCompany; - if (post.company && post.company !== "외부 사용자") return post.company; - const company = user && (user.company || user.tenantName); - if (company && post.requesterTenantId && user.tenantId && String(post.requesterTenantId) === String(user.tenantId)) return company; return post.company || "외부 사용자"; } @@ -305,7 +305,7 @@ const userUuid = raw.userUuid || raw.userId || claims.userId || claims.user_id || sessionStorage.getItem('descopeUserId') || ''; const loginId = raw.loginId || (raw.loginIds || [])[0] || raw.email || claims.email || claimProfile.email || sessionStorage.getItem('loginId') || readCookie('descope_login_id') || ''; const email = raw.email || claims.email || claimProfile.email || sessionStorage.getItem('loginId') || ''; - const company = getCompanyNameFromEmail(email) || custom.company || raw.company || claims.company || sessionStorage.getItem('company') || ''; + const company = raw.company || custom.company || claims.company || sessionStorage.getItem('company') || getCompanyNameFromEmail(email) || ''; return { userUuid: userUuid, ssoSubject: ssoSubject, @@ -322,7 +322,7 @@ tenantName: tenantName, company: company, familyCompany: custom.familyCompany || custom.family_company || raw.familyCompany || sessionStorage.getItem('familyCompany') || '', - department: custom.team || custom.department || raw.department || claims.department || sessionStorage.getItem('team') || '', + department: raw.department || custom.team || custom.department || claims.department || sessionStorage.getItem('team') || '', rawClaims: claims }; } @@ -351,10 +351,6 @@ async function loadAuthUser() { const localUser = getAuthUser(); - if (localUser && localUser.tenantId) { - resolvedAuthUser = localUser; - return localUser; - } if (!config.ssoSessionEndpoint) { resolvedAuthUser = localUser; return localUser; diff --git a/egbim/detail.html b/egbim/detail.html index 0e0afa8..d5e24f9 100644 --- a/egbim/detail.html +++ b/egbim/detail.html @@ -4,5 +4,5 @@
EG-BIM 관련 문의하기
문의 내용과 답변을 확인할 수 있습니다.
담당자가 처리를 완료했습니다. 내용을 확인하셨다면 완료 확인을 눌러주세요.
답변 0