Skip to content

FE handoff — Organization·Workspace 로그인 IA와 접근 상태

목표 로그인 IA (2026-07-25)

  • 최상위 탭은 계정 직급이 아니라 Workspace / Organization 로그인 문맥이다. Platform Administrator는 이 탭에 넣지 않는다.
  • canonical route는 /auth/workspace, /auth/organization, /auth/workspace-select, /auth/organization-select다.
  • /auth/staff, /auth/signin-auth, /auth/signin, /auth/office-select, /auth/company-select는 기존 링크 호환 alias로만 유지한다. 신규 코드와 문서는 canonical route를 사용한다.
  • /와 보호 경로의 미인증 fallback, 일반 로그아웃 기본 목적지는 /auth/workspace다.
  • Organization 화면 안에서 이메일Organization ID 인증 방식을 2차 탭으로 선택한다. 상위 문맥과 인증 방식을 한 탭에 섞지 않는다.
  • 워크스페이스는 워크스페이스 코드 + 로그인 ID + 패스워드, 조직 로그인 ID 방식은 조직 코드 + 로그인 ID + 패스워드를 받는다. Code와 ID는 별도 입력이고 화면에서 username@code를 직접 받지 않는다.
  • SigninContextTabs.vue가 두 페이지의 상위 문맥 탭을 소유한다. 별도 CSS·override 없이 EDS tabs-list/tabs-trigger를 사용한다.
  • 모바일 390×844 실측: Workspace 카드 347×454, Organization ID 카드 347×582, document scroll width 379px로 가로 overflow가 없다. 데스크톱 1440×900에서 Organization 카드 폭은 448px다.

구현 상태와 연결 경계

  • ✅ 세 로그인 방식 모두 iam-login Edge Function을 사용한다. 브라우저는 Code와 Login ID를 구조화해서 보내며 {login_id}@{code}를 조립하거나 Auth 이메일을 탐색하지 않는다.
  • ✅ Organization 이메일은 identifierType=EMAIL, Organization ID는 ORGANIZATION_LOGIN_ID, Workspace ID는 WORKSPACE_LOGIN_ID로 전송한다.
  • iamAuthRepo.signInIam()이 Edge Function 결과의 access/refresh token으로 Supabase session을 시작한다.
  • ✅ 복수 Organization 이메일 로그인은 선택 확정 시 bindOrganizationContext()iam-context를 호출해 현재 Auth session과 Organization Membership을 다시 검증한다.
  • workforceAuthRepo.loadCurrentWorkforcePrincipal()workforce_identities/office_memberships가 아니라 canonical Principal·Session·Affiliation·Workspace Membership·Role Assignment에서 재수화한다.
  • ❌ Platform Administrator 인증은 고객 콘솔에 form·route·toggle을 만들지 않는다.
  • 자격증명 오류는 조직·Workspace·사용자 존재 여부를 구분하지 않는 동일 메시지로 유지한다.
  • Supabase Auth와 RLS는 Prototype 실행 어댑터다. 목표 AWS API 계약은 IAM-ARCHITECTURE-REVIEW-2026-07-25.md이며 브라우저 구조는 그대로 유지한다.

출시 전 전체 메뉴 검수 프로필

  • prototypeCompanyAccess.js가 전체 메뉴 검수 Company slug와 PRODUCT_MODULES 전체 키를 정의한다.
  • 인증 후 useAuthPrincipal().principal.companySlug === 'leysys'일 때 useModuleSubscription().enabledModules는 Company·Office 범위 모두 전체 상품 키를 반환한다.
  • 실제 selectedModules, 결제 상태, Office entitlement를 덮어쓰지 않는다. 상품 관리 화면은 계약 정본을 유지하고, 서버 데이터 접근은 기존 RLS/API 권한을 따른다.
  • companyLandingPath()는 이 검수 Company에 한해 비활성 Prototype 계약이어도 /workspace/home으로 보낸다.
  • 다른 Company의 메뉴·라우트 게이트는 기존 계약 및 Office 배정 규칙을 그대로 사용한다.

목표 정본 경계

  • Principal: 로그인 방법과 분리된 불변 주체
  • Organization Affiliation: 조직별 프로필·관리·좌석·수명주기, 접근권 없음
  • Organization/Workspace Membership: 실제 접근 관계
  • Authentication Assurance: 민감 작업을 제한하지만 권한을 생성하지 않음
  • 컨텍스트: useAuthPrincipal은 서버가 돌려준 principalId, loginContext, 최초 Workspace 문맥을 보관한다. useAccountContext는 현재 Organization/Workspace 표시 범위를 담당하며 Active Context 자체는 권한 근거가 아니다.
  • 목록·역할 Prototype: useWorkforceDirectory
  • 감사 Prototype: useAccountAuditTrail

복수 Company 선택 흐름

  • SigninPag.vueiam-login이 활성 canonical Organization Membership을 확인한 뒤, 업무 데이터 adapter인 loadCompanyAccountOptions(client, authenticatedUser.id)현재 사용자 자신의 표시 옵션을 읽는다. .limit(1)을 사용하지 않는다.
  • 0개면 업무 진입을 막고, 1개면 loadCompanyAccount(companyId, client, authenticatedUser.id) 후 바로 진입한다.
  • 2개 이상이면 /auth/organization-select로 이동한다. CompanySelectPag.vue는 Organization 용어, 회사명·사업자등록번호 검색과 공식 radio-card 선택 패턴을 사용한다.
  • 선택 확정 시 먼저 bindOrganizationContext(companyId, client)가 canonical Membership을 검증하고 session context를 기록한다. 이후 applyCompanyAccountSession()useAuthPrincipal, useAccountContext, useModuleSubscription을 같은 Company 데이터로 교체한다.
  • AccountScopeSwitcherMolOrganization 전환은 현재 내부 redirect를 유지한 채 같은 선택 화면을 다시 연다. Workspace-only 사용자에게는 노출하지 않는다.
  • 선택 화면은 인증 세션이 필요한 보호 라우트다. 단일 Company로 바뀐 뒤 직접 열면 해당 Company로 자동 진입한다.
  • companySignupRepo는 목록과 선택 단건 쿼리 모두 .eq('user_id', userId).eq('status', 'active')와 함께 적용한다. RLS가 같은 Company의 다른 관리자 멤버십을 노출해도 목록 중복과 maybeSingle() 실패가 발생하지 않아야 한다. 호출부가 이미 auth.getUser() 결과를 가지고 있으면 그 검증된 ID를 전달하고, 직접 호출 경로만 repository 내부에서 auth.getUser()로 보완한다.
  • redirect는 내부 절대 경로만 허용하며 //로 시작하는 값은 거부한다.

로그인 상태머신

resolveAccountAccess(principal) 결과는 ready | choose | no-offices | invited | suspended다. 로딩과 통신 오류는 loading | error로 별도 표시한다. 권한 없음이나 오류에서 임의 Office로 fallback하지 않는다.

Workspace가 1개면 즉시 진입하고 여러 개면 /auth/workspace-select로 보낸다. Organization이 1개면 즉시 진입하고 여러 개면 /auth/organization-select로 보낸다. redirect는 내부 절대 경로만 허용한다.

Workspace 전환 UX

AccountScopeSwitcherMol은 Workspace 범위를 고르는 확정형 dialog다. 검색어는 이름·약칭·코드·주소를 포함한다. 선택만으로 컨텍스트를 변경하지 않고 전환 버튼에서 커밋한다. company:{id} 가상 행은 표시하지 않으며 useAccountContext.selectScope()도 이를 거부한다. 이전 localStorage에 Company 범위가 남아 있으면 유효한 번호순 첫 Workspace, 즉 일반적인 가입 계정의 Workspace 0으로 복구한다. Organization 전환은 계층을 섞지 않고 별도 /auth/organization-select 화면에서 처리한다. 모바일에서는 아이콘 버튼, dialog 내부는 스크롤 목록을 사용한다.

최고관리자의 계약·결제·조직 정보·Workspace 권한 진입은 workspace/AppAsideOrg.vue와 관리비 LineAsideOrg.vuemt-auto 하단 조직관리가 담당한다. 목적지는 /administration/organization/account/general이며 company-admin에게만 보인다. 개인 설정은 일반 업무 메뉴의 내 계정으로 분리한다.

Office 0·순번 표시 계약 (2026-07-16)

  • companySignupRepo.loadCompanyAccount(companyId)offices.office_number를 조회하고 .order('office_number')로 정렬해 officeNumber로 매핑한다.
  • useAccountContext는 Office를 officeNumber 오름차순으로 노출한다. 레거시 prototype localStorage에는 본사 후보를 0, 나머지를 1 이상으로 한 번 보정하고 schema version 5로 저장한다.
  • 회원가입 완료 뒤 Office 0이 첫 활성 범위다. 범위 라벨, 로그인 Office 선택, 전환 dialog, Company Office 목록·상세에 Office N을 일관되게 표시한다.
  • SheetCreateOfficeArt의 다음 번호는 안내용이다. 실제 번호는 create_company_office_with_property가 잠금 안에서 발급하며 응답의 officeNumber를 정본으로 addOfficeRecord()에 넣는다.
  • Office 생성 RPC에는 현재 선택된 account.company.value.idp_company_id로 반드시 보낸다. 복수 Company 계정에서 첫 Company fallback을 두지 않는다.
  • 긴 순번 정책 설명은 생성 시트의 InfoHint에 두고 화면에는 Office N으로 생성됩니다만 유지한다.
  • Office 번호는 편집 폼 필드가 아니다. Office 0에는 삭제 액션을 노출하지 않는다.
  • Office 등록코드(office.code)도 생성 뒤 불변이다. 편집 화면을 추가하더라도 이름·주소 등 가변 속성과 분리하고 등록코드 입력을 다시 활성화하지 않는다. DB trigger가 직접 변경도 거부한다.

운영 전환 시:

  1. 서버 검색·커서 페이지네이션 또는 가상 스크롤
  2. 선택 시 현재 페이지가 새 Office에서도 유효한지 검사
  3. 미저장 draft가 있으면 이탈 확인
  4. 캐시·API query key에 Company/Office scope 포함
  5. 전환 성공 후 메뉴·권한·AI 컨텍스트 동시 갱신

공통 통합 상태

  • authSession.js는 보호 라우트 첫 진입·access token 변경 시 getUser()로 사용자를 검증하고 hydrateAuthenticatedAccount()로 서버 Company/Office/상품 범위를 재수화한다. 같은 token의 후속 이동만 검증 캐시를 재사용한다.
  • workforceAuthRepo.loadCurrentWorkforcePrincipal()은 현재 Principal의 유효한 Workspace session, Affiliation, Workspace Membership, Role Assignment, module activation adapter를 다시 조립한다. Organization 사용자와 Workspace 사용자 모두 오래된 localStorage principal을 라우트 권한의 정본으로 사용하지 않는다.
  • ✅ 복수 Company/Office 선택 상태는 각각 선택 화면에서 확정하기 전 업무 경로로 진행하지 않는다. 선택 완료 시 markAuthenticatedContextReady()가 현재 token의 재수화 상태를 완료로 전환한다.
  • pageHelpCatalogassistantFeatureCatalog에는 Company 계정·Office 전환 도움말 목적지가 연결돼 있다.
  • i18n locale에는 본 화면의 한국어 문자열을 이전해야 한다.

회귀 테스트:

  • src/composables/__tests__/authSession.spec.js
  • src/composables/__tests__/companyAccountSession.spec.js
  • src/composables/__tests__/workforceAuthRepo.spec.js
  • src/composables/__tests__/companyAuthRepo.spec.js
  • src/composables/__tests__/iamAuthRepo.spec.js
  • src/composables/__tests__/iamArchitectureMigration.spec.js
  • src/composables/__tests__/companySignupRepo.spec.js
  • src/components/auth/company-select/CompanySelectPag.spec.js

셸별 Office 화면 (2026-07-13)

  • 조직관리: /administration/organization/office/general → administration의 AppLayoutOrgAppAsideOrg를 유지한다.
  • Company 콘솔: /headquarters/console/office/general → headquarters 셸을 유지한다.
  • administration 뷰는 src/views/administration/organization/office/general/IndexView.vue이며, Office 본문은 headquarters의 MainOrg를 재사용하고 헤더만 계정·계약 문맥으로 분리한다.
  • AppAsideOrg.vue와 Company 기본정보의 Office 링크는 administration 경로만 사용한다.
  • src/router/__tests__/globalIaIntegrity.spec.js에서 administration matched chain을 회귀 검사한다.