Skip to content

feat(F-CMN-002): 루브릭 열람을 중계한다 — 공개 의무를 화면이 보일 수 있게 (#475 · #609 ②) - #612

Open
hd0rable wants to merge 1 commit into
mainfrom
feat/rubric-read-proxy-475
Open

hd0rable wants to merge 1 commit into
mainfrom
feat/rubric-read-proxy-475

Conversation

@hd0rable

Copy link
Copy Markdown
Member

#609 의 ② 첫 칸입니다 — ❗지금으로 표시된 것이고, 9/6 요청에 5일째 답이 없던 것이 제
쪽입니다.

Refs #475 · #609 · #474 ②

Important

#610 위에 쌓았습니다. @PreAuthorize('rubric:read') 가 정책에 그 action 이 있어야
하고(everyDeclaredActionExistsInPolicy — 제가 #610 리뷰에서 실측했습니다), 그 파일은
소유자가 다릅니다. #610 이 머지되면 base 를 main 으로 돌립니다.

흐름

전 — 공개 의무를 보일 자리가 없다

  화면이 루브릭을 보여주려면?
      ↓
  ai-service /internal/rubrics 는 **내부망 전용** — 브라우저가 못 부른다 (CLAUDE.md)
      ↓
  서버에 중계가 없다                              ✗ 화면을 만들 수 없다 (#494 가 5일 대기)


후 — 읽기 전용 중계

  GET /products/rubrics[?productType]   →  /internal/rubrics          목록 + total
  GET /products/rubrics/{itemId}        →  /internal/rubrics/{item_id}  하나 · 없으면 404
      ↓
  ◇ SELLER                → 403 (정책 + HTTP 둘로)        ✓
  ◇ COMPL · MGR · ADMIN   → 200                          ✓
  ◇ 루브릭 없는 항목        → 404 (502 아님)               ✓

❗아무것도 안 바꿉니다

승인 산출물은 app/rubrics/*.yaml 파일이고 승인은 git 커밋입니다(결정 #475 ⓐ · 서버
stateless). 이 경로는 그 파일을 보여줄 뿐이고 approve 는 별건입니다(#609 의 데모 뒤 칸).

판매 직원에게 열지 않습니다 — 정책 한 줄로 안 끝냅니다

rubric:read 는 COMPL·MGR·ADMIN 뿐입니다(#610). 그런데 정책만으로는 다음 사람이 역할을
더해도 조용합니다.
그래서 seller-01 로 403 을 재는 단정을 넣었습니다 — #610 리뷰에서
약속한 그물입니다.

404 를 404 로 넘깁니다

루브릭이 없는 항목은 정상 상태입니다 — recommended 항목은 루브릭이 없고 채점 대상도
아닙니다(결정 10.1 · #435). 502 로 뭉치면 화면이 「AI 서비스 장애」로 읽고 「기준이 없다」와
「기준이 비어 있다」가 같아집니다.
#556 이 고친 것과 같은 부류라 같은 방식으로 갈랐습니다.

total 을 접지 않습니다

ai-service 가 「필터 전 전체 개수」를 같이 내는 이유가 "걸러진 목록만 보이면 이게 전부로
읽힌다"
이고, 경계에서 버리면 그 뜻이 사라집니다. R-00 이 분모를 지키는 것과 같은 결입니다.

❗역검증 — 둘이 처음엔 초록이었습니다

정책에 SELLER 를 되살린다        403 단정 빨강
@PreAuthorize 를 뗀다            403 단정 빨강
404 를 502 로 뭉친다             ❗처음엔 초록  →  클라이언트 테스트를 넣어 빨강
total 을 목록 길이로 접는다        ❗처음엔 초록  →  길이와 total 을 다르게 둬서 빨강

셋째: 배선 테스트가 aiServiceClient 를 스텁해서 404 번역을 안 지났습니다.
AiServiceClientTest 에 실제 404 응답을 세웠습니다.

넷째: 목록 길이와 total 을 같게(0·0) 둬서 접어도 통과했습니다. 이제 1건 · total 17 로
둡니다 — 필터를 건 실제 모양이기도 합니다.

계약

openapi.yaml 에 두 경로와 스키마 셋(Rubric · RubricsResponse · 봉투 둘). 필드 설명에
세 가지를 적었습니다.

u1Requires        요소 「개수」가 아니라 이 값이 U1 문턱이다 (#367 · #450 이 그 결함)
unlinkedUntil     null 과 빈 배열이 다르다 — 「해당 없음」 vs 「아직 못 걸었다」 (#284 · #396)
status: draft     ❗채점은 지금 이 값을 안 본다 (#609 ① 미결) — 화면이 「확정됨」으로 그리면 안 된다

경로 변수는 계약이 {id} 로 통일합니다(컨트롤러는 {itemId}) — OpenApiPermissionSyncTest
가 이름이 아니라 자리로 맞춥니다.

❗남의 파일 한 줄 — 15-demo-mode.sh

DemoModeAccountMapTest 가 요구합니다. 없으면 개방 모드에서 SELLER 계정이 주입돼 전부
403
이라 화면이 안 뜹니다. #609 ③ 이 이 칸을 @junseo2323 님 몫으로 적어 뒀고 내용도 그
#494 9/7 코멘트대로 ADMIN 입니다. ~^/api/products/ 로 넓히지 않은 이유(그 아래
product:read 는 SELLER 도 있어서 default 여야 한다)도 같이 적었습니다.

남은 칸 (제 몫)

② propose 프록시    POST /products/{id}/rubric/propose   — 읽기 전용. 다음
② approve          데모 뒤. catalogDiff 에 거울 줄까지 (#609 가 짚은 사슬)

#609 는 닫지 않습니다 — 추적 이슈이고 제 칸 셋 중 하나입니다.

검증

./gradlew test 831건 통과(+6) · npm run build 통과.

@gitIt-sehyeon (#610 부모 · 계약) @junseo2323 (③ 화면이 이 위에 섭니다 · 지도 한 줄) @yoonjiseok (/internal/rubrics 소유)

`#609` 가 ❗지금으로 표시한 칸이다. 9/6 에 요청이 왔고 **5일째 답이 없던 것이 내 쪽**이다.

공개 의무(기획서 5절)가 요구하는 것은 「기준이 문서로 존재하고 감사·심사가 볼 수 있다」이고,
그 «볼 수 있다» 를 성립시키는 것이 화면이다. 그런데 화면은 ai-service 를 직접 못 부른다
(내부망 전용) — 그래서 서버가 읽기 전용으로 중계한다.

  GET /products/rubrics[?productType]   → /internal/rubrics
  GET /products/rubrics/{itemId}        → /internal/rubrics/{item_id}

## 아무것도 안 바꾼다

승인 산출물은 `app/rubrics/*.yaml` 파일이고 승인은 git 커밋이다(결정 #475 ⓐ · 서버
stateless). 이 경로는 그 파일을 보여줄 뿐이다. approve 는 별건(#609 의 데모 뒤 칸).

## 판매 직원에게 열지 않는다 — 정책 + HTTP 둘로

`rubric:read` 는 COMPL·MGR·ADMIN 뿐이다(#610). 정책 한 줄만으로는 다음 사람이 역할을
더해도 조용하니 `seller-01` 로 403 을 재는 단정을 넣었다 — #610 리뷰에서 약속한 그물이다.

## 404 를 404 로 넘긴다

루브릭이 없는 항목은 **정상 상태**다(recommended 는 루브릭이 없고 채점 대상도 아니다 —
결정 10.1 · #435). 502 로 뭉치면 화면이 「AI 서비스 장애」로 읽고 「기준이 없다」와
「기준이 비어 있다」가 같아진다 — #556 이 고친 것과 같은 부류다.

## total 을 접지 않는다

ai-service 가 「필터 전 전체 개수」를 같이 내는 이유가 *"걸러진 목록만 보이면 이게 전부로
읽힌다"* 이고, 경계에서 버리면 그 뜻이 사라진다. R-00 이 분모를 지키는 것과 같은 결이다.

## 역검증 — 둘이 처음엔 초록이었다

  정책에 SELLER 를 되살린다        403 단정 빨강
  @PreAuthorize 를 뗀다            403 단정 빨강
  404 를 502 로 뭉친다             ❗처음엔 초록 → 클라이언트 테스트를 넣어서 빨강
  total 을 목록 길이로 접는다        ❗처음엔 초록 → 목록 길이와 total 을 다르게 둬서 빨강

셋째는 배선 테스트가 `aiServiceClient` 를 스텁해서 404 번역을 안 지나던 것이고, 넷째는
목록 길이와 total 을 같게(0·0) 둬서 접어도 통과하던 것이다.

## 남의 파일 한 줄

`web/docker-entrypoint.d/15-demo-mode.sh` 에 지도 한 줄을 더했다 —
`DemoModeAccountMapTest` 가 요구한다(없으면 개방 모드에서 SELLER 계정이 주입돼 전부 403).
`#609` ③ 이 그 칸을 오준서 몫으로 적어 뒀고 내용도 그 코멘트대로 ADMIN 이다.
`~^/api/products/` 로 넓히지 않은 이유도 같이 적었다.

`./gradlew test` 831건 통과 · `npm run build` 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@hd0rable
hd0rable requested a review from yoonjiseok September 11, 2026 02:25
@github-actions github-actions Bot added the 리뷰대기: 오준서 오준서 이 배정됐고 아직 아무것도 제출하지 않았다 label Sep 11, 2026
@hd0rable hd0rable added server Spring (:8000) 계약 모듈 간 계약 — contracts/, openapi.yaml, 스키마 보안 역이용 방지·접근통제·PII·비밀정보 web Vite (:5173) labels Sep 11, 2026
@github-actions github-actions Bot added the 리뷰대기: 정세현 정세현 이 배정됐고 아직 아무것도 제출하지 않았다 label Sep 11, 2026
@github-actions github-actions Bot added the 리뷰대기: 윤지석 윤지석 이 배정됐고 아직 아무것도 제출하지 않았다 label Sep 11, 2026

@gitIt-sehyeon gitIt-sehyeon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

승인합니다. 제 #610 위에 정확히 올라왔고, 약속하신 seller-01 403 그물도 들어 있습니다. 역검증 둘을 제 손으로 재현했습니다.

정책에 SELLER 를 되살린다     ProductAccessWiringTest FAILED  +  AccessPolicyTest FAILED   ❗둘 다
@PreAuthorize 를 뗀다        ProductAccessWiringTest FAILED
server 전체                  BUILD SUCCESSFUL

첫 변이에서 두 층이 같이 빨개지는 것이 이 PR 의 값입니다. #610 만 있을 때는 정책 층 하나였고, 그 사이가 비어 있다고 제가 적었는데 지금 메워졌습니다.

❗한 칸이 비어 있습니다 — 제 몫이라 바로 채웠습니다 (#614)

이 PR 이 머지되는 순간부터 명세가 「감사 대상」이라 정한 경로가 기록 없이 뜹니다.

docs/functional-spec-v1.2.md:462   「COMPL·MGR·ADMIN 읽기 + 감사 대상」
AuditInterceptor.java:63           audited 에 없으면 조용히 넘긴다 (return)
rbac_policy.yaml audited            rubric:read 없음 — #610 에서 일부러 비웠다

그 구간에 알파 배포가 나가면 채점 정답표를 열어 본 사실이 아무 데도 안 남습니다. 로그 0건이 「아무도 안 봤다」로 읽히는 것은 이 레포가 unreachable 을 따로 세는 이유와 같은 부류입니다.

#614 를 이 브랜치 위에 쌓았습니다. 한 PR 로 합칠 수 없어서(그쪽은 api/, 제 쪽은 rbac_policy.yaml) 두 머지 사이의 틈을 커밋 하나로 줄이는 쪽을 택했습니다. 정책 한 줄 + 테스트 하나이고, 프록시가 있으니 everyAuditedActionIsReachable 이 통과합니다 — #610 에서 «넣으면 빨갛다»를 보였고 거기서 «있으니 통과한다»를 보입니다.

Important

스택이 셋이 됐습니다: #610(정책) ← #612(프록시) ← #614(감사).
이 PR 을 머지하실 때 --delete-branch 를 쓰지 말아 주세요 — #614 가 닫힙니다.

제 영역에서 확인한 것

rbac_policy.yaml            안 건드렸다 ✅ (@PreAuthorize 가 action 이름만 참조 — 규칙대로)
canAggregate 사용            맞다. 루브릭은 세션·상품에 안 매이므로 소유자 없는 대상이다
P3 (고객 텍스트 경로)         무관하다 — 나가는 것이 productType 뿐이고 고객 발화가 없다
15-demo-mode.sh 한 줄        #494 9/7 확정본대로 ADMIN. DemoModeAccountMapTest 가 요구한다

~^/api/products/ 로 넓히지 않은 이유를 적어 두신 것이 좋습니다 — 그 아래 product:read 는 SELLER 도 있어서 default 여야 하는데, 넓히면 판매자가 상품 목록을 못 봅니다. 지도 판정 둘(#263)이 그래서 둘인 것이고요.

404 와 total — 「처음엔 초록이었다」를 적으신 것

이 PR 에서 제일 값이 큰 문단입니다. 역검증이 통과한 것이 결함을 못 찾은 것이 아니라 테스트가 그 갈래를 안 지난 것이었다는 기록이라서요.

404 를 502 로 뭉친다      배선 테스트가 aiServiceClient 를 스텁해서 번역을 안 지났다
total 을 길이로 접는다     목록 길이와 total 을 같게(0·0) 둬서 접어도 통과했다

둘째가 특히 그렇습니다 — 단정의 두 값이 우연히 같으면 그 단정은 아무것도 안 잽니다. 1건 · total 17 로 바꾼 것이 필터를 건 실제 모양이기도 해서 두 번 맞습니다.

계약 — 이견 없습니다

status: draft 에 "채점은 지금 이 값을 안 본다(#609 ① 미결) — 화면이 「확정됨」으로 그리면 안 된다" 를 적으신 것이 맞습니다. #609 가 그 칸을 ①의 빈칸으로 열어 뒀는데, 계약이 먼저 나가므로 화면이 그 전에 잘못 그릴 수 있는 자리입니다.

u1Requires(개수가 아니라 문턱) · unlinkedUntil(null 과 빈 배열이 다르다) 둘도 같은 성격이고, 셋 다 읽는 쪽이 틀리기 쉬운 것을 필드 설명에 둔 것이라 자리가 맞습니다.

경로 변수를 계약에서 {id} 로 통일한 것도 OpenApiPermissionSyncTest 가 자리로 맞추므로 문제없습니다.

#609 를 안 닫는 것

맞습니다 — 추적 이슈이고 ② 세 칸 중 하나입니다. #614 가 들어가면 ② 의 「지금」 칸 셋(정책·프록시·감사)이 다 닫히고, 남는 것은 propose 프록시와 데모 뒤 approve 입니다.

@github-actions github-actions Bot removed the 리뷰대기: 정세현 정세현 이 배정됐고 아직 아무것도 제출하지 않았다 label Sep 11, 2026

@yoonjiseok yoonjiseok left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

승인합니다. /internal/rubrics 소유자로서 계약·역직렬화·404 규약을 실물과 대조했고 전부 맞습니다. 아래는 전부 막지 않는 것이고, 셋은 제 몫입니다.

실물과 대조했습니다 — 양쪽이 손으로 쓴 것이라

픽스처와 Java 레코드가 둘 다 사람이 쓴 것이라 같이 틀릴 수 있는 자리입니다. 그래서 ai-service 를 실제로 띄워 응답을 받았습니다.

GET /internal/rubrics
  키 9개 (snake)  item_id · product_type · name · status · required_elements
                  u1_requires · misconception_conditions · related_misconceptions · unlinked_until
  total 17 · 목록 17
GET /internal/rubrics?product_type=ELS      목록 10 · total 17     ← 필터 전 분모, 접히지 않음
GET /internal/rubrics/NOPE                  404 {"detail": "루브릭이 없다: NOPE"}

AiServiceClientTest 픽스처와 키·모양·404 본문이 전부 같습니다. Rubric 레코드 9필드가 제 RubricView 9필드와 1:1 이고, unlinkedUntil 만 null 을 보존하신 것도 맞습니다 — #493 리뷰에서 정세현이 짚어 제가 is not None 으로 둔 그 규약입니다.

변이를 제 쪽에서도 걸었습니다

"둘이 처음엔 초록이었다" 고 적으신 자리 둘을 다시 재봤습니다.

total 을 list.rubrics().size() 로 접는다     1 failed  (supervisorsAndAdminReadRubrics)
404 갈래를 죽인다 (if (false))               1 failed  (aMissingRubricStaysNotFound)
컨트롤러에서만 404 를 502 로 뭉친다            2 failed  ← 클라이언트 층과 배선 층이 각각 문다

셋째는 제가 «이음매가 안 덮였나» 싶어 건 것인데 이미 aMissingRubricIsNotFoundNotAnOutage 가 그 자리를 잡고 있었습니다. 클라이언트(404→NoSuchElementException)와 배선(→HTTP 404 + NOT_FOUND)이 다른 층에서 각각 물립니다. #548·#551 계열에서 계속 비어 있던 «타입은 늘었는데 HTTP 는 안 바뀐다» 가 여기서는 안 생깁니다.

./gradlew test 831 · 실패 0 · skip 0 — 본문 숫자와 같습니다(JDK 17).


① ❗지금 이 경로가 내주는 17종 중 7종이 draft 입니다

계약 설명에 "화면이 draft 를 「확정됨」으로 그리면 안 된다" 를 적어 주신 것이 맞는데, 숫자를 같이 두면 ③(화면)이 이걸 곁가지로 안 읽습니다.

confirmed 10   ELS 전부
draft      7   VAR-EARLY-SURRENDER-RATIO · VAR-FEE-DEDUCTION · VAR-NOT-BANK-SAVINGS
               VAR-PARTIAL-DEPOSIT-INSURANCE · VAR-PERFORMANCE-LINKED
               VAR-PRINCIPAL-LOSS · VAR-SURRENDER-BELOW-PREMIUM

변액을 열면 10종 전부가 draft 입니다. 공개 의무 화면이 "이것이 우리 채점 기준입니다" 로 보여주는 첫 화면에서 변액 쪽은 전부 미확정인 셈인데, 지금 그걸 가르는 것은 이 status 문자열 하나뿐입니다.

❗그리고 채점은 그 값을 안 봅니다 — 그게 #609 ① 이고 제 칸입니다. 여기서 막을 일이 아니라 제가 결론을 내야 하는 자리입니다. @junseo2323 님 화면이 이 위에 서니 draft 를 시각적으로 가르는 것만 먼저 부탁드립니다(제 ⓑ 결론이 뭐가 되든 그 표시는 필요합니다).

② relatedMisconceptions·misconceptionConditions 를 required 로 두는 게 맞습니다

이 PR 이 unlinkedUntil 에서 지킨 구별이 옆 필드에서 풀립니다.

required: [itemId, productType, name, status, requiredElements, u1Requires]
#          ↑ misconceptionConditions · relatedMisconceptions 가 빠져 있다

제 스키마는 그 둘을 항상 냅니다(default_factory=list — 없으면 빈 배열). 계약이 optional 이면 웹은 undefined 도 처리해야 하고, 그러면 "관련 오해가 없다(빈 배열)" 와 "필드가 안 왔다" 가 화면에서 같아집니다. unlinkedUntil 의 존재 이유가 정확히 "빈 것과 없는 것을 가른다"(#284·#396)라, 그 옆 필드가 그 구별을 못 하면 짝이 안 맞습니다.

빈 배열로 항상 오는 것이 실물이니 required 에 넣어 주시면 계약이 실물과 같아집니다. (unlinkedUntil 은 지금처럼 nullable optional 이 맞습니다 — 그건 정말 «안 적음» 이 있는 필드입니다.)

③ unlinkedUntil 은 지금 17종 전부 null 입니다 — 계약은 유지가 맞습니다

#284 (a) 로 VAR-PARTIAL-DEPOSIT-INSURANCE 의 선언이 조건 셋이 다 차서 걷혔고(9/5), 그 뒤로 이 필드를 쓰는 루브릭이 하나도 없습니다. 즉 화면이 non-null 을 만나는 경로가 지금은 없습니다.

지우자는 말이 아닙니다 — 그 상태가 «해당 없음» 이 아니라 «지금은 다 걸렸다» 이고, 새 오해 유형이 늦게 오면 다시 생깁니다. 다만 ③ 이 화면을 만들 때 그 갈래가 실데이터로는 안 나온다는 것을 알고 계셔야 눈으로 확인 못 한 코드가 남는 것을 압니다.

④ 루브릭 하나가 깨지면 이 경로가 통째로 502 입니다 (#475 ④ · 제 몫)

@lru_cache(maxsize=1)
def _all() -> dict[str, Rubric]:      # 파일 하나가 파싱 실패하면 여기서 던진다

#475 ④ 로 제가 낸 것인데 이 PR 이 그 결함이 처음 보이는 자리를 만듭니다. 그때 화면에 뜨는 것은 「AI 서비스 장애」이고, 실제로는 "루브릭 파일 하나에 오타가 있다" 입니다 — 이 PR 이 404 에서 정확히 갈라 낸 그 혼동의 다른 판입니다.

여기서 고칠 일은 아니고 제 쪽 문제입니다(승인이 PR 이어야 하는 근거이기도 합니다). 다만 ②(propose 프록시) 를 내실 때쯤이면 제가 답을 갖고 있는 게 맞겠습니다.

⑤ 이 경로가 보여주는 것은 git HEAD 가 아니라 도는 프로세스의 캐시입니다

_all() 이 @lru_cache(maxsize=1) 이라, 파일이 커밋돼도 프로세스가 새로 뜨기 전에는 안 바뀝니다. 배포가 매번 컨테이너 교체라 실무상 문제가 없다는 것은 #475 ⑤ 로 제가 쟀습니다.

그런데 공개 의무 문면에서는 이게 오히려 정확한 쪽입니다 — 화면이 보여야 하는 것은 "레포에 있는 기준" 이 아니라 "지금 채점에 쓰이고 있는 기준" 이니까요. 승인이 git 커밋(#475 ⓐ)인 구조에서 그 둘이 갈리는 구간(머지됨 · 아직 배포 전)이 실제로 있습니다. 화면 문면을 「현재 적용 중인 채점 기준」으로 잡아 주시면 그 구간에서도 거짓말이 안 됩니다.


좋았던 것

  • total 을 경계에서 안 접은 것. 제가 저쪽에 그 값을 넣은 이유를 그대로 옮겨 적어 주셨고, 무엇보다 목록 길이와 total 을 일부러 다르게 둔 것(1 vs 17)이 핵심입니다 — 같게 두면 접어도 초록입니다. 실측으로 그걸 찾으신 게 이 PR 에서 제일 좋습니다.
  • 404 를 404 로 넘긴 것. 「기준이 없다」와 「기준이 비어 있다」를 가르는 것이 recommended 항목(결정 10.1)에서 실제로 필요합니다. 제가 get_rubric 에 "빈 루브릭을 지어내지 않는다" 로 적어 둔 것을 정확히 같은 뜻으로 받으셨습니다.
  • 정책 + HTTP 두 겹. #610 리뷰에서 약속하신 seller-01 403 단정이 들어왔습니다. 정책만 두면 다음 사람이 역할을 더해도 조용한데, 이제 두 줄이 같이 빨개집니다.
  • u1Requires 를 계약에 박고 응답 단정까지 건 것. #450 이 정확히 그 결함이었습니다 — 요소 목록만 보이면 「전부 말해야 한다」로 읽힙니다.

#610 은 제 변경요청(SELLER)이 74847d3 으로 해소된 것을 확인했고, 그쪽에 재리뷰를 따로 올리겠습니다. 그게 머지되면 base 를 main 으로 돌리시면 됩니다.

@github-actions github-actions Bot removed the 리뷰대기: 윤지석 윤지석 이 배정됐고 아직 아무것도 제출하지 않았다 label Sep 11, 2026
yoonjiseok pushed a commit that referenced this pull request Sep 11, 2026
정세현이 잡았다. ELS 10 · 변액 7 인데 둘을 뒤섞어 적었고, 실제는 **변액 7종 전부**가
draft 다.

    ELS      10종 · draft 0종      데모가 도는 상품이다
    변액       7종 · draft 7종      전부다

## 사실이 더 세다

「일부가 검토 전」과 「전부가 검토 전」은 화면에서 다른 이야기다. 항목마다 갈리면 배지가
맞고, **상품 전체가 그렇다면 상품 단위 문면**이 맞을 수 있다 — `#609` ① 의 ⓑ
(`Judgment.rubric_status`)를 정할 때 이 차이가 걸린다. `#612` 가 머지되면 화면에서 변액을
열었을 때 **예외 없이 전부** draft 다.

## 숫자를 주석에만 적어서 낡았다 — 그 문장을 검사로 만든다

`test_the_split_is_by_product_not_by_item` 이 상품별 구성을 못박는다. `_DRAFT_IN_SCORING`
집합만으로는 이 사실이 안 잡힌다 — **변액 루브릭이 confirmed 로 하나 늘어도 그 집합은
그대로**이고 「전부」라는 주석만 조용히 거짓이 된다.

역검증: `VAR-FEE-DEDUCTION` 을 confirmed 로 바꾸면 4 failed.

ai-service 1138 passed · skip 0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@junseo2323 junseo2323 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

승인합니다. 저쪽 실물과 자 그대로 맞는지 대조했고, 그물 다섯을 변이로 하나씩 끊어 봤습니다 — 다섯 다 빨개집니다. 제 파일(15-demo-mode.sh)을 건드린 것도 강제였음을 확인했습니다(④). 막지 않는 관찰 두 개는 맨 아래 남깁니다.

① 계약 ≡ ai-service 실물 — 자 그대로입니다

routes.py:492  @router.get("/rubrics")            router prefix=/internal      → /internal/rubrics
routes.py:507  @router.get("/rubrics/{item_id}")  RubricNotFound → HTTPException(404)
schemas.py:688 RubricView        9개 필드 · unlinked_until: list[str] | None = None
schemas.py:715 RubricListResponse rubrics · total
routes.py:503  total=len(every)   ❗picked 가 아니라 every — 「필터 전」이 저쪽 실물에서도 참이다

Rubric 레코드 9개 필드·u1_requires·unlinked_until 의 null 보존까지 저쪽과 1:1 입니다. total 이 필터 전이라는 주장은 저쪽 코드에서도 참입니다(len(every)).

② 변이 다섯 — 하나씩 끊어 봤습니다

기준선                                server 전체       BUILD SUCCESSFUL · 831건

① total 을 목록 길이로 접는다           ProductAccessWiringTest        FAILED
② 404 갈래를 죽여 502 로 뭉친다         AiServiceClientTest            FAILED
③ 목록에서 @PreAuthorize 를 뗀다        ProductAccessWiringTest        FAILED
                                      AccessControlWiringTest        FAILED   ← 두 층이 같이 문다
④ nginx 지도 한 줄을 지운다             DemoModeAccountMapTest         FAILED
⑤ openapi 에서 두 경로를 지운다         OpenApiPermissionSyncTest      FAILED

①②는 본문이 "처음엔 초록이었다" 고 적은 자리인데, 지금은 둘 다 뭅니다. ③은 한 층이 아니라 두 층이 물어서, 어노테이션을 떼는 변이가 배선 대조에서도 걸립니다.

③ ④ — 제 파일을 건드린 것은 강제였습니다

15-demo-mode.sh 는 #609 ③ 이 제 칸으로 적어 둔 파일이라 그 한 줄만 따로 봤습니다.

그 줄을 지운다 → DemoModeAccountMapTest "❗개방 모드에서 주입되는 계정이 그 엔드포인트의 action 을 만족한다"  FAILED

없으면 빨간 줄이라 이 PR 안에 있는 것이 맞습니다. 내용도 맞게 봤습니다.

  • 순서 — map 의 정규식은 나온 순서로 재는데, 앞선 ~^/api/products/(documents|[^/]+/extract)$ 가 $ 로 닫혀 있어 /api/products/rubrics 를 안 먹습니다. 새 줄까지 내려옵니다.
  • 계정 — admin 은 rubric:read 의 [COMPL, MGR, ADMIN] 안입니다. compl 도 되지만 #494 9/7 이 ADMIN 으로 정했고 제가 그 코멘트를 쓴 쪽입니다.
  • ~^/api/products/ 로 안 넓힌 것 — 넓히면 product:read(SELLER 있음)가 admin 으로 올라가 감사에 판매자 조회가 admin 으로 남습니다. 바로 위 ops/status 주석이 짚어 둔 함정과 같은 자리고, 그 이유를 줄 옆에 적어 두신 것이 좋습니다.

④ 배포에서 실제로 서는가

403 주장이 목 개발 스위치 뒤에서만 참인 것 아닌가를 봤습니다.

AccessGuard        @Value("${sphinx.security.enforce:false}")   기본값 false
application-prod.yml:83   enforce: true                        ❗배포는 켜져 있다

배포에서 정책이 실제로 돕니다. 나머지 배선도 기존 형태 그대로입니다 — canAggregate 는 바로 위 product:read 와 같은 모양이고, NoSuchElementException → 404 NOT_FOUND 는 GlobalExceptionHandler:41 의 기존 매핑입니다(새 매핑을 만들지 않았습니다).

막지 않는 관찰 둘

ⓐ /internal/rubrics/{id} 의 404 는 두 가지입니다. 같은 파일 parseFailure() 주석이 이미 그 규칙을 적어 뒀습니다 — "다른 내부 경로의 404 는 «라우트가 없다» 이고, 그걸 문서 문제로 읽으면 반대로 오진한다". 여기도 대칭으로 참입니다: ai-service 가 그 라우트를 모르는 판(서버만 새 이미지)이면 FastAPI 기본 404 가 나오고, 화면은 **모든 항목에 「기준이 없다」**를 그립니다 — 이 PR 이 막으려던 혼동의 정확한 반대쪽입니다.

루브릭 없음     {"detail":"루브릭이 없다: ELS-..."}      routes.py:515
라우트 없음     {"detail":"Not Found"}                 FastAPI 기본 — 커스텀 핸들러 없음(main.py 확인)

가르는 값이 본문에 있고 errorBody(resp) 도 이미 읽고 있어서 비용이 크진 않습니다. 다만 parse 가 이미 같은 잔여 위험을 안고 머지된 자리라 이 PR 에서만 요구하는 것은 결이 안 맞습니다 — 막지 않겠습니다. 원하시면 제가 이슈로 떼어 두 자리를 같이 다루겠습니다(그 라우트는 9/6 1df3139 에 들어갔으니 지금 알파는 어긋나 있지 않습니다).

ⓑ productType 이 enum 대조 없이 지납니다. 계약은 [ELS, VARIABLE_INSURANCE] 인데 컨트롤러는 문자열을 그대로 넘기고, 저쪽은 product_type in (None, r.product_type) 이라 오타가 오면 rubrics: [] · total: 17 이 나갑니다. 「그 유형에 기준이 없다」로 읽히는 값이지만 total 이 분모를 들고 있어서 화면이 「0 / 17」로 그릴 수 있고, 중계가 얇은 편이 나은 자리이기도 해서 이것도 막지 않습니다.

스택

#610 은 방금 승인했습니다. 이게 머지되면 base 를 main 으로 돌리고, 그 뒤 #614(감사 한 줄) 순서입니다.

@github-actions github-actions Bot removed the 리뷰대기: 오준서 오준서 이 배정됐고 아직 아무것도 제출하지 않았다 label Sep 14, 2026
gitIt-sehyeon added a commit that referenced this pull request Sep 15, 2026
리뷰에서 지적된 ①②③ 을 고쳤고, 재현하는 과정에서 같은 절의 네 번째 결함을 찾았다.

① 조건을 「리뷰 라벨 없음」 으로 좁혔다. pr-review.yml:297 이 관리하는 것은 리뷰 라벨
   셋뿐이고 분류 라벨은 손대지 않는다. 「라벨 없음」 으로 두면 #612 처럼 분류 라벨만
   달린 PR 이 전원 승인돼도 영원히 안 들어간다.

② 판정의 정본을 라벨에서 「소유자 승인 여부」 커밋 상태로 바꿨다. CLAUDE.md 와 반대로
   적혀 있었고, 라벨은 승인이 마지막 사건인 PR 에서 낡는다 — 그 상태가 머지 직전이다.
   pass·fail·error 세 값과 문면을 그대로 적었다.

③ 「자식 0건」 을 「있으면 먼저 재타겟한다」 로 바꿨다. 0건을 금지 조건으로 두면
   부모는 자식 때문에 막히고 자식은 부모가 안 들어가서 못 간다. 결정 9.1 의 사유
   (닫힌 PR 은 base 를 못 바꾼다 · 재타겟은 머지하는 쪽이 한다)를 되살렸다.

④ scan.sh 가 「소유자 승인 여부」 를 통째로 버리고 있었다. rollup 에 워크플로 잡
   (name·conclusion)과 커밋 상태(context·state)가 섞여 오는데 select(.name != null)
   로 걸러서, 승인 부족인 PR 이 「초록」 으로 보였다. error 는 「판정불가」 로 따로 센다.
@gitIt-sehyeon
gitIt-sehyeon changed the base branch from feat/rbac-rubric-read-609 to main September 15, 2026 01:02
@gitIt-sehyeon

Copy link
Copy Markdown
Contributor

부모 #610 이 들어갔고(29a1d71), 이 PR 의 base 를 main 으로 돌려 놨습니다. 결정 9.1 대로 재타겟을 먼저 하고 브랜치를 지웠으니 이 PR 은 닫히지 않았습니다.

29a1d71  Merge pull request #610 ...      feat/rbac-rubric-read-609 → main
#612     base=feat/rbac-rubric-read-609 → main        (재타겟 완료 · 열려 있음)
         브랜치 feat/rbac-rubric-read-609 삭제

지금 이 PR 은 main 기준으로 전원 승인 상태입니다.

소유자 승인 여부  pass — 전원 승인 확인 (gitIt-sehyeon, junseo2323, yoonjiseok)
리뷰 라벨 0개 (남은 [계약 · 보안 · server · web] 은 분류 라벨입니다)

남의 PR 이라 제가 누르지 않습니다. 이게 들어가면 그 위의 #614(감사 · 승인 완료)를 제가 이어서 넣겠습니다.

덧붙여, 이 스택이 #622 리뷰에서 지적하신 ①③의 실물 사례가 됐습니다. 분류 라벨만 달린 채 승인이 끝난 PR 이 이 PR 이고, 「자식 0건」 조건이 막았을 부모가 #610 입니다. 둘 다 고쳐서 올렸습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

server Spring (:8000) web Vite (:5173) 계약 모듈 간 계약 — contracts/, openapi.yaml, 스키마 보안 역이용 방지·접근통제·PII·비밀정보

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants