법인 회원 초대 방법 개선

This commit is contained in:
Rinjae
2025-11-25 14:29:22 +09:00
parent d7a32e1dcf
commit 39ca5837ef
8 changed files with 245 additions and 96 deletions
+49 -4
View File
@@ -2,6 +2,55 @@
광주은행 엔터프라이즈 API 포털 관리 시스템
## 프로젝트 클론 (Git Clone)
아래 스크립트를 복사하여 한 번에 실행하면 모든 프로젝트를 클론합니다:
```bash
# 변수 설정 (환경에 맞게 수정하세요)
GIT_URL="ssh://git@192.168.240.178:18081/eapim"
PROJECT_HOME="/c/eactive/workspaces/kjb-eapim-devPortal"
DEFAULT_BRANCH="jenkins_with_weblogic"
# 프로젝트 홈 디렉토리 생성 및 이동
mkdir -p "$PROJECT_HOME"
cd "$PROJECT_HOME"
# 메인 프로젝트 클론
git clone -b "$DEFAULT_BRANCH" "$GIT_URL/kjb-eapim.git" kjb-eapim
# 의존 모듈 클론 (kjb-eapim 폴더와 동일 레벨)
mkdir -p eapim-online
git clone -b "$DEFAULT_BRANCH" "$GIT_URL/elink-online-core-jpa.git" eapim-online/elink-online-core-jpa
git clone -b "$DEFAULT_BRANCH" "$GIT_URL/elink-portal-common.git" elink-portal-common
git clone -b "$DEFAULT_BRANCH" "$GIT_URL/kjb-safedb.git" kjb-safedb
# 클론 후 폴더 구조 확인
echo "=== 프로젝트 구조 ==="
ls -la "$PROJECT_HOME"
```
### 클론 후 폴더 구조
```
$PROJECT_HOME/
├── kjb-eapim/ # 메인 프로젝트
│ └── eapim-portal/ # 이 프로젝트 (Portal)
├── eapim-online/ # Gateway 데이터 모델
│ └── elink-online-core-jpa/ # JPA 엔티티 모듈
├── elink-portal-common/ # 공통 유틸리티
└── kjb-safedb/ # SafeDB 암호화 라이브러리
```
### 의존 관계 (settings.gradle 참조)
```
eapim-portal
├── elink-online-core-jpa → ../eapim-online/elink-online-core-jpa
├── elink-portal-common → ../elink-portal-common
└── kjb-safedb → ../kjb-safedb
```
## 프로젝트 개요
**EAPIM Portal**은 광주은행의 API 서비스를 관리하고, 사용자 등록, API 키 발급, 문서화, 테스트 기능을 제공하는 웹 기반 플랫폼입니다.
@@ -136,7 +185,3 @@ docker run -p 8080:8080 eapim-portal:latest
- **프로젝트 상세 지침**: `CLAUDE.md` (한글)
- **사용자 가이드**: `개발자포탈.md` (한글)
- **빌드 스크립트**: `build-gf63.sh`, `deploy_portal.sh`
## 라이선스
© 광주은행 (Kwangju Bank)
+62 -56
View File
@@ -1,99 +1,105 @@
flowchart TD
Start([법인 관리자가 사용자 초대]) --> GenerateToken[8글자 토큰 생성<br/>예: K7MNPQ2R<br/>UserInvitation 저장]
Start([법인 관리자가 사용자 초대]) --> GenerateToken[8글자 토큰 생성<br/>예: K7MNPQ2R<br/>UserInvitation 저장<br/>상태: PENDING]
GenerateToken --> SendEmail[이메일 발송<br/>- 링크 포함<br/>- 8글자 토큰 표시]
SendEmail --> UserType{사용자 유형}
%% ========================================
%% 신규 회원 플로우
%% ========================================
UserType -->|신규 회원<br/>미가입자| NewUserPath{초대 확인 방법}
NewUserPath -->|폐쇄망 환경| LoginPage1[로그인 페이지 접근]
LoginPage1 --> InviteLink1["초대를 받으셨나요?" 링크 클릭]
InviteLink1 --> InputCodePage1[초대코드 입력 페이지]
InputCodePage1 --> InputCode1[8자리 코드 입력<br/>예: K7MNPQ2R]
InputCode1 --> ValidateCode1{코드 유효성 검증}
ValidateCode1 -->|유효하지 않음| Error1[에러: 유효하지 않은 초대코드]
ValidateCode1 -->|유효| SignupPage1[회원가입 페이지<br/>/signup/portalUser?invitation=CODE]
NewUserPath -->|이메일 링크| EmailLink1[이메일 링크 클릭<br/>/signup/portalUser?invitation=CODE]
EmailLink1 --> SignupPage1
NewUserPath -->|일반 회원가입| DirectSignup[일반 회원가입 진행<br/>ROLE_USER로 생성]
SignupPage1 --> RegisterForm[가입 정보 입력<br/>- 이메일 자동 입력됨<br/>- 법인명 표시<br/>- 약관 동의]
RegisterForm --> RegisterComplete[회원가입 완료<br/>ROLE_CORP_USER로 생성]
EmailLink1 --> SignupPage1[회원가입 페이지<br/>- 이메일 자동 입력됨<br/>- 법인명 표시]
SignupPage1 --> RegisterForm[가입 정보 입력<br/>약관 동의]
RegisterForm --> RegisterComplete[회원가입 완료<br/>ROLE_CORP_USER로 생성<br/>상태: COMPLETED]
RegisterComplete --> NewUserEnd([가입 완료 안내])
DirectSignup --> Login4[로그인]
Login4 --> AfterLogin4[로그인 성공]
AfterLogin4 --> CheckInvitation
%% ========================================
%% 기존 회원 플로우
%% ========================================
UserType -->|기존 회원<br/>ROLE_USER| ExistingUserPath{초대 확인 방법}
ExistingUserPath -->|폐쇄망 환경| LoginPage2[로그인 페이지 접근]
LoginPage2 --> InviteLink2["초대를 받으셨나요?" 링크 클릭]
InviteLink2 --> InputCodePage2[초대코드 입력 페이지]
InputCodePage2 --> CheckLogin1{로그인 여부}
ExistingUserPath -->|이메일 링크<br/>비로그인 상태| EmailLink2[이메일 링크 클릭<br/>/signup/decision?invitation=CODE]
ExistingUserPath -->|이메일 링크<br/>로그인 상태| EmailLinkLoggedIn[이메일 링크 클릭<br/>/signup/decision?invitation=CODE]
ExistingUserPath -->|직접 로그인| DirectLogin[로그인 페이지에서 로그인]
CheckLogin1 -->|미로그인| RedirectLogin1[로그인 페이지로 리다이렉트<br/>코드 저장]
RedirectLogin1 --> Login1[로그인]
Login1 --> AfterLogin1[로그인 성공]
CheckLogin1 -->|로그인됨| InputCode2[8자리 코드 입력]
InputCode2 --> ValidateCode2{코드 유효성 검증}
ValidateCode2 -->|유효하지 않음| Error2[에러: 유효하지 않은 초대코드]
ValidateCode2 -->|유효| DecisionPage1[초대 수락 페이지<br/>/signup/decision_process]
ExistingUserPath -->|이메일 링크| EmailLink2[이메일 링크 클릭<br/>/signup/decision?invitation=CODE]
%% 비로그인 상태에서 이메일 링크 클릭
EmailLink2 --> DecisionPagePublic[초대 수락 페이지<br/>비로그인 상태]
DecisionPagePublic --> RedirectLogin2[로그인 유도]
RedirectLogin2 --> Login2[로그인]
Login2 --> AfterLogin2[로그인 성공]
ExistingUserPath -->|직접 로그인| DirectLogin[로그인 페이지에서 로그인]
%% 로그인 상태에서 이메일 링크 클릭 → 바로 수락 페이지
EmailLinkLoggedIn --> DecisionPageDirect[초대 수락 페이지<br/>/signup/decision_process<br/>약관 동의]
DecisionPageDirect --> UserDecision
%% 직접 로그인
DirectLogin --> Login3[로그인]
Login3 --> AfterLogin3[로그인 성공]
%% 로그인 후 자동 초대 확인
AfterLogin1 --> CheckInvitation[초대 코드 자동 확인<br/>findFirstByInvitationEmailAndStatus]
AfterLogin2 --> CheckInvitation
%% ========================================
%% 로그인 후 초대 확인 (PortalAuthenticationSuccessHandler)
%% ========================================
AfterLogin2 --> CheckInvitation[PortalAuthenticationSuccessHandler<br/>PENDING 초대 확인<br/>findFirstByInvitationEmailAndStatus]
AfterLogin3 --> CheckInvitation
CheckInvitation --> HasInvitation{초대 코드 존재?}
CheckInvitation --> HasInvitation{PENDING 초대 존재?}
HasInvitation -->|없음| NormalHome[일반 홈페이지]
HasInvitation -->|있음| AutoRedirect[자동으로 초대 수락 페이지로 이동<br/>/signup/decision_process]
HasInvitation -->|없음| NormalHome[메인 페이지 이동]
HasInvitation -->|있음| SaveToSession[세션에 초대 정보 저장<br/>pendingInvitation=true<br/>pendingInvitationToken<br/>pendingInvitationOrgName]
AutoRedirect --> DecisionPage1
DecisionPage1 --> UserDecision{사용자 선택}
SaveToSession --> RedirectHome[메인 페이지로 이동]
UserDecision -->|수락| AcceptProcess[초대 수락 처리<br/>1. PortalOrg 변경<br/>2. ROLE_CORP_USER 전환<br/>3. 약관 재동의]
UserDecision -->|거절| RejectProcess[초대 거절 처리<br/>Invitation.status = REJECTED]
%% ========================================
%% 메인 페이지 팝업 (index.html)
%% ========================================
RedirectHome --> ShowPopup[팝업 표시<br/>OOO에서 초대가 도착했습니다<br/>초대를 확인하시겠습니까?]
AcceptProcess --> CompleteInvitation[Invitation.status = COMPLETED]
CompleteInvitation --> LogoutPrompt[로그아웃 후 재로그인 안내]
ShowPopup --> PopupChoice{사용자 선택}
PopupChoice -->|확인| GoDecisionPage[초대 수락 페이지 이동<br/>/signup/decision?invitation=TOKEN]
PopupChoice -->|취소| StayHome[메인 페이지 유지<br/>다음 로그인 시 팝업 재표시]
StayHome --> NextLogin[다음 로그인]
NextLogin --> CheckInvitation
GoDecisionPage --> DecisionPage[초대 수락 페이지<br/>/signup/decision_process<br/>약관 동의]
DecisionPage --> UserDecision{사용자 선택}
UserDecision -->|수락| AcceptProcess[초대 수락 처리<br/>1. PortalOrg 변경<br/>2. ROLE_CORP_USER 전환<br/>3. 약관 재동의<br/>상태: COMPLETED]
UserDecision -->|거절| RejectProcess[초대 거절 처리<br/>상태: REJECTED]
AcceptProcess --> LogoutPrompt[로그아웃 후 재로그인 안내]
LogoutPrompt --> ExistingUserEnd([초대 수락 완료])
RejectProcess --> RejectEnd([초대 거절 완료])
%% 에러 처리
Error1 --> ErrorEnd([처리 종료])
Error2 --> ErrorEnd
RejectProcess --> RejectEnd([초대 거절 완료<br/>다음 로그인 시 팝업 안 뜸])
%% ========================================
%% 스타일링
%% ========================================
style Start fill:#e1f5e1
style NewUserEnd fill:#c8e6c9
style ExistingUserEnd fill:#c8e6c9
style RejectEnd fill:#fff9c4
style ErrorEnd fill:#ffcdd2
style Error1 fill:#ffcdd2
style Error2 fill:#ffcdd2
style NormalHome fill:#e3f2fd
style GenerateToken fill:#bbdefb
style AcceptProcess fill:#c5cae9
style CheckInvitation fill:#ffe0b2
style AutoRedirect fill:#ffe0b2
style RejectProcess fill:#ffecb3
style LoginPage1 fill:#e1bee7
style LoginPage2 fill:#e1bee7
style InviteLink1 fill:#f8bbd0
style InviteLink2 fill:#f8bbd0
style InputCodePage1 fill:#f8bbd0
style InputCodePage2 fill:#f8bbd0
style CheckInvitation fill:#ffe0b2
style SaveToSession fill:#ffe0b2
style ShowPopup fill:#f8bbd0
style PopupChoice fill:#f8bbd0
style StayHome fill:#e1bee7
style NextLogin fill:#e1bee7
+74 -16
View File
@@ -6,10 +6,17 @@ sequenceDiagram
participant MessageService as Message Handler
participant Email as Email System
actor 초대받은사용자
participant AuthHandler as PortalAuthenticationSuccessHandler
participant IndexController
participant MainPage as 메인 페이지 (index.html)
participant UserRegisterController
participant UserRegisterFacade
participant PortalUserRepo as PortalUser Repository
participant PortalOrgRepo as PortalOrg Repository
%% ========================================
%% 1. 초대 발송
%% ========================================
법인관리자->>Portal: 사용자 이메일 입력<br/>초대 요청
Portal->>UserManFacade: sendInvitation(sender, emailAddr)
@@ -17,7 +24,7 @@ sequenceDiagram
alt 사용자가 없거나 이미 법인 사용자
UserManFacade-->>Portal: 에러 반환
Portal-->>법인관리자: 초대 불가 알림
else 일반 사용자 ROLE_USER
else 일반 사용자 ROLE_USER 또는 비회원
UserManFacade->>UserManFacade: generateEightCharToken()<br/>예: K7MNPQ2R
UserManFacade->>UserInvitationRepo: save(UserInvitation)<br/>status: PENDING<br/>expiresOn: 7일 후
UserManFacade->>MessageService: publishEvent(UserInvitationEvent)
@@ -28,25 +35,74 @@ sequenceDiagram
Email->>초대받은사용자: 초대 이메일 수신
alt 폐쇄망 환경
초대받은사용자->>Portal: 8글자 토큰 직접 입력<br/>/signup/decision?invitation=K7MNPQ2R
else 이메일 링크
초대받은사용자->>Portal: 링크 클릭<br/>/signup/decision?invitation=BASE64TOKEN
%% ========================================
%% 2-A. 로그인 상태에서 이메일 링크 클릭 (바로 수락 페이지)
%% ========================================
alt 로그인 상태에서 이메일 링크 클릭
초대받은사용자->>Portal: 이메일 링크 클릭<br/>/signup/decision?invitation=TOKEN
Portal->>UserRegisterController: showDecisionPage(invitationToken)
UserRegisterController->>UserInvitationRepo: findByToken(decodedToken)
UserRegisterController-->>Portal: 약관 동의 페이지
Portal-->>초대받은사용자: 초대 수락/거절 화면<br/>(바로 표시, 팝업 없음)
Note over 초대받은사용자: 6. 초대 수락/거절 처리로 이동
end
Portal->>UserRegisterController: showDecisionPage(invitationToken)
UserRegisterController->>UserRegisterController: decodeInvitationToken()<br/>(Base64 or 직접입력 처리)
UserRegisterController->>UserInvitationRepo: findByToken(decodedToken)
alt 토큰 무효 or 상태가 PENDING 아님
UserRegisterController-->>Portal: 에러: 유효하지 않은 초대
Portal-->>초대받은사용자: 에러 페이지
else 토큰 유효
UserRegisterController->>PortalUserRepo: findByEmailAddr()
UserRegisterController-->>Portal: 약관 동의 페이지<br/>(법인명, 사용자명 표시)
Portal-->>초대받은사용자: 수락/거절 선택 화면
%% ========================================
%% 2-B. 로그인 (기존 회원 또는 신규 가입 후)
%% ========================================
alt 기존 회원 (비로그인 상태)
초대받은사용자->>Portal: 로그인
else 비회원이 일반 회원가입 후
초대받은사용자->>Portal: 회원가입 (ROLE_USER)
초대받은사용자->>Portal: 로그인
end
%% ========================================
%% 3. 로그인 성공 처리 (AuthenticationSuccessHandler)
%% ========================================
Portal->>AuthHandler: onAuthenticationSuccess()
AuthHandler->>AuthHandler: 이메일 인증, 휴면계정,<br/>비밀번호 만료 체크
AuthHandler->>UserInvitationRepo: findFirstByInvitationEmailAndStatus<br/>(loginId, PENDING)
alt PENDING 초대 없음
AuthHandler->>Portal: redirect to /
Portal-->>초대받은사용자: 메인 페이지 (팝업 없음)
else PENDING 초대 있음 & 만료 안됨
AuthHandler->>PortalOrgRepo: findById(invitation.orgId)
PortalOrgRepo-->>AuthHandler: PortalOrg (기관명)
AuthHandler->>AuthHandler: session.setAttribute<br/>("pendingInvitation", true)<br/>("pendingInvitationToken", token)<br/>("pendingInvitationOrgName", orgName)
AuthHandler->>Portal: redirect to /
end
%% ========================================
%% 4. 메인 페이지 팝업 표시
%% ========================================
Portal->>IndexController: GET /
IndexController->>MainPage: render index.html
MainPage->>MainPage: 세션에서 pendingInvitation 확인
alt pendingInvitation == true
MainPage-->>초대받은사용자: 팝업 표시<br/>"[기관명]에서 초대가 도착했습니다.<br/>초대를 확인하시겠습니까?"
end
%% ========================================
%% 5. 사용자 팝업 선택
%% ========================================
alt 팝업에서 "확인" 클릭
초대받은사용자->>Portal: /signup/decision?invitation=TOKEN
Portal->>UserRegisterController: showDecisionPage(invitationToken)
UserRegisterController->>UserInvitationRepo: findByToken(decodedToken)
UserRegisterController-->>Portal: 약관 동의 페이지
Portal-->>초대받은사용자: 초대 수락/거절 화면
else 팝업에서 "취소" 클릭
Note over 초대받은사용자,MainPage: 메인 페이지 유지<br/>다음 로그인 시 팝업 재표시
end
%% ========================================
%% 6. 초대 수락/거절 처리
%% ========================================
초대받은사용자->>Portal: 수락 or 거절 선택
Portal->>UserRegisterController: processInvitation(action)
UserRegisterController->>UserRegisterFacade: processInvitation(action, invitation)
@@ -57,8 +113,10 @@ sequenceDiagram
UserRegisterFacade->>UserInvitationRepo: save(invitation)<br/>status: COMPLETED<br/>completeDate: now()
UserRegisterFacade-->>Portal: 수락 성공
Portal-->>초대받은사용자: 성공 메시지<br/>로그아웃 후 재로그인 안내
Note over 초대받은사용자: 다음 로그인 시<br/>PENDING 초대 없음 → 팝업 안 뜸
else 거절
UserRegisterFacade->>UserInvitationRepo: save(invitation)<br/>status: REJECTED<br/>completeDate: now()
UserRegisterFacade-->>Portal: 거절 처리 완료
Portal-->>초대받은사용자: 거절 완료 메시지
Note over 초대받은사용자: 다음 로그인 시<br/>REJECTED 상태 → 팝업 안 뜸
end
+33 -9
View File
@@ -1,20 +1,44 @@
stateDiagram-v2
[*] --> 초대생성: 법인관리자가 초대 시작
초대생성 --> PENDING: 8글자 토큰 생성<br/>UserInvitation 저장
초대생성 --> PENDING: 토큰 생성 저장
PENDING --> 토큰전달: 이메일 발송
PENDING --> 이메일발송: 초대 이메일 발송
토큰전달 --> 사용자접근: 폐쇄망 직접입력 or<br/>이메일 링크 클릭
state 로그인후팝업 {
이메일발송 --> 로그인: 사용자 로그인
로그인 --> 초대확인: AuthSuccessHandler
초대확인 --> 세션저장: 세션에 초대 정보 저장
세션저장 --> 팝업표시: 메인 페이지 팝업
팝업표시 --> 팝업확인: 확인 클릭
팝업표시 --> 팝업취소: 취소 클릭
팝업취소 --> 로그인: 다음 로그인 시 재표시
}
사용자접근 --> 약관동의: 토큰 유효성 검증 성공
사용자접근 --> 에러: 토큰 무효
팝업확인 --> 약관동의: 수락 페이지 이동
약관동의 --> COMPLETED: 사용자 수락<br/>ROLE_CORP_USER 전환<br/>PortalOrg 변경
약관동의 --> REJECTED: 사용자 거절
약관동의 --> COMPLETED: 수락
약관동의 --> REJECTED: 거절
COMPLETED --> [*]: 초대 완료
REJECTED --> [*]: 초대 거절
PENDING --> CANCELED: 관리자가 초대 취소
PENDING --> CANCELED: 관리자 취소
PENDING --> EXPIRED: 만료일 경과
CANCELED --> [*]: 초대 취소
에러 --> [*]: 처리 종
EXPIRED --> [*]: 초대 만
note right of PENDING
PENDING 상태일 때만
로그인 시 팝업 표시
end note
note right of REJECTED
REJECTED 상태가 되면
더 이상 팝업 표시 안 함
end note
note right of COMPLETED
COMPLETED 상태가 되면
더 이상 팝업 표시 안 함
ROLE_CORP_USER로 전환됨
end note
@@ -19,6 +19,7 @@ import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequiredArgsConstructor
public class IndexController {
@@ -1,9 +1,11 @@
package com.eactive.apim.portal.config;
import com.eactive.apim.portal.apps.user.repository.PortalOrgRepository;
import com.eactive.apim.portal.apps.user.service.PortalUserLogService;
import com.eactive.apim.portal.invitation.entity.UserInvitation;
import com.eactive.apim.portal.invitation.entity.UserInvitationEnums.InvitationStatus;
import com.eactive.apim.portal.invitation.repository.UserInvitationRepository;
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
import com.eactive.apim.portal.portaluser.entity.PortalUser;
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums;
import com.eactive.apim.portal.portaluser.entity.UserPasswordHistory;
@@ -38,6 +40,7 @@ public class PortalAuthenticationSuccessHandler implements AuthenticationSuccess
private final UserPasswordHistoryRepository passwordHistoryRepository;
private final MessageRequestRepository messageRequestRepository;
private final UserInvitationRepository userInvitationRepository;
private final PortalOrgRepository portalOrgRepository;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
@@ -78,18 +81,23 @@ public class PortalAuthenticationSuccessHandler implements AuthenticationSuccess
session.setAttribute("redirectUrl", contextPath + "/new_password");
}
// 초대 코드 확인 - ROLE_USER만 확인
// 초대 코드 확인 - ROLE_USER만 확인 (세션에 저장하여 메인 페이지에서 팝업으로 표시)
if (user.getRoleCode() == PortalUserEnums.RoleCode.ROLE_USER) {
Optional<UserInvitation> pendingInvitation =
userInvitationRepository.findFirstByInvitationEmailAndStatus(
user.getLoginId(), InvitationStatus.PENDING);
if (pendingInvitation.isPresent()) {
// 만료되지 않은 초대가 있으면 자동으로 초대 수락 페이지로 이동
UserInvitation invitation = pendingInvitation.get();
if (invitation.getExpiresOn().isAfter(LocalDateTime.now())) {
response.sendRedirect(contextPath + "/signup/decision?invitation=" + invitation.getToken());
return;
// 세션에 초대 정보 저장 (메인 페이지에서 팝업으로 표시)
session.setAttribute("pendingInvitation", true);
session.setAttribute("pendingInvitationToken", invitation.getToken());
// orgId로 기관명 조회
String orgName = portalOrgRepository.findById(invitation.getOrgId())
.map(PortalOrg::getOrgName)
.orElse("알 수 없는 기관");
session.setAttribute("pendingInvitationOrgName", orgName);
}
}
}
@@ -70,13 +70,6 @@
<img th:src="@{/img/icon/icon_login_join.png}" alt="">
<a th:href="@{/signup}">회원가입</a>
</li>
<li>
<span>|</span>
</li>
<li class="icon_join">
<img th:src="@{/img/icon/icon_login_join.png}" alt="">
<a th:href="@{/signup/invitation-code}">초대를 받으셨나요?</a>
</li>
</ul>
</div>
<div class="login_list m-only">
@@ -299,6 +299,20 @@
customPopups.hideAlert('#customAlert');
window.location.href = redirectUrl;
});
return; // 다른 체크 중단
}
// 초대 대기 확인 (로그인할 때마다 표시)
const pendingInvitation = [[${session.pendingInvitation}]];
if (pendingInvitation) {
const invitationToken = /*[[${session.pendingInvitationToken}]]*/ '';
const orgName = /*[[${session.pendingInvitationOrgName}]]*/ '';
customPopups.showConfirm(`<strong>${orgName}</strong>에서 초대가 도착했습니다.<br>초대를 확인하시겠습니까?`, function (selection) {
if (selection) {
window.location.href = '/signup/decision?invitation=' + invitationToken;
}
});
}
});
</script>