BizCrush 어드민 가이드

내부 문서 · 2026-08-18 실측 기준

BizCrush 어드민 가이드

검수·발행·차단·정정·계정 발급을 실제로 하는 방법과, 제휴사가 아직 로그인할 수 없는 이유. 이 문서 하나로 착수할 수 있게 썼습니다.

대상 어드민을 처음 여는 팀원 · 검수 담당 정본 레포 docs/ — 이 문서는 읽기 편한 뷰입니다 서비스 apps/admin-web (Cloud Run SSR)

지금 상태

한 줄로: 화면은 다 만들어졌고, 제휴사는 아직 못 씁니다. 막고 있는 것은 미구현이 아니라 인프라 스위치 하나입니다.

12

화면

파트너 5장 + 내부 7장. 검수 전이 액션 6종, 큐 탭 7종까지 실동작합니다.

403

제휴사 로그인 차단

라이브 어드민은 IAM 미개방입니다. 익명 접속은 403, 준비 상태 점검은 503(필요 설정 3종 미주입)입니다.

1

남은 차단 단서

IAM 호출 권한 1건. 결정은 8월 18일에 전부 끝났고, 그 앞에 남은 것은 게이트 구현 3건입니다.

가장 흔한 오해

개시일(8월 18일)의 스위치와 어드민 개방은 다른 스위치입니다. 날짜가 붙은 것은 수집 축 2개(스케줄러 재개 + 소스 활성화)뿐이고, 어드민 개방에는 날짜가 없습니다. 섞어서 이야기하면 없는 개발 일정이 잡힙니다.

개시 후 검수·승인·발행은 공개 URL 없이 됩니다 — 로컬 어드민 화면이 라이브 api를 가리키면 됩니다. 공개 URL이 실제로 필요해지는 시점은 제휴사가 직접 로그인하는 순간(삭제·정정 접수 창구)입니다.

정본 ADR-0018 · docs/spec/deploy.md §1

화면 12장

파트너가 보는 화면과 내부 검수 화면은 세션 종류가 다릅니다. 파트너는 계정 로그인, 내부는 공용 패스프레이즈입니다.

경로하는 일
파트너/login파트너 로그인. 비밀번호 검증은 api가 합니다. 세션은 12시간이고 연장되지 않습니다.
/my/articles자사 기사 목록. 다른 회사 기사는 경로 자체가 없습니다 — 범위는 api가 강제합니다.
/my/articles/{id}기사 상세. 출처 표기·AI 고지 미리보기와 함께 삭제 요청·정정 요청 버튼.
/my/requests삭제·정정 요청 이력과 진행 상태. 상태는 요청 행이 아니라 차단 행에서 유도합니다 — 제휴사가 묻는 것은 "지금 내려가 있나"입니다.
/my/password비밀번호 변경. 임시 비밀번호 계정은 변경 전까지 모든 파트너 화면이 여기로 튑니다.
내부/internal/login공용 패스프레이즈 로그인.
/review검수 큐. 탭 7종으로 상태별 목록. 계약 협의 기한 배너와 매체 태그가 붙습니다.
/review/{id}검수 상세. 본문·AI 요약·출처 표기를 한 화면에서 대조하고 액션 6종을 누릅니다.
/review/corrections정정 요청 목록.
/review/corrections/{id}정정 요청 상세 — 행을 누르면 우리 재구성 기사로 옵니다.
/review/suppressions차단 목록·해제·수기 차단, 그리고 해제한 기사를 다시 검수에 넣는 동선.
/review/accounts파트너 계정 발급·비밀번호 재발급·정지/해제.
설계 제약 하나

이 서비스는 DB에 직접 붙지 않습니다. 모든 조회·전이가 api를 거칩니다. 화면이 DB에 직접 붙으면 "제휴사는 자기 기사만" 경계가 화면 코드의 성실성에만 의존하게 되기 때문입니다. 의존성과 권한 양쪽으로 막아 두었습니다.

정본 apps/admin-web/engine_admin_web/routes/

상태 머신

이 문서에서 가장 중요한 그림입니다. 화면에서 버튼이 거부되는 거의 모든 경우가 이 그림으로 설명됩니다.

