Files
llm-wiki/raw/branch-notes/feature-keycloak-spring-rs-role-mapping.md
T

26 KiB

title, source_type, status, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, branch, parent_branch, related_projects, tags, created, target_merge, status_label, contract_packet_sha256
title source_type status id kind project work_item inherits refines overrides depends_on contract_packet branch parent_branch related_projects tags created target_merge status_label contract_packet_sha256
branch / feature-keycloak-spring-rs-role-mapping (Keycloak role → Spring RBAC mapping) branch-note raw BR-KEYCLOAK-CHILD-783CA54B branch-child keycloak-patterns-overview WI-KEYCLOAK-PATTERNS-OVERVIEW-004
DEC-KEYCLOAK-PATTERNS-OVERVIEW-SPA-CLIENT-001@1
DEC-KEYCLOAK-PATTERNS-OVERVIEW-ACCEPTANCE-001@1
WI-KEYCLOAK-PATTERNS-OVERVIEW-003
1 feature-keycloak-spring-rs-role-mapping feature-keycloak-spring-rs-audience-validator
keycloak-patterns
branch
keycloak-patterns
p3a
implementation
spring-boot
resource-server
jwt
authorization
2026-05-25 in-progress dd660a8ddd1df3dabc7775e90818479fd5796ee4072d707242dc10c70827c701

branch: feature-keycloak-spring-rs-role-mapping (Keycloak role → Spring RBAC mapping)

Layer: raw/branch-notes/raw/branch-notes/feature-keycloak-spring-rs-audience-validator의 WI004 child branch. P3A는 실 구현 대상. 본 sub-sub는 학습 + 작업 plan 기록 — 실 구현은 /home/donghyeon/workspace/keycloak-patterns/.

정합 노트 (2026-07-14 감사): 본 노트의 Spring RS 공통 셋업 + aud 검증 내용은 형제 raw/branch-notes/feature-keycloak-spring-rs-audience-validatorowner (중복 정리). 본 노트의 고유 책임 = role → @PreAuthorize (RBAC 인가) 이며, 이는 §5 deferred authZ 트랙이다(4 패턴 authN E2E 이후 착수). hub 분류: FOLD-IN(→ audience-validator 근거) + RBAC 부분 DEFERRED.

부모 (필수)

raw/branch-notes/feature-keycloak-spring-rs-audience-validator

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 1
  • 패킷 스키마: contract_packet: 1
  • 완료 조건: foreign audience token 수용 실패를 재현하고 validator 적용 후 401을 검증한다

상속한 프로젝트 결정

Decision Ref Project Summary Branch Application Source
DEC-KEYCLOAK-PATTERNS-OVERVIEW-SPA-CLIENT-001@1 AP1은 public client와 Authorization Code + PKCE를 사용한다 AP1 Resource Server의 role claim 변환과 authorization에 적용한다 raw/project-notes/keycloak-patterns-overview
DEC-KEYCLOAK-PATTERNS-OVERVIEW-ACCEPTANCE-001@1 done-bar는 E2E success와 signature security failure 재현·해결 evidence다 audience validator parent의 security verification contract를 승계한다 raw/project-notes/keycloak-patterns-overview

브랜치 지역 결정

기존 branch-local 결정은 아래 ## Decision Evidence Map / 결정-근거 매핑의 D-row가 소유하며 이 packet에서 복제하지 않는다.

Decision ID Decision Relation Supporting Claims Status

선언한 예외

Override ID Overrides Reason Approval Status

없음.

목표

검증을 통과한 Keycloak JWT의 realm_access.roles를 Spring ROLE_* authority로 변환하고 /api/admin에 RBAC를 강제한다. RS 공통 셋업과 audience 검증은 raw/branch-notes/feature-keycloak-spring-rs-audience-validator D1/D6을 선행 계약으로 소비한다.

면접 질문: "Keycloak이 발급한 JWT를 Spring에서 어떻게 검증하나요?" → "토큰 검증은 audience owner 계약을 따르고, 이 branch에서는 realm_access.roles의 nested claim을 custom converter로 읽어 ROLE_* authority로 바꿉니다. /api/adminadmin-role을 요구하고 prefix 중복을 음성 테스트합니다."

  • 이슈:
  • PR: (별도 keycloak-patterns repo)

