FileStore 독립 관리자 콘솔
관리자 화면은 admin-web의 React 앱으로 제공하며, @hohyun94/portal-foundation의 PortalAdminShell, PortalConfigProvider, 공통 페이지·패널·상태 컴포넌트와 Ant Design을 사용한다. pnpm run build:admin으로 public/admin에 번들을 생성하면 Express가 /admin에서 정적 앱을 제공한다. GitHub Packages 인증이 필요한 환경에서는 admin-web/.npmrc.example을 참고해 NODE_AUTH_TOKEN을 주입한다. 번들이 없는 개발·롤백 환경에서는 기존 서버 렌더러가 자동 fallback으로 사용된다.
FileStore는 첨부파일 원본을 위한 선택형 backend다. 각 사용자 포털은 첨부파일 기능이 필요할 때만 FileStore service API를 연결하며, PortalConsole·UiPlatform와 독립적으로 결합할 수 있다. /admin은 backend 정의와 포털별 attachment policy를 관리하는 독립 작업대다.
| 저장 유형 | UI 선택값 | 필수 metadata | server environment secret |
|---|---|---|---|
| Cloudflare R2 | Cloudflare R2 | endpoint, bucket, secretRef, region=auto | ${secretRef}_ACCESS_KEY_ID, ${secretRef}_SECRET_ACCESS_KEY |
| Amazon S3 | Amazon S3 | bucket, secretRef, AWS region, custom endpoint는 선택 | ${secretRef}_ACCESS_KEY_ID, ${secretRef}_SECRET_ACCESS_KEY |
| 온프레미스 디스크 | Local server disk | writable mounted localRoot | 없음 |
Access key, secret key, Database URL, PortalConsole credential은 admin form이나 JSON configuration에 입력·저장하지 않는다. 관리자는 secret namespace만 지정하며, 실제 credential은 배포 환경 변수 또는 secret manager가 FileStore server process에 주입한다.
시작 방식
FILE_STORE_SERVICE_TOKEN은 포털·RagService가 service API를 호출할 때 사용한다. production에서 이 값이 없으면 startup을 거부한다(개발에서만 명시적인 *-development-only fallback이 허용된다). /admin은 쿠키 세션(FILE_STORE_ADMIN_USERNAME·FILE_STORE_ADMIN_PASSWORD 로 만든 로컬 계정 또는 PortalConsole SSO)으로 인증하고, FILE_STORE_ADMIN_TOKEN 은 그 대체 경로(레거시)일 뿐 없어도 된다 — PortalConsole의 FileStore 데스크가 관리 API를 호출할 때 쓰는 값이므로 콘솔과 같은 값으로 두는 것이 보통이다. 환경변수 소유·정리는 docs/DEPLOY_ENV.md 참고.
FILE_STORE_CONFIG_PATH는 backend와 mapping metadata의 local persistence 위치다. 파일은 atomic write와 0600 mode로 저장한다. secret 원문은 저장하지 않는다. immutable 배포는 첫 시작에만 FILE_STORE_BOOTSTRAP_CONFIG로 metadata를 seed할 수 있고, 이후 /admin에서 추가·수정한다.
포털 attachment policy
각 mapping은 backend ID 외에 다음 정책을 독립적으로 저장한다.
| 정책 | 적용 시점 |
|---|---|
pathPrefix | object key를 <prefix>/<portal>/<asset-id>/... 형태로 분리 |
maxBytes·allowedContentTypes | upload request validation |
encryptionRequired | portal policy metadata; object-store encryption control과 연동할 확장 지점 |
ragPublishAllowed | ACTIVE lifecycle 이후 RagService 발행 허용 여부 |
readEnabled·writeEnabled | portal별 object access authorization |
FileStore는 요청의 portalId와 mappingId 일치를 강제한다. 다른 포털의 mapping을 제출하면 upload가 거부된다. 매핑이 남아 있는 backend는 관리자 UI/API에서 삭제할 수 없다.
검증 API와 운영 원칙
POST /api/file-store/v1/admin/backends/validate는 local disk의 실제 읽기·쓰기 권한을 확인한다. Object storage는 credential namespace가 server environment에 존재하는지만 확인하며, 관리자 저장 action이 임의 remote bucket write/list를 발생시키지 않는다. POST /api/file-store/v1/admin/backends/test-connection은 Cloudflare R2는 HeadBucket 으로, OAuth 저장소(구글드라이브·원드라이브)는 등록된 refresh token 으로 access token 갱신을 실측한다. 실제 파일 upload는 여전히 portal policy에 따라 수행된다.
/api/file-store/v1/admin/* API는 X-FileStore-Admin-Token으로만 접근한다. portal service token을 전달해도 admin API는 401로 거부한다. admin configuration response에는 endpoint·bucket·secretRef는 표시할 수 있지만 access key, secret key, token 원문은 절대 포함하지 않는다.
Cloudflare R2 설정 순서
Cloudflare Dashboard의 R2에서 bucket을 만든 다음 API Tokens에서 Object Read & Write 권한을 선택하고 가능하면 해당 bucket으로 범위를 제한한다. FileStore 관리자 화면에는 다음 값을 입력한다.
| 입력값 | 예시 | 설명 |
|---|---|---|
| 저장 유형 | Cloudflare R2 | CLOUDFLARE_R2 |
| Region | auto | R2 S3 API의 고정 호환 값 |
| Access Key ID · Secret Access Key | (R2 API Token 값) | 화면에서 입력해 저장한다 — 백엔드별로 봉투 암호화 보관(다시 표시하지 않음, 재입력 = 회전) |
| Secret namespace | R2_PRIMARY (선택) | 환경변수 경로를 쓸 때만: 위 자격증명을 비우고 이 접두사를 넣는다 |
| Bucket | company-attachments | R2 bucket 이름 |
| R2 S3 endpoint | https://<ACCOUNT_ID>.r2.cloudflarestorage.com | Cloudflare account ID 기반 endpoint — FileStore 는 path-style(/버킷/키)로 호출한다 |
자격증명은 두 경로 중 하나면 된다 — ① 화면 입력(Access Key ID·Secret Access Key, 권장: 백엔드마다 값이 달라도 재기동이 필요 없다) ② 서버 환경변수 {secretRef}_ACCESS_KEY_ID·{secretRef}_SECRET_ACCESS_KEY. 저장소를 저장하기 전에 모달의 연결 테스트를 누르면(입력한 값 또는 저장된 값으로) FileStore가 R2 HeadBucket 요청으로 bucket 접근 권한을 확인하며, 자격증명 원문은 설정 파일·API 응답·로그에 저장하지 않는다.
OAuth 저장소(구글드라이브·원드라이브) 연결 — refresh token 은 권한 연결 로 받는다
client id/secret 은 공급자가 발급하고(구글 클라우드 콘솔·Azure 앱 등록), refresh token 은 사용자가 동의해야 생긴다 — 관리자가 값을 넣었다고 자동으로 생기지 않는다. 그 절차를 손으로 하면 실수하기 쉬우므로 FileStore 가 동의 화면으로 보내고 돌아온 코드를 토큰으로 교환한다.
저장소→+ 추가로 backend 등록: provider=Google Drive, Client ID·Client Secret 입력 후 저장(이 값은 봉투 암호화 보관).- 저장하면 모달의 리디렉션 URI 가 채워진다(예:
https://file-store.example.com/auth/google-drive/callback). 구글 클라우드 콘솔에서- 프로젝트에서 Google Drive API 사용 설정,
- OAuth 동의 화면(외부/테스트) 구성 + 테스트 사용자에 로그인할 계정 추가,
- OAuth 클라이언트 ID(웹 애플리케이션) 생성 → 승인된 리디렉션 URI 에 위 주소를 그대로 넣는다. 주소가 다르면 동의 화면이
redirect_uri_mismatch로 거부한다.
- 모달의 권한 연결 을 누르면 새 창에서 동의 화면이 열린다 → 동의하면
/auth/{provider}/callback으로 돌아와 refresh token 이 이 backend 에 봉투 암호화로 저장되고 화면은/admin?oauthConnect=ok로 복귀한다.- FileStore 는 구글에
access_type=offline·prompt=consent를 함께 보낸다 — 재동의 시에도 refresh token 이 나온다. - 실패는
/admin?oauthConnect=failed&oauthConnectReason=…로 돌아오고 화면이 사유를 보여 준다(state_invalid,client_credentials_missing,provider_invalid_request(redirect_uri_mismatch 포함),refresh_token_missing,token_exchange_unreachable…). state는 1회용·10분 만료이고,start는 관리자 세션이 있어야 한다(공급자에서 돌아오는 콜백만 state 로 검증).
- FileStore 는 구글에
연결 테스트로 실측한다(OAuth 는 refresh token 으로 access token 갱신까지 확인). 결과는 목록의연결 테스트열에 남는다(회전 후재시험 필요).
환경변수는 FILE_STORE_PUBLIC_URL 하나가 기준이다 — 리디렉션 주소를 만들 때 쓰므로 프록시 뒤 배포에서는 바깥에서 보이는 주소로 둔다. 자동 흐름을 쓸 수 없는 배포(폐쇄망 등)를 위해 모달의 Refresh token 직접 입력은 그대로 유지한다.
구글 403 access_denied 와 테스트 모드 주의 (운영)
액세스 차단됨: … 앱은 현재 테스트 중이며 개발자가 승인한 테스터만 액세스할 수 있습니다(403access_denied): OAuth 동의 화면이 테스트 중(Testing) 이고 로그인한 계정이 테스트 사용자에 없어서 생긴다. 구글 클라우드 콘솔 → API 및 서비스 → OAuth 동의 화면 →테스트 사용자에 그 계정을 추가(권장)하거나 앱을 게시한다. 대상(Audience)이내부(Internal)이면 개인 Gmail 계정은 애초에 추가할 수 없으므로외부(External)로 둔다. 화면에는provider_access_denied로 표시된다.- 테스트 모드의 refresh token 은 7일 후 만료된다 — 무인 업로드(서버 통합)에는 부적합하다. 7일마다
권한 연결을 다시 하거나 앱을 게시해야 하고, 만료되면 업로드가invalid_grant로 실패하며 화면 사유도provider_invalid_grant로 돌아온다. - FileStore 가 요청하는 구글 범위는
https://www.googleapis.com/auth/drive(전체 드라이브)다 — 프로덕션 게시에는 Google 검증(제한된 범위)이 필요하다. 검증 부담을 줄이려면 어댑터가 실제로 다루는 범위(앱이 만든 파일)에 맞춰drive.file로 좁히는 것을 검토한다.
저장소 backend 등록 (ID 서버 채번 · 연결 테스트)
저장소 탭의 + 저장소 등록은 backend metadata를 저장하고, 자격증명도 같은 모달에서 함께 입력받는다 — 객체 저장소(R2·S3)는 Access Key ID·Secret Access Key, OAuth 저장소(구글드라이브·원드라이브)는 Client ID·Client Secret(+OneDrive Tenant ID). 입력한 값은 백엔드별로 봉투 암호화되어 file_store_backend_credentials(DATABASE_URL이 없으면 <FILE_STORE_CONFIG_PATH>.backend-credentials.json)에 저장되고, 화면·API 응답·로그·감사 기록에는 남지 않는다. 자격증명을 저장하려면 FILE_STORE_ENVELOPE_MASTER_KEY가 필요하다.
- 표에는
자격증명 설정됨(또는자격증명 미설정)만 표시하고, 값은 다시 보여 주지 않는다. 수정 모달에서 두 필드를 비우면 기존 값이 유지되고, 입력하면 그 값으로 교체(회전)된다.자격증명 삭제버튼은 저장된 값만 지운다(이후로는 환경변수 경로만 남는다). - 환경변수 경로는 대체 수단이다 —
{secretRef}_ACCESS_KEY_ID·{secretRef}_SECRET_ACCESS_KEY. 백엔드가 여러 개면 백엔드마다 env 한 쌍이 필요하고 재기동이 필요하므로, 멀티 백엔드는 화면 입력을 쓴다. - 연결 테스트 결과가 목록에 남는다: 검증·연결 테스트를 실행하면 그 결과(
통과/실패·시각·사유)를 backend 별로 저장한다(file_store_backend_checks, 파일 배포는<FILE_STORE_CONFIG_PATH>.backend-checks.json). 값은 저장하지 않고 지문(HMAC, 마스터 키) 만 남기므로 자격증명이나 마운트 경로를 바꾸면 자동으로재시험 필요가 된다 — "설정됨"과 "실제로 붙는다"를 화면에서 구분하기 위해서다. - **자격증명이 어느 경로로도 없으면 저장이 거부되고(저장 시점), 이미 저장된 백엔드도 업로드 때
저장 backend {id}의 자격증명이 없습니다로 즉시 실패한다.
봉투 암호화 키와 자격증명 상태 (배포 점검)
FILE_STORE_ENVELOPE_MASTER_KEY(32바이트 Base64)는 자격증명·refresh token 의 저장/조회와 연결 테스트 지문(HMAC) 계산에 쓰인다. 키가 없거나 저장 당시와 다르면 저장된 값을 해독할 수 없다.
이때 관리자 API 는 실패하지 않는다.
GET /api/file-store/v1/admin/configuration이envelopeKeyConfigured:false와 backend 별credentialsError(원인 메시지)를 함께 내려보내고, 화면은 표 위에 경고 배너를, 해당 행에는자격증명 확인 불가태그(툴팁에 원인)를 보여 준다. 다른 백엔드 목록은 그대로 보인다.- 예전에는 이 오류가 async 라우트 밖으로 새어(Express 4 는 async 거부를 잡지 않는다) 프로세스가 종료됐고, 프록시가 502 를 돌려주며 저장소 목록이 통째로 비었다. 지금은
asyncRoute래퍼가 오류를 JSON 응답으로 바꾸고, 만에 하나 새더라도file_store_unhandled_rejection로그만 남기고 서비스는 계속 돈다.
- 예전에는 이 오류가 async 라우트 밖으로 새어(Express 4 는 async 거부를 잡지 않는다) 프로세스가 종료됐고, 프록시가 502 를 돌려주며 저장소 목록이 통째로 비었다. 지금은
키를 바꾸면 기존 값은 복호화되지 않는다 — 자격증명·refresh token 을 다시 입력해야 한다(값은 어디에도 평문으로 남지 않으므로 되돌릴 수 없다).
배포 점검: 컨테이너에 키가 있는지 확인(
docker exec <container> printenv FILE_STORE_ENVELOPE_MASTER_KEY— 값은 로그·화면에 남기지 않는다), 저장소 화면 상단에 키 경고 배너가 없으면 정상이다.ID 는 입력받지 않는다(서버 채번): 등록 모달에 ID 필드가 없다 — 저장은
POST /admin/backends이고 서버가label·provider로 채번한다. 이름의 로마자 slug(예:Cloudflare R2 primary→cloudflare-r2-primary,포탈콘솔→potalkonsol)를 쓰고, 이름에서 slug 를 못 만들면 provider 기반(store-r2)으로 떨어진다. 이미 쓰는 ID 면-2·-3을 붙인다 — 등록이 upsert 로 기존 backend 를 조용히 덮어쓰는 사고를 막기 위해서다. 수정은 ID 를 바꿀 수 없고 경로(PUT /admin/backends/:id)로만 지정된다.폼 검증(검증·연결 테스트)은 ID 가 없어도 된다: 등록 전 폼 값으로 실행하면 서버가 임시 ID(
pending-backend)로 검사만 수행한다.연결 테스트(모달 왼쪽 버튼): 저장 전에 실측한다. 모달에 입력한 자격증명으로 실행되므로(비어 있으면 이미 저장된 값) 저장하기 전에 "이 키로 붙는가"를 확인할 수 있다. 결과는 backend 별로 기록되어 목록의
연결 테스트열에 남는다(회전 후에는재시험 필요).Cloudflare R2→POST /api/file-store/v1/admin/backends/test-connection— 실제HeadBucket으로 bucket 접근 권한 확인Google Drive·OneDrive→ 같은 API — 등록된 refresh token으로 access token 갱신 확인Amazon S3·S3-compatible·Local server disk·Network drive→POST /api/file-store/v1/admin/backends/validate— 자격증명 namespace 존재/마운트 경로 읽기·쓰기 권한 확인- 폼 검증을 통과해야 실행되며, 실패 사유는 서버가 그대로 내려준다(예:
저장소 secret이 설정되지 않았습니다: R2_PRIMARY_ACCESS_KEY_ID).
소비 포털(consumerId)은 PortalConsole에서 가져온다
소비 포털 ID(consumerId)의 원본은 PortalConsole의 포털 설정(file_store_enabled, file_store_consumer_id)이다. 포털이 업로드 때 보내는 consumerId와 정확히 같아야 하므로, 콘솔은 목록을 받아 검색·선택하게 하고 직접 입력도 허용한다.
- 콘솔은
GET /api/file-store/v1/admin/portal-directory를 호출하고, FileStore 서버가 PortalConsole 내부 endpointGET /api/internal/file-store/v1/portals를PORTAL_CONSOLE_URL·PORTAL_CONSOLE_SYNC_TOKEN(R2 자격증명 전달과 같은 토큰)으로 대신 호출한다. - 목록은 활성 포털 전체를 오고, 각 항목은 PortalConsole이 FileStore를 켰는지(
fileStoreEnabled)를 함께 담는다. 저장 정책을 먼저 만들고 그 정책 ID를 PortalConsole에 등록하는 순서라, 아직 FileStore를 켜지 않은 포털도 선택지에 보인다(배지로 구분).file_store_consumer_id가 비어 있으면portal_id로 대신한다. PORTAL_CONSOLE_URL·PORTAL_CONSOLE_SYNC_TOKEN이 없는 배포는configured:false로 저하해 기존처럼 직접 입력할 수 있다.- 응답에는 access key·secret·token 원문이 포함되지 않는다.
- PortalConsole 자신은 이 목록에 나오지 않는다 — 목록의 원본은 콘솔의
portal_configurations(관리 대상 포털)이고, 콘솔은 Directory 발급자이면서 동시에 FileStore 소비자라 자기 자신을 포털 행으로 두지 않는다. 콘솔이 FileStore 에 자기를 소개하는 값은 콘솔 환경변수PORTAL_CONSOLE_FILESTORE_CONSUMER_ID(예:portal-console)이고, 콘솔의 프로필 사진 본문이 그 어댑터를 지난다(PortalConsole/server/fileStoreClient.ts). 따라서 콘솔을 소비 포털로 등록할 때는 목록에서 찾지 말고 그 값을 ID 필드에 직접 입력한다 —소비 포털표에 같은 ID가 이미 있으면 중복 등록하지 말고 정책(consumerId)만 붙인다. 목록에서 골라서 보고 싶으면 콘솔 쪽에 포털 행(portal_id·file_store_consumer_id)을 만들어야 하지만, FileStore 등록의 필수 조건은 아니다.
2차 고도화: consumerId와 자체 관리자 계정
FileStore 내부 저장 정책의 표준 식별자는 consumerId입니다. 기존 API의 portalId는 호환 입력으로만 취급되며, 서버는 이를 canonical consumer ID로 정규화합니다. 신규 시스템은 sales-registration, hr-document-system처럼 시스템 단위의 consumerId를 사용해야 합니다.
최초 배포 시 FILE_STORE_ADMIN_USERNAME, FILE_STORE_ADMIN_PASSWORD, FILE_STORE_ADMIN_AUTH_PATH를 설정합니다. 서버는 초기 비밀번호를 scrypt 해시로 저장하고 이후 관리자 화면에서 계정을 추가하거나 비활성화할 수 있습니다. 비밀번호 원문은 설정 파일이나 로그에 저장되지 않습니다.
관리자 역할은 OWNER(계정·저장소·자격증명 전달 전체), ADMIN(소비 시스템·저장소·자격증명 전달), OPERATOR(소비 시스템·저장 정책·저장소 운영), AUDITOR(조회 전용)로 구분됩니다. OWNER만 관리자 계정을 추가하거나 역할을 변경할 수 있으며, 비활성화된 계정의 세션은 무효화됩니다.
PortalConsole 선택 연동
/admin은 로컬 관리자 계정과 PortalConsole 계정을 함께 받습니다. 로컬 계정은 비상용으로 유지하고 기본 경로는 PortalConsole 중앙 로그인입니다.
| 경로 | 설명 |
|---|---|
GET /auth/directory/start | PKCE 코드 챌린지를 만들고 PortalConsole /connect/authorize로 이동 |
GET /auth/directory/callback | 인가 코드를 filestore.directory.session 쿠키(8시간)로 교환 |
GET /auth/end-session | PortalConsole 콘솔이 로그아웃할 때 브라우저가 호출 — 앱 세션과 PortalConsole 세션을 함께 정리 |
POST /auth/local/login | 비밀번호 폴백(비상 로그인) |
/admin은 앱 세션이 없고 PortalConsole의 공유 쿠키directory.sso.session이 있을 때 자동으로/auth/directory/start로 보냅니다. 공유 세션이 없으면 기존 로그인 카드를 그대로 보여 줍니다.- 로그인 여부 확인은
GET /api/file-store/v1/admin/session이 담당합니다. 화면 분기용 조회라서 미인증에도 200 +{authenticated:false}로 답합니다(자산·정책 같은 데이터 API는 그대로 401admin_auth_required). 미인증에 401을 쓰면 로그인 화면 첫 로딩마다 브라우저 네트워크/콘솔에 오류가 찍혀 실제 장애처럼 보입니다. - 중앙 로그인 완료가 실패하면
/auth/directory/callback은 오류 텍스트 대신/admin?directoryLogin=failed&directoryLoginReason=…으로 돌려보내고, 로그인 카드가 사유를 보여 줍니다. 사유는directorySessionStatus가 한곳에서 계산합니다 —portal_admin_required(DirectoryService 관리자 아님),portal_mismatch(다른 포털 토큰),signature_invalid(PORTAL_JWT_SECRET 불일치),token_exchange_failed(코드 교환 실패) 등. 같은 사유가/admin진입 로그(file_store_admin_entry)에도 남으므로 운영에서는 컨테이너 로그만으로 원인을 찾을 수 있습니다. - 받아들일 수 없는 토큰(예:
portalAdmin=false)은 쿠키로 저장하지 않습니다. 저장하면/admin이 다시 로그인 화면을 보여 주고, 공유 세션이 있을 때 중앙 로그인 ↔/admin왕복이 끝나지 않습니다. 실패로 돌아온 요청(directoryLogin=failed)은 자동 시작을 건너뜁니다. - 로그인에 필요한 환경값:
PORTAL_CONSOLE_URL(브라우저가 이동할 중앙 로그인),FILE_STORE_PUBLIC_URL(callback 기준 주소 — 프록시 뒤에서는 필수),PORTAL_JWT_SECRET(PortalConsole과 같은 값),FILE_STORE_CLIENT_ID(기본file-store),FILE_STORE_PORTAL_ID(기본file-store). 서명 비밀값이 없으면completeDirectoryLogin이 받은 토큰을 확인할 수 없어 로그인이 항상 실패하므로 서버는 부팅 때file_store_directory_login_misconfigured경고를 남깁니다. - 쿠키 스코프에는 포트가 없으므로, PortalConsole와 같은 호스트의 다른 포트에서 실행되는 개발 스택은 공유 쿠키를 그대로 읽습니다. 호스트가 다르면 자동 로그인이 동작하지 않아 로그인 카드로 돌아갑니다.
- 공유 쿠키(
azp=directory-service-sso)는 앱 세션 토큰으로 사용할 수 없습니다. 앱은azp=file-store이고portalAdmin=true인 토큰만 세션으로 인정합니다. - 앱 쿠키는 이 오리진에서만 지울 수 있으므로 로그아웃 전파는 서버 간 호출이 아니라 브라우저의
GET /auth/end-session호출로 수행합니다.
출처 삭제 (delete-by-source)
파일 삭제는 FileStore 가 결정하지 않는다. 소비 포탈이 게시물/노드를 삭제할 때 POST /api/file-store/v1/assets/delete-by-source 를 서비스 토큰으로 호출하면, FileStore 는 그 출처(consumerId+sourceEntityType+sourceEntityId)에 묶인 파일의 레코드와 저장 객체를 즉시 하드 삭제하고 파일별 DELETED 감사 이벤트를 남긴다. 보존·법적보존(soft delete 후 cleanup)은 두지 않는다 — 관리자 화면(/admin)에도 파일 삭제 버튼이 없고, 관리자 API DELETE /admin/assets/:assetId 와 서비스 API DELETE /assets/:assetId 는 제공하지 않는다. 관리를 위한 설정 삭제(백엔드·정책·소비 포털)만 관리자에게 남는다.
R2 자격증명을 PortalConsole에 중앙 등록해야 하는 경우 관리자 화면의 소비 시스템별 R2 자격증명 전달 영역을 사용합니다. FileStore에는 다음 환경변수를 설정하고, PortalConsole에는 같은 토큰을 FILE_STORE_SYNC_TOKEN으로 설정합니다.
PORTAL_CONSOLE_URL=https://<directory-service-host>
PORTAL_CONSOLE_SYNC_TOKEN=<long-random-internal-token>FileStore는 자격증명을 자체 설정 파일에 저장하지 않고 PortalConsole 내부 endpoint로 전달합니다. PortalConsole 측 수신 endpoint는 다음과 같습니다.
POST /api/internal/file-store/v1/portal-r2-credentials
Authorization: Bearer <FILE_STORE_SYNC_TOKEN>운영 환경에서는 반드시 HTTPS를 사용하고, Access Key ID·Secret Access Key·동기화 토큰을 Git, 브라우저 localStorage, 일반 애플리케이션 로그에 기록하지 않아야 합니다.
데이터 공급 (Data Feed)
관리자 메뉴 데이터 공급 탭은 분석 파이프라인용 스냅샷을 내보낸다.
- 파일 피드(pull):
GET /api/file-store/v1/data-feed/assets— keyset(createdAt|id) 커서, 서비스 토큰. - CDC 이벤트(pull):
GET /api/file-store/v1/data-feed/events— 감사 이벤트의 단조증가seq커서로 증분 적재. - push:
POST /api/file-store/v1/admin/data-feed/publish— 오브젝트 스테이징(R2/S3/로컬)에 JSONL 을 쓰거나 REST 웹훅으로 전송한다. 레코드는recordType(asset/event)으로 구분된다. - 상태:
GET /api/file-store/v1/admin/data-feed/status— 내보낼 수 있는ACTIVE파일 수, 최신seq, 발행 이력, sink 별 진행 커서. - 증분 push:
publish는 sink 별로 저장한 커서(file_store_feed_cursors) 이후의 레코드만 내보낸다.reset:true로 전체 재전송한다.
거버넌스: 파일 피드는 ACTIVE 만 내보내고 consumerId·classification 필터를 적용한다. 삭제·격리 이벤트는 CDC 로만 흐른다. FileStore 는 raw/bronze 공급자이며, Delta/Iceberg 테이블은 데이터 플랫폼(Databricks 등)이 소유한다.
개요 대시보드 (사용량)
관리자 메뉴 개요 탭은 기간 사용량을 일/월/분기 버킷으로 보여 준다.
- 축: 등록(업로드된 파일) · 소비(
READ_LOCATOR_ISSUED다운로드 locator 발급) · 공급(RAG_PUBLISHED·DATA_FEED_PUBLISHED) · 삭제(DELETED). - 차원: 소비 포탈(
consumerId). 포탈별 사용량 표에서 포탈별 등록 건수·용량, 소비·공급·삭제 건수, 마지막 활동일을 본다. - API:
GET /api/file-store/v1/admin/usage?granularity=day|month|quarter&portalId=<id>&from=YYYY-MM-DD&to=YYYY-MM-DD. 기본 기간은 일 30 · 월 12 · 분기 8 버킷이고, 빈 버킷도 0 으로 채워 추이가 끊기지 않게 한다. 버킷 키는 UTC 기준이다. 관리자 화면에서 시작일·종료일을 직접 고르거나 초기화할 수 있고, 버킷이 400개를 넘는 기간은 400 응답으로 거부한다(차트·응답 크기 보호). - 집계 방식: Postgres 는
file_store_assets(등록)와file_store_audits(소비·공급·삭제)를to_char(... AT TIME ZONE 'UTC')로 일 단위 GROUP BY 해서 내려주고, 메모리·파일 저장소는 같은 순수 함수(usageDayRowsOf)를 쓴다. 일/월/분기 접기·합계·breakdown 은 세 백엔드 공통으로src/usageMetrics.ts가 한다. - 그래프:
admin-web/src/trendChart.tsx의 SVG 차트(외부 차트 라이브러리 없음). 건수 모드는 등록·소비·공급 3계열, 용량 모드는 등록·소비 바이트를 그린다.
소비 용량은 READ_LOCATOR_ISSUED 이벤트의 detail.bytes(locator 발급 시점의 객체 크기)를 쓴다. 크기를 남기지 않은 과거 기록은 파일 원장의 현재 크기로 대체한다(assetId 로 조인). CREATED·ACTIVATED 감사는 등록으로 세지 않는다(파일 원장과 중복 집계 방지).