운영팀 프로젝트 멤버십으로 관리자/회장 권한 자동 동기화 - #51
Conversation
There was a problem hiding this comment.
Pull request overview
This PR changes the source of truth for admin/president permissions: instead of manually toggling User.is_admin / User.is_president (and maintaining president_terms), those flags are now derived from membership/leadership in a single “운영팀” project, with a new is_superadmin break-glass override. It also removes the president-term API and rewires certificate “executive history” rendering to reconstruct president periods from project membership audit logs.
Changes:
- Add “운영팀(admin team)” project concept (
Project.is_admin_team) and a global resync (ProjectService.sync_admin_team_roles) to deriveis_admin/is_presidentfrom active membership + leader role, withis_superadminexemption for admin. - Remove president-term administration flow (schemas/routes/service/model exports) and update tests/fixtures accordingly.
- Rebuild certificate executive history from PROJECT_* audit logs rather than
president_terms.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_member_detail.py | Removes user PATCH role-change audit-log test that no longer applies. |
| tests/test_certificates.py | Updates certificate tests to stop depending on PresidentTerm and president-term APIs. |
| tests/test_auth.py | Removes dev-signin president tests tied to the old president-term mechanism. |
| tests/test_admin_team_roles.py | Adds new coverage for admin-team-derived role syncing and president-appointment permission rule. |
| tests/conftest.py | Updates fixtures to no longer create PresidentTerm; sets is_president directly for gating tests. |
| app/services/project.py | Introduces admin-team lookup and role resync; triggers resync after roster replace operations. |
| app/services/certificate.py | Removes PresidentService; resolves signer via User.is_president helper. |
| app/services/certificate_render.py | Reconstructs “회장” periods by replaying PROJECT_* audit logs. |
| app/services/init.py | Stops exporting PresidentService. |
| app/schemas/user.py | Removes is_admin/is_president/is_leader from user update request schema. |
| app/schemas/certificate.py | Removes president-term request/response schemas. |
| app/schemas/auth.py | Clarifies dev-signin role flags and their relationship to derived roles. |
| app/schemas/init.py | Removes president-term schemas from exports. |
| app/routes/users.py | Removes role-change audit logging + president sync logic from PATCH /users/{id}. |
| app/routes/projects.py | Adds “only sitting president can appoint next leader (unless no leader exists)” guard + triggers admin-team resync on membership changes. |
| app/routes/certificates.py | Removes president-term administration endpoints and updates module docs accordingly. |
| app/routes/auth.py | Dev-signin now adjusts admin-team membership instead of setting derived flags directly. |
| app/models/user.py | Removes single-president constraint machinery; adds is_superadmin; updates role-derivation commentary. |
| app/models/project.py | Adds is_admin_team column to mark the single admin-team project. |
| app/models/president.py | Deletes the PresidentTerm model. |
| app/models/init.py | Removes PresidentTerm export. |
| app/exceptions.py | Replaces president-appointment conflict/term errors with forbidden appointment error for the new rule. |
| alembic/versions/f8a3c1d92b6e_add_admin_team_project_and_superadmin.py | Bootstraps admin-team project + migrates existing admin/president users; adds is_superadmin and drops is_president_marker. |
Comments suppressed due to low confidence (1)
app/routes/auth.py:665
- After sync_admin_team_roles() runs, the dev-signin response may return stale is_admin/is_president values because the resync uses bulk UPDATE with synchronize_session=False and the session is configured with expire_on_commit=False. Refresh the user before returning it so the response reflects the derived roles.
ProjectService.sync_admin_team_roles(db)
db.commit()
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| desired_role = ( | ||
| MemberRole.LEADER if is_president else MemberRole.MEMBER if is_admin else None | ||
| ) | ||
| existing = MemberService.get_active(db, admin_team.id, user.id) | ||
|
|
||
| if desired_role is None: | ||
| if existing is not None: | ||
| existing.left_at = date.today() | ||
| db.flush() | ||
| elif existing is None: | ||
| MemberService.add( | ||
| db, | ||
| project_id=admin_team.id, | ||
| user_id=user.id, | ||
| role=desired_role, | ||
| position=None, | ||
| actor_id=user.id, | ||
| ) | ||
| elif existing.role != desired_role: | ||
| existing.role = desired_role | ||
| db.flush() |
| `is_president`는 운영팀 프로젝트 리더십에서 파생되며 | ||
| (`ProjectService.sync_operations_roles`), 인수인계 중 일시적으로 여러 | ||
| 명이 동시에 `is_president=True`일 수 있다 -- `.first()`는 그중 임의의 | ||
| 한 명을 반환할 뿐, "그 한 명"이 유일한 현직이라는 보장은 없다.""" | ||
| return db.query(User).filter(User.is_president.is_(True)).first() |
There was a problem hiding this comment.
정렬 기준을 id로 하는 건 별로 효과가 없을 것 같고.. auditlog 활용해 임명일 순 정렬하는 게 제일 좋아 보이는데 트랜잭션이 너무 복잡하거나 쿼리를 짜기 너무 까다로우면 그냥 둬도 괜찮을 것 같아요
There was a problem hiding this comment.
가장 최근에 임명된 회장을 기준으로 하겠습니다!
| `is_president`는 운영팀 프로젝트 리더십에서 파생되며 | ||
| (`ProjectService.sync_operations_roles`), 인수인계 중 일시적으로 여러 | ||
| 명이 동시에 `is_president=True`일 수 있다 -- `.first()`는 그중 임의의 | ||
| 한 명을 반환할 뿐, "그 한 명"이 유일한 현직이라는 보장은 없다.""" | ||
| return db.query(User).filter(User.is_president.is_(True)).first() |
There was a problem hiding this comment.
정렬 기준을 id로 하는 건 별로 효과가 없을 것 같고.. auditlog 활용해 임명일 순 정렬하는 게 제일 좋아 보이는데 트랜잭션이 너무 복잡하거나 쿼리를 짜기 너무 까다로우면 그냥 둬도 괜찮을 것 같아요
| desired_role = ( | ||
| MemberRole.LEADER if is_president else MemberRole.MEMBER if is_admin else None | ||
| ) | ||
| existing = MemberService.get_active(db, admin_team.id, user.id) | ||
|
|
||
| if desired_role is None: | ||
| if existing is not None: | ||
| existing.left_at = date.today() | ||
| db.flush() | ||
| elif existing is None: | ||
| MemberService.add( | ||
| db, | ||
| project_id=admin_team.id, | ||
| user_id=user.id, | ||
| role=desired_role, | ||
| position=None, | ||
| actor_id=user.id, | ||
| ) | ||
| elif existing.role != desired_role: | ||
| existing.role = desired_role | ||
| db.flush() |
What feature does this PR add?
is_admin/is_president를 관리자가 직접 켜고 끄는 대신, "운영팀" 프로젝트의 멤버십으로 자동 결정되게 바꿨습니다.
운영팀 활성 멤버 → is_admin = True, 그 외 전원 → False
운영팀 팀장 → is_president = True, 그 외 전원 → False
팀장 임명(승격)은 현재 회장만 가능합니다. 단, 팀장이 아예 없는 경우(초기 세팅 등)는 일반 admin도 임명할 수 있습니다.
팀장 인원 수는 제한하지 않습니다. 인수인계 기간에는 회장이 일시적으로 두 명이 될 수 있습니다.
기존 회장 임기 관리 기능(PresidentTerm/PresidentService)은 제거했습니다. 회장 이력은 이제 운영팀 프로젝트의 활동 로그에서 재구성해서 보여줍니다.
PATCH /users/{id}}와 dev-login에서 is_admin/is_president/is_leader를 직접 지정하던 기능을 없앴습니다. dev-login은 대신 실제로 운영팀 프로젝트에 멤버를 추가/삭제하는 방식으로 동작하도록 바꿔서, 운영 환경과 동일한 경로로 권한이 부여되게 했습니다.마이그레이션에서 기존 회장/관리자를 운영팀 프로젝트 멤버로 옮기고, 수퍼관리자 계정 한 명을 지정했습니다.(운영팀 소속 여부와 무관하게 항상 관리자 권한 유지).
Are there any caveats or things to watch out for?
president_terms테이블은 이번 마이그레이션에서 삭제하지 않았습니다. 예전 회장 이력 데이터가 남아있어서, 필요 없다고 확인되면 이후 별도 마이그레이션으로 정리하면 됩니다.