범위

포함 범위

  • JwtAuthenticationConverter — Keycloak realm_access.roles → Spring ROLE_*
  • /api/me endpoint: @AuthenticationPrincipal Jwt → JWT claims 반환
  • /api/admin endpoint: @PreAuthorize("hasRole('admin-role')") 또는 SecurityFilterChain matcher
  • RBAC matcher/method-security 선택과 double-prefix 음성 테스트

제외 범위

  • Opaque token introspection (Keycloak access token은 JWT)
  • Custom JWT claim 변환 (예: preferred_usernameUser 도메인 객체 매핑)
  • Spring Session / 서버 측 세션
  • Method-level security 정밀 튜닝
  • Spring RS 의존성·issuer-uri·JwtDecoder·audience value/validator·CORS → raw/branch-notes/feature-keycloak-spring-rs-audience-validator D1/D6. expected audience는 owner의 backend-client-id를 소비하며 여기서 재명세하지 않는다

근거 (필수, 최소 1개+)

TODO

  • 선행 계약 확인: raw/branch-notes/feature-keycloak-spring-rs-audience-validator D1/D6이 정상·wrong-audience E2E를 통과 — 등급: planned
  • JwtAuthenticationConverter 빈: realm_access.rolesSimpleGrantedAuthority("ROLE_" + role) 매핑 — 등급: planned
  • @RestController MeController: GET /api/me@AuthenticationPrincipal Jwt jwtMap.of("sub", jwt.getSubject(), "preferred_username", jwt.getClaim("preferred_username"), "roles", jwt.getClaim("realm_access")) 반환 — 등급: planned
  • @RestController AdminController: GET /api/admin@PreAuthorize("hasRole('admin-role')") 또는 matcher 기반 — 등급: planned
  • Dockerfile (multi-stage: gradle build → JRE 21 runtime) — 등급: planned
  • 로컬 검증: regular-user 토큰으로 /api/me 200, /api/admin 403 — 등급: planned
  • 로컬 검증: admin-user 토큰으로 /api/admin 200 — 등급: planned
  • 선행 owner의 wrong-audience 401 결과를 consume하고 본 branch에서는 RBAC 200/403만 추가 검증 — 등급: planned
  • 로컬 검증: hasRole("admin-role")(prefix 자동) vs hasRole("ROLE_admin-role")(double-prefix 버그) 대조 — 등급: planned

진행 중 메모

  • RS/audience prerequisite: raw/branch-notes/feature-keycloak-spring-rs-audience-validator D1/D4/D6이 owner다. 본 branch는 검증을 통과한 JWT만 입력으로 받는다.
  • role mapping: Keycloak token claim 구조 — realm_access: { roles: [admin-role, user-role] }. resource_access는 client별 role (out of scope).
  • /api/me 응답에 raw JWT claims 노출 신중: 학습 목적이라 OK, prod에서는 필요한 claim만 반환.
  • Spring Boot 3 + Spring Security 6 기준 lambda DSL 사용. 옛 fluent API는 deprecated.
  • (2026-07-18 자동조사) setAuthoritiesClaimName("realm_access.roles") 는 nested 미지원: 공식 확인된 사실 = nested realm_access.roles 는 이 API 로 못 읽고 custom Converter 또는 SS ≥6.4 의 ExpressionJwtGrantedAuthoritiesConverter 로만 처리(SS-15201-C2/C3). 실패하는지의 내부 원리("dot 을 경로 구분자로 안 쓰고 top-level claim 을 literal lookup")는 추정 — SS-15201 는 이를 증명하지 않으며 소스/Javadoc 별도 확인 필요. 관측 결과는 0 authority(silent 403) 로 예상. §Decision Evidence Map D6 + §구현 가이드 1 참조.

결정 사항 (decisions)

  • 2026-07-18 (delegated): RS 셋업·audience 검증은 raw/branch-notes/feature-keycloak-spring-rs-audience-validator D1/D6을 따른다.
  • 2026-05-25: realm-global 권한만 필요한 baseline에서는 realm role만 매핑한다. client-specific 권한 namespace가 필요하면 resource_access.<client>.roles variant를 별도 결정한다. client 개수 자체는 선택 근거가 아니다.
  • 2026-05-25: @PreAuthorize 대신 SecurityFilterChain matcher 우선. 이유: 권한 정책 한 곳 집중 → 면접 답변 일관성.
  • 2026-07-18: realm role → authority 매핑에 custom Converter<Jwt, Collection<GrantedAuthority>> 채택 (setAuthoritiesClaimName 폐기). 이유: setAuthoritiesClaimName 은 nested realm_access.roles 를 파싱 못함(literal top-level lookup, silent 403). 대안: Boot ≥3.4 로 pin 시 ExpressionJwtGrantedAuthoritiesConverter + SpEL 한 줄. 근거: SS-15201, SSRS-JWT-C4, betweendata 사례. (자동조사 /branch-spec)
  • 2026-07-18: D5(RBAC 강제 지점)의 UNSUPPORTED_DECISION 해소 — 근거 확보. 기본 A(HTTP matcher), 조건부 B(@PreAuthorize+catch-all)/C(defense-in-depth). 근거: 공식 authorize-http-requests / method-security / features-authorization. (자동조사 /branch-spec)

결정-근거 매핑

각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. Decision ID 는 본 branch-note 안에서 안정적으로 유지. 선택 조건 열: "이 조건일 때 이 결정, 다른 조건이면 어떤 대안". 분기 없으면 N/A. 정합 (2026-07-14): RS-common(D1·D2·D3)은 형제 raw/branch-notes/feature-keycloak-spring-rs-audience-validator 가 owner — 본 노트 in-scope 는 role→RBAC(D4·D5·D6). 상세는 §Audit & Findings.

Decision ID Decision 선택 조건 (언제 이 결정 / 언제 대안) Supporting Claims Evidence Strength Open Risk
D1 DELEGATEDraw/branch-notes/feature-keycloak-spring-rs-audience-validator D6의 RS 공통 셋업을 consume RBAC는 검증 완료 JWT 위에 얹힘 owner D6 delegated 본 branch에서 버전·decoder wiring을 재명세하지 않음
D2 DELEGATEDraw/branch-notes/feature-keycloak-spring-rs-audience-validator D1/D4의 audience 계약(backend-client-id)을 consume expected audience 변경은 owner에서만 owner D1/D4 delegated 본 branch는 audience 구현·테스트를 복제하지 않음
D3 DELEGATEDraw/branch-notes/feature-keycloak-spring-rs-audience-validator D6의 bearer RS 실행 계약을 consume RBAC 입력 전제 owner D6 delegated session/CORS 세부를 재명세하지 않음
D4 realm-global 권한이면 realm_access.roles만 매핑; client-specific 권한이 필요하면 resource_access.<client>.roles variant 선택 기준은 권한 namespace다. client 수가 많아도 공통 권한이면 realm role을 유지할 수 있고, client별 격리가 필요하면 client role을 추가한다 raw/official-docs/spring-security-resource-server-jwt.md#SSRS-JWT-C4 official-vendor-doc + project policy 현재 authZ는 deferred. 실제 client별 권한 요구를 확정하기 전 realm-only를 외부 경험으로 승격 금지
D5 RBAC 강제 지점 — 기본 SecurityFilterChain matcher(A), 조건부 B/C 기본=A(authorizeHttpRequests matcher): endpoint 소수 + role↔URL 안정 + "정책 한 곳 집중/면접 일관성" 목표. B(@PreAuthorize, A catch-all 유지 필수): 파라미터/소유권 기반 판단 또는 비-HTTP 진입점. C(A+B 병행=defense-in-depth): 프로덕션 노출 + matcher/annotation 누락이 실제 위협일 때 raw/official-docs/spring-security-authorize-http-requests.md#SS-AUTHZ-HTTP-C1 (request-level 모델링 — /admin 하위 authority), raw/official-docs/spring-security-authorize-http-requests.md#SS-AUTHZ-HTTP-C3 (AuthorizationFilter 가 DispatcherServlet/컨트롤러 실행 전 차단), raw/official-docs/spring-security-method-security.md#SPRING-MS-C5 (unannotated method 미보호 → B 시 catch-all 필수), raw/official-docs/spring-security-authorization-defense-in-depth.md#SS-AUTHZ-DID-C1 (request+method = defense in depth) official-vendor-doc (+ personal/company-blog corroborate: Okta·Marco Behler·howtodoinjava — 미아카이브, official 로 충분) C 채택 시 두 계층 role 조건 동기화 미스매치가 "단일 설명 위치" 목표 훼손; A 단독 시 URL glob drift(SS-AUTHZ-HTTP-C4 path-only); B 단독 시 미어노테이트/self-invocation 무보호
D6 Keycloak realm_access.rolesGrantedAuthority 매핑 메커니즘 = 수동 custom Converter<Jwt, Collection<GrantedAuthority>> (setAuthoritiesClaimName 폐기) nested claim(realm_access.roles)이라 flat-claim 전용 setAuthoritiesClaimName 는 확정 실패(nested 미지원 = SS-15201 공식; 내부 lookup 원리는 추정 → §진행 중 메모). 대안: Boot ≥3.4 / SS ≥6.4 로 pin 가능하면 ExpressionJwtGrantedAuthoritiesConverter + SpEL [realm_access][roles] (커스텀 클래스 없이 한 줄) raw/official-docs/spring-security-nested-authorities-claim-issue-15201.md#SS-15201-C2 (custom JwtGrantedAuthoritiesConverter 구현이 nested role 추출에 필요), raw/official-docs/spring-security-nested-authorities-claim-issue-15201.md#SS-15201-C3 (ExpressionJwtGrantedAuthoritiesConverter fix, milestone 6.4.0), raw/official-docs/spring-security-resource-server-jwt.md#SSRS-JWT-C4 (default 는 scope/scp 만 매핑), raw/company-tech-blogs/keycloak-jwt-role-extraction-betweendata.md#KC-ROLE-BD-C1 (custom Converterrealm_access 읽는 구현 예) official-vendor-doc + engineering-blog jwt.getClaim() 이 claim 부재 시 빈 컬렉션 반환(silent 403)은 소스 self-grep 전까지 needs-confirmation; betweendata 예제는 ROLE_realm_ prefix + resource role 도 매핑(D4 범위 밖) → 본 브랜치는 ROLE_ + realm-only 로 조정