draft gate_failed in_review approved published unpublished rejected 종단 — 되돌릴 수 없음 재검수 — 재발행도 검수를 거친다 재생성 강등 직행 금지
기사 상태 전이. 굵은 테두리가 검수·발행의 두 축이고, 파란 선이 재발행 경로입니다.

여기서 외울 것 세 가지

  1. 승인 없이 발행할 수 없습니다. in_review에서 발행 버튼을 눌러도 거부됩니다.
  2. 발행은 종단이 아닙니다. 계약이 "삭제·정정 요청 접수 즉시 반영"을 요구하므로 내리는 경로가 반드시 있어야 합니다.
  3. 내린 기사를 바로 다시 올릴 수 없습니다. 내린 이유가 사실 오류일 수 있어서, 재발행도 검수를 다시 거칩니다.
고장이 아닙니다

화면에서 거부(409)와 붉은 사유 배너가 뜨는 것은 정상 동작입니다. 위 규칙이 눈에 보이는 형태입니다. 화면은 사유를 가공하지 않고 그대로 보여 주며, 강제하는 주체는 화면이 아니라 전이 모듈과 api입니다. 규칙을 우회할 방법은 없고, 우회가 필요하다고 느껴지면 그 자체가 논의거리입니다.

기사 말고 하나 더 있습니다 — 정정 요청

정정 요청도 자체 상태 머신을 갖습니다. 기사 전이도만 보고 있으면 여기서 걸립니다.

received ──▶ reprocessing ──▶ in_review ──▶ resolved └──▶ rejected

순차 진행이고 건너뛸 수 없습니다. 로컬 실증에서 reprocessing에서 곧바로 「반영 종결」을 누르면 거부됐습니다 — 반드시 「재검수 진입」을 거쳐야 합니다. api가 요청 상태를 기사 상태보다 먼저 검사하기 때문입니다.

그리고 한 기사에 열린 요청은 하나뿐입니다. 접수·재처리·재검수 중 하나라도 진행 중이면 두 번째 접수가 DB에서 거부되고, 화면은 「처리 중인 정정 요청이 이미 있습니다 — 현재 상태: …」로 이유를 말해 줍니다. 종결·기각 후에는 다시 접수할 수 있습니다.

큐 탭 7종in_review · approved · published · unpublished · draft · gate_failed · rejected. 뒤의 두 탭은 나중에 추가됐는데, 없던 동안 반려·탈락 기사는 어느 화면에도 보이지 않았습니다. 반려 사유를 다시 볼 수 있는 곳은 이 탭뿐입니다.

정본 packages/transitions/magazine_transitions/transitions.py · docs/spec/db-schema.md

액션 6종

검수 상세에서 누를 수 있는 전부입니다. 목록에 없는 동작은 경로 자체가 없습니다.

액션무엇을 하나결과
approve검수 통과 처리. 발행의 전제입니다.in_review → approved
reject반려. 사유를 남깁니다.in_review → rejected
publish발행. 주소(slug)가 이때 확정되고 이후 바뀌지 않습니다.approved → published
unpublish게재 중단. 삭제 요청 처리와 정정 접수가 이 경로를 씁니다.published → unpublished
requeue다시 검수에 넣기. 정정 요청이 없는 재발행용입니다(실수로 내렸거나 회수를 철회할 때).unpublished → in_review
regenerate검수자 재생성 — 같은 사실로 문장만 다시 씁니다. 다음 사이클이 주워서 다시 만듭니다.→ draft
섞으면 이력이 끊깁니다

정정 요청이 있는 건은 requeue를 쓰지 않습니다. 정정 화면의 "재검수 진입"을 쓰세요 — 그쪽은 요청 상태와 기사 상태를 함께 옮깁니다. requeue로 기사만 옮기면 정정 요청은 진행 중인 채로 남습니다.

정본 apps/admin-web/engine_admin_web/routes/review.py

화면 열기

라이브 어드민이 아직 미개방이므로 경로가 두 가지입니다. 먼저 A로 연습하고, 실제 업무는 B로 합니다.

A. 연습용 — 전부 로컬 안전

로컬 화면이 로컬 api를 보고, 로컬 DB를 씁니다. 무엇을 눌러도 실제 기사에 영향이 없으니 여기서 마음껏 익히세요.

  1. 로컬 Postgres와 스키마 준비

    로컬 개발 가이드의 컨테이너 기동과 마이그레이션 적용을 먼저 끝냅니다.

  2. 시드 넣기

    파트너 계정 1개와, 검수 큐에 뜰 기사 1건을 만듭니다. 큐 데이터는 한 줄 삽입으로 안 되고 매거진 5단계 체인이 필요합니다 — 가이드에 SQL이 그대로 있습니다.

  3. api와 화면을 각각 띄우기

    터미널 두 개를 씁니다. api는 8100, 화면은 8200입니다. 값은 로컬 전용을 명령줄로 줍니다.

    # 터미널 1 — api
    uvicorn engine_api.main:app --port 8100
    
    # 터미널 2 — 어드민 화면
    ENGINE_API_BASE_URL=http://localhost:8100 \
    uvicorn engine_admin_web.main:app --port 8200
  4. 브라우저로 왕복

    http://localhost:8200 — 파트너로 로그인해 정정을 접수하고, 내부 로그인으로 넘어가 승인·발행·반려를 눌러 봅니다. 미승인 기사에서 발행을 눌러 거부 배너를 일부러 한 번 보는 것을 권합니다.

지켜야 할 것

연습 단계에서 라이브 DB 접속 정보를 쓰지 않습니다. 시드와 기동에 쓰는 값은 전부 로컬입니다.

B. 실진행 — 로컬 화면 + 라이브 api 실 데이터

개방 전까지 실제 검수·승인·발행을 하는 유일한 경로입니다. 화면만 로컬에서 띄우고 api는 라이브를 가리킵니다. 라이브 api는 이미 열려 있어서 별도 인프라 권한이 필요 없습니다.

누르기 전에 세 가지

실제 기사 상태가 바뀝니다. 발행하면 제휴사와 소비자가 보는 표면에 반영됩니다. 연습이 필요하면 A로 돌아가세요.

어드민 키는 흔적을 남기지 않습니다. 값은 Secret Manager에서 그때 꺼내 쓰고, 문서·채팅·커밋에 붙여넣지 않습니다. 화면을 띄운 셸 히스토리도 신경 쓰세요.

발행 직후 저장 스냅샷이 바로 생기지 않습니다. 스냅샷은 워커가 다음 사이클에 채웁니다 — 발행 직후 스냅샷이 없는 것은 정상이며, 계속 비어 있으면 그때가 이상 신호입니다.

정본 docs/guide/dev-local.md §2-3 (시드·기동 명령 전문)

업무 절차

실제로 하게 되는 네 가지입니다. 클릭 순서로 적었습니다.

1. 기사 검수하고 발행하기

  1. 큐에서 고르기

    /reviewin_review 탭. 행 머리에 매체 라벨이 붙습니다.

  2. 상세에서 대조하기

    본문과 AI 요약을 나란히 봅니다. 요약은 원문 요약이 아니라 본문 요약이므로, 요약의 모든 사실이 본문에 있는지가 확인 지점입니다.

  3. 출처 표기 확인

    재구성 기사인데 출처 표기가 비어 있으면 붉은 경고 배너가 뜹니다. 발행을 막지는 않습니다 — 판단은 검수자 몫이라는 뜻이니, 배너가 뜨면 원인을 확인하고 넘어갈지 결정하세요.

  4. 승인 → 발행

    두 번 누릅니다. 한 번에 되지 않는 것이 설계입니다.

2. 삭제 요청 처리하기

제휴사가 자기 기사 상세에서 삭제를 요청하면 접수와 차단이 함께 일어나고, 발행본은 즉시 내려갑니다. 내부에서 할 일은 사후 관리입니다.

  • /review/suppressions에서 무엇이 내려가 있는지 봅니다.
  • 잘못 내려간 건은 해제합니다. 해제만 하면 발행본이 살아나지 않으므로, 같은 화면의 되살리기로 검수에 다시 넣습니다.
  • 내부 판단으로 먼저 내려야 할 때는 같은 화면에서 수기 차단이 됩니다.

3. 정정 요청 처리하기

/review/corrections에서 목록을 보고, 행을 누르면 우리 재구성 기사로 옵니다. 진행은 네 가지입니다 — 재생성 시작 · 재검수 진입 · 반영 종결 · 기각.

순서를 건너뛸 수 없습니다

「재생성 시작」 다음에 곧바로 「반영 종결」을 누르면 거부됩니다. 반드시 「재검수 진입」을 거쳐야 합니다 — 위 정정 요청 상태 머신 그대로입니다. 거부는 고장이 아니고, 화면이 사유를 그대로 보여 줍니다.

아직 없는 것

원문 재수집이 자동으로 돌지 않습니다. 원문 자체가 바뀌어서 다시 받아야 하는 건은 지금 수동 작업이 필요합니다(백필 경로 미구현 — 별 티켓). 문장만 다시 쓰면 되는 건은 재생성으로 처리됩니다.

4. 파트너 계정 발급하기

  1. 담당자 지정 받기

    계약 채널로 이름과 업무 이메일 하나를 받습니다. 시범 기간은 1인 원칙입니다.

  2. 화면에서 발급

    /review/accounts에서 만듭니다. 임시 비밀번호는 서버가 만들고 화면에 딱 한 번 보입니다 — 새로 고치면 다시 볼 수 없으니 그 자리에서 옮겨 두세요.

  3. 분리 전달

    주소와 아이디는 메일로, 임시 비밀번호는 다른 채널로 보냅니다. 한 채널이 유출돼도 로그인이 안 되게 하는 최소 조치입니다.

  4. 정지·재발급

    같은 화면에서 정지/해제와 비밀번호 재발급이 됩니다. 정지된 계정의 로그인 응답은 없는 계정과 똑같습니다 — 계정 존재 여부를 알려 주지 않기 위해서입니다.

낡은 문서 주의

레포의 파트너 계정 발급 가이드는 "정지 수단이 없다 · 계정 화면이 없다 · 강제 변경이 없다"고 말합니다. 셋 다 지금은 있습니다(8월 14일 구현). 그 문서에서 아직 유효한 것은 운영 정책(발급 대상·분리 전달·만료 시 로그인 차단)이고, 화면 절차는 이 문서가 현행입니다.

정본 정책 docs/guide/partner-account-issuance.md §1·§3·§4 / 화면 절차 이 문서

개방까지

제휴사가 직접 로그인하게 되기까지 남은 순서입니다. 순서가 강제돼 있어서 건너뛸 수 없습니다.

01 시크릿 3종 생성 + 값 주입어드민 전용 api 키 · 세션 서명 시크릿 · 내부 패스프레이즈. 값 주입 시 개행 혼입까지 점검하고, 전용 키가 마스터와 다른 값인지 대조했습니다. 재민 · 완료
02 인프라 적용 + 리비전 재생성설정 4종이 컨테이너에 들어갔습니다. 적용이 한 번 실패했는데 원인은 서비스 계정에 시크릿 읽기 권한이 없던 것이었고 — 계획 단계가 못 잡는 종류입니다 — 시크릿 단위로만 열어 해소했습니다. 재민 · 완료
03 개방 게이트 7건결정 5건은 8월 18일 전부 확정됐습니다(ADR-0018 rev2). 남은 것은 구현 3건 · 스위치 1건 · 실증 1건 — 다음 절에 항목과 담당이 있습니다. 재민 · 어진
04 개방 커밋 — 마지막 스위치호출 권한 공개 + 상시 인스턴스 1로. 이 한 줄을 되돌리면 즉시 다시 닫힙니다. 재민
지금 눌러 보면 (2026-08-18 실측)

준비 상태 점검은 200 ready입니다 — 설정 4종이 다 들어갔다는 뜻입니다. 익명 접속만 403이고, 이제 그것이 유일한 기술적 차단입니다.

화면이 실제로 api를 부르는 것도 라이브에서 확인됐습니다 — 내부 로그인 후 검수 큐가 렌더되고, 없는 계정으로 로그인하면 401 문면이 정상적으로 뜹니다. 전용 키를 api가 수용한다는 증거입니다.

정본 ADR-0018 Decision 6 · rev1(게이트 재정의) · rev2(결정 확정)

남은 구현 · 분배

개방 게이트 7건 중 결정 몫 5건이 8월 18일 전부 확정됐습니다(ADR-0018 rev2 · PR #62). G2·R1은 결정만으로 닫혔고, 남은 것은 구현 3건(어진) · 스위치 1건(재민) · 실증 1건(어진)입니다. 담당은 재민·어진 2인 — 정훈이 갖던 인프라 축은 재민이 받습니다.

판정 기준이 바뀌었습니다

초판은 「노출면이 생기는가」로 게이트를 갈랐습니다. 그런데 개방이 만드는 변화는 노출면만이 아닙니다 — 계약 상대방이 자기 자격증명으로 우리 시스템에 들어옵니다. 그 순간부터 자격증명 수명 관리(만료·회수)가 운영 문제가 되고, 수단이 없으면 계약 이행이 사람의 기억에 의존합니다.

그래서 「개방과 독립」이던 세션 무효화삭제 축 스코프, 그리고 초판이 언급하지 않았던 계약 만료 차단을 개방 전 착지로 올렸습니다. 🔴 이 승격은 개방을 늦춥니다 — 급하면 「알고 여는 것」으로 명시하고 여는 선택도 성립하지만, 그때는 무엇을 알고 열었는지 기록에 남깁니다.

그리고 8월 18일, 결정이 끝났습니다 (rev2)

다섯 건의 결정 중 하나는 위 승격을 도로 내렸습니다 — 🔴 R1은 「막지 않는다」로 확정입니다. 만료 후에도 발행분 보관은 계약이 허용하고, 보관되는 동안 제휴사가 자기 기사를 지우거나 고쳐 달라고 할 창구는 계약상 접수 의무입니다. 그 창구를 코드가 끊는 쪽이 더 위험하다는 판정이고, 차단은 협의 결렬이 확정될 때 계정 화면의 수동 정지가 수행합니다. 「알고 여는 것」의 기록은 ADR-0018 rev2에 남겼습니다.

게이트무엇을 해야 하나재민어진
G1
구현 남음
로그인 시도 제한 — 결정 완료: 앱 층 로그인 카운터, 저장은 DB(인스턴스가 여러 개라 메모리 카운터는 상한이 곱해집니다). 계정 10회/15분(파트너) · IP 30회/15분(파트너) · IP 10회/15분(내부), 초과 시 15분 자동 해제 잠금 + 429.
느슨해 보여도 bcrypt 지연·인스턴스 상한과 겹으로 작동합니다. 수치 근거는 OWASP·NIST 기준선입니다.
결정 완료 · 리뷰구현·테스트
G2
닫힘
내부 계정 체계공용 패스프레이즈 유지로 확정, 구현 없음. 「누가 발급·정지했나」의 공백은 알고 유지합니다 — 내부 인원 증가·제휴 2사째·이력이 실제로 문제 되는 첫 사건이 재론 지점입니다. 결정 완료
R1
닫힘
계약 만료 → 파트너 축 차단막지 않기로 확정. 만료(2027-02-17 23:59:59 KST) 후에도 로그인·열람·삭제·정정 창구는 유지합니다. 차단은 협의 결렬 확정 시 계정 화면의 수동 정지가 수행합니다. 결정 완료(api 배선 취소)
R2
구현 남음
세션 무효화 수단 — 방식 확정: 페이로드 대조. 세션에 계정 상태 값을 싣고 api가 대조합니다 — 「정지해도 이미 로그인한 브라우저가 12시간 유효」를 이걸로 닫습니다. 리뷰구현
불변 조건을 테스트로 못박는 것이 핵심
R3
구현 남음
삭제 축 스코프 대칭화 — 결정 완료: api 차단 경로에 스코프 인자를 두고 서버가 소유권을 강제합니다. 합격선은 화면을 우회한 직접 호출로 남의 기사를 차단할 수 없는 것. 결정 완료 · 리뷰구현 + 양 표면 e2e
G3 개방 커밋 — 마지막 스위치 — 호출 권한 공개 + 상시 인스턴스 1로. 되돌림은 리소스 한 개 제거입니다. 수행
G4 라이브 발행 왕복 실증 1회 — 승인 → 발행 → 저장 스냅샷 → 발행 API.
⚠️ 로그인·큐 렌더는 이미 라이브에서 확인됐습니다. 이 실증은 개방과 무관하게 지금 할 수 있습니다(로컬 화면 → 라이브 api).
수행 + 기록

재민 결정 · 계약 · 인프라 · 리뷰

깊고 짧은 작업 위주. 협약·라이선스 인접은 전부 이 축입니다.

  • 결정 5건(G1·G2·R1·R2·R3) — 8월 18일 전부 확정했습니다 (ADR-0018 rev2)
  • G3 개방 커밋 — 인프라 + 호출 권한. 낡은 주석 정정을 함께 태웁니다
  • 파트너 계정 발급 안내 문면 — 계약 인접
  • 전 PR 리뷰 게이트 — R2 불변 조건 테스트 리뷰 포함

어진 구현 · 검증

모든 작업에 리뷰어가 붙습니다. 게이트 밖 항목은 대기 시간에 채웁니다.

  • G1 구현 — 확정된 임계 그대로, 파트너·내부 로그인 양쪽 + 테스트
  • R2 세션 무효화 — 페이로드 대조 구현과 불변 조건 테스트
  • R3 구현 — api 스코프 인자 + 양 표면 e2e
  • G4 발행 왕복 실증 — 개방을 기다리지 않습니다. 지금 착수 가능
  • 빠진 것 둘: R1 api 배선은 취소(막지 않기로 결정), G2 구현분은 불발생(공용 유지)
  • 게이트 밖: 정정 상태 머신 문서화 · 차단 화면 되살리기 버튼 규명 · 로컬 셋업 문서 보강

임계 경로

어진 G1 · R2 · R3 구현 (서로 독립 — 병행) ─→ 재민 리뷰 ─→ 재민 G3 개방 ─→ 제휴사 로그인 어진 G4 발행 왕복 실증 ──────────────── 개방과 무관 — 지금 착수 가능

결정 대기는 0입니다. 일정을 정하는 것은 이제 어진의 구현 속도입니다 — G2가 공용 유지로 확정되면서 개인 계정 구현이 빠졌고, R1이 「막지 않음」으로 닫히면서 api 배선 하나가 더 빠졌습니다.

정본 ADR-0018 rev1 D7·8·9(게이트·분배) · rev2 D10~14(결정 확정 · PR #62) · TODO.md T5

알아야 할 한계

네 가지입니다. 8월 18일 결정으로 성격이 갈렸습니다 — 둘(세션·스코프)은 개방 전에 구현으로 닫고, 둘(만료·내부 주체)은 알고 유지하기로 확정했습니다. 유지 쪽도 방치가 아니라 수동 수단과 재론 지점이 붙어 있습니다.

계약 만료가 파트너 접속을 막지 않습니다

수집 축만 코드가 막고, 파트너 축은 막지 않기로 확정했습니다(8월 18일) — 보관이 허용되는 동안 제휴사의 삭제·정정 창구를 끊는 쪽이 계약상 더 위험하다는 판정입니다. 만료(2027-02-17) 대응은 계정 화면의 수동 정지 + 검수 화면의 협의 기한 배너입니다.

rev2 D12 · 결정으로 닫힘 — 구멍이 아니라 의도된 상태

개별 세션을 무효화할 수 없습니다

세션이 서버에 저장되지 않는 서명 쿠키라 취소 지점이 없습니다. 계정을 정지시켜도 이미 로그인한 브라우저는 최대 12시간 유효합니다. 구현 방식은 확정됐습니다 — 세션에 계정 상태 값을 싣고 api가 대조합니다(페이로드 대조).

게이트 R2 · 방식 확정 · 구현 어진 / 리뷰 재민

삭제 축의 범위 검사가 더 얇습니다

정정 요청은 api가 다시 한 번 "자기 기사인가"를 검사하는데, 차단 경로는 화면에서만 검사합니다. api에 스코프 인자를 두고 서버가 소유권을 강제하는 것으로 확정됐습니다 — 구현이 남았습니다.

게이트 R3 · 결정 완료 · 구현 어진

내부 로그인에 주체가 없습니다

공용 패스프레이즈라 "누가 발급했나 / 누가 정지시켰나"를 남길 곳이 없습니다. 8월 18일에 공용 유지로 확정하면서 이 공백은 알고 유지가 됐습니다 — 내부 인원 증가, 제휴 2사째, 이력이 실제로 문제 되는 첫 사건이 재론 지점입니다.

rev2 D11 · 알고 유지 — 게이트 G2 닫힘