페르소나 & API 시나리오 테스트 리포트

RFQ Tracker를 8개 유저 페르소나로 시나리오 테스트. 변경·RBAC 시나리오는 로컬 dev에서 실측, 읽기·검색은 라이브 프로덕션 API로 검증. 권한은 코드(서버액션)와 실제 동작을 교차 확인.

👥 8 personas🧪 39+ scenarios🔬 RBAC 코드+실측🌏 prod API🗓️ 2026-06-23

0요약

핵심: 리드 쓰기·유저초대·소스 액션의 RBAC는 서버액션 레벨에서 정상 작동, 그러나 회사 프로필(companies) 액션은 역할 체크가 없어 뷰어도 생성·수정·삭제 가능(실측 확인). API 시맨틱 검색 품질은 우수(요구사항 프롬프트 4/4 적중).

28PASS정상 동작
1RBAC 갭companies 생성 뚫림
4/4검색 품질프롬프트 vertical 적중
3예상실패키·인프라 미설정

1유저 페르소나 (역할·기능 기반)

ID페르소나실체 / 역할표면테스트 위치
P1운영 관리자호진 PM/대표 · role=admin로컬 :3002
P2VA 리드 오퍼레이터외주 가상비서 · role=va로컬 (변경)
P3뷰어읽기전용 영업 · role=viewer로컬 (RBAC)
P4소싱 사용자셀러 브리프 작성자 · admin로컬
P5파트너 통합 개발자rinda.ai API 소비자API프로덕션
P6K-뷰티 수출기업스킨케어 셀러API프로덕션
P7식품 수출기업유기농 식품 셀러API프로덕션
P8JP 아웃리치 / 의류JP 바이어 발굴·의류 셀러API프로덕션

2웹 페르소나 — 변경·RBAC 실측

페르소나시나리오기대실측 결과판정
P1 admin로그인 → 대시보드KPI·월드맵바이어80·17개국·퍼널·핫리드26 렌더PASS
P1 admin/sources · /users · /settings전체 접근71소스 + 멤버3명 표시, 접근 OKPASS
P1 admin소스 Run NowenqueueSQS 미설정 → 예상실패예상실패
P2 va로그인(role=va) · 리드·필터·drawer읽기·상세RFQ80 렌더, drawer 오픈PASS
P2 va상태변경 new→contactedDB 영속status=contacted, status_by=va, audit 이력 기록PASS
P2 vaCSV 내보내기S3 업로드S3 미설정 → 예상실패예상실패
P3 viewer로그인(role=viewer) · 리드 읽기조회 가능200 렌더PASS
P3 viewer리드 상태변경 시도차단 기대버튼 클릭했으나 서버액션 차단(status=new 유지)PASS(보안)
P3 viewer회사 프로필 생성 시도차단 기대생성 성공(count 0→1, role=seller)FAIL(갭)
P3 viewer/users 멤버 페이지차단/숨김 기대전체 멤버 명단·이메일 열람됨노출
P4 seller로그인 · /companies 브리프 생성저장프로필 DB 저장 OKPASS
P4 seller/ai-search결과LLM키 미설정 → 예상실패예상실패
P4 seller/matches렌더가이드·범례 렌더(데이터0)PASS

3RBAC 판정 — 코드 + 실측 교차

권한 정책 SSOT: lib/permissions.ts (viewer=*:view만 / va=+write·export·enrich·run-now / admin=전체). 게이팅은 서버액션 레벨, 페이지 레벨은 일부만.

액션 영역코드 게이팅 (근거)실측판정
리드 쓰기(상태·이메일·export)can(session,'leads:write/enrich/export') · leads/actions.ts:67,173,385viewer 클릭 → 차단enforced
유저 초대role!=='admin'→forbidden · users/actions.ts:26(코드) va/viewer 거부enforced
소스 액션(run-now·toggle·edit)assertCan(session,'sources:*') · sources/actions.ts 전체(코드) 게이팅 존재enforced
회사 프로필 생성/수정/삭제역할 체크 없음 · companies/actions.ts:48,87,128 (auth만)viewer 생성 성공
페이지 레벨 가드sources/page.tsx만 가드. leads·users 페이지 가드 없음viewer가 전 페이지 200·멤버명단 노출부분
UI 컨트롤 숨김viewer에게 쓰기 버튼 비노출 안 함viewer 드로어에 쓰기버튼 그대로UX

🔴 핵심 보안 갭 — 회사 프로필 RBAC 누락

companies/actions.tscreateCompanyAction·updateCompanyAction·deleteCompanyActionauth()만 확인하고 역할을 검사하지 않음 → 정책상 쓰기 권한이 없는 viewer가 회사 프로필을 생성·수정·삭제 가능(실측: count 0→1). 다른 쓰기 액션처럼 assertCan(session,'companies:write') 추가 필요. saveListAction도 동일 누락.

4API 엔드포인트 검증 (프로덕션)

대상 /api/v1/* (OpenAPI 3.1 "Companies API v2.0.0") · 인증 x-api-key. 코퍼스 1,206,093건.

시나리오엔드포인트결과판정
인증 강제(키 없음/오류)GET /companies401 / 401PASS
목록 + keyset 커서GET /companies?limit200, {data,next_cursor,count}PASS
코퍼스 분포GET /meta/facetstotal 1,206,093 (JP118만·beauty100만)PASS
단건 / 없는 idGET /companies/{id}200(37필드) / 404PASS
필터 정확성(~40종)GET /companies?...반환값 필터 일치, ZZ→0PASS
필드 프로젝션fields=url|contact_form|full3/3/37 필드PASS
대량 페이지네이션 3×100GET ?limit=100 ×3300행 고유300, 중복0PASS
입력 검증POST /search 잘못된 filter400 + 상세(expected array)PASS

5요구사항 프롬프트별 검색 품질

POST /companies/search (시맨틱 + Qwen 리랭크)에 실제 셀러 요구사항을 입력해 결과 관련성 검증. vertical·업종·바이어유형 적중 여부로 판정. (리랭크 ~17–23s)

요구사항 프롬프트기대 도메인상위 결과 (이름·유형)적중
K-스킨케어 → 일본 뷰티 리테일러·디스트리뷰터beautyMOLVANY JAPAN(retailer)·チャームゾーン(distributor)·Yatsen Beauty(wholesaler)3/3
유기농 식품 수출 → 수입·유통사 filters.vertical=[food]foodSK食品ジャパン(wholesaler)·ミトク(distributor)·ORGANIC HOUSE(wholesaler)3/3
K-뷰티 OEM → 화장품 브랜드·도매beautySourcing-Lab Cosmetics·Yours Cosme·Korea Trade(distributor)3/3
천장형 선풍기 제조 → 건설·도매 바이어 [ceiling_fan]ceiling_fanヨシムラエアサプライ(contractor)·田中冷熱設備(distributor)·住設建材卸売(wholesaler)3/3

✅ 검색 품질 우수

4개 요구사항 모두 의도한 vertical·바이어 유형(리테일러/디스트리뷰터/도매/건설)에 정확히 매칭. 시맨틱 + 리랭크가 도메인 의도를 잘 반영.

6검색 로직 분석 — 키워드? 임베딩? 리랭커?

코드 기준 결론: 단순 키워드 매칭이 아니다. 메인 검색은 임베딩 벡터 ANN + LLM 리랭커(하이브리드). 자유텍스트 목록조회만 키워드(ILIKE). 임베딩 공간은 파트너용(Qwen)·내부용(Gemini) 2개로 분리.

엔드포인트1차 검색(retrieval)2차 리랭크방식
POST /api/v1/companies/search
(파트너 시맨틱)
pgvector HNSW ANN · Qwen3-Embedding-8B(1536-dim halfvec, L2정규화, cosine, ef_search=150) · 비대칭 instruction · 구조필터는 WHERE에 포함 · pool 기본50/최대200 Qwen3 Chat 리랭커 · tool-forced score_companies(ICP 적합도 0–100) · 25개씩 청크 병렬 · 실패 시 cosine로 폴백 벡터 ANN + LLM 리랭크
GET /api/v1/companies?q=
(목록 자유텍스트)
ILIKE 키워드 (company_name/name_en/company_brief/industry_en) · trgm 인덱스 없음(테이블 스캔) 없음 키워드(ILIKE)
POST /api/hub-search
(내부 NL 검색)
Claude Sonnet이 NL 파싱→구조필터+semanticText · Gemini embedding-001(1536) · 하이브리드 블렌딩 40% 구조점수 + 50% cosine + 10% 키워드 Opus 오케스트레이터(필터 자동 relax/tighten) + Sonnet이 상위 12개 intent 적합도 판정 하이브리드 + LLM

임베딩 입력 구성 (search)

company(name·industry·desc·sells·target)+about+query+keywords를 한 텍스트로 결합 → Qwen 쿼리 임베딩 + 리랭커 SELLER 섹션에 동일 사용. 필드 간 우선순위 없이 동등 반영.

근거: lib/ai-company-search/search.ts:50–107 · qwen.ts:21,40–76

점수 필드 (정정)

search 응답은 fit_score(0–100, 리랭커) + similarity(cosine 0–1)로 내려옴. rerank=false면 fit_score 없이 similarity만. 앞서 "score=null"은 응답 키가 score가 아니라 fit_score였던 테스트 측 오독 — API 정상.

근거: lib/ai-company-search/search.ts:67–79,120–145

결론

메인 셀러 검색 = Qwen 임베딩 ANN + Qwen Chat 리랭커. 내부 hub = Gemini 하이브리드 + Claude 리랭크. 키워드(ILIKE)는 ?q= 목록조회 한정. → "단순 키워드 매칭"이 아니라 의미 기반 검색.

7검색 결과 실데이터 검증 (서브에이전트)

4개 요구사항 프롬프트 × 상위 5결과 = 20개 회사를, 서브에이전트 4개가 웹 검색으로 실존 여부·업종 일치를 개별 검증. 또 각 결과 id를 GET /companies/{id}로 조회해 실제 코퍼스 레코드인지 교차 확인. 원시 데이터 CSV: rfq-search-rawdata.csv.

20/20실제 코퍼스 행GET /id 조회됨 · 리랭커 가공 0
19/20웹 실존 확인공식사이트·법인번호·JETRO
19/20업종 일치검색 의도와 매칭
1웹 근거 부재디렉토리 품질 이슈

결과 개수 분석 — 왜 그만큼 나오나?

프롬프트vertical 필터limit=5limit=20해당 vertical 코퍼스 총량
S1 K-스킨케어→뷰티 리테일러(없음, beauty 의미검색)520beauty 1,008,499
S2 유기농 식품→수입·유통food520food 14,441
S3 K뷰티 OEM→화장품 브랜드(없음, beauty 의미검색)520beauty 1,008,499
S4 천장팬→건설·설비ceiling_fan520ceiling_fan 164,605

이유: 반환 개수 = min(limit, candidates pool, 매칭 가능 행). 네 프롬프트 모두 limit=5→5, limit=20→20을 정확히 채움 → 개수는 데이터 부족이 아니라 limit 파라미터에 의한 캡이다. 1차 ANN은 1,206,093건 halfvec HNSW 인덱스에서 후보를 뽑고(candidates 기본50/최대200), 최종은 상위 limit만 반환. 구조 필터(vertical)를 걸어 가장 작은 food(14,441)도 limit을 수만 배 초과 → 어떤 프롬프트도 "결과 부족"이 발생하지 않음. 즉 개수 0/부족이 나오려면 필터가 과도하게 좁거나(예: country=ZZ→0) 해당 vertical 데이터가 없을 때뿐.

프롬프트결과(상위)fit코퍼스웹실존업종근거(예)
S1 K-스킨케어→JP 뷰티 리테일러MOLVANY JAPAN0.95yesyesmolvany.co.jp·법인 4010401173448
MARENE JAPAN0.95nouncertainbizmaps_jp 레코드이나 외부 근거 부재
Yatsen Beauty Japan0.9yesyesPerfect Diary 일본법인·gBizINFO
COSME TOKYO0.9yesyescosmetokyo.co.jp
株式会社ベレーザ0.9yesyes화장품 EC 18,000점+·자사브랜드
S2 유기농 식품→수입·유통ミトク0.95yesyesmitoku.co.jp·JETRO 수출협력
LS商事·yesyesls-corp.co.jp 한국식품
SK食品ジャパン·yesyesgBizINFO 食料品卸売業
グレースフィールド·yesyesgracefield.jp 유기농 허브티
ナチュラルマート·yesyesnaturalmart.jp 오가닉
S3 K뷰티 OEM→화장품 브랜드·도매パル興商·yesyes화장품 웹판매·해외브랜드
Yours Cosme·yesyesyourscosme.com 도매·수입
メイクアップ·yesyesmakeup-inc.com 향수·화장품
flowli·yesyes한국 PHARMOLOGY 일본 총대리점
FAMILIAR·yesyesfamiliar.jp.net 화장품 도매
S4 천장팬→건설·설비 바이어創輝建設·yesyessouki-co.com 설비공사
潮産業·yesyes건자재·설비 NIKKEI COMPASS
大成設備·yesyes大成建設그룹 HVAC 설비
大一工務店·yesyesdaiichikomuten.com 積水ハウス 지정
五建工業·yesyes5ken.co.jp 토목·설비

✅ 실데이터 검증 결론

리랭커는 결과를 지어내지 않음 — 20건 전부 GET /companies/{id}로 조회되는 실제 코퍼스 행(source=bizmaps_jp 등). 19/20은 공식사이트·법인번호로 실존+업종 일치 확인. 유일 예외 MARENE JAPAN은 디렉토리 스크래핑 레코드이나 외부 웹 근거 부재 — 검색 로직이 아닌 소스 데이터 품질 이슈.

8발견 이슈

🔴

#1 회사 프로필 액션 RBAC 누락 (실측 확인)

viewer가 /companies에서 프로필 생성 성공(0→1). createCompany/updateCompany/deleteCompany가 역할 미검사. assertCan('companies:write') 추가 필요.

companies/actions.ts:48,87,128 · 실측 DB count 0→1
🟠

#2 페이지/UI 레벨 게이팅 부재

viewer가 /users 멤버 명단·이메일 열람, 리드 드로어 쓰기 버튼도 노출됨(서버는 차단하나 사용자 혼란·정보노출). leads·users 페이지에 역할 가드 추가 권장.

leads/page.tsx·users/page.tsx 가드 없음 (sources/page.tsx만 존재)
🟡

#3 검색 바디 top-level vertical 무시

POST /search에서 {"vertical":"food"}는 조용히 무시되어 beauty 결과 반환. 반드시 filters:{vertical:["food"]}(배열) 사용해야 함 — 문서/검증 강화 권장.

Q2: top-level vertical → beauty 결과 / filters.vertical=[food] → food 3/3
🟡

#4 /companies?q= ILIKE 무인덱스 (perf)

목록 자유텍스트 검색이 ILIKE이고 pg_trgm 인덱스가 없어 대용량(120만 행)에서 테이블 스캔 가능. trgm GIN 인덱스 추가 권장. (의미 검색은 별도 벡터 경로라 무관)

lib/companies-api/filters.ts:170–174 · 스키마에 q용 trgm 인덱스 없음
✔️

정정: "score=null"은 버그 아님

앞선 검색 테스트의 score:null은 응답 키가 score가 아니라 fit_score/similarity였던 테스트 측 오독. API는 정상적으로 점수를 반환.

lib/ai-company-search/search.ts:67–79
🟢

정상: 리드·유저·소스 RBAC, API 인증·페이지네이션·검색 품질

리드 쓰기/유저초대/소스 액션 RBAC enforced, API 인증·필터·키셋·입력검증 정상, 시맨틱 검색 4/4 적중.

🔧

로컬 dev SSL 패치(테스트용)

lib/db의 ssl:'require'가 로컬 docker pg 접속을 막아 localhost 분기 패치로 우회(커밋 안 함). 근본은 Aurora 전제 하드코딩.