Skip to content

운영팀 프로젝트 멤버십으로 관리자/회장 권한 자동 동기화 - #51

Merged
Mr-chimp1 merged 5 commits into
mainfrom
feat/sync-admin-roles-from-operations-team
Jul 30, 2026
Merged

운영팀 프로젝트 멤버십으로 관리자/회장 권한 자동 동기화#51
Mr-chimp1 merged 5 commits into
mainfrom
feat/sync-admin-roles-from-operations-team

Conversation

@Mr-chimp1

Copy link
Copy Markdown
Collaborator

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 테이블은 이번 마이그레이션에서 삭제하지 않았습니다. 예전 회장 이력 데이터가 남아있어서, 필요 없다고 확인되면 이후 별도 마이그레이션으로 정리하면 됩니다.
  • "팀장 임명은 회장만 가능" 제한은 개별 멤버 추가/변경 API에만 걸려 있고, 명단 일괄 교체(벌크) API에는 걸려 있지 않습니다. 벌크 교체로는 누구나(관리자 권한만 있으면) 운영팀 명단을 통째로 바꿀 수 있습니다.
  • 이제 is_admin/is_president를 부여하는 유일한 방법은 운영팀 프로젝트에 멤버로 추가하는 것입니다(수퍼관리자 계정 제외). 별도의 "관리자 지정" 버튼 같은 게 없다면 프론트에서도 이 흐름에 맞춰 UI를 만들어야 합니다.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 derive is_admin/is_president from active membership + leader role, with is_superadmin exemption 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.

Comment thread app/routes/auth.py Outdated
Comment on lines +641 to +661
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()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

필요하네요

Comment thread app/services/certificate.py Outdated
Comment on lines +224 to +228
`is_president`는 운영팀 프로젝트 리더십에서 파생되며
(`ProjectService.sync_operations_roles`), 인수인계 중 일시적으로 여러
명이 동시에 `is_president=True`일 수 있다 -- `.first()`는 그중 임의의
한 명을 반환할 뿐, "그 한 명"이 유일한 현직이라는 보장은 없다."""
return db.query(User).filter(User.is_president.is_(True)).first()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정렬 기준을 id로 하는 건 별로 효과가 없을 것 같고.. auditlog 활용해 임명일 순 정렬하는 게 제일 좋아 보이는데 트랜잭션이 너무 복잡하거나 쿼리를 짜기 너무 까다로우면 그냥 둬도 괜찮을 것 같아요

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

가장 최근에 임명된 회장을 기준으로 하겠습니다!

Comment thread app/models/user.py
Comment thread app/routes/users.py
Comment thread app/services/certificate_render.py
Comment thread app/services/certificate.py Outdated
Comment on lines +224 to +228
`is_president`는 운영팀 프로젝트 리더십에서 파생되며
(`ProjectService.sync_operations_roles`), 인수인계 중 일시적으로 여러
명이 동시에 `is_president=True`일 수 있다 -- `.first()`는 그중 임의의
한 명을 반환할 뿐, "그 한 명"이 유일한 현직이라는 보장은 없다."""
return db.query(User).filter(User.is_president.is_(True)).first()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정렬 기준을 id로 하는 건 별로 효과가 없을 것 같고.. auditlog 활용해 임명일 순 정렬하는 게 제일 좋아 보이는데 트랜잭션이 너무 복잡하거나 쿼리를 짜기 너무 까다로우면 그냥 둬도 괜찮을 것 같아요

Comment thread app/routes/auth.py Outdated
Comment on lines +641 to +661
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()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

필요하네요

@Mr-chimp1
Mr-chimp1 merged commit d0acb19 into main Jul 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants