Skip to content

feat(F-OPS-001): 운영 콘솔 /console — 「떠 있는데 못 하는 상태」를 화면이 가른다 (#522) - #594

Merged
junseo2323 merged 2 commits into
mainfrom
feat/F-OPS-001-console
Sep 10, 2026
Merged

junseo2323 merged 2 commits into
mainfrom
feat/F-OPS-001-console

Conversation

@junseo2323

@junseo2323 junseo2323 commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

#522 의 web 몫입니다. 서버는 #531 로 9/7 에 끝나 있었고 남은 게 화면뿐이었습니다.

Important

범위를 콘솔 화면 하나로 좁혔습니다(63d91f7). 처음 올린 판에 있던 /guide 진입점 한 줄과 tokens.css 의 판정 3색 예외 문단을 뺐습니다. @hd0rable @gitIt-sehyeon 두 분 승인 뒤의 변경이고, 두 리뷰 다 그 둘을 좋게 보신 자리라 승인이 이 범위에도 서는지는 다시 봐 주세요. 귀결은 아래 「빠진 둘」에 적었습니다.

이 PR 이 만드는 것은 web/src/ 안의 넷입니다.

web/src/pages/Console.tsx    화면
web/src/pages/Console.css    스타일
web/src/api/types.ts         OpsStatus · OpsDeployment · OpsComponent · OpsFact · OpsHealth
web/src/App.tsx              /console 라우트 한 줄

무엇을 그리나

상단   프로파일 · 스택 · 기동 · 가동        + 실측 시각 · [잠시 멈춤]
카드   server → database → ai-service → data-volumes    ← 서버가 보낸 순서 그대로
       상태 칩(색 + 글자) · 왕복 시간 · note · facts · 왕복 추이

DEGRADED 가 이 화면의 요점입니다. UP/DOWN 둘로만 그리면 「떠 있는데 못 하는 상태」가 전부 정상으로 보입니다 — 키 없이 뜬 ai-service, 마운트가 빠진 채 뜬 server. 그 셋이 겉으로 같은 502 하나였다는 것이 이 이슈의 출발점입니다.

❗못 잰 값을 0 으로 접지 않습니다

계약이 latencyMs 를 nullable 로 준 이유가 「즉시 응답」과 「안 쟀다」를 가르는 것인데, 화면이 다시 뭉치면 그 뜻이 사라집니다. 실측에서 그 두 값이 실제로 같이 나옵니다.

database     latencyMs: 0      → "왕복 0ms"
API 서버      latencyMs: null   → "왕복 — (안 쟀어요)"

추이 막대도 같습니다 — 못 잰 자리는 높이 0 이 아니라 바닥 점입니다. 0 으로 두면 「빠르다」로 읽힙니다.

❗폴링 자체가 실패한 회차도 한 칸 남깁니다(@hd0rable 리뷰). 앞 판은 성공한 회차만 이어 붙여서 끊긴 구간이 이어진 것처럼 보였습니다 — 카드가 없는 동안이 아니라 복구된 뒤에 거짓이 됩니다. 스텁으로 4·5 회차만 연결 실패시켜 재봤습니다.

막대   ▮▮▮··▮▮        ← · 가 못 잰 자리. 고치기 전에는 ▮▮▮▮▮ 다섯이었다
aria   왕복 시간 추이 — 최근 7회, 최대 17ms

❗요청이 실패하면 직전 카드를 지웁니다

「지금」을 답하는 화면에서 남은 카드는 전부 낡은 값이고, 그중 초록이 하나라도 있으면 화면이 서버가 하지 않은 말을 합니다. /ops/status 가 안 오는 것 자체가 답이기도 합니다(API 서버가 죽었다).

판정 3색을 씁니다 (#522 질문 1 — 팔레트는 제 영역입니다)

새 상태색 체계를 만드는 쪽이 더 나쁘다고 봤습니다: 화면마다 다른 상태색이 생기면 읽는 사람이 색 체계를 두 벌 배워야 하고, 그러면 규칙 1 이 지키려던 「이 색을 보면 판정이다」가 오히려 옅어집니다. 세 분 다 찬성하신 방향입니다.

조건 넷을 다 만족할 때만입니다 — 제품 흐름 밖 · 라벨 병기(규칙 3) · 고객 비노출 · 값이 판정과 같은 종류(정상·주의·불가). S-08 이 집계에 3색을 안 쓰는 것과 모순이 아닙니다: 그쪽은 수치라 3색을 얹으면 「지점 성과 지표」로 읽힙니다.

화면 번호는 안 줍니다(질문 3 — @gitIt-sehyeon @yoonjiseok 합의 그대로). 파일도 Guide.tsx 와 같은 모양으로 Console.tsx 이고 SCREENS 에 안 넣습니다.

❗빠진 둘 — 이 PR 이 남기는 상태

① /console 이 어디서도 안 걸립니다. 닿는 길이 주소 직접 입력뿐입니다 — /upload 가 지금 그 상태입니다(#406). 제품 흐름에서 링크하면 판매자에게는 전부 403 이라(ops:status:read 는 ADMIN 뿐) 그쪽도 답은 아닙니다. App.tsx 주석에 그 사실을 적어 뒀고, 진입점은 이 PR 밖에서 정합니다.

② tokens.css 규칙 1 은 예외를 모르는 채로 남습니다. 그래서 3색을 쓰는 근거와 조건 넷이 Console.tsx 머리말 한 곳에만 있습니다. 두 분 다 "안 적으면 다음 사람이 「3색은 아무 데나」의 근거로 쓴다" 를 이 결정의 값으로 꼽으셨는데, 그 문단이 팔레트 파일이 아니라 화면 파일에 있는 상태입니다. Console.css 머리말도 그 자리를 가리키게 고쳤습니다. 옮겨 적을지는 따로 정합니다.

이미 배선돼 있던 것 — 다시 안 만졌습니다

#531 이 남의 파일 셋을 같이 실어 준 덕에 이 PR 은 web/src/ 안에서 끝납니다. 확인만 했습니다.

15-demo-mode.sh:112    ~^/api/ops/status$ → $admin      ← 있습니다. 알파에서 403 안 납니다
rbac_policy.yaml:219   ops:status:read                  ← 있습니다
docker-compose.yml:162 SPHINX_STACK                     ← 있습니다

실측

npm run build (tsc)          통과
server ./gradlew test        812건 · 실패 0

❗**WebTypesMirrorContractTest 가 새 타입을 정말 짝지었는지 변이로 확인했습니다** — 초록은 「짝을 못 찾아 0개를 대조했다」와 구분이 안 되는 자리입니다.

types.ts 의 checkedAt → checkedAtX      FAILED
  OpsStatus="계약에만 [checkedAt] · 화면에만 [checkedAtX]"      ◀━ 이름이 실제로 걸렸다
원복 후                                  BUILD SUCCESSFUL

화면은 로컬 실서버(UP·DOWN)와 스텁(DEGRADED · 측정 실패 · 403 · 연결 실패 · 추이 빈 칸)으로 다 그려 봤습니다. 런타임 오류 0건. 로컬 실서버에서 나온 것이 그대로 이 화면이 말해야 하는 것이었습니다.

API 서버      정상    인가가 꺼져 있다 — 역할 차단이 시연되지 않는다
데이터베이스   정상    인메모리 — 재기동하면 세션·감사 기록이 사라진다
AI 서비스      안 됨   I/O error … localhost:8100/healthz: Connection refused
데이터 볼륨    정상    지수 시계열 · 상품 원문 둘 다 있음

리뷰에서 나온 것 — 이 PR 밖

  • 「측정 실패」와 「상류 장애」가 응답에서 구분이 안 된다 → #595. 두 분 다 계약에 신호가 필요하다는 데 동의하셨고 @gitIt-sehyeon 은 Health.UNKNOWN(ⓐ) 을 지지하셨습니다. 계약에 신호가 없는 상태에서 화면이 추측하지 않습니다.
  • #596 의 다섯째 카드(extraction) → 먼저 머지되는 쪽이 나머지 유니온을 맞춥니다(@hd0rable). 이 PR 이 먼저면 그쪽에서, #596 이 먼저면 여기 | "extraction" 한 줄입니다. 카드 수에는 안 걸립니다(auto-fit).
  • 명세 8절 「제품 흐름 밖 화면」 절 → @gitIt-sehyeon 이 이 PR 머지 뒤에.

@hd0rable @gitIt-sehyeon @yoonjiseok

서버 몫(`GET /ops/status`)은 #531 로 끝났고 web 만 남아 있었다. 이슈가 남긴 결정 넷 중
내 몫 셋을 확정하고 화면을 낸다.

무엇을 그리나
- 카드 넷(server · database · ai-service · data-volumes)을 **서버가 보낸 순서 그대로**.
  순서가 곧 중요도라 화면이 다시 정하지 않는다.
- 상태는 UP · DEGRADED · DOWN 셋. **`DEGRADED` 가 요점이다** — UP/DOWN 둘로만 그리면
  「떠 있는데 못 하는 상태」가 전부 정상으로 보이고, 그게 #522 의 출발점이다.
- 5초 폴링 · 일시정지 · 멈춘 동안 「지금 한 번」.
- 왕복 시간 추이는 **이 화면을 연 뒤로만**(브라우저 메모리). 서버에 시계열을 안 쌓는다.
- 403 은 오류가 아니라 「차단됨」으로 그린다(S-08 과 같은 판단).

내가 정한 것 셋
- **판정 3색을 쓴다**(질문 1). 새 상태색 체계를 만드는 쪽이 더 나쁘다 — 화면마다 다른
  상태색이 생기면 색 체계를 두 벌 배워야 한다. 조건 셋(제품 흐름 밖 · 라벨 병기 · 고객
  비노출)을 `tokens.css` 규칙 1 옆에 못 박았다. 팀 셋 다 찬성한 방향이다.
- **화면 번호를 안 준다**(질문 3, 정세현·윤지석 합의를 따른다). 파일도 `Guide.tsx` 와
  같은 모양으로 `Console.tsx` 다. `SCREENS` 에 안 넣는다.
- **진입점은 `/guide` 의 「여기서 자주 막혀요」 하나**. 제품 흐름에서 링크하면 판매자에게는
  전부 403 이고, 아무 데서도 안 열리면 URL 을 외워야 한다(#406 에서 실제로 그 상태였다).

❗못 잰 값을 0 으로 접지 않는다
계약이 `latencyMs` 를 nullable 로 준 이유가 「즉시 응답」과 「안 쟀다」를 가르는 것이라,
화면이 다시 뭉치면 그 뜻이 사라진다. 추이 막대도 그 자리를 빈 칸으로 둔다.

❗요청이 실패하면 직전 카드를 지운다
「지금」을 답하는 화면에서 남은 카드는 전부 낡은 값이고, 그중 초록이 하나라도 있으면
화면이 서버가 하지 않은 말을 한다.

실측
- `npm run build`(tsc) 통과 · server `./gradlew test` 812건 통과.
- `WebTypesMirrorContractTest` 가 새 타입을 실제로 짝지었다 — `checkedAt` 를 한 글자
  바꿔 재니 `OpsStatus="계약에만 [checkedAt] · 화면에만 [checkedAtX]"` 로 빨개졌다.
- 로컬 실서버로 UP·DOWN 을, 스텁으로 DEGRADED·측정 실패·403·연결 실패를 그려 봤다.
  런타임 오류 0건.

Refs #522

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vJboj38TYcxUpG5ZZcQKs
@github-actions github-actions Bot added the 리뷰어 미배정 리뷰어가 배정되지 않았다 — 붙이는 게 다음 할 일 label Sep 10, 2026
@github-actions github-actions Bot added 리뷰대기: 강희진 강희진 이 배정됐고 아직 아무것도 제출하지 않았다 리뷰대기: 윤지석 윤지석 이 배정됐고 아직 아무것도 제출하지 않았다 리뷰대기: 정세현 정세현 이 배정됐고 아직 아무것도 제출하지 않았다 and removed 리뷰어 미배정 리뷰어가 배정되지 않았다 — 붙이는 게 다음 할 일 labels Sep 10, 2026

@hd0rable hd0rable left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

승인합니다. DEGRADED 를 화면의 요점으로 잡은 것이 이 이슈의 답이고, 서버가 그 구별을
내려도 화면이 UP/DOWN 둘로 접으면 #531 이 한 일이 사라집니다.

계약 쪽을 제가 대조했습니다 (서버가 제 영역입니다)

OpsStatus.java:29·54     checkedAt · deployment · components / id·name·health·latencyMs·note·facts
types.ts:911-936         같은 이름·같은 nullable — 못 잰 자리는 null
카드 순서                 OpsStatusService.java:94 의 List.of(...) 가 고정하고
                         OpsStatusEndpointTest:144 「구성요소가 넷이고 순서가 고정이다」가 잠근다
                         → 화면이 그 순서에 기대는 것이 계약에 서 있다
facts 가 배열             객체면 순서가 구현에 달린다 — 그 요청대로 배열이다
rbac_policy.yaml:220     ops:status:read → roles: [ADMIN]  · 15-demo-mode.sh:112 가 주입한다

latencyMs 를 0 으로 접지 않은 것이 특히 맞습니다. 서버가 nullable 로 낸 이유를 그
javadoc 이 "0 으로 채우면 «즉시 응답» 과 «재지 않았다» 가 화면에서 같아진다" 로 적어 뒀고,
실측에서 database: 0 과 server: null 이 실제로 같이 나옵니다 — 화면이 다시 뭉치면 그
필드가 존재할 이유가 없어집니다.

요청 실패에 직전 카드를 지우는 것도 맞습니다. "그중 초록이 하나라도 있으면 화면이 서버가
하지 않은 말을 한다"
가 이 화면의 성격을 정확히 짚습니다.

❗한 자리 — 실패한 폴링만 추이에서 흔적 없이 사라집니다

화면의 규칙이 "못 잰 자리는 높이 0 이 아니라 바닥 점" 인데, 폴링 자체가 실패하면 그
자리에 아무것도 안 남습니다.

Console.tsx:128-155
  성공  setTrend((prev) => … [...prev[c.id], c.latencyMs] …)     null 도 그대로 쌓인다  ✓
  실패  setStatus(null) · setError(...)                          setTrend 를 안 부른다  ✗

그래서 이렇게 됩니다.

1·2·3 성공(3 표본) → 4 실패(표본 없음) → 5 성공
추이 막대:  ▮▮▮▮   ← 넷이 연속으로 보인다. 4 회차에 못 쟀다는 흔적이 없다

가장 강한 「안 쟀다」가 유일하게 안 보입니다. 카드가 없는 동안엔 추이도 안 보이니 그 순간
문제는 아니고, 복구된 뒤에 끊긴 구간이 이어진 것처럼 남습니다.

실패 때도 아는 id 마다 null 을 한 칸 쌓으면 그 자리가 바닥 점으로 남습니다 —
Trend 가 null 을 이미 그렇게 그립니다(:341-343). 지우는 쪽(추이 초기화)도 되지만,
그러면 복구 직후에 «처음부터 잰 것» 처럼 보여서 이쪽이 나아 보입니다.

코드를 읽고 낸 것이고 화면으로는 못 재봤습니다(web 에 러너가 없어서) — 판단은 그쪽 몫입니다.
막지는 않습니다.

정한 것 셋 — 이견 없습니다

  • ① 판정 3색 — "화면마다 다른 상태색이 생기면 색 체계를 두 벌 배워야 하고, 그러면 규칙 1
    이 지키려던 것이 오히려 옅어진다"
    가 설득력 있습니다. 그리고 예외를 tokens.css 규칙 1
    옆에 조건 셋으로 못 박은 것
    이 이 결정의 값입니다(:26-37) — 안 적으면 다음 사람이
    "3색은 아무 데나" 의 근거로 씁니다. S-08 과의 구분(«수치에 3색을 얹으면 지점 성과 지표로
    읽힌다»)도 맞습니다.
  • ② 번호 없음 · Console.tsx — Guide.tsx 와 같은 모양이고 SCREENS 에 안 넣는 것이
    「제품 흐름 밖」이라는 사실과 맞습니다.
  • ③ 진입점을 /guide 하나로 — 제품 흐름에서 링크하면 판매자에게 전부 403 이라는 지적이
    정확합니다(ops:status:read 는 ADMIN 뿐입니다). 그리고 아무 데도 안 걸면 URL 을 외워야
    한다는 것도 — /upload 가 지금 그 상태입니다.

서버 쪽 전제 하나만 적어 둡니다

이 화면이 그리는 값에 고객 데이터가 0건인 것이 ops:status:read 를 ADMIN 에 준 근거입니다
(ADR-001). types.ts:925-928 이 "이 인터페이스에 그런 필드가 생기면 고칠 자리는 화면이
아니라 서버 응답"
이라고 적어 둔 것이 정확합니다 — 서버 쪽은 OpsStatusHasNoCustomerDataTest
가 허용 목록으로 막고 있고, 그건 제 영역이라 계속 그렇게 둡니다.

WebTypesMirrorContractTest 를 변이로 확인한 것(checkedAt → checkedAtX 에서 이름이 실제로
걸리는 것)도 좋습니다 — 그 대조는 «짝을 못 찾아 0개를 대조했다» 와 초록이 구분되지 않는
자리입니다.

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

Copy link
Copy Markdown
Member

승인은 그대로 두고 유니온 한 줄만 알립니다.

#596(추출 스냅샷 카드 · 이슈 #568)이 다섯째 카드를 냅니다. 그래서 이 PR 의 유니온이
넷이면 실물과 갈립니다.

openapi.yaml:1063   enum: [server, database, ai-service, data-volumes, extraction]   ← #596
types.ts            id: "server" | "database" | "ai-service" | "data-volumes"        ← 이 PR

❗**WebTypesMirrorContractTest 는 필드 이름만 봅니다**(확인함) — enum ↔ 유니온 불일치를
안 잡습니다. 그래서 사람이 맞춰야 합니다.

먼저 머지되는 쪽이 나머지를 맞추면 됩니다. 이 PR 이 먼저면 제가 #596 에서 유니온까지
같이 고치고(그때는 그 파일이 main 에 있으니), #596 이 먼저면 여기에 | "extraction" 한
줄만 더하시면 됩니다.

화면 쪽은 확인했습니다 — components 를 그대로 map 하고 Console.css:78 이 auto-fit 이라
카드 수에 안 걸립니다. 카드가 다섯이 되어도 코드는 안 바뀝니다.

@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.

승인합니다. 배선 셋과 미러 대조를 제 손으로 재현했고, 물어보신 「측정 실패」건은 응답으로 못 가르는 게 맞습니다 — 서버 코드로 확인했습니다. 아래에 제 의견과, 제 몫으로 남은 명세 8절 건을 적습니다.

재현한 것

npm run build (tsc)                  통과
server ./gradlew test                812건 · 실패 0

배선 3건 (다시 안 만지셨다는 것)
  15-demo-mode.sh:112   ~^/api/ops/status$ → $admin      있다
  rbac_policy.yaml:219  ops:status:read                  있다
  docker-compose.yml    SPHINX_STACK                     있다

미러 대조가 새 타입을 정말 짝지었는지는 변이 셋으로 확인했습니다 — 초록이 「짝을 못 찾아 0개를 대조했다」와 구분이 안 되는 자리라 하신 것이 맞습니다.

checkedAt → checkedAtX       빨강
latencyMs 를 지운다            빨강
health 를 지운다               빨강

셋 다 걸립니다. OpsStatus 가 실제로 대조 대상에 들어가 있습니다.

❗물어보신 것 — 「측정 실패」는 응답으로 못 가릅니다. 맞습니다

OpsStatusService.isolated() 를 읽었습니다.

return new Component(id, name, Health.DOWN, null,
        "상태를 재지 못했다(" + oneLine(e.toString()) + ") — 서버 로그를 본다",
        List.of());

Health.DOWN · latencyMs=null · facts=[] — 진짜 DOWN 과 필드가 하나도 안 다릅니다. 가르려면 note 문면을 파싱해야 하고, 그건 types.ts 주석이 "문면 파싱으로 가르면 서버 문구가 바뀔 때 조용히 깨진다" 로 막아 둔 그 방식입니다. 안 하신 판단이 맞습니다.

그리고 이건 화면 문제가 아니라 서버가 모르는 것을 안다고 적는 문제로 보입니다

결정 5.40   못 잰 값은 `0` 이 아니라 「모른다」로 적는다

latencyMs 는 그 규칙을 지킵니다(null). 그런데 health 는 안 지킵니다 — 측정이 터졌을 때 우리가 아는 것은 "못 쟀다" 뿐인데 DOWN 은 "그 구성요소가 죽었다" 를 단정합니다. 화면이 그걸 그대로 그리면 운영자는 멀쩡한 ai-service 를 재시작하러 갑니다 — #556 이 없앤 그 고리와 같은 모양입니다.

그래서 저는 note 옆에 신호를 붙이는 것보다 health 를 정직하게 만드는 쪽이 맞다고 봅니다.

ⓐ Health 에 UNKNOWN 을 더한다        「못 쟀다」가 상태값이 된다. 화면이 색·문면을 따로 줄 수 있다
                                    비용: enum 이 계약이라 수요자 전원 · 3색 매핑을 다시 정해야 한다
ⓑ Component 에 measured: boolean     enum 을 안 건드린다. 다만 health=DOWN 이 여전히 거짓말이다

ⓐ 를 봅니다. ⓑ 는 「DOWN 인데 안 쟀다」라는 모순된 조합을 계약이 허용하게 만들고, 읽는 쪽이 두 필드를 같이 봐야 뜻이 섭니다. latencyMs 를 nullable 로 둔 것과 같은 결로 가려면 상태값 자체가 「모른다」를 가져야 합니다.

core/ops/ 와 contracts/ 가 @hd0rable 님 것이라 제가 정할 자리는 아닙니다. 이슈로 떼시는 데 동의하고, 이 PR 은 지금 이대로가 맞습니다 — 계약에 신호가 없는 상태에서 화면이 추측하면 그게 더 나쁩니다.

제가 정한 것 셋에 대한 답

① 판정 3색 재사용 — 동의합니다. 특히 예외 조건을 tokens.css 규칙 1 옆에 못 박은 것이 이 결정의 값입니다. 안 적었으면 다음 사람이 "콘솔도 3색 쓰던데" 로 아무 데나 가져다 씁니다. 네 조건 중 «값이 판정과 같은 종류(정상·주의·불가)» 가 특히 좋습니다 — S-08 이 3색을 안 쓰는 이유(수치라 성과 지표로 읽힌다)와 대칭이라 두 판단이 한 규칙에서 나옵니다.

② 화면 번호 없음 · Console.tsx — 합의 그대로입니다. SCREENS 에 안 넣은 것도 맞습니다.

③ 진입점을 /guide 하나로 — 이게 셋 중 제일 좋습니다. "링크가 있다 ≠ 그 API 를 부를 권한이 있다" 가 정확하고, 반대로 아무 데도 안 걸면 /upload 처럼 URL 을 외워야 하는 상태(#406)가 됩니다. 막힌 사람이 찾아오는 자리에서만 보인다가 그 둘 사이의 정확한 지점입니다.

못 잰 값을 0 으로 안 접은 것

database   latencyMs: 0      "왕복 0ms"
API 서버    latencyMs: null   "왕복 — (안 쟀어요)"

추이 막대를 바닥 점으로 둔 것까지 간 것이 좋습니다. 높이 0 으로 두면 그래프에서는 「빠르다」로 읽히는데, 그 자리가 결정 5.40 이 말하는 «부재를 0 으로 적으면 다음 사람이 없었다고 읽는다» 와 같은 함정입니다.

요청 실패 시 직전 카드를 지우는 것

동의합니다. "「지금」을 답하는 화면에서 남은 카드는 전부 낡은 값이고, 그중 초록이 하나라도 있으면 화면이 서버가 하지 않은 말을 한다" — 이게 pr-review.yml 가드에서 밟은 것과 같은 종류입니다(갱신 실패가 화면에 흔적을 안 남기면 낡은 초록이 남는다).

제 몫 — 명세 8절

명세 8절 「제품 흐름 밖 화면」 절 — @gitIt-sehyeon (/guide 와 /console 같이)

가져갑니다. /guide 와 /console 을 같이 적고, SCREENS 에 없는 이유(제품 흐름 밖 · 화면 번호 없음 · ADMIN 전용)를 그 절이 말하게 하겠습니다. 이 PR 이 머지된 뒤에 내겠습니다 — 지금 쓰면 아직 없는 화면을 명세가 먼저 말합니다.

/guide 캡처를 alpha 실화면 규약대로 배포 뒤로 미루신 것도 맞습니다.

@github-actions github-actions Bot removed the 리뷰대기: 정세현 정세현 이 배정됐고 아직 아무것도 제출하지 않았다 label Sep 10, 2026
이 PR 이 만드는 것은 `/console` 화면 하나다. 앞 커밋이 같이 실었던 둘을 뺀다.

- `web/src/pages/Guide.tsx` — 「여기서 자주 막혀요」 진입점 한 줄
- `web/src/styles/tokens.css` — 판정 3색 예외를 규칙 1 옆에 적은 문단

❗**두 분이 승인하신 뒤의 변경이다**(@hd0rable @gitIt-sehyeon). 두 리뷰 다 그 둘을 좋게
보셨으므로, 승인이 이 범위에도 그대로 서는지는 다시 봐 주셔야 한다.

남는 것과 그 귀결
- 화면이 **어디서도 안 걸린다** — 닿는 길이 주소 직접 입력뿐이다(`/upload` 와 같은 상태 ·
  #406). `App.tsx` 주석에 그 사실을 적었다. 진입점은 이 PR 밖에서 정한다.
- `tokens.css` 규칙 1 은 **예외를 모르는 채로 남는다.** 그래서 이 화면이 3색을 쓰는 근거와
  조건 넷이 `Console.tsx` 머리말 한 곳에만 있다 — 팔레트 쪽에 옮겨 적을지는 따로 정한다.
  `Console.css` 머리말도 그 자리를 가리키게 고쳤다.

❗같이: 실패한 폴링도 추이에 한 칸 남긴다 (리뷰 · 강희진)

성공한 회차만 이어 붙어서 **끊긴 구간이 이어진 것처럼** 보였다 — 카드가 없는 동안이 아니라
**복구된 뒤**에 거짓이 된다. 이 화면의 규칙이 「못 잰 자리는 흔적을 남긴다」인데 정작 가장
강한 「안 쟀다」가 유일하게 안 보이는 자리였다. 화면 안의 결함이라 같이 고쳤다.

실측(스텁 · 4·5 회차만 연결 실패):

    막대  ▮▮▮··▮▮      ← · 가 못 잰 자리. 고치기 전에는 ▮▮▮▮▮ 다섯이었다
    aria  왕복 시간 추이 — 최근 7회, 최대 17ms

`npm run build`(tsc) 통과 · 계약 미러 대조 통과.

Refs #522

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vJboj38TYcxUpG5ZZcQKs
@junseo2323

Copy link
Copy Markdown
Contributor Author

두 분 승인 뒤에 범위를 좁혔습니다 — 다시 봐 주셔야 합니다(63d91f7). 본문도 그에 맞춰 고쳤습니다.

뺀 것 둘

web/src/pages/Guide.tsx      「여기서 자주 막혀요」 진입점 한 줄
web/src/styles/tokens.css    판정 3색 예외를 규칙 1 옆에 적은 문단

두 분이 정확히 그 둘을 이 PR 의 값으로 꼽으셨습니다. @gitIt-sehyeon 은 진입점을 "셋 중 제일 좋습니다 … 막힌 사람이 찾아오는 자리에서만 보인다가 그 둘 사이의 정확한 지점" 으로, @hd0rable 은 예외 문면을 "안 적으면 다음 사람이 「3색은 아무 데나」의 근거로 씁니다" 로 보셨습니다. 그래서 승인이 이 범위에도 그대로 서는지는 두 분 판단입니다.

남는 상태를 숨기지 않고 적어 둡니다.

① /console 이 어디서도 안 걸린다      닿는 길이 주소 직접 입력뿐 — /upload 와 같은 상태(#406)
                                    App.tsx 주석이 그 사실을 말한다
② 규칙 1 이 예외를 모른다             3색 근거·조건 넷이 Console.tsx 머리말 한 곳에만 있다
                                    Console.css 머리말도 그 자리를 가리키게 고쳤다

둘 다 이 PR 밖에서 정합니다.

같이 고친 것 — 실패한 폴링도 추이에 남깁니다

@hd0rable 이 코드로 짚어 주신 자리입니다. 화면 안의 결함이라 같이 고쳤고, 스텁으로 4·5 회차만 연결 실패시켜 재봤습니다.

막대   ▮▮▮··▮▮      ← · 가 못 잰 자리. 고치기 전에는 ▮▮▮▮▮ 다섯이었다
aria   왕복 시간 추이 — 최근 7회, 최대 17ms

추이를 비우는 쪽도 봤는데, 그러면 복구 직후가 «처음부터 잰 것» 처럼 보여서 말씀하신 쪽으로 갔습니다. 근거는 load() 의 catch 절 주석에 남겼습니다.

npm run build(tsc) · 계약 미러 대조 다시 통과했습니다.

@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.

좁힌 범위에도 승인 유지합니다. 다만 "이 PR 밖에서 정한다" 두 건에 받는 사람이 없어서 이슈로 떼겠습니다 — 이 레포가 반복해서 밟은 자리가 정확히 그것입니다.

확인한 것

tokens.css 되돌림    주석 문단만 빠졌다 — 색 토큰은 안 건드렸다 (확인함)
npm run build (tsc)  통과
계약 미러 대조         통과

좁힌 판단 — 맞습니다

두 조각이 서로 다른 질문을 엽니다. Guide.tsx 는 제품 문안이고 tokens.css 는 팔레트 규칙이라, 이 PR(화면 하나)과 되돌릴 단위가 다릅니다. 그리고 뺀 사실과 그 귀결을 숨기지 않고 적으신 것이 이 커밋의 값입니다 — 특히 App.tsx 주석에 "지금은 어디서도 안 걸린다" 를 남긴 것.

❗다만 「밖에서 정한다」에 주소가 없습니다

① /console 이 어디서도 안 걸린다      #406 이 만든 /upload 상태와 같다
② tokens.css 규칙 1 이 예외를 모른다   규칙은 절대문이고 화면 하나가 그것을 깨는데 포인터가 없다

②가 특히 그렇습니다. Console.css 머리말이 Console.tsx 를 가리키게 하셨지만, 문제는 반대 방향입니다 — 다음에 3색을 쓰려는 사람은 Console.tsx 를 안 읽고 tokens.css 규칙 1 을 읽습니다. 거기엔 예외가 없으니 "규칙이 절대문인데 콘솔은 쓰네" 를 보고 근거 없이 따라 쓰거나, 반대로 콘솔을 규칙 위반으로 고칩니다. 앞 판의 그 문단이 막던 것이 그것이었습니다.

이슈 둘로 떼겠습니다 — 제가 냅니다. 그래야 데모 전에 ①이 남아 있는지 목록에서 보입니다.

같이 고친 것 — 실패한 폴링을 추이에 남긴 것

이게 좁히기보다 큰 변경인데 방향이 맞습니다.

카드가 없는 동안이 아니라 복구된 뒤에 거짓이 된다.

정확합니다. 실패 중에는 화면 전체가 「지금 못 잰다」를 말하므로 사람이 안 속는데, 복구되고 나면 추이만 남고 그 막대가 끊긴 적 없는 것처럼 보입니다. 그리고 이 화면의 규칙이 "못 잰 자리는 0 이 아니라 흔적" 인데 가장 강한 「안 쟀다」가 유일하게 안 보이던 자리였다는 정리가 그 규칙을 스스로 적용한 것입니다.

추이를 비우는 쪽을 안 고른 근거("복구 직후가 «처음부터 잰 것» 처럼 보인다")도 맞습니다 — 그건 같은 거짓말을 다른 모양으로 하는 것입니다.

남는 하나 — 「측정 실패」

앞 리뷰에서 적은 Health 에 UNKNOWN 건은 그대로 열려 있습니다. 계약이라 @hd0rable 님 자리이고 이 PR 밖입니다.

@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.

승인합니다 — 범위 축소 뒤의 판에 대해서도 섭니다(아래). web/ 은 제 영역이 아니라 화면 자체는 두 분 리뷰에 얹고, ai-service 쪽 사실이 이 화면까지 제대로 오는가를 봤습니다.

내 쪽 → 화면 사슬이 P1 모양입니다

ai-service /healthz     status:"ok" 를 **항상** 낸다 + llm_configured 를 사실로 낸다   ← 측정
server OpsStatusService llmConfigured != true → problems → Health.DEGRADED           ← 판정
web /console            노랑 칩 + note "LLM 키가 없다 — 채점(F-SCR-001)이 502 로 떨어진다"

제 서비스가 스스로 «나 degraded 야» 라고 말하지 않는 것이 맞습니다. 제 쪽은 사실만 내고 그것을 건강으로 바꾸는 것은 룰입니다 — P1(«AI 는 측정, 룰은 결정»)이 운영 상태 축에서도 같은 모양으로 서 있습니다. 화면이 그 판정을 색으로만 말하지 않고 note 로 다음 행동까지 적는 것도 이 이슈의 출발점(«셋이 겉으로 같은 502 하나였다»)에 맞습니다.

새 타입이 그물 안입니다 — 재봤습니다

이 레포가 「N 벌」로 여러 번 데인 자리라, types.ts 에 사본이 하나 느는 것을 봤습니다.

기준선                                   BUILD SUCCESSFUL
OpsComponent 에서 latencyMs 를 뺀다        2 tests, 1 failed
health → status 로 이름을 바꾼다           2 tests, 1 failed

WebTypesMirrorContractTest 가 openapi.yaml ↔ types.ts 를 대조하고 OpsComponent(:1055)가 그 안에 있습니다. 그리고 그 테스트가 이 PR 에서 실제로 뜹니다 — web/src/api/types.ts 가 ci.yml 의 server_extra 에 있어서 web/ 만 건드리는 PR 인데도 server 잡이 뜹니다. 실행이 최종 커밋 위인 것도 확인했습니다.

PR head              63d91f7
테스트 (4모듈) 실행    success · 63d91f7 · 07:10:38     ← 같은 SHA

범위 축소에 대해 — 승인 섭니다

물어보신 자리입니다. 뺀 둘이 이 PR 의 값을 안 줄입니다.

/guide 진입점    화면이 어디서도 안 걸린다 → /upload 와 같은 상태(#406). 진입점은 밖에서 정한다
tokens.css 예외  규칙 1 이 예외를 모르는 채 남는다

첫째는 이 PR 이 만드는 것과 성격이 다릅니다 — 「어디서 들어가나」는 화면 목록 전체의 문제이고(S-00 을 지운 뒤 #406 이 다루는 축), 여기 한 줄로 붙이면 그 결정이 이 PR 에 묻힙니다. 둘째는 tokens.css 가 오준서 님 파일이라도 규칙 문서 성격이라, 예외를 규칙 옆에 적는 것은 그 규칙을 쓰는 화면 전부에 걸리는 결정입니다. 둘 다 나중에 하는 게 맞아 보입니다.

다만 App.tsx 주석에 «닿는 길이 주소 직접 입력뿐»을 적어 두신 것이 이 축소를 안전하게 만듭니다 — 안 적었으면 다음 사람이 «진입점이 있겠지» 로 읽습니다.

❗내 쪽 사실 셋이 카드까지 안 옵니다 (이 PR 밖 · 서버 몫)

/healthz 가 내는 것 중 카드에 안 실리는 것이 셋입니다.

llm_model       카드에 없음     ❗아래
llm_base_url    카드에 없음     ❗아래
env_files       카드에 없음     (어느 .env 를 읽었나 — 데이터 디렉토리로 대체됨. 이견 없습니다)

앞 둘이 걸립니다. 「떠 있는데 못 하는 상태」의 한 갈래가 «키는 있는데 다른 모델·다른 프로바이더를 보고 있다» 입니다. 그때 서비스는 UP 이고 채점도 돌지만, 나오는 수가 우리가 잰 수가 아닙니다. 제 config.py 가 그 귀결을 이미 적어 뒀습니다.

# config.py:59
# .env 의 LLM_MODEL 로 덮어쓸 수 있지만, 정책 모델이 아니면 경고를 남긴다 —
# 정책에서 벗어난 실행이 조용히 지나가면 **성능 수치의 출처를 알 수 없게 된다.**

그 경고는 로그뿐입니다(config.py:351). 그리고 #266 이후 프로바이더를 되돌리는 것은 LLM_API_BASE 한 줄이라 실수로 갈릴 수 있는 자리입니다. 카드가 프롬프트 버전·오해 라이브러리 버전을 이미 싣는 것과 같은 이유(«이미지 세대를 잡는다»)로, 모델과 base_url 도 같은 축입니다 — 그 셋이 같이 있어야 «지금 화면의 수가 어느 조합에서 나왔나» 에 답이 됩니다.

이 PR 밖입니다 — facts 는 OpsStatusService(강희진)가 만들고 #531 로 이미 머지됐습니다. 화면은 오는 대로 그리면 되고 실제로 그렇게 하고 있습니다. 제가 이슈로 뗄까요? 값을 내는 쪽은 제 /healthz 라 이미 다 나가 있고, 서버가 두 줄 더 담으면 됩니다.

제일 좋았던 것

못 잰 값을 0 으로 접지 않습니다. … 추이 막대도 같습니다 — 못 잰 자리는 높이 0 이 아니라 바닥 점입니다. 0 으로 두면 「빠르다」로 읽힙니다.

❗폴링 자체가 실패한 회차도 한 칸 남깁니다 … 앞 판은 성공한 회차만 이어 붙여서 끊긴 구간이 이어진 것처럼 보였습니다 — 카드가 없는 동안이 아니라 복구된 뒤에 거짓이 됩니다.

둘째가 특히요. 거짓이 되는 시점이 장애 중이 아니라 복구 뒤라는 것 — 그때는 아무도 안 보고 있고 화면은 정상입니다. 제 쪽에서 #207(«0 은 무엇의 0인가»)로 반복해 밟은 것과 같은 축인데, 여기는 시간축이라 한 겹 더 어렵습니다.

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

Copy link
Copy Markdown
Contributor

제 리뷰의 ❗ 절을 정정합니다 — 절반이 틀렸습니다. 이 PR 에 대한 승인은 그대로입니다.

제가 이렇게 적었습니다.

llm_model 카드에 없음 ❗ / llm_base_url 카드에 없음 ❗
… 「떠 있는데 못 하는 상태」의 한 갈래가 «키는 있는데 다른 모델을 보고 있다» 입니다

llm_model 은 카드에 있습니다.

// OpsStatusService.java:184  — facts 목록의 첫 줄이다
facts.add(new Fact("모델", blankToDash(report.llmModel())));

제가 196 행부터 잘라 보면서 그 바로 위 줄을 놓쳤습니다. 「프롬프트 버전은 있는데 모델은 없다」로 읽은 것이 그래서이고, 실제로는 모델이 facts 의 첫 항목입니다. 제 논거(«수치의 출처를 화면이 말해야 한다»)는 이미 충족돼 있습니다.

남는 것은 둘인데 약합니다

llmBaseUrl    AiHealth 레코드가 받고 서버 어디에서도 안 쓴다 (선언 외 참조 0건)
envFiles      같음

llm_base_url 을 제가 걸었던 이유는 "#266 이후 프로바이더 되돌리기는 LLM_API_BASE 한 줄" 이었는데, 그 경우 모델 이름도 같이 바뀝니다(gemini-*) — 그러면 모델 fact 가 이미 잡습니다. 같은 모델 이름을 다른 엔드포인트가 서빙하는 경우(프록시)만 남는데, 우리 배포에 그런 자리가 없습니다.

그래서 이슈로 안 뗍니다. 제가 만든 오독에서 일을 만드는 셈이 됩니다. 나중에 프록시나 다중 프로바이더가 실제로 생기면 그때 자리가 명확해집니다.

나머지 리뷰 내용은 그대로입니다

llm_configured → DEGRADED → 노랑 칩 사슬, WebTypesMirrorContractTest 변이 둘, 최종 커밋 위에서 테스트 (4모듈) 이 돈 것, 범위 축소에 승인이 서는 것 — 다 재본 것이고 안 바뀝니다.

죄송합니다. grep -A/sed 로 잘라 본 범위가 답을 정한 자리였고, 저는 그 범위를 안 의심했습니다 — 「facts 목록 전체」를 봤어야 하는데 「196행 이후」를 봤습니다.

@junseo2323
junseo2323 merged commit 62f1ed4 into main Sep 10, 2026
5 checks passed
@junseo2323

Copy link
Copy Markdown
Contributor Author

머지했습니다. 세 분 다 감사합니다.

@yoonjiseok — 떼 주세요. 다만 셋이 아니라 하나입니다.

llm_model 은 이미 카드에 실립니다.

OpsStatusService.java:184   facts.add(new Fact("모델", blankToDash(report.llmModel())));

제 스텁 캡처에도 「모델 —」로 나옵니다(키가 없어 값이 빈 경우). 그러니 안 오는 것은 llm_base_url 하나입니다 — AiServiceClient.AiHealth:504 에 llmBaseUrl 로 이미 받아 놓고 facts 에 안 담습니다. 서버 한 줄입니다.

그리고 그 한 줄의 근거가 짚어 주신 그대로입니다 — «키는 있는데 다른 프로바이더를 보고 있다» 는 UP 이고 채점도 도는데 나오는 수가 우리가 잰 수가 아닙니다. #266 이후 그게 LLM_API_BASE 한 줄이라면, 모델만 실리고 base_url 이 안 실리는 지금 상태는 절반만 잡습니다 — 같은 모델 이름으로 다른 엔드포인트를 보는 조합이 카드에서 정상으로 보입니다. 프롬프트 판·오해 라이브러리 판을 실은 이유(«이미지 세대를 잡는다»)와 같은 축이라는 정리에 동의합니다.

env_files 는 저도 이견 없습니다 — 데이터 디렉토리로 갈음됩니다.

화면 쪽은 값이 오면 그대로 한 줄 늡니다. facts 를 순서대로 그리기만 하므로 web 은 안 고쳐도 됩니다.

@gitIt-sehyeon — 「밖에서 정한다」 둘을 이슈로 떼 주시는 것 감사합니다. ②의 방향 지적("다음 사람은 Console.tsx 를 안 읽고 tokens.css 규칙 1 을 읽는다")이 맞습니다 — 포인터가 규칙 쪽에 있어야 합니다.

hd0rable pushed a commit that referenced this pull request Sep 10, 2026
`#594` 가 먼저 들어가서 `types.ts` 의 `OpsComponent` 가 main 에 있다. 그래서 그 PR
코멘트에 적은 규약대로 여기서 유니온을 같이 고친다 — 「먼저 머지되는 쪽이 나머지를 맞춘다」.

  openapi.yaml:1063   enum: [… , extraction]      다섯
  types.ts:913        | "extraction" 추가          다섯   ← 이 커밋

이걸 무는 것이 없다는 것도 확인했다. `WebTypesMirrorContractTest` 는 필드 «이름» 만 보고
enum 값은 안 본다. 화면은 `c.id` 를 key·인덱스로만 써서 유니온이 넷이어도 tsc 가 안 깨진다.
즉 갈리는 것은 타입 한 겹이고, 그게 `#316` 이 낸 자리다 — 「유니온이 계약보다 뒤처져
있었고 tsc 는 통과했다」.

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hd0rable pushed a commit that referenced this pull request Sep 10, 2026
`#594` 가 먼저 들어가서 `types.ts` 의 `OpsComponent` 가 main 에 있다. 그래서 그 PR
코멘트에 적은 규약대로 여기서 유니온을 같이 고친다 — 「먼저 머지되는 쪽이 나머지를 맞춘다」.

  openapi.yaml:1063   enum: [… , extraction]      다섯
  types.ts:913        | "extraction" 추가          다섯   ← 이 커밋

이걸 무는 것이 없다는 것도 확인했다. `WebTypesMirrorContractTest` 는 필드 «이름» 만 보고
enum 값은 안 본다. 화면은 `c.id` 를 key·인덱스로만 써서 유니온이 넷이어도 tsc 가 안 깨진다.
즉 갈리는 것은 타입 한 겹이고, 그게 `#316` 이 낸 자리다 — 「유니온이 계약보다 뒤처져
있었고 tsc 는 통과했다」.

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants