# task-3042 — 조직관리 사용자 배정이 조용히 실패하는 문제

**레벨**: Lv.2 · **팀**: dev3-team · **회장 승인 완료** (2026-08-28)
**저장소**: `Jeon-Jonghyuk/InsuRo` (`/home/jay/projects/InsuRo`, 기본 브랜치 **main**)

## ★ 회장 제보
> "인슈로 관리자 조직관리 메뉴에서 **사용자배정을 해도 적용이 안 되는** 거 같음."

---

## ANU 가 이미 규명한 원인 (재조사 불필요 — 그대로 쓰라)

### 근인: `profiles` 에만 관리자 정책이 없다
프로덕션 DB 직독 결과다.
```
[profiles]  RLS=True · 정책 3건
  INSERT  Users can insert own profile
  SELECT  Users can read all profiles
  UPDATE  Users can update own profile      USING: (auth.uid() = id)   ← 자기 자신만
  ★ System admins 정책 없음

[organizations]              ALL  System admins can manage organizations
[user_subscriptions]         ALL  System admins can manage user subscriptions
[organization_subscriptions] ALL  System admins can manage org subscriptions
```
**다른 테이블에는 전부 있는 관리자 정책이 `profiles` 에만 빠져 있다.**

기존 패턴(그대로 재사용할 것):
```sql
USING: has_role(auth.uid(), 'system_admin'::app_role)
```

### 왜 "성공한 것처럼" 보이는가
```typescript
// src/pages/AdminOrganizations.tsx:154
const { error } = await supabase.from("profiles")
  .update({ organization_id: assignOrgId }).eq("id", selectedUserId);
// PostgREST 는 RLS 거부 시 에러가 아니라 0행 처리 → error === null → 성공 토스트
```
**아무것도 안 바뀌었는데 성공 메시지가 뜬다.**
★ 이 조직에서 같은 유형 사고가 이미 있었다 — 확장프로그램이 전송 실패를 "저장됐어요"로
표시해 **4주간 아무도 몰랐다**(task-3006).

---

## 고칠 것

### ① `profiles` 관리자 UPDATE 정책 추가 (근인)
- 다른 테이블과 **동일한 패턴**(`has_role(auth.uid(), 'system_admin'::app_role)`)을 쓴다.
  새 판별 체계를 만들지 마라
- ★ **기존 `Users can update own profile` 정책을 건드리지 마라.** 일반 사용자의 자기 프로필
  수정은 계속 되어야 한다
- ★ 관리자가 **바꿀 수 있어야 하는 컬럼 범위**를 검토하라. `organization_id` 만 필요한데
  정책이 전 컬럼을 허용하면 과다 권한이다. 컬럼 제한이 가능한지 확인하고 판단 근거를 남겨라
- **DDL 은 작성만 하고 적용하지 마라.** SQL 파일로 제출하고 ANU 가 적용한다
  (프로덕션 DDL 은 ANU 권한)

### ② 조용한 실패 제거 (근본) ★ 이게 더 중요하다
정책만 고치면 이번 건은 풀리지만, **다음에 또 다른 곳에서 같은 일이 난다.**

`AdminOrganizations.tsx` 에서 브라우저가 DB 를 직접 쓰는 지점이 **10곳**이다:
```
121  organizations.insert          145  organizations.delete
154  profiles.update (배정)         170  profiles.update (해제)
194  user_subscriptions.update     196  user_subscriptions.insert
205  organization_subscriptions.update  207  ...insert
230  user_subscriptions.update     232  organization_subscriptions.update
```
- 쓰기 결과를 **`.select()` 로 반영 행을 받아 검증**하라. 0행이면 **실패로 처리**하고
  사용자에게 실패를 알려야 한다
- ★ **성공 토스트를 기본값으로 두지 마라.** 반영이 확인된 뒤에만 성공이라고 말한다
- 10곳 전부에 적용하라. 일부만 고치면 나머지가 같은 함정으로 남는다

### ③ 서버 API 이관은 이번 범위가 아니다
관리자 작업이 브라우저에서 테이블을 직접 조작하는 구조 자체가 바람직하지 않지만,
10곳을 한 번에 서버로 옮기는 것은 이 태스크 범위를 넘는다.
- **①②로 증상과 조용한 실패를 먼저 잡는다**
- 서버 이관은 **설계 제안만** 보고서에 남기고 ANU 판단을 받아라

---

## ★ 하지 말 것
- **기존 RLS 정책 삭제·변경 금지** (추가만)
- **프로덕션 DDL 직접 적용 금지** — SQL 파일 제출까지가 범위
- 새 권한 판별 체계 신설 금지 (`has_role` 재사용)
- `server/**` 대규모 수정 금지
- 다른 관리자 화면 변경 금지
- 자격증명 값을 저장소에 커밋

## allowed_resources
```yaml
allowed_resources:
  paths:
    - "src/pages/AdminOrganizations.tsx"
    - "src/**/__tests__/**"
    - "supabase/migrations/**"
    - "memory/reports/task-3042.md"
  forbidden_paths:
    - "server/**"
    - "extension/**"
    - "src/pages/PolicyAnalysis.tsx"
    - "src/pages/KeywordAnalysis.tsx"
    - "src/data/generateOptions.ts"
    - ".github/workflows/**"
  commands: ["npm", "npx", "node", "bash", "python3", "gh"]
  merge_policy: "tiered"
  ttl_hours: 24
```

## 검증 (실측 강제)
1. **현재 실패 재현** — 수정 전 상태에서 `profiles.update` 가 **0행**으로 끝나고 `error` 가
   `null` 인 것을 실측으로 보여라. 추정 금지
2. **정책 SQL 검증** — 작성한 DDL 을 **트랜잭션 안에서 적용→검증→ROLLBACK** 으로 시험하라.
   커밋하지 마라. 적용 후 관리자가 남의 `organization_id` 를 바꿀 수 있고,
   **일반 사용자는 여전히 못 바꾸는지** 양방향 확인
3. **조용한 실패 제거 확인** — 10곳 각각에서 0행일 때 사용자에게 **실패가 보이는지**.
   화면 문구를 캡처하거나 코드로 제시하라
4. **회귀** — `vitest` **base 재측정 기준선**
5. **봉인** — 성공 토스트를 무조건 띄우는 변이, `.select()` 검증을 제거하는 변이를 만들어
   테스트가 FAIL 하는지 확인·복원. 변이가 no-op 이 아님을 `assert` 로 먼저 증명하라
6. **실 브라우저** — 배정 화면 스크린샷 (성공/실패 양쪽)

## 완료 조건 (DoD)
**관리자 정책 SQL 이 트랜잭션 시험으로 검증되어 제출되고, 조직관리 화면의 쓰기 10곳이
반영 행 수를 확인해 0행이면 실패로 표시하며, 성공 토스트가 기본값이 아니게 된다.**

## ★ 세션 조기 종료 대책
시작 즉시 브랜치 + 드래프트 PR 을 먼저 열어라. **단계마다 커밋하라.**
★ ANU 명세 전제가 틀렸다고 판단되면 구현하지 말고 **반증을 먼저 기록한 뒤 보고하라.**

## 운영 계약
- `origin/main`(=`2c5c67b`) 기준 `git pull --ff-only` 후 시작
- ★ **worktree 를 써라.** 메인 저장소는 cron 이 매일 06:00·08:00 에 직접 실행한다
- ★ `gh` 호출 시 `GH_TOKEN="$BOT_GITHUB_TOKEN"` 주입 필수 (회장 개인 PAT 금지 — 감사기록 오염)
- ★ Supabase DDL 경로는 `psycopg2` + `aws-1-ap-northeast-2.pooler.supabase.com:5432` 뿐이다.
  비밀번호는 `/home/jay/projects/InsuRo/.env` — **로그·커밋 금지**
- 워크플로우 `/home/jay/workspace/prompts/DIRECT-WORKFLOW.md` · QC `/home/jay/workspace/teams/shared/QC-RULES.md`
- `WORKSPACE_ROOT=/home/jay/workspace` · `CHAT_ID=6937032012` · 수집자 key `ANU_KEY=c119085addb0f8b7`
- 완료 경로는 `finish-task.sh` 실행이 유일하다. 수동 `.done` 금지.

## 보고
**PR 생성까지가 범위다. 머지는 ANU 가 한다. 자동 머지 금지. DDL 적용도 ANU 가 한다.**
`memory/reports/task-3042.md` 작성 후 표준 완료 콜백 등록. 콜백 프롬프트 **UTF-8 3900 bytes 이하**.
★ 봉투 첫 줄에 **"0행 실패 재현 여부 + 쓰기 10곳 검증 적용 여부"** 를 담아라.