저장소에 릴리즈 이력이 전혀 없습니다: git 태그 0개(refs/tags 자체가 없음), GitHub Release 0개, CHANGELOG/릴리즈 노트 없음, .github/workflows 없음. 유일한 CI인 .gitlab-ci.yml은 main/develop 브랜치 푸시에 반응해 빌드 후 PVC 디렉터리로 복사할 뿐 버전·태그·산출물 발행을 하지 않습니다. 버전 필드는 package.json(0.0.0)과 upgrade/admin-v2/package.json(0.1.0)에 있으나 둘 다 private이며 git log -p 확인 결과 최초 커밋에 기록된 뒤 이후 15개 fix PR을 포함한 30여 커밋 동안 한 번도 증가한 적이 없어 릴리즈 관례로 볼 수 없습니다(docs/INDEX.md:168도 '실제 release 근거가 있을 때만 기록한다'고 명시). upgrade/admin-v2/deploy의 두 스크립트는 배포·오프라인 npm 캐시용이며 버전명 산출물을 만들지 않습니다. 따라서 첫 버전 번호와 태그 형식, 노트 위치를 새로 정하는 것은 사람의 몫이라 판단해 아무 커밋·태그도 만들지 않았고 작업 트리는 그대로 두었습니다.
루트 앱의 crypto-js local tarball 의존성 제거로 clean install 복구
4/4/M
대기
루트 package.json이 crypto-js를 file:crypto-js-4.2.0.tgz로 참조해 npm ci가 불가하다. admin-v2는 별도 package.json이라 영향이 없다(이번 세션에도 admin-v2 npm ci 정상 확인). 루트 앱은 테스트가 없어 회귀 검증 비용이 크다.
2026-09-10
monitoringParser.toPod의 restarts NaN 노출
2/1/S
대기
src/api/monitoring.ts:43이 Number(row.restarts ?? row.restartCount ?? restartSum)이라 kubectl 스타일 '3 (5d ago)' 문자열이 오면 NaN이 표 칸에 그대로 나온다(합계 MetricCard는 NaN이 falsy라 영향 없음). 선행 정수만 파싱하고 실패하면 0으로 떨어뜨린다. 순수 함수라 기존 monitoring.test.ts로 검증 가능. 2026-09-10 재확인 — 해당 줄 그대로 유효.
2026-09-10
결과 0건인 탭을 누를 때마다 같은 조회를 되풀이
2/1/S
대기
ContentAccessView.changeTab은 !rules.value.length / !menus.value.length, CatalogView는 !sharedApps.value.items.length, DirectoryView는 !users.value.items.length로 재조회를 판단해, 결과가 정말 0건인 조건에서는 탭을 오갈 때마다 같은 요청을 되풀이한다. 조회 완료 여부를 탭별 플래그로 기억하면 되고, 이미 있는 tabLoading·tabError 레코드 옆에 두면 자연스럽다. 2026-09-10 세 화면 모두 재확인.
2026-09-10
컴포넌트 테스트가 mount한 wrapper를 정리하지 않는 누수
2/1/S
대기
OperationsView·Overview·Policy 두 화면 테스트에는 afterEach 정리가 있지만 CatalogView·DirectoryView·ContentAccessView 테스트는 여전히 mount 후 unmount하지 않는다. 지금은 전역 이벤트를 듣는 화면이 OperationsView뿐이라 드러나지 않았을 뿐, 다른 화면이 document/window 구독을 추가하면 같은 방식으로 다음 테스트의 호출 횟수가 어긋난다. 여섯 파일에 같은 mounted 배열 패턴이 반복되므로 공용 mount 헬퍼로 모을 시점이 됐다.
2026-09-10
AdvancedPolicyView가 snapshot.extensions를 v-model로 직접 변형
2/2/S
대기
저장 실패 시 체크박스는 바뀐 상태로 남고 서버는 그대로라 화면과 서버가 어긋난다. form에 복사본을 두고 저장 성공 시에만 반영해야 한다. 2026-09-10에 AdvancedPolicyView.test.ts를 만들었으므로 이제 그 파일에 테스트를 붙이면 된다. 운영에서는 enablePolicyWrites가 false라 실사용 영향은 작다.
2026-09-10
다른 탭에서 로그아웃해도 이 탭은 라우트를 옮길 때까지 복구하지 않음
2/2/S
대기
session.ts 맨 아래 storage 이벤트 구독은 다른 탭이 auth·user를 지우면 state.user를 비우고 세대를 올리지만 만료 handler를 부르지 않는다. 만료(401)는 즉시 복구하는데 교차 탭 로그아웃은 다음 라우트 이동까지 방치된다. 다만 그 만료 handler를 만든 커밋 c1a4d7b가 아직 main에 없으므로(브랜치 auto/2026-09-10-1113), 그 PR이 병합된 뒤에 착수해야 한다. '로그아웃'과 '세션 소실'을 구분할 근거를 세우는 것이 실제 작업량.
2026-09-10
toast가 수동으로 닫혀도 타이머가 남고 개수 제한이 없음
1/1/S
대기
shared/notifications.ts의 notify가 항목마다 4.5초 setTimeout을 걸고 remove로 지워도 타이머를 정리하지 않는다. 연속 저장 시 toast가 쌓일 수 있다. 가치가 낮아 우선순위 하위.
2026-09-10
정책·Overview 화면이 요청 순서를 관리하지 않고, 재조회할 때마다 화면 전체를 skeleton으로 되돌림
3/1/M
완료
2026-09-10 구현. 보류 항목 두 개(createRequestGuard 미사용, 저장할 때마다 패널 전체 skeleton)가 같은 load() 하나의 문제라 함께 고쳤다. OverviewView·PolicyCenterView·AdvancedPolicyView에 initialLoading/refreshing 분리와 createRequestGuard를 적용하고 컴포넌트 테스트 8개를 새로 붙였다. 커밋 1709f01.
요약: validateRuntimeConfig와 scripts/validate-runtime-config.mjs가 //evil.test, /\evil.test를 “상대 경로”로 보고 통과시켜 allowedHosts allowlist를 우회할 수 있었다. backendBaseUrl은 access_token 헤더·withCredentials와 함께 쓰이고 authSsoUrl은 window.location.assign 대상이라 토큰 유출·오픈 리다이렉트로 이어진다. 상대 경로 값을 sentinel origin(https://relative.invalid/)에 resolve해 origin이 유지되는지 확인하도록 두 검증 지점을 동일하게 고치고 회귀 테스트 3개를 추가했다. 검증은 npm run verify(typecheck + vitest 24개 통과, 기존 21개)와 npm run build(build → runtime-config 검증 → offline 검사 → integrity manifest 18개 검증) 전체 통과, 그리고 악성 값을 넣은 임시 runtime.json으로 build-time script가 exit 1을 내는지 직접 확인했다. 커밋 fdb7ab2.
보류 아이디어:
ensureAdminSession(force=true)가 진행 중인 비강제 요청 promise를 그대로 반환하는 재진입 버그 수정 (가치 3 / 위험 2 / S)
절대 URL allowlist를 hostname뿐 아니라 port·scheme까지 비교하도록 강화 (가치 3 / 위험 2 / S)
루트 앱의 crypto-js local tarball 의존성 제거로 clean install 복구 — 현재 npm ci 자체가 불가해 검증 비용 큼 (가치 4 / 위험 4 / M)
monitoringParser.findRows 재귀에 깊이 제한·순환 참조 방어 추가 및 테스트 (가치 2 / 위험 1 / S)
루트 앱 Playwright 설정만 있고 @playwright/test dependency와 test script가 없는 상태 정비 (가치 3 / 위험 2 / M)
요약: validateNetworkValue가 url.hostname만 allowlist와 비교해 https://api.intranet.local:8443처럼 allowlist된 host의 다른 port로 우회할 수 있었고, http:// 절대 URL도 그대로 통과해 access_token 헤더와 withCredentials cookie가 평문으로 나갈 수 있었다. url.host 비교로 바꾸고(allowedHosts 항목을 host/host:port로 정규화, 기본 port 명시는 동일 취급), loopback(localhost/127.0.0.1/[::1]) 외 http를 거부하며, 절대 URL이 없어도 allowedHosts 항목 형식(scheme·경로·자격증명 포함 금지)을 검사하도록 했다. 동일 규칙을 scripts/validate-runtime-config.mjs에도 반영했다. 검증은 npm run verify(typecheck + vitest 30개 통과, 기존 24개)와 npm run build(build → runtime-config 검증 → offline 검사 → integrity manifest 18개) 전체 통과, 그리고 임시 runtime.json 7종(평문 http, 다른 port, host:port 항목, loopback http, 잘못된 allowedHosts 항목 2종, 상대 경로)으로 build-time script의 exit code를 직접 대조했다. 커밋 d2796b9.
보류 아이디어:
ensureAdminSession(force=true)가 진행 중인 비강제 요청 promise를 그대로 반환하는 재진입 버그 수정 (가치 3 / 위험 2 / S)
목록 화면(Catalog/Directory/ContentAccess)의 stale response가 최신 결과를 덮어쓰는 race 방지 — 컴포넌트 테스트 환경(jsdom, @vue/test-utils)이 없어 선행 정비 필요 (가치 3 / 위험 3 / M)
루트 앱의 crypto-js local tarball 의존성 제거로 clean install 복구 — 현재 npm ci 자체가 불가해 검증 비용 큼 (가치 4 / 위험 4 / M)
monitoringParser.findRows 재귀에 깊이 제한 추가 및 테스트 (가치 2 / 위험 1 / S)
shared/format.ts 단위 테스트 공백 보강 (가치 2 / 위험 1 / S)
2026-09-03
선택: 목록 화면의 stale response가 최신 결과를 덮어쓰는 race 방지 (가치 4 / 위험 2 / 작업량 M)
결과: 성공
요약: CatalogView·DirectoryView·ContentAccessView의 조회 함수들이 요청 순서를 추적하지 않아, PaginationBar 버튼·탭 전환·행 선택처럼 :disabled="loading"이 걸리지 않은 동선에서 요청이 겹치면 늦게 도착한 이전 응답이 최신 결과를 덮어썼다(다른 페이지 목록 표시, 취소한 상세 재출현, 지나간 오류 메시지 노출). 새 shared/async.ts의 createRequestGuard()로 ticket을 발급해 최신 요청의 결과만 상태에 반영하고, 목록을 다시 읽을 때는 진행 중인 상세 요청을 invalidate()로 버리며, UUID 검증 실패 같은 조기 반환 경로에서도 loading을 정리하도록 했다. 검증은 npm run verify(typecheck + vitest 35개 통과, 기존 30개 + guard 단위 테스트 5개로 응답 역순 도착 시나리오 포함)와 npm run build(build → runtime-config 검증 → offline 검사 → integrity manifest 19개) 전체 통과. 커밋 bbab374.
보류 아이디어:
unwrapPageEnvelope가 서버 pageInfo 누락 시 요청한 pageNum/pageSize를 잃고 1/10으로 되돌려 페이지 이동이 막히는 문제 (가치 3 / 위험 1 / S)
ensureAdminSession(force=true)가 진행 중인 비강제 요청 promise를 그대로 반환하는 재진입 버그 (가치 3 / 위험 2 / S)
monitoringParser.findRows 재귀에 깊이 제한 추가 및 테스트 (가치 2 / 위험 1 / S)
shared/format.ts 단위 테스트 공백 보강 (가치 2 / 위험 1 / S)
루트 앱의 crypto-js local tarball 의존성 제거로 clean install 복구 — 현재 npm ci 자체가 불가해 검증 비용 큼 (가치 4 / 위험 4 / M)
2026-09-03
선택: 세션 확인 실패 시 SSO 무한 왕복 차단 (가치 4 / 위험 2 / 작업량 S)
결과: 성공
요약: router guard가 ensureAdminSession() 실패를 (관리자 권한 없음 메시지를 뺀) 전부 redirectToSso()로 처리해, backend /sso/ssologin이 장애이거나 네트워크가 끊기면 SSO가 앱으로 돌려보낼 때마다 다시 SSO로 나가는 무한 왕복이 생기고 사용자는 오류 화면조차 볼 수 없었다. 새 auth/ssoRedirect.ts에 sessionStorage 기반 사용량 기록을 두어 60초 창에서 자동 이동을 2회로 제한하고(초과 시 access-denied 화면 + 수동 SSO 버튼), 세션 확보 시 사용량을 비우며, 저장소를 못 쓰거나 예외를 던지면 왕복 감지가 불가하므로 자동 이동을 막도록 했다. 검증은 npm run verify(typecheck + vitest 46개 통과, 기존 35개 + 신규 11개로 한도·창 만료·시계 역행·저장소 장애 시나리오 포함)와 npm run build(build → runtime-config 검증 → offline 검사 → integrity manifest 19개) 전체 통과. 커밋 74f26a1.
보류 아이디어:
unwrapPageEnvelope가 서버 pageInfo 누락 시 요청한 pageNum/pageSize를 잃고 1/10·totalCount 0으로 되돌려 페이지 이동 버튼이 잠기는 문제 (가치 3 / 위험 1 / S)
ensureAdminSession(force=true)가 진행 중인 비강제 요청 promise를 그대로 반환하는 재진입 버그 (가치 3 / 위험 2 / S)
monitoringParser.toPod이 restarts에 비숫자 문자열이 오면 NaN을 그대로 노출하는 문제 (가치 2 / 위험 1 / S)
shared/format.ts 단위 테스트 공백 보강 (가치 2 / 위험 1 / S)
루트 앱의 crypto-js local tarball 의존성 제거로 clean install 복구 — 현재 npm ci 자체가 불가해 검증 비용 큼 (가치 4 / 위험 4 / M)
2026-09-03
선택: 서버가 pageInfo의 pageNum/pageSize를 생략할 때 요청 페이지 유지 (가치 4 / 위험 1 / 작업량 S)
결과: 성공
요약: docs/BACKEND_API_REFERENCE.md·docs/API_GUIDE.md에 기록된 기존 backend 공통 목록 계약은 pageInfo: { totalCount }만 반환하는데, unwrapPageEnvelope가 pageNum/pageSize를 서버 값에서만 읽어 매 요청마다 1/10으로 되돌렸다. 그래서 실제 backend에서는 Catalog·Directory 목록이 2페이지 데이터를 받고도 1페이지로 표시되고 다음 버튼이 같은 페이지를 반복 요청해 페이지 이동이 사실상 막혔다(mock의 paginate는 요청 페이지를 그대로 돌려주므로 드러나지 않았다). 서버 값이 없거나 유효하지 않으면 요청에 사용한 값을 유지하도록 하고 apiGetPage가 요청 params에서 그 값을 읽어 넘기게 했으며, QUICK_WINS 문서에 계약을 명시했다. 검증은 npm run verify(typecheck + vitest 52개 통과, 기존 46개 + 신규 6개로 totalCount만 오는 응답·유효하지 않은 서버 값·서버 값 우선·params 파싱, 그리고 axios adapter로 apiGetPage 전체 경로 포함)와 npm run build(build → runtime-config 검증 → offline 검사 → integrity manifest 19개) 전체 통과. 커밋 e05b613.
보류 아이디어:
ensureAdminSession(force=true)가 진행 중인 비강제 요청 promise를 그대로 반환하는 재진입 버그 (가치 3 / 위험 2 / S)
monitoringParser.toPod이 restarts에 비숫자 문자열이 오면 NaN을 그대로 표에 노출하는 문제 (가치 2 / 위험 1 / S)
monitoringParser.findRows 재귀에 깊이 제한·순환 참조 방어 추가 (가치 2 / 위험 1 / S)
shared/format.ts 단위 테스트 공백 보강 (가치 2 / 위험 1 / S)
루트 앱의 crypto-js local tarball 의존성 제거로 clean install 복구 — 현재 npm ci 자체가 불가해 검증 비용 큼 (가치 4 / 위험 4 / M)
2026-09-03
선택: API 401 응답에서 캐시된 관리자 세션 무효화 (가치 4 / 위험 1 / 작업량 S)
결과: 성공
요약: 쿠키 세션이 만료돼 API가 401을 돌려줘도 ensureAdminSession()이 state.initialized && is_admin === 'Y' 캐시를 그대로 신뢰해 라우트를 옮겨도 /sso/ssologin을 다시 호출하지 않았고, 사용자는 전체 새로고침 전까지 모든 조회가 “API 요청에 실패했습니다.”로 끝나는 화면에 갇혔다(router guard는 navigation 때만 돌고 http layer에는 401 처리가 전혀 없었다). http.ts에 setUnauthorizedHandler 등록 지점을 두어 401에서만(403은 인가 실패이므로 제외) handler를 호출하고, session.ts가 localStorage와 reactive 상태를 비우는 invalidateAdminSession()을 module scope에서 등록하도록 했다(session→http 단방향 import라 순환 없음). 서버가 message를 주지 않는 401·403·네트워크 단절에는 다음 행동을 알 수 있는 문구를 쓰도록 fallbackErrorMessage를 분리했다. 검증은 npm run verify(typecheck + vitest 60개 통과, 기존 52개 + 신규 8개로 axios adapter가 던지는 401/403/서버 message 우선/handler 예외 격리, 그리고 vi.mock('@/api/http') + vi.resetModules()로 세션 캐시 재사용·무효화 후 재요청 시나리오 포함)와 npm run build(build → runtime-config 검증 → offline 검사 → integrity manifest 19개) 전체 통과. 커밋 0f98a0a.
보류 아이디어:
ensureAdminSession(force=true)가 진행 중인 비강제 요청 promise를 그대로 반환하는 재진입 버그 (가치 3 / 위험 2 / S)
CatalogView·ContentAccessView가 route query를 onMounted에서만 읽어 같은 경로의 query 변경(딥링크 재진입)에 반응하지 않는 문제 (가치 3 / 위험 2 / M)
monitoringParser.toPod이 kubectl 스타일 restarts("3 (5d ago)")에서 NaN을 표에 노출하는 문제 (가치 2 / 위험 1 / S)
AdvancedPolicyView가 snapshot.extensions를 v-model로 직접 변형해 저장 실패 시 화면과 서버 상태가 어긋나는 문제 (가치 2 / 위험 2 / S)
루트 앱의 crypto-js local tarball 의존성 제거로 clean install 복구 — 현재 npm ci 자체가 불가해 검증 비용 큼 (가치 4 / 위험 4 / M)
2026-09-04
선택: 뒤늦게 도착한 401이 방금 확보한 세션을 지우는 문제 수정 (가치 4 / 위험 2 / 작업량 S)
결과: 성공
요약: 목록 화면은 한 번에 여러 요청을 보내므로 세션 만료 시 401도 여러 개 돌아오는데, 첫 401이 세션을 버리고 router guard가 /sso/ssologin으로 세션을 다시 확보한 뒤 남은 401이 도착하면 방금 저장한 user와 access token을 다시 지웠다(직전 세션에서 도입한 401 handler가 무조건 invalidateAdminSession()을 부른 탓). 그러면 이후 요청이 access_token 헤더 없이 나가 또 401을 받는 악순환이 생긴다. http.ts가 요청 interceptor에서 세션 세대를 config에 새겨 401 handler에 넘기고, session.ts가 그 세대가 현재 세대와 같을 때만 캐시를 버리도록 했다(확보·무효화·로그아웃·다른 탭 storage 변경마다 세대 증가 → 같은 세대의 중복 401도 한 번만 처리). 로드맵 P2가 요구하는 “단일 상태기계” 방향으로 ensureAdminSession(force=true)가 진행 중인 비강제 promise를 재사용하던 재진입 결함도 함께 고쳤다. 검증은 npm run verify(typecheck + vitest 64개 통과, 기존 60개 + 신규 4개)와 npm run build(build → runtime-config 검증 → offline 검사 → integrity manifest 19개) 전체 통과, 그리고 두 수정을 각각 되돌려 신규 테스트가 실제로 실패하는지 직접 확인했다. 커밋 f02fb08.
보류 아이디어:
CatalogView·ContentAccessView가 route query를 onMounted에서만 읽어 같은 경로의 query 변경(딥링크 재진입)에 반응하지 않는 문제 — 컴포넌트 테스트 환경(jsdom, @vue/test-utils) 선행 정비 필요 (가치 3 / 위험 3 / M)
monitoringParser.toPod이 kubectl 스타일 restarts("3 (5d ago)")나 boolean ready에서 NaN·”true”를 표에 그대로 노출하는 문제 (가치 2 / 위험 1 / S)
AdvancedPolicyView가 snapshot.extensions를 v-model로 직접 변형해 저장 실패 시 화면과 서버 상태가 어긋나는 문제 (가치 2 / 위험 2 / S)
shared/format.ts 단위 테스트 공백 보강 + formatDateTime이 epoch millis를 날짜로 해석하지 못하고 원시 숫자 문자열을 그대로 보여주는 문제 (가치 2 / 위험 1 / S)
루트 앱의 crypto-js local tarball 의존성 제거로 clean install 복구 — 현재 npm ci 자체가 불가해 검증 비용 큼 (가치 4 / 위험 4 / M)
2026-09-05
선택: 기존 backend의 400 + 토큰 code를 세션 만료로 처리 (가치 5 / 위험 1 / 작업량 S)
결과: 성공
요약: 지난 두 세션이 고친 401 세션 정리 경로가 실환경에서는 아예 동작하지 않았다. 루트 앱 src/api/common/interceptors.js:192-195와 docs/API_GUIDE.md(오류와 재시도 표), docs/ARCHITECTURE.md가 모두 기록하듯 기존 backend는 토큰 문제를 HTTP 401이 아니라 HTTP 400 + body code 401(토큰 없음)/403(만료)/405(토큰 정보 오류) 로 알리는데, admin-v2 http.ts는 error.response?.status === 401만 봤다. 그래서 세션이 끝나도 캐시된 관리자 세션이 계속 유효해 보이고 router guard가 /sso/ssologin을 다시 부르지 않아 사용자는 새로고침 전까지 모든 조회가 “API 요청에 실패했습니다.”로 끝나는 화면에 갇혔다. 판정을 isSessionExpired() 한곳에 모아 세션 정리 handler 호출과 fallbackErrorMessage가 같은 기준을 쓰게 했고(HTTP 403 인가 실패와 BZ01 같은 400 업무 오류는 제외), admin-v2 ARCHITECTURE.md에 이 신호 계약을 문서화했다. 검증은 npm run verify(typecheck + vitest 70개 통과, 기존 64개 + 신규 6개로 isSessionExpired 단위 판정 3개와 axios adapter를 통한 400+403 handler 호출·400+BZ01 미호출·fallback 문구)와 npm run build(build → runtime-config 검증 → offline 검사 → integrity manifest 19개) 전체 통과, 그리고 수정을 되돌려 신규 interceptor 테스트 2개가 실제로 실패하는지 직접 확인했다. 커밋 c2b6751.
보류 아이디어:
세션 만료 판정 후 refresh token으로 원요청을 1회 재시도하는 경로 도입 — 루트 앱은 이미 하지만 admin-v2는 SSO 재확인에만 의존한다 (가치 3 / 위험 3 / M)
CatalogView·ContentAccessView가 route query를 onMounted에서만 읽어 같은 경로의 query 변경(딥링크 재진입)에 반응하지 않는 문제 — 컴포넌트 테스트 환경(jsdom, @vue/test-utils) 선행 정비 필요 (가치 3 / 위험 3 / M)
AdvancedPolicyView가 snapshot.extensions를 v-model로 직접 변형해 저장 실패 시 화면과 서버 상태가 어긋나는 문제 (가치 2 / 위험 2 / S)
monitoringParser.toPod이 kubectl 스타일 restarts("3 (5d ago)")에서 NaN을 표에 그대로 노출하는 문제 (가치 2 / 위험 1 / S)
shared/format.ts 단위 테스트 공백 보강 + formatDateTime이 epoch millis를 날짜로 해석하지 못하는 문제 (가치 2 / 위험 1 / S)
2026-09-06
선택: 세션 만료를 라우트 이동 없이 즉시 복구 (가치 4 / 위험 2 / 작업량 M)
결과: 성공
요약: 지난 세 세션이 만든 만료 감지 경로(HTTP 401과 기존 backend의 400 + 토큰 code)는 캐시된 관리자 세션을 버리기만 하고, 다시 확인하는 주체는 router.beforeEach뿐이었다. 그래서 한 화면에 머무르며 검색·페이지 이동·운영 현황 자동 갱신만 하는 사용자는 라우트를 옮기기 전까지 모든 조회가 “세션이 만료됐습니다. 다시 로그인하십시오.”로 끝나는 화면에 갇혔다(루트 앱의 refresh 재시도는 별도 python backend endpoint라 admin-v2의 backendBaseUrl 계약 밖이므로 채택하지 않았다). session.ts에 setSessionExpiredHandler()를 두어 캐시를 버린 직후 복구를 알리고, guard 본문을 새 auth/guard.ts의 resolveAdminAccess(force)(allow/redirected/denied)로 옮겨 router guard와 만료 복구가 같은 판단과 같은 SSO 왕복 제한을 공유하게 했다. /sso/ssologin 자체가 만료 응답을 받는 경우는 확인이 끝나기 전에 또 확인을 시작하는 재귀가 되므로 state.checking으로 걸러낸다. 검증은 npm run verify(typecheck + vitest 80개 통과, 기존 70개 + 신규 10개로 resolveAdminAccess 6가지 판단과 만료 handler 호출·오래된 세대 무시·확인 중 재귀 차단·handler 예외 격리)와 npm run build(build → runtime-config 검증 → offline 검사 → integrity manifest 19개) 전체 통과, 그리고 만료 알림 호출과 재귀 차단 가드를 각각 되돌려 신규 테스트가 실제로 실패하는지 직접 확인했다. 커밋 e425b54.
보류 아이디어:
PaginationBar가 요청 페이지를 유지하다 총건수가 줄면 범위 밖 페이지에 머물러 “31–25 / 25” 같은 표시를 내는 문제 — 컴포넌트 테스트 환경 선행 필요 (가치 2 / 위험 2 / S)
CatalogView·ContentAccessView가 route query를 onMounted에서만 읽어 같은 경로의 query 변경(딥링크 재진입)에 반응하지 않는 문제 — 컴포넌트 테스트 환경(jsdom, @vue/test-utils) 선행 정비 필요 (가치 3 / 위험 3 / M)
AdvancedPolicyView가 snapshot.extensions를 v-model로 직접 변형해 저장 실패 시 화면과 서버 상태가 어긋나는 문제 (가치 2 / 위험 2 / S)
monitoringParser.toPod이 kubectl 스타일 restarts("3 (5d ago)")에서 NaN을 표에 그대로 노출하는 문제 (가치 2 / 위험 1 / S)
shared/format.ts 단위 테스트 공백 보강 + formatDateTime이 epoch millis·yyyyMMddHHmmss를 날짜로 해석하지 못하는 문제 (가치 2 / 위험 1 / S)
2026-09-06
선택: 목록의 날짜 값을 시간대·형식에 흔들리지 않게 표시 (가치 3 / 위험 1 / 작업량 S)
결과: 성공
요약: shared/format.ts는 단위 테스트가 전혀 없었고 formatDateTime이 new Date(String(value)) 하나로 모든 입력을 처리해, 자료실·공유 앱·정책·콘텐츠 접근·운영 현황 표의 날짜 칸이 두 가지로 어긋났다. (1) 날짜만 있는 2026-08-25는 UTC 자정으로 해석돼 KST에서는 없는 시각 “09:00”이 붙고 UTC-8 환경에서는 전날로 밀린다(node로 직접 재현 확인). (2) Java backend가 흔히 문자열로 내려주는 yyyyMMdd·yyyyMMddHHmmss와 epoch millis는 해석하지 못해 20260825143005 같은 원시 숫자가 그대로 표에 남는다. offset이 없는 값은 브라우저 구현에 맡기지 않고 지역 시각으로 직접 만들고, 날짜만 있는 값은 시각 없이 날짜로만 표시하며, Z·+09:00 offset은 그대로 존중하고, 자릿수로 단위를 단정할 수 없는 숫자(10자리 등)나 달력에 없는 날짜(2026-13-01)는 조작하지 않고 원본을 남기도록 했다. 이 계약을 admin-v2 ARCHITECTURE.md에 문서화했다. 검증은 npm run verify(typecheck + vitest 83개 통과, 기존 70개 + 신규 13개)와 npm run build(build → runtime-config 검증 → offline 검사 → integrity manifest 19개) 전체 통과, 그리고 신규 테스트를 KST·UTC·America/Los_Angeles 세 시간대에서 각각 돌려 기대값이 시간대에 의존하지 않는지 확인했다. 커밋 ad2f001.
보류 아이디어:
monitoringParser.toPod이 kubectl 스타일 restarts("3 (5d ago)")에서 NaN을 표에 그대로 노출하는 문제 (가치 2 / 위험 1 / S)
CatalogView·ContentAccessView가 route query를 onMounted에서만 읽어 같은 경로의 query 변경(딥링크 재진입)에 반응하지 않는 문제 — 컴포넌트 테스트 환경(jsdom, @vue/test-utils) 선행 정비 필요 (가치 3 / 위험 3 / M)
AdvancedPolicyView가 snapshot.extensions를 v-model로 직접 변형해 저장 실패 시 화면과 서버 상태가 어긋나는 문제 (가치 2 / 위험 2 / S)
PaginationBar가 범위 밖 페이지(총건수 감소)에 머물면 “31–25 / 25”로 표시되고 이전 버튼이 move() 가드에 막혀 아무 동작도 하지 않는 문제 (가치 2 / 위험 2 / S)
루트 앱의 crypto-js local tarball 의존성 제거로 clean install 복구 — 현재 npm ci 자체가 불가해 검증 비용 큼 (가치 4 / 위험 4 / M)
2026-09-07
선택: 딥링크 query 변경에 화면이 반응하지 않는 문제 (가치 3 / 위험 2 / 작업량 M)
결과: 성공
요약: 사용자·부서 화면이 거는 #/content?tab=access&userId=…와 #/catalog?tab=shared&userId=… 링크는 경로가 같고 query만 바뀌므로 vue-router가 화면을 다시 mount하지 않는데, 두 화면 모두 queryValue()를 setup/onMounted에서만 읽어 이전 사용자의 조건과 결과가 그대로 남았다(사이드바로 같은 화면을 다시 눌러 query를 비워도 마찬가지). shared/routeQuery.ts에 useRouteQuerySync()를 두어 진입과 query 변경이 같은 적용 함수를 부르게 하고, 화면이 쓰지 않는 query가 바뀌거나 다른 경로로 떠나는 중(route는 컴포넌트보다 먼저 바뀐다)이면 조회하지 않도록 진입 경로를 기억해 걸렀다. 세 세션째 “컴포넌트 테스트 환경 선행 필요”로 미뤄지던 항목이라 jsdom·@vue/test-utils를 devDependency로 넣고 파일 단위 // @vitest-environment jsdom으로 전역 설정 변경 없이 두 화면의 딥링크 재진입 테스트를 붙였다. 검증은 npm run verify(typecheck + vitest 83개 통과, 기존 70개 + 신규 13개)와 npm run build(build → runtime-config 검증 → offline 검사 → integrity manifest 19개) 전체 통과, 그리고 query 감시를 꺼서 신규 컴포넌트 테스트 5개가 실제로 실패하는지 직접 확인했다. 커밋 c99bca9.
보류 아이디어: monitoringParser.toPod이 kubectl 스타일 restarts("3 (5d ago)")에서 NaN을 표에 노출 (2/1/S) · AdvancedPolicyView가 snapshot.extensions를 v-model로 직접 변형해 저장 실패 시 화면과 서버가 어긋남 (2/2/S) · PaginationBar가 범위 밖 페이지에서 “31–25 / 25”를 표시하고 이전 버튼이 move() 가드에 막힘 — 이제 컴포넌트 테스트로 검증 가능 (2/2/S) · CatalogView·DirectoryView가 두 탭이 공유하는 loading·error를 써서 한쪽 탭의 오류가 다른 탭에 남음 (2/2/S) · 루트 앱의 crypto-js local tarball 의존성 제거로 clean install 복구 (4/4/M)
2026-09-08
선택: 자동 갱신이 운영 현황 표를 주기마다 비우는 문제 수정 (가치 3 / 위험 1 / 작업량 S)
결과: 성공
요약: OperationsView는 자동 갱신을 켜면 pollIntervalMs(운영 기본 30초)마다 load()를 부르는데, loading ref 하나로 표 전체를 LoadingBlock skeleton으로 갈아끼워 사용자가 보던 POD 목록과 스크롤 위치가 30초마다 사라졌다(같은 이유로 수동 새로고침도 표를 비웠다). 결과가 아직 없는 첫 조회에서만 skeleton을 보여주는 initialLoading과, 이미 결과가 있을 때만 참인 refreshing으로 나눠 이후 갱신은 이전 결과를 유지한 채 헤더의 “갱신 중…” 표시로만 알리도록 했다(실패해도 이전 snapshot이 남으므로 기존 오류 배너 동작은 그대로다). 테스트가 없던 화면이라 OperationsView.test.ts를 새로 만들어 첫 조회 skeleton·자동 갱신 중 목록 유지·수동 새로고침·unmount 시 타이머 정리 4개를 덮었고, 검증은 npm run build(typecheck + vitest 87개 통과, 기존 83개 + 신규 4개 → vite build → runtime-config 검증 → offline 검사 → integrity manifest 19개) 전체 통과, 그리고 수정을 되돌려 신규 테스트 2개가 실제로 실패하는지 직접 확인했다. 커밋 e7b7527.
보류 아이디어: monitoringParser.toPod이 kubectl 스타일 restarts(“3 (5d ago)”)에서 NaN을 표에 노출 (2/1/S) · CatalogView·DirectoryView·ContentAccessView가 두 탭이 공유하는 loading·error를 써서 느린 탭의 응답이 다른 탭의 skeleton을 풀어버림 (2/2/S) · PaginationBar가 범위 밖 페이지에서 “31–25 / 25”를 표시하고 이전 버튼이 move() 가드에 막힘 (2/2/S) · AdvancedPolicyView가 snapshot.extensions를 v-model로 직접 변형해 저장 실패 시 화면과 서버가 어긋남 (2/2/S) · 루트 앱의 crypto-js local tarball 의존성 제거로 clean install 복구 (4/4/M)
2026-09-08
선택: 탭이 공유하는 loading·error 상태가 다른 탭으로 새는 문제 수정 (가치 3 / 위험 2 / 작업량 S)
결과: 성공
요약: CatalogView(자료실·공유 앱), DirectoryView(사용자·부서), ContentAccessView(메뉴 트리·접근 권한)는 두 탭이 서로 다른 API를 각자의 createRequestGuard로 조회하면서 loading·error ref 하나를 함께 썼다. 그래서 (1) 느린 탭이 조회 중이면 이미 결과가 있는 다른 탭도 skeleton으로 바뀌고, (2) 그 응답이 끝나면 지금 보는 탭이 아직 조회 중인데도 loading이 풀려 “조회된 …이 없습니다” 빈 표가 잠깐 보이고, (3) 탭을 옮긴 뒤 도착한 실패 문구가 지금 보는 탭 위에 남았다(changeTab이 error를 지우는 임시방편은 전환 시점 이전의 오류만 가렸다). 세 화면 모두 탭별 tabLoading·tabError를 reactive 레코드로 두고 활성 탭의 값만 computed로 노출해, 템플릿은 그대로 두고 상태만 탭 범위로 좁혔다(임시방편이던 changeTab의 error 초기화는 걷어냈다 — 대상 탭이 다시 조회하면 그 탭의 error만 지워진다). 테스트가 없던 DirectoryView에 DirectoryView.test.ts를 새로 만들어 4개(다른 탭 조회 중에도 결과 유지 / 느린 탭 응답이 다른 탭 skeleton을 풀지 않음 / 탭 이동 후 도착한 실패가 새지 않음 / 보고 있는 탭의 실패는 그대로 표시)를, CatalogView.test.ts에 2개를 더했다. 검증은 npm run build(typecheck + vitest 93개 통과, 기존 87개 + 신규 6개 → vite build → runtime-config 검증 → offline 검사 → integrity manifest 19개) 전체 통과, 그리고 세 화면의 수정을 되돌려 신규 테스트 6개 중 5개가 실제로 실패하는지 직접 확인했다(나머지 1개는 기존 동작을 고정하는 가드). 커밋 4ec1f38.
보류 아이디어: monitoringParser.toPod이 kubectl 스타일 restarts(“3 (5d ago)”)에서 NaN을 표에 노출 (2/1/S) · PaginationBar가 범위 밖 페이지에서 “31–25 / 25”를 표시하고 이전 버튼이 move() 가드에 막힘 (2/2/S) · AdvancedPolicyView가 snapshot.extensions를 v-model로 직접 변형해 저장 실패 시 화면과 서버가 어긋남 (2/2/S) · OperationsView 자동 갱신이 document.hidden 동안에도 30초마다 폴링 (2/2/S) · 루트 앱의 crypto-js local tarball 의존성 제거로 clean install 복구 (4/4/M)
2026-09-09
선택: 총건수가 줄어 범위를 벗어난 페이지 복구 (가치 3 / 위험 2 / 작업량 S)
결과: 성공
요약: 기존 backend의 pageInfo는 totalCount만 담는 경우가 많아 unwrapPageEnvelope가 요청 페이지를 그대로 유지한다. 그래서 사용자 포털에서 자료가 지워져 총건수가 줄면 보고 있던 페이지가 범위를 벗어난 채 남고 backend는 빈 목록을 주는데, 화면은 빈 표 위에 “31–25 / 25”처럼 뒤집힌 구간을 띄우고 페이지가 마지막 페이지보다 두 칸 이상 앞서면 이전 버튼이 활성 상태인데도 move() 가드에 막혀 아무 동작도 하지 않아 사용자가 스스로 빠져나올 수 없었다. shared/paging.ts의 lastPageWhenOutOfRange()로 응답이 범위 밖인지 확인해 네 목록(사용자·부서·자료실·공유 앱)이 마지막 페이지를 한 번 더 읽게 했고(돌려주는 번호가 항상 요청 페이지보다 작아 되짚기가 반드시 끝나고, 총건수 0은 결과가 정말 없는 것이므로 되짚지 않는다), PaginationBar는 범위 밖 페이지를 존재하는 페이지로 맞춰 표시·이동하게 해 되짚기가 실패해도 화면이 어긋나지 않게 했다. 이 계약을 admin-v2 ARCHITECTURE.md에 문서화했다. 검증은 npm run build(typecheck + vitest 106개 통과, 기존 93개 + 신규 13개 → vite build → runtime-config 검증 → offline 검사 → integrity manifest 19개) 전체 통과, 그리고 PaginationBar와 DirectoryView 수정을 각각 되돌려 신규 테스트 3개가 실제로 실패하는지 직접 확인했다. 커밋 815b010.
보류 아이디어: monitoringParser.toPod이 kubectl 스타일 restarts(“3 (5d ago)”)에서 NaN을 표에 노출 (2/1/S) · OperationsView 자동 갱신이 document.hidden 동안에도 30초마다 폴링해 만료 세션에서는 SSO 재확인까지 반복 (3/2/S) · AdvancedPolicyView가 snapshot.extensions를 v-model로 직접 변형해 저장 실패 시 화면과 서버가 어긋남 (2/2/S) · ContentAccessView·CatalogView가 조회 결과 0건인 탭을 누를 때마다 같은 요청을 되풀이 (2/1/S) · 루트 앱의 crypto-js local tarball 의존성 제거로 clean install 복구 (4/4/M)
2026-09-09
선택: 보이지 않는 탭에서는 운영 현황 폴링을 멈춘다 (가치 3 / 위험 2 / 작업량 S)
결과: 성공
요약: OperationsView의 자동 갱신은 pollIntervalMs(운영 기본 30초)마다 GET /monitoring을 부르는데, 타이머가 탭 표시 상태와 무관해 사용자가 다른 탭·다른 창으로 옮겨간 뒤에도 아무도 보지 않는 화면 때문에 오프라인망 backend가 namespace를 계속 훑어야 했다(자동 갱신을 켠 채 하루를 두면 그것만으로 2천 번이 넘는다). 자동 갱신 ON/OFF는 사용자의 의사로 그대로 두고 실제 폴링만 document.hidden이 아닌 동안으로 좁혔으며(syncTimer() 한 곳이 타이머 수명을 판단한다), 탭이 다시 보이는 순간에는 멈춰 있는 동안 낡은 화면을 다음 주기까지 두지 않도록 한 번 조회하고 폴링을 재개한다 — 이미 보이는 탭에서 온 visibilitychange는 재개가 아니므로 추가 조회하지 않는다. visibilitychange 구독은 onBeforeUnmount에서 타이머와 함께 정리한다. 이 계약을 admin-v2 ARCHITECTURE.md에 문서화했다. 검증은 npm run build(typecheck + vitest 111개 통과, 기존 106개 + 신규 5개 → vite build → runtime-config 검증 → offline 검사 → integrity manifest 19개) 전체 통과, 그리고 document.hidden 판정과 구독 해제를 각각 되돌려 신규 테스트 5개가 모두 실제로 실패하는지 직접 확인했다. 기존 테스트가 mount한 컴포넌트를 unmount하지 않아 다음 테스트의 visibilitychange까지 받는 누수가 있어 afterEach 정리도 함께 넣었다. 커밋 ccd146d.
보류 아이디어: monitoringParser.toPod이 kubectl 스타일 restarts(“3 (5d ago)”)에서 NaN을 표에 노출 (2/1/S) · ContentAccessView·CatalogView가 조회 결과 0건인 탭을 누를 때마다 같은 요청을 되풀이 (2/1/S) · AdvancedPolicyView가 snapshot.extensions를 v-model로 직접 변형해 저장 실패 시 화면과 서버가 어긋남 (2/2/S) · AdvancedPolicyView가 저장할 때마다 load()로 패널 전체를 skeleton으로 되돌림 (2/1/S) · 루트 앱의 crypto-js local tarball 의존성 제거로 clean install 복구 (4/4/M)
2026-09-09
선택: 목록의 날짜 값을 시간대·형식에 흔들리지 않게 표시 (가치 3 / 위험 1 / 작업량 S)
결과: 성공
요약: 원장에는 2026-09-06 회차가 이 문제를 고쳤다고 적혀 있지만, 그 커밋 ad2f001은 로컬 브랜치 auto/2026-09-06-2330에만 남고 원격 브랜치도 PR도 없이 main에 들어오지 않아(git merge-base --is-ancestor ad2f001 main → NO) 버그가 그대로 살아 있었다. 실제로 재현해 확인했다 — formatDateTime('2026-08-25')는 KST에서 없는 시각 “2026. 08. 25. 09:00”을 붙이고 America/Los_Angeles에서는 “2026. 08. 24. 17:00”로 하루가 밀리며, Java backend가 문자열로 내려주는 20260825·20260825143005와 epoch millis는 원시 값 그대로 표에 남는다. 잃어버린 변경을 되살려(ARCHITECTURE.md는 그 뒤 추가된 “범위 밖 페이지”·”자동 갱신”·”딥링크 query” 절과 충돌해 양쪽을 모두 남기도록 직접 병합했다) offset 없는 값은 지역 시각으로 직접 만들고, 날짜만 있는 값은 시각 없이 날짜로만 표시하며, Z·+09:00 offset은 존중하고, 자릿수로 단위를 단정할 수 없는 숫자(10자리 등)나 달력에 없는 날짜(2026-13-01)는 조작하지 않고 원본을 남기게 했다. 검증은 npm run build(typecheck + vitest 124개 통과, 기존 111개 + 신규 13개 → vite build → runtime-config 검증 → offline 검사 → integrity manifest 19개) 전체 통과, 신규 테스트를 Asia/Seoul·UTC·America/Los_Angeles·Pacific/Kiritimati 네 시간대에서 각각 돌려 기대값이 시간대에 의존하지 않는지 확인했고, format.ts만 이전 구현으로 되돌려 신규 테스트 3개가 실제로 실패하는지 직접 확인했다. 커밋 732897a.
보류 아이디어: 세션 만료를 라우트 이동 없이 즉시 복구 — 커밋 e425b54(원장 2026-09-06 ‘성공’)도 브랜치 auto/2026-09-06-0140에만 남고 main에 없다. setSessionExpiredHandler()·auth/guard.ts가 지금 코드에 없어 다음 회차 최우선 후보 (4/2/M) · monitoringParser.toPod이 kubectl 스타일 restarts(“3 (5d ago)”)에서 NaN을 표에 노출 (2/1/S) · ContentAccessView·CatalogView가 조회 결과 0건인 탭을 누를 때마다 같은 요청을 되풀이 (2/1/S) · AdvancedPolicyView가 저장할 때마다 load()로 패널 전체를 skeleton으로 되돌림 (2/1/S) · AdvancedPolicyView가 snapshot.extensions를 v-model로 직접 변형해 저장 실패 시 화면과 서버가 어긋남 (2/2/S) · PolicyCenterView·AdvancedPolicyView·OverviewView가 createRequestGuard 없이 조회해 새로고침을 연달아 누르면 늦게 온 이전 응답이 최신 결과를 덮어씀 (2/1/S) · 루트 앱의 crypto-js local tarball 의존성 제거로 clean install 복구 (4/4/M)
2026-09-10
선택: 세션 만료를 라우트 이동 없이 즉시 복구 — 잃어버린 커밋 e425b54 복구 (가치 4 / 위험 2 / 작업량 M)
결과: 성공
요약: git branch --no-merged main으로 확인한 대로 원장에 ‘성공’으로 적힌 e425b54가 브랜치 auto/2026-09-06-0140에만 남고 main에 없어, setSessionExpiredHandler()와 auth/guard.ts가 지금 코드에 없었다(src/auth에 session.ts·ssoRedirect.ts·storage.ts뿐, router.beforeEach도 e425b54 이전 형태 그대로). 그래서 세션 만료 응답은 캐시된 관리자 세션을 버리기만 하고 다시 확인하는 주체는 router guard뿐이라, 한 화면에 머무르며 검색·페이지 이동·자동 갱신만 하는 사용자는 라우트를 옮기기 전까지 모든 조회가 실패하는 화면에 갇혔다. 지금 코드 기준으로 재검토한 뒤(현재 router.ts·session.ts가 e425b54의 base와 동일해 cherry-pick이 충돌 없이 적용됐고, ARCHITECTURE.md는 그 뒤 추가된 절들과 자동 병합됐다) 복구했다 — session.ts가 캐시를 버린 직후 만료 handler를 부르고, guard 본문을 새 auth/guard.ts의 resolveAdminAccess(force)(allow/redirected/denied)로 옮겨 router guard와 만료 복구가 같은 판단과 같은 SSO 왕복 제한을 공유하며, /sso/ssologin 자체가 만료 응답을 받는 재귀는 state.checking으로 걸러낸다. 여기에 더해, 이 변경이 두 번이나 조용히 사라져도 아무도 알아채지 못한 이유인 router.ts 배선(만료 handler 등록과 세 판단 처리)에 테스트가 전혀 없던 공백을 src/app/router.test.ts 8개로 덮었다(guard 세 갈래 + public 라우트, 만료 복구의 강제 재확인·화면 유지·access-denied 이동·SSO 이동 중 덧씌우지 않음). 검증은 npm run build(typecheck + vitest 142개 통과, 기존 124개 + 복구 10개 + 신규 8개 → vite build → runtime-config 검증 → offline 검사 → integrity manifest 19개) 전체 통과, 그리고 만료 알림 호출·재귀 차단 가드·force 전달을 각각 되돌리고 router 배선을 네 가지로 변형해(handler 등록 제거, redirected를 access-denied로 처리, 복구 실패 시 이동 안 함, 복구가 캐시 세션 사용) 해당 테스트가 정확히 실패하는지 모두 직접 확인했다. 커밋 c1a4d7b.
보류 아이디어: monitoringParser.toPod이 kubectl 스타일 restarts(“3 (5d ago)”)에서 NaN을 표에 노출 (2/1/S) · 결과 0건인 탭을 누를 때마다 ContentAccessView·CatalogView·DirectoryView가 같은 조회를 되풀이 (2/1/S) · PolicyCenterView·AdvancedPolicyView·OverviewView가 createRequestGuard 없이 조회해 늦게 온 이전 응답이 최신 결과를 덮어씀 (2/1/S) · AdvancedPolicyView가 저장할 때마다 load()로 패널 전체를 skeleton으로 되돌림 (2/1/S) · 다른 탭에서 로그아웃해도(storage 이벤트) 이 탭은 라우트를 옮길 때까지 복구하지 않음 — 이번에 만든 만료 handler를 재사용하면 되는 후속 (2/2/S) · AdvancedPolicyView가 snapshot.extensions를 v-model로 직접 변형 (2/2/S) · 루트 앱의 crypto-js local tarball 의존성 제거로 clean install 복구 (4/4/M)
2026-09-10
선택: 재조회 중에도 정책·Overview 화면을 비우지 않는다 (가치 3 / 위험 1 / 작업량 M)
결과: 성공
요약: 보류 아이디어 두 개(createRequestGuard 없는 세 화면, 저장할 때마다 패널 전체를 skeleton으로 되돌리는 AdvancedPolicyView)가 사실 같은 load() 하나의 문제라 함께 고쳤다. OverviewView·PolicyCenterView·AdvancedPolicyView는 loading ref 하나로 화면 전체를 LoadingBlock으로 바꿔, 랜딩 화면인 Overview에서 새로고침을 누르거나 정책을 저장할 때마다 보고 있던 수치·패널과 스크롤 위치가 통째로 사라졌다 돌아왔다. OperationsView에서 쓴 처방 그대로 결과가 아직 없을 때만 참인 initialLoading과 이미 결과가 있을 때만 참인 refreshing으로 나눠 이후 조회는 이전 결과를 유지한 채 헤더의 “갱신 중…” 표시로만 알린다. 여기에 더해 저장 중에는 새로고침 버튼이 잠기지 않아(saving만 걸리고 loading은 false다) 사용자의 새로고침과 runSave가 부르는 재조회가 겹칠 수 있었고, 늦게 도착한 이전 응답이 hydrate()로 최신 snapshot과 form 값을 덮어썼다 — 세 화면 모두 shared/async.ts의 기존 createRequestGuard를 붙여 최신 ticket만 결과·오류·loading을 반영하게 했다. 테스트가 전혀 없던 세 화면에 컴포넌트 테스트 8개를 새로 붙였고(첫 조회 skeleton / 재조회 중 화면 유지 + 진행 표시 / 저장 중 시작한 새로고침이 늦게 도착해도 최신 정책을 덮어쓰지 않음), 이 계약을 admin-v2 ARCHITECTURE.md에 기록했다. 검증은 npm run build(typecheck + vitest 132개 통과, 기존 124개 + 신규 8개 → vite build → runtime-config 검증 → offline 검사 → integrity manifest 19개) 전체 통과, 그리고 initialLoading 분리와 ticket 가드를 각각 되돌려 신규 테스트 3개와 2개가 정확히 실패하는지 직접 확인했다. 커밋 1709f01. (참고: 직전 회차 커밋 c1a4d7b는 브랜치 auto/2026-09-10-1113에 있고 아직 main에 병합되지 않아 이번 base에는 없다 — src/auth·src/app/router.ts는 건드리지 않아 병합 충돌 여지를 남기지 않았다.)
보류 아이디어: monitoringParser.toPod이 kubectl 스타일 restarts(“3 (5d ago)”)에서 NaN을 표에 노출 (2/1/S) · 결과 0건인 탭을 누를 때마다 ContentAccessView·CatalogView·DirectoryView가 같은 조회를 되풀이 (2/1/S) · AdvancedPolicyView가 snapshot.extensions를 v-model로 직접 변형해 저장 실패 시 화면과 서버가 어긋남 (2/2/S) · 다른 탭에서 로그아웃해도(storage 이벤트) 이 탭은 라우트를 옮길 때까지 복구하지 않음 — c1a4d7b가 main에 들어온 뒤에 가능 (2/2/S) · CatalogView·DirectoryView·ContentAccessView 테스트가 mount한 wrapper를 정리하지 않는 누수 (2/1/S) · 루트 앱의 crypto-js local tarball 의존성 제거로 clean install 복구 (4/4/M)