Skip to content
created by Aha00aAha00a at 2026-10-25
last modified by Aha00aAha00a at 2026-10-25
revision: 1

UserNicknameChange

사용자는 닉네임을 스스로 바꾸지 못한다. Account Settings 에서 신청을 만들고, 관리자가 승인해야 User.nickname 이 바뀐다. 2026-09-02 에 배포했다. 이 문서는 그 구현이 어떤 결정 위에 서 있는지 적는다 — 만들 때의 체크리스트(ToDo-User-Nickname-Change)를 끝내며 남길 것만 옮겼다.

1. 흐름

  • 사용자가 /account 의 닉네임 절에서 새 닉네임을 신청한다. 대기 신청이 있으면 입력과 버튼이 잠기고, 자기 대기 신청은 취소할 수 있다(Canceled).
  • 관리자가 /Admin/NicknameRequests 에서 대기 목록을 보고 승인하거나 거절한다. 승인은 확인을 한 번 받고, 거절은 사유가 없으면 보내지지 않는다 — 사유가 거절의 내용 전부이고 신청자가 보는 것도 그것뿐이다.
  • 승인하면 UserNicknameChangeLogic.approve 가 한 트랜잭션에서 User.nickname·User.dateNicknameChanged 를 바꾸고 UserNicknameHistory 에 옛 값·새 값·승인한 관리자를 적고 신청을 Approved 로 바꾼다.

2. 규칙

닉네임은 ^[a-zA-Z0-9가-힣]{5,16}$ 이고, 검증은 logics.UserNicknamePolicy.validate 한 곳에서 한다. 앞뒤 공백은 자르고, 자른 뒤 비면 실패하고, 안의 공백은 실패한다. 대소문자는 표시용으로 보존하되 중복 비교는 DB collation 대로 대소문자를 가리지 않는다. 예약어 다섯 개(Anonymous·Guest·Admin·AhaWiki·User)는 막는다. 오류는 길이·허용 문자·이미 사용 중·이미 신청 중·대기 신청이 이미 있음으로 갈라 돌려준다(UserNicknameChangeLogic.RequestError).

검증을 두 번 한다. 신청할 때 한 번, 승인할 때 다시 한 번. 두 번째가 군더더기가 아닌 까닭은 규칙이 그 사이에 바뀔 수 있어서다 — 오늘 통과한 이름이 관리자가 일주일 뒤 볼 때도 통과한다는 보장은 없다. 승인할 때는 중복도 다시 본다.

기존 닉네임은 건드리지 않는다. 새 규칙에 맞지 않는 옛 닉네임도 그대로 산다. User.nickname 을 쓰는 코드는 승인 하나뿐이고 승인은 신청된 이름만 검증한다.

3. 데이터

UserNicknameChangeRequest 가 신청을 담는다(컬럼·키는 schema/schema.sql 이 정본이라 여기 옮기지 않는다). 이유가 있는 선택만 적는다.

  • 상태에 Canceled 가 있다. 취소를 row 삭제로 처리하면 신청했다는 사실 자체가 사라진다.
  • requestedNickname 은 User.nickname 과 같은 utf8mb4_general_ci 다. «이미 신청됨» 과 «이미 사용 중» 이 unique index 와 같은 대소문자 기준으로 답해야 한다. 테이블의 나머지는 다른 테이블처럼 utf8mb4_bin 이다 — 이것이 어긋났던 일은 아래 배포에서 배운 것과 Dev Database.
  • 대기 신청의 중복은 DB 제약이 아니라 애플리케이션 트랜잭션이 막는다. 같은 사용자의 Pending 은 하나, 같은 요청 닉네임의 Pending 도 하나. MySQL 에는 partial unique index 가 없다. 최종 방어는 승인 시점의 User.nickname unique 제약이다.
  • 신청 생성은 이력에 적지 않는다. UserNicknameHistory 는 실제로 바뀐 경우에만, 승인한 관리자를 changedBy 로 적는다. 이 테이블은 evolution 38 이 만들어 두고 아무도 읽지 않던 것인데 이제 쓰인다.

검사를 전부 끝낸 뒤에 첫 쓰기를 한다. Database.withTransaction 은 예외가 빠져나가야 롤백한다 — 중간에 Left 를 돌려주면 그때까지 쓴 것이 그대로 커밋된다. 신청 row 를 FOR UPDATE(selectBySeqForUpdate)로 잡고 있으니 «먼저 검사, 나중에 쓰기» 가 안전하다. 순서는 Pending 인지 → 닉네임 재검증 → 중복 재확인 → 쓰기.

4. 세션: 일부러 하지 않은 것

닉네임의 source of truth 는 User 테이블이다. 세션의 nickname 은 표시용 스냅샷일 뿐이고, 권한·작성자·승인에는 쓰지 않는다 — 신원은 계속 User.seq 다.

  • 세션을 자동으로 갱신하지 않는다. Play 기본 session 은 서버 저장소가 아니라 서명된 클라이언트 쿠키라서, 승인하는 순간 다른 브라우저·기기·탭의 쿠키를 서버가 고칠 방법이 없다. 낡은 닉네임을 감지해 쿠키를 다시 쓰는 middleware, 서버 쪽 session store, session version 표는 만들지 않았다. 기존 로그인은 재로그인 전까지 옛 닉네임을 표시할 수 있다 — 주로 상단 nav 같은 표시 영역에서.
  • 강제 로그아웃도 하지 않는다. 닉네임은 인증 권한의 근거가 아니다(권한은 User.seq 와 연결된 email 로 평가한다). 꼭 필요하면 운영자가 Play session signing key 를 교체해 모든 세션 쿠키를 무효화한다 — 별도 운영 작업이다.
  • WebSocket 은 다시 연결해야 새 닉네임을 쓴다. cursor.hello 가 연결 시점의 닉네임을 쓰고, 승인 뒤에 이미 열린 연결은 hello 를 다시 보내지 않는다. 페이지를 다시 열거나 재연결하면 새 이름이다.
  • Account 와 Admin 화면은 세션이 아니라 DB 의 값을 읽는다 — 목록 응답의 userNickname 은 User 를 다시 읽은 값이다. 낡은 쿠키로 불러도 Account 화면이 DB 값을 답하는지 ApiUserNicknameSpec 이 확인한다.

5. 위키 내용과 링크

닉네임이 바뀌어도 위키 본문을 대량으로 고치지 않는다. 옛 [User:oldNickname] 링크는 그대로 둔다 — 사용자가 쓴 내용을 자동으로 바꾸는 부작용이 크다. 페이지 이력과 blame 은 User.seq 로 join 하므로 새 이름으로 보인다.

User:<옛 닉네임> 프로필 페이지도 자동으로 옮기지 않는다. 승인 뒤 새 프로필 링크는 /w/User:<새 닉네임> 이고, 옛 페이지가 있으면 관리자가 필요할 때 위키의 이름 바꾸기를 쓴다. 안전하다고 판단되면 #!redirect User:<새 닉네임> 을 자동으로 만드는 옵션을 더할 수 있다(하지 않았다). 페이지 캐시도 무효화하지 않는다 — 본문이 바뀌지 않기 때문이다.

6. API

  • 사용자: GET /api/account/NicknameRequests(현재 닉네임·대기 신청·최근 결과), POST /api/account/NicknameRequests, POST /api/account/NicknameRequests/:seq/cancel
  • 관리자: GET /api/Admin/NicknameRequests?status=Pending, POST /api/Admin/NicknameRequests/:seq/approve, POST /api/Admin/NicknameRequests/:seq/reject

관리자 쪽은 다른 관리 API 처럼 AdminAuth 의 isAdmin 으로 막는다. 관리자 판정이 하드코딩인 것은 AdminLogic.SuperAdminUserSeq 한 곳으로 모여 있고, role 기반으로 옮기지 않기로 한 근거와 다시 볼 조건은 AccessControl 의 남은 과제에 있다.

경로는 처음 계획과 다르다. 계획은 POST /account/nickname/request 와 /api/Admin/UserNicknameChangeRequests 였다. 실제로는 옆에 이미 있는 것과 같은 모양을 골랐다 — /api/account/ApiKeys 와 /api/Admin/ApiKeys. 둘 다 JSON API 이고 ApiApiKey 와 같은 컨트롤러 구조라, 한 화면에서 두 가지 경로 규칙을 쓰는 것보다 낫다. 취소도 :seq 를 받는다 — 계획의 /request/cancel 은 어느 신청인지 말하지 않고, 그러면 «내 대기 신청» 을 서버가 추측해야 한다.

7. 화면

Account 쪽은 Account/settings.scala.html 의 accountNickname 절이고, 관리자 쪽은 Admin UI 의 NicknameRequestsPage.jsx(/Admin/NicknameRequests)다. 기본 필터는 Pending 이고 Approved·Rejected·Canceled·All 로 바꿀 수 있다.

  • 공용 helper 를 끌어올렸다. escapeHtml·formatDate·csrfToken 과 CSRF 헤더를 붙이는 send 가 API Key 절 안에 있었는데 닉네임 절도 넷 다 필요했다. 베끼면 escapeHtml 이 두 개인 페이지가 되고 그중 하나만 고쳐지는 날이 온다. CSS 의 apiKeyForm·apiKeyMessage 도 같은 이유로 accountForm·accountMessage 로 바꿔 둘이 같이 쓴다.
  • 관리자 화면의 쓰기는 api.js 의 sendJson 을 쓴다. CSRF 헤더를 붙이고 {error} 봉투를 푸는 것을 손으로 조립하던 자리가 이미 열 곳이라, 열한 번째를 만드는 대신 helper 를 뒀다. 기존 열 곳을 옮기는 것은 따로 남아 있다.

8. 알려진 한계와, 하지 않기로 한 것

  • 승인 뒤 프로필 이미지 캐시를 무효화하지 않는다. UserProfileImageUrl 의 캐시 키가 클래스:site:nickname 이라 사이트마다 하나씩이고, 승인 로직이 무효화하려면 사이트 목록과 AhaWikiCache 를 승인 트랜잭션 안으로 끌고 들어와야 한다. 실제 피해는 작다 — 옛 이름 키에 남는 값은 아무도 더는 조회하지 않는 이름의 것이고(옛 [User:이름] 링크는 그대로 두기로 했다), 새 이름 키는 보통 비어 있어서 처음 조회에 새로 계산된다. 운영 만료가 10분이라 그 사이에 스스로 낫는다. 성능이나 정합 문제가 실제로 보이면 그때 한다 — 지금 하면 승인 경로가 캐시와 사이트 목록에 묶인다. 이 항목이 ToDo-User-Nickname-Change 의 마지막 미완이었고, 그 문서를 닫으며 여기로 옮겼다.
  • 세션·WebSocket 의 낡은 닉네임(위 세션).
  • User: 프로필 페이지 자동 rename·redirect(위 위키 내용과 링크).

9. 배포에서 배운 것

2026-09-02 릴리스 20260902-081352 로 배포했다. 이 기능이 처음 겪은 것들:

  • 마이그레이션 전 백업을 배포 뒤에야 확인했다. RDS 자동 백업은 켜져 있었고 시점 복구도 가능했으니 돌아갈 길은 내내 있었다. 그러나 그것을 배포 전에 본 것이 아니라서, 그날 아침의 두 evolution 은 안전망을 확인하지 않은 채 나갔다. 확인 명령은 운영 저장소 ahawiki/README.md 에 있다(인스턴스·프로필 이름이라 이 공개 저장소에는 적지 않는다). conf/evolutions 가 바뀌는 배포는 이것부터 본다(Dev Deploying).
  • evolution 68 이 테이블 collation 을 안 적어 DB 기본값(utf8mb4_unicode_ci)을 받았다. 다른 테이블은 전부 utf8mb4_bin 이었다. 동작에는 영향이 없었지만 evolution 69 가 맞췄다. 규약과 가드는 Dev Database.
  • evolution 69 의 첫 배포는 실패했다. 주석 안의 세미콜론 때문이다 — Play 가 주석을 모른 채 ; 로 문장을 자른다. 사이트는 한 인스턴스가 옛 릴리스로 버텨 내내 200 이었다. 실패한 play_evolutions 행은 옛 릴리스도 막으므로, 문장이 하나도 실행되지 않은 것을 확인한 뒤 그 행을 지우고 current 를 되돌려 복구했다. 경위는 Dev Database.
  • 개발 스키마에 68·69 가 없어 schemaDump.sh --check 가 한동안 이 테이블만큼 다르다고 보고했다. 개발 스키마가 운영에서 갱신되며 해소됐다(Dev Testing).

10. 확인하지 못한 것

staging 이 없어서, ApiUserNicknameSpec(요청 생성·승인·거절·취소·낡은 세션)과 stub API 에 물린 실제 번들에서의 클릭으로 대신했다. 운영에서 실제로 신청하고 승인을 눌러 본 사람은 아직 없다.

11. 관련

12. See Also

12.2. Similar Pages

Similar pages by cosine similarity. Words after page name are term frequency.

  • 81.57% ToDo-User-Nickname-Change user(33:59), nickname(34:55), api(23:31), 않는다(14:21), 승인(8:27), admin(13:21), account(13:14), 닉네임(7:20), 신청(3:23), seq(9:14)
  • 45.08% Dev Api api(23:147), key(5:85), user(33:44), nickname(34:1), page(1:34), admin(13:20), 않는다(14:16), account(13:10), wiki(1:22), aha(1:19)
  • 39.49% Dev UserEmail user(33:30), nickname(34:1), admin(13:5), 않는다(14:2), 같은(8:5), seq(9:3), email(1:11), site(1:10), dev(7:1), 한다(7:1)
  • 39.05% Dev ApiControllers api(23:24), nickname(34:2), user(33:3), admin(13:12), seq(9:3), 같은(8:3), dev(7:3), site(1:9), 페이지(3:6), 닉네임(7:1)
  • 34.91% Dev Database nickname(34:6), user(33:6), api(23:8), 같은(8:15), db(5:17), 않는다(14:7), 다른(4:15), seq(9:8), 다시(8:9), 있다(8:8)
  • 32.57% Dev AdminUIRoleMenu nickname(34:2), user(33:3), api(23:8), admin(13:14), 않는다(14:1), site(1:14), seq(9:5), requests(10:3), 같은(8:1), dev(7:1)
  • 31.17% Dev ApiResponse api(23:42), admin(13:33), json(2:42), site(1:36), error(2:21), seq(9:9), 않는다(14:3), 같은(8:7), dev(7:5), get(2:10)
  • 31.12% Api api(23:90), user(33:8), key(5:35), page(1:27), 페이지(3:20), ahawiki(1:16), account(13:3), admin(13:3), wiki(1:15), aha(1:14)

12.3. Adjacent Pages

Control
≤ 32
all
1.0x
1.0x
80
-120
ON
Metrics
Nodes(visible/total)0/0
Links(visible/total)0/0
Avg degree0.00
Depth coverage0
Queue(fetch/graph)0 / 0
Zoom(scale)1.00x
Ctrl/⌘ + Scroll: Zoom
Root 1-hop 2-hop+