= [Dev] UserEmail == 설계 의도 * 사용자의 identity는 이메일이 아니라 `User.seq`다. 한 사람이 여러 Google 이메일로 로그인하더라도 같은 `User.seq`에 연결되면 같은 사용자로 취급한다. * 로그인 가능한 이메일 목록은 `UserEmail`이 관리한다. 이메일은 로그인 수단이자 권한 actor 후보로만 사용한다. * 한 이메일은 하나의 user에만 연결된다. 이미 다른 user에 연결된 이메일을 현재 계정에 연결하려면, Google OAuth로 소유권을 확인한 뒤 사용자가 명시적으로 병합을 승인해야 한다. * 권한 평가는 단일 로그인 이메일이 아니라 현재 user에 연결된 모든 로그인 이메일 기준으로 수행한다. 따라서 기존 이메일 기반 permission은 여러 이메일을 가진 사용자에게도 자연스럽게 적용된다. == 정책 * 초기 UI에서는 로그인 이메일 삭제를 제공하지 않는다. 최소 하나의 로그인 이메일은 migration, login, 계정 연결 flow에서 유지한다. * Google OAuth profile email만 로그인 이메일로 연결한다. Google People API의 여러 verified email을 자동 등록하지 않는다. * 대표 이메일은 `UserEmail.isPrimary = true`로 표현한다. * 병합 후 duplicate `User` row는 주요 FK와 `UserEmail`을 canonical `User.seq`로 이동한 뒤 삭제한다. == 구현 결과 * `User` 모델은 이메일을 직접 갖지 않고, `UserEmail` 모델이 이메일 조회, 추가, primary 설정, 중복 확인을 담당한다. 로그인은 `UserEmail.email`로 user를 찾고, 없으면 새 `User`와 첫 `UserEmail`을 만든다. * 세션은 `seq`, `nickname`, `loginEmail` 중심으로 정리했다. identity 판단은 `seq`로 하고, `loginEmail`은 표시나 감사 목적의 보조 정보다. 다만 `UserEmail` 행이 하나도 없는 사용자에게는 `WikiPermission` 이 `loginEmail` 을 권한 actor 로 대신 쓴다. * 계정 설정 화면에 로그인 이메일 목록과 Google 계정 연결 버튼을 추가했다. 연결된 이메일이 다른 user 소유이면 병합 확인 화면으로 이동한다. * 병합 로직은 DB 메타데이터에서 `User(seq)` 참조 FK를 읽어 duplicate user의 참조를 canonical user로 옮긴다. `UserEmail`도 canonical user로 이동하고, 이동이 끝나면 duplicate `User` row를 삭제한다. 다만 (user, site) 유니크 키를 가진 테이블(`UserSite`·`SiteAdmin`)은 그대로 옮기면 키가 충돌하므로, canonical 이 이미 가진 site 는 duplicate 행을 지우고 나머지만 옮긴다. * `Permission`은 현재 user의 모든 로그인 이메일을 actor 후보로 사용하도록 변경했다. `Exact`, `Domain`, `Login` 권한이 여러 로그인 이메일 기준으로 동작한다. * `UserMergeSpec`가 FK 이동(`Page`, `AccessLog`), `UserEmail` 이동, primary 하나 유지, duplicate `User` 삭제, 그리고 두 사용자가 같은 site 의 관리자일 때 `SiteAdmin` 행이 충돌 없이 하나로 합쳐지는 것을 검증한다. (user, site) 충돌 처리는 `UserMerge.mergeUniqueUserSiteRows` 하나로 `UserSite`·`SiteAdmin` 에 함께 쓴다 — `UserSite` 는 evolution 55 가 지웠지만 테이블이 있을 때만 도는 가드 뒤에 남아 있고, `SiteAdmin` 은 2026-09-15 부터 같은 처리를 받는다. 전에는 `SiteAdmin` 겹침(둘 다 같은 site 관리자)이 primary key 를 위반해 병합 전체를 실패시켰다. == 운영 확인 배포 전에는 DB를 백업하고 staging에서 migration rehearsal을 수행한다. `UserEmail.email` unique 제약, 기존 user 수와 migration된 primary email 수, 현재 DB의 `User(seq)` 참조 FK 목록, Google OAuth Console의 `/google/oauth/callback` redirect URI 등록 여부를 확인한다. 배포 후에는 대표 계정의 여러 이메일로 각각 로그인하고, 관리자 권한, 이메일 기반 page permission, 계정 설정의 로그인 이메일 목록, 병합 flow를 확인한다.