구현 가이드

본 §는 이 branch 의 in-scope = RBAC/role 매핑 트랙(D4·D5·D6) 만 구체화한다. RS 공통 셋업(Boot 의존성·issuer-uri·JwtDecoder·aud 검증 = D1·D2·D3)은 형제 raw/branch-notes/feature-keycloak-spring-rs-audience-validator 가 owner(2026-07-14 감사) → 여기서 재명세하지 않고 §엣지·실패·의존 "다른 계약 의존" 으로 링크(R3 OUT_OF_BRANCH_SCOPE). keycloak-patterns repo 부재(NO_GROUND_TRUTH, §Audit) → 아래 전부 planned. 실 구현 후 코드 grep 으로 등급 승급.

1. Realm role → GrantedAuthority 매핑 (핵심)

Trace: D4(realm-only) + D6(custom Converter 메커니즘) — SS-15201-C2/SS-15201-C3, SSRS-JWT-C4, KC-ROLE-BD-C1.

  • UNSUPPORTED_IMPL_DECISION: (1) authority prefix 문자열 = ROLE_ (betweendata 사례는 ROLE_realm_) — hasRole("admin-role")ROLE_admin-role 을 기대하므로 ROLE_ 채택. trade-off: betweendata 예제와 불일치하나 표준 hasRole 계약에 정합(§3). (2) claim 부재 시 빈 컬렉션 반환(null-safe) — 근거 raw 는 방어 코드 형태를 규정 안 함, silent-403 진단성 위한 임의 선택.
항목 명세
클래스 KeycloakRealmRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> (별도 파일 또는 SecurityConfig static nested class)
읽기 Map<String,Object> realmAccess = jwt.getClaimAsMap("realm_access");realmAccess.get("roles")Collection<String> 으로
방출 각 role → new SimpleGrantedAuthority("ROLE_" + role)
null-safety realmAccess == null 또는 rolesCollection 아니면 → Collections.emptyList()
wiring JwtAuthenticationConverter jac = new JwtAuthenticationConverter(); jac.setJwtGrantedAuthoritiesConverter(new KeycloakRealmRoleConverter());.oauth2ResourceServer(o -> o.jwt(j -> j.jwtAuthenticationConverter(jac)))
대안(버전 pin 시) Boot ≥3.4 / SS ≥6.4 → 커스텀 클래스 대신 ExpressionJwtGrantedAuthoritiesConverter + SpEL "[realm_access][roles]" (SS-15201-C3) — 단 본 브랜치는 버전 미pin 이라 custom Converter 를 기본으로 함

2. /api/admin RBAC 강제 지점 (기본 A)

Trace: D5 — SS-AUTHZ-HTTP-C1/SS-AUTHZ-HTTP-C3, SPRING-MS-C5(backstop), SS-AUTHZ-DID-C1(조건부 C).

  • UNSUPPORTED_IMPL_DECISION: URL glob /api/admin/** + role 명 admin-role — 공식 예시는 illustrative(SS-AUTHZ-HTTP-C1 "Does not prove admin-role name"); glob/명명은 프로젝트 임의 결정(Keycloak realm role 명명은 feature-keycloak-realm-client-export 소관).
항목 명세
강제(A) .authorizeHttpRequests(a -> a.requestMatchers("/api/admin/**").hasRole("admin-role").anyRequest().authenticated())
/api/me 별도 role 없이 authenticated() (위 anyRequest() 로 커버)
타이밍 AuthorizationFilterDispatcherServlet 이전 실행 → 컨트롤러 도달 전 차단(SS-AUTHZ-HTTP-C3)
조건부 승격(B) 파라미터/소유권 기반 필요 시 @EnableMethodSecurity + @PreAuthorize("hasRole('admin-role')"), anyRequest().authenticated() catch-all 유지 필수(SPRING-MS-C5 — unannotated method 무보호 방지)
조건부 승격(C) 프로덕션 노출 시 A+B 병행(defense in depth, SS-AUTHZ-DID-C1) — 두 계층 role 조건 동기화 규율 전제

3. hasRole prefix 계약 (double-prefix 함정)

Trace: D6 — SS-AUTHZ-ARCH-C5(ROLE_ 자동 prefix = Spring Security 기본값, 공식), KC-ROLE-BD-C2(double-prefix 위험 사례).

hasRole("admin-role") 은 내부적으로 ROLE_ 를 자동 prefix (SS-AUTHZ-ARCH-C5) → §1 converter 가 이미 ROLE_admin-role 을 만들었으므로 인자는 prefix 없이 hasRole("admin-role") 로 호출한다. hasRole("ROLE_admin-role") 로 부르면 ROLE_ROLE_admin-role 을 조회 → admin 이 항상 403. §Claims To Verify + TODO 에 이 self-check(prefix 유무 대조) 추가.

엣지·실패·의존

R4(깊이 게이트) 캡처용. 정상 경로 외 실패/엣지/다른 계약 의존을 미리 열거.

  • 실패·엣지 경로:
    • nested claim silent failuresetAuthoritiesClaimName("realm_access.roles") 사용 시 literal top-level lookup 실패 → 0 authority → 모든 hasRole false → 전 요청 403, 예외/로그 없음(SS-15201-C2). 기대 동작: custom converter(§1)로 회피 + /api/me 응답에 ROLE_user-role 존재 확인.
    • realm_access claim 부재 — Keycloak client 에 realm-role mapper 없으면 claim 누락 → converter empty → 403. 기대: null-safe converter(§1) + realm role mapper 설정(→ 아래 의존).
    • double-prefixhasRole("ROLE_admin-role") 오용 시 ROLE_ROLE_admin-role → admin 항상 403(§3).
    • unannotated-method gap (조건부 B 채택 시) — 어노테이션 누락 endpoint 무보호. catch-all anyRequest().authenticated() 유지로 방어(SPRING-MS-C5).
  • 다른 계약 의존:

Audit & Findings

§2 ground-truth 확인 + 결정 정합 감사 결과. 자동 rewrite 대상 아님(surface + 정합 권고).

  • BROKEN_CODE_DRIFT (surface-only, read-only 권고): 인용 근거 raw/official-docs/spring-security-resource-server-jwt 의 §"권한 추출 customize (해석)" 코드가 setAuthoritiesClaimName("realm_access.roles") 를 사용 — nested claim 을 파싱하지 못해 작동하지 않는 패턴(SS-15201-C2). 그 raw 는 이미 "추가 확인 필요" 로 flag 되어 있으나, 코드 블록 자체에 "nested 미지원 → custom Converter / ExpressionJwtGrantedAuthoritiesConverter(6.4+) 필요" caveat 추가를 권고. 해당 raw 는 별도 소유 → 자동 수정 안 함(정합 권고만).
  • DELEGATION (2026-07-14 감사 정합): RS 공통 셋업 + aud(D1·D2·D3)는 형제 raw/branch-notes/feature-keycloak-spring-rs-audience-validator 가 owner. 본 노트 in-scope = role→RBAC(D4·D5·D6) = deferred authZ 트랙(raw/project-notes/keycloak-patterns-overview §Deferred — 4 패턴 authN E2E 후 착수). D1·D3 가 여기서 UNSUPPORTED_DECISION 인 것은 RS-common(sibling 소유 rationale)이기 때문 — 본 브랜치 추가 조사 대상 아님(R3 OUT_OF_BRANCH_SCOPE).
  • NO_GROUND_TRUTH: 실 구현 대상 repo /home/donghyeon/workspace/keycloak-patterns/ 부재(2026-07-18 확인) → 본 노트 모든 항목 planned/documented-only. actually-implemented 주장은 코드 대조 불가이므로 하지 않음(§구현 가이드는 사전 명세일 뿐).

검증해야 할 주장

공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다.

Claim Why uncertain How to verify Status
RS/audience prerequisite가 완료된 JWT만 RBAC converter에 도달 선행 owner repo가 아직 미구현 raw/branch-notes/feature-keycloak-spring-rs-audience-validator D1/D6 E2E 결과를 consume하고 본 branch 테스트 fixture의 전제로 기록 planned (delegated)
custom KeycloakRealmRoleConverter(§1)가 실제 Keycloak token 에서 ROLE_user-role/ROLE_admin-role authority 를 방출 (setAuthoritiesClaimName 은 D6/SS-15201 로 이미 폐기 확정) 메커니즘은 확정됐으나 로컬 실동작 + 실제 token 의 realm_access.roles 구조/composite role 확장 여부 미확인 regular-user 로 token 발급 → backend /api/me 응답에서 ROLE_user-role granted authority 존재 확인 planned
@PreAuthorize("hasRole('admin-role')") / .hasRole("admin-role") 가 converter 의 ROLE_admin-role 과 정확히 매칭(double-prefix 없음) hasRoleROLE_ 를 자동 prefix — converter 도 ROLE_ 를 붙이므로 인자에 ROLE_ 재기입 시 ROLE_ROLE_ 버그(KC-ROLE-BD-C2) admin-user token 으로 /api/admin 200 확인 → 인자를 hasRole("ROLE_admin-role") 로 바꿔 403 되는지 대조 planned

마주친 문제

  • (선행 계약) audience 발급·검증 문제는 raw/branch-notes/feature-keycloak-spring-rs-audience-validator D1/D4에서 추적한다. 본 branch는 검증 완료 뒤의 RBAC만 소유한다.
  • (구현 시작 후 추가) issuer-uri가 backend 기동 시점에 reachable하지 않으면 Spring startup 실패 — docker-compose depends_on healthy로 해결.

묶음

본 sub-sub-branch 는 leaf — 자식 자료 없음. Phase 3 P3A 실 구현 또는 외부 산출물 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.

오류 기록 (이 sub-sub-branch 작업 중 발생)

  • (없음 — 현재 documented-only 단계)

면접 준비 (이 작업에서 나올 수 있는 면접 질문)

  • (없음 — Phase 3 실 구현 단계에 누적)

관련 일일 노트

완료 후 정리

로컬 curl 검증 통과 시 plannedactually-implemented/locally-verified 승급.

  • PR 링크: (별도 keycloak-patterns repo)
  • 리뷰 메모:
  • 머지 결과 / 배포 환경: 로컬 docker-compose / 단일 EC2 시뮬레이션
  • wiki 추출 대상:
    • actually-implemented 항목: (구현 후 채움)
    • locally-verified 항목: (구현 후 채움)
    • prod-verified 항목: (없음)
  • 추출하지 않을 항목: 현재 전부 planned.