- 초대 로직 변경: 이메일→휴대폰 기반 초대 전환 및 UI 업데이트
- 알림 수신 동의 추가: 초대/취소 시 알림 발송 옵션 탑재 - 관리자 기능 추가: 소속 제외 및 권한 조정 기능 구현
This commit is contained in:
+34
-9
@@ -15,7 +15,11 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/agreements")
|
@RequestMapping("/agreements")
|
||||||
public class AgreementsController {
|
public class AgreementsController {
|
||||||
public static final String TERMS_AGREEMENTS = "fragment/kjbank/terms_agreements";
|
public static final String TERMS_AGREEMENTS = "fragment/djbank/terms_agreements";
|
||||||
|
|
||||||
|
/** 개인정보처리방침은 약관 페이지에서 분리되어 제주은행 공식 사이트 외부 링크로 대체됨 */
|
||||||
|
public static final String PRIVACY_POLICY_EXTERNAL_URL =
|
||||||
|
"https://www.jejubank.co.kr/hmpg/csct/secuCenr/ptctPlcy/procsPlcy/ctnt.do";
|
||||||
|
|
||||||
private final AgreementsFacade agreementsFacade;
|
private final AgreementsFacade agreementsFacade;
|
||||||
|
|
||||||
@@ -28,28 +32,49 @@ public class AgreementsController {
|
|||||||
public String showTerms(@RequestParam(required = false) String tab,
|
public String showTerms(@RequestParam(required = false) String tab,
|
||||||
@RequestParam(required = false) String publishedOn,
|
@RequestParam(required = false) String publishedOn,
|
||||||
Model model) {
|
Model model) {
|
||||||
|
String currentTab = tab != null ? tab : "terms";
|
||||||
|
|
||||||
|
// 구 개인정보처리방침 탭(tab=privacy)은 외부 링크로 이동했으므로 외부 URL로 리다이렉트(북마크 호환)
|
||||||
|
if ("privacy".equals(currentTab)) {
|
||||||
|
return "redirect:" + PRIVACY_POLICY_EXTERNAL_URL;
|
||||||
|
}
|
||||||
|
|
||||||
AgreementType type;
|
AgreementType type;
|
||||||
if ("privacy".equals(tab)) {
|
switch (currentTab) {
|
||||||
type = AgreementType.PRIVACY_POLICY;
|
case "privacy-collect":
|
||||||
} else {
|
// 개인정보수집동의서 = PRIVACY_COLLECT (신설항목)
|
||||||
type = AgreementType.TERMS_OF_USE;
|
type = AgreementType.PRIVACY_COLLECT;
|
||||||
|
break;
|
||||||
|
case "notification":
|
||||||
|
type = AgreementType.NOTIFICATION_CONSENT;
|
||||||
|
break;
|
||||||
|
case "terms":
|
||||||
|
default:
|
||||||
|
currentTab = "terms";
|
||||||
|
type = AgreementType.TERMS_OF_USE;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<AgreementsDTO> agreementsList = agreementsFacade.getAgreementsList(String.valueOf(type));
|
List<AgreementsDTO> agreementsList = agreementsFacade.getAgreementsList(String.valueOf(type));
|
||||||
model.addAttribute("agreementsList", agreementsList);
|
model.addAttribute("agreementsList", agreementsList);
|
||||||
|
|
||||||
AgreementsDTO selectedAgreement = publishedOn != null && !publishedOn.isEmpty() ?
|
// 해당 약관 종류가 아직 시드되지 않았을 수 있으므로 빈 리스트 방어
|
||||||
findAgreementByDate(agreementsList, publishedOn) : agreementsList.get(0);
|
AgreementsDTO selectedAgreement = null;
|
||||||
|
if (agreementsList != null && !agreementsList.isEmpty()) {
|
||||||
|
selectedAgreement = publishedOn != null && !publishedOn.isEmpty() ?
|
||||||
|
findAgreementByDate(agreementsList, publishedOn) : agreementsList.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
model.addAttribute("selectedAgreement", selectedAgreement);
|
model.addAttribute("selectedAgreement", selectedAgreement);
|
||||||
model.addAttribute("selectedDate", publishedOn);
|
model.addAttribute("selectedDate", publishedOn);
|
||||||
|
|
||||||
model.addAttribute("isTermsOfUse", type == AgreementType.TERMS_OF_USE);
|
model.addAttribute("isTermsOfUse", type == AgreementType.TERMS_OF_USE);
|
||||||
model.addAttribute("isPrivacyPolicy", type == AgreementType.PRIVACY_POLICY);
|
model.addAttribute("isPrivacyCollect", type == AgreementType.PRIVACY_COLLECT);
|
||||||
|
model.addAttribute("isNotification", type == AgreementType.NOTIFICATION_CONSENT);
|
||||||
model.addAttribute("agreementTitle", type.getDescription());
|
model.addAttribute("agreementTitle", type.getDescription());
|
||||||
model.addAttribute("agreementType", type.getCode());
|
model.addAttribute("agreementType", type.getCode());
|
||||||
|
|
||||||
model.addAttribute("currentTab", tab != null ? tab : "terms");
|
model.addAttribute("currentTab", currentTab);
|
||||||
|
|
||||||
return TERMS_AGREEMENTS;
|
return TERMS_AGREEMENTS;
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-2
@@ -71,8 +71,9 @@ public class AgreementsFacadeImpl implements AgreementsFacade {
|
|||||||
throw new NotFoundException("현재 시행중인 이용약관이 없습니다.");
|
throw new NotFoundException("현재 시행중인 이용약관이 없습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 개인정보수집동의서 처리
|
// 2. 개인정보수집동의서 처리 (개인정보수집동의서 = PRIVACY_COLLECT 신설항목 포함)
|
||||||
if (privacyCollectType != null && privacyCollectType.isPrivacyCollect()) {
|
if (privacyCollectType != null
|
||||||
|
&& (privacyCollectType.isPrivacyCollect() || privacyCollectType == AgreementType.PRIVACY_COLLECT)) {
|
||||||
Optional<Agreements> privacyCollect = agreementsService
|
Optional<Agreements> privacyCollect = agreementsService
|
||||||
.findLastesBeforeDate(
|
.findLastesBeforeDate(
|
||||||
privacyCollectType,
|
privacyCollectType,
|
||||||
@@ -199,6 +200,10 @@ public class AgreementsFacadeImpl implements AgreementsFacade {
|
|||||||
defaultAgreement.setName("개인정보처리방침");
|
defaultAgreement.setName("개인정보처리방침");
|
||||||
defaultAgreement.setContents("현재 개인정보처리방침을 불러올 수 없습니다. 잠시 후 다시 시도해 주세요.");
|
defaultAgreement.setContents("현재 개인정보처리방침을 불러올 수 없습니다. 잠시 후 다시 시도해 주세요.");
|
||||||
break;
|
break;
|
||||||
|
case "PRIVACY_COLLECT":
|
||||||
|
defaultAgreement.setName("개인정보수집동의서");
|
||||||
|
defaultAgreement.setContents("현재 개인정보수집동의서를 불러올 수 없습니다. 잠시 후 다시 시도해 주세요.");
|
||||||
|
break;
|
||||||
case "PRIVACY_COLLECT_IND":
|
case "PRIVACY_COLLECT_IND":
|
||||||
defaultAgreement.setName("개인용 개인정보수집동의서");
|
defaultAgreement.setName("개인용 개인정보수집동의서");
|
||||||
defaultAgreement.setContents("현재 개인용 개인정보수집동의서를 불러올 수 없습니다. 잠시 후 다시 시도해 주세요.");
|
defaultAgreement.setContents("현재 개인용 개인정보수집동의서를 불러올 수 없습니다. 잠시 후 다시 시도해 주세요.");
|
||||||
|
|||||||
@@ -142,8 +142,8 @@ public class AccountController {
|
|||||||
// ROLE_USER인 경우 초대 여부 확인
|
// ROLE_USER인 경우 초대 여부 확인
|
||||||
if (currentUser.getRoleCode() == RoleCode.ROLE_USER) {
|
if (currentUser.getRoleCode() == RoleCode.ROLE_USER) {
|
||||||
java.util.Optional<UserInvitation> pendingInvitation =
|
java.util.Optional<UserInvitation> pendingInvitation =
|
||||||
userInvitationRepository.findFirstByInvitationEmailAndStatus(
|
userInvitationRepository.findFirstByInvitationMobileAndStatus(
|
||||||
user.getEmailAddr(), InvitationStatus.PENDING);
|
currentUser.getMobileNumber(), InvitationStatus.PENDING);
|
||||||
|
|
||||||
if (pendingInvitation.isPresent()) {
|
if (pendingInvitation.isPresent()) {
|
||||||
mav.addObject("hasPendingInvitation", true);
|
mav.addObject("hasPendingInvitation", true);
|
||||||
@@ -262,7 +262,7 @@ public class AccountController {
|
|||||||
model.addAttribute("portalOrg", new PortalOrgRegistrationDTO());
|
model.addAttribute("portalOrg", new PortalOrgRegistrationDTO());
|
||||||
model.addAttribute("registrationType", "corporate");
|
model.addAttribute("registrationType", "corporate");
|
||||||
model.addAttribute("termsOfUse", agreementsFacade.getAgreement("TERMS_OF_USE"));
|
model.addAttribute("termsOfUse", agreementsFacade.getAgreement("TERMS_OF_USE"));
|
||||||
model.addAttribute("privacyCollect", agreementsFacade.getAgreement("PRIVACY_COLLECT_ORG"));
|
model.addAttribute("privacyCollect", agreementsFacade.getAgreement("PRIVACY_COLLECT"));
|
||||||
model.addAttribute("registrationType", "corporate");
|
model.addAttribute("registrationType", "corporate");
|
||||||
return "apps/mypage/orgTransfer";
|
return "apps/mypage/orgTransfer";
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -29,7 +29,7 @@ public class OrgRegisterController {
|
|||||||
model.addAttribute("authTtl", portalProperties.getAuthTtl());
|
model.addAttribute("authTtl", portalProperties.getAuthTtl());
|
||||||
model.addAttribute("portalOrg", new PortalOrgRegistrationDTO());
|
model.addAttribute("portalOrg", new PortalOrgRegistrationDTO());
|
||||||
model.addAttribute("termsOfUse", agreementsFacade.getAgreement("TERMS_OF_USE"));
|
model.addAttribute("termsOfUse", agreementsFacade.getAgreement("TERMS_OF_USE"));
|
||||||
model.addAttribute("privacyCollect", agreementsFacade.getAgreement("PRIVACY_COLLECT_ORG"));
|
model.addAttribute("privacyCollect", agreementsFacade.getAgreement("PRIVACY_COLLECT"));
|
||||||
model.addAttribute("notificationConsent", agreementsFacade.getAgreement("NOTIFICATION_CONSENT"));
|
model.addAttribute("notificationConsent", agreementsFacade.getAgreement("NOTIFICATION_CONSENT"));
|
||||||
return MAIN_ORG_REGISTER;
|
return MAIN_ORG_REGISTER;
|
||||||
}
|
}
|
||||||
@@ -75,6 +75,7 @@ public class OrgRegisterController {
|
|||||||
|
|
||||||
if (response.isValid()) {
|
if (response.isValid()) {
|
||||||
model.addAttribute("message", response.getMessage());
|
model.addAttribute("message", response.getMessage());
|
||||||
|
model.addAttribute("registrationType", "corporate");
|
||||||
return MAIN_REGISTER_RESULT;
|
return MAIN_REGISTER_RESULT;
|
||||||
} else {
|
} else {
|
||||||
setModelForErrorOrg(model, orgDTO, response.getMessage());
|
setModelForErrorOrg(model, orgDTO, response.getMessage());
|
||||||
@@ -92,7 +93,7 @@ public class OrgRegisterController {
|
|||||||
model.addAttribute("portalOrg", orgDTO);
|
model.addAttribute("portalOrg", orgDTO);
|
||||||
model.addAttribute("error", errorMessage);
|
model.addAttribute("error", errorMessage);
|
||||||
model.addAttribute("termsOfUse", agreementsFacade.getAgreement("TERMS_OF_USE"));
|
model.addAttribute("termsOfUse", agreementsFacade.getAgreement("TERMS_OF_USE"));
|
||||||
model.addAttribute("privacyCollect", agreementsFacade.getAgreement("PRIVACY_COLLECT_ORG"));
|
model.addAttribute("privacyCollect", agreementsFacade.getAgreement("PRIVACY_COLLECT"));
|
||||||
model.addAttribute("notificationConsent", agreementsFacade.getAgreement("NOTIFICATION_CONSENT"));
|
model.addAttribute("notificationConsent", agreementsFacade.getAgreement("NOTIFICATION_CONSENT"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-15
@@ -4,6 +4,7 @@ import com.eactive.apim.portal.apps.user.dto.PortalUserDTO;
|
|||||||
import com.eactive.apim.portal.apps.user.facade.UserManFacade;
|
import com.eactive.apim.portal.apps.user.facade.UserManFacade;
|
||||||
import com.eactive.apim.portal.common.dto.ResponseDTO;
|
import com.eactive.apim.portal.common.dto.ResponseDTO;
|
||||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||||
|
import com.eactive.apim.portal.common.validator.CellPhoneValidator;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.security.access.annotation.Secured;
|
import org.springframework.security.access.annotation.Secured;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -18,12 +19,16 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
public class UserManRestController {
|
public class UserManRestController {
|
||||||
|
|
||||||
private final UserManFacade userManFacade;
|
private final UserManFacade userManFacade;
|
||||||
|
private final CellPhoneValidator cellPhoneValidator;
|
||||||
|
|
||||||
// 1. Cancel invitation
|
// 1. Cancel invitation
|
||||||
@PostMapping("/cancel-invitation")
|
@PostMapping("/cancel-invitation")
|
||||||
public ResponseDTO cancelInvitation(@RequestBody PortalUserDTO user) {
|
public ResponseDTO cancelInvitation(@RequestBody PortalUserDTO user) {
|
||||||
userManFacade.cancelInvitation(SecurityUtil.getPortalAuthenticatedUser(), user.getId());
|
boolean notifyConsent = user.isNotifyConsent();
|
||||||
return new ResponseDTO(200, "SUCCESS", "초대가 취소되었습니다.");
|
userManFacade.cancelInvitation(SecurityUtil.getPortalAuthenticatedUser(), user.getId(), notifyConsent);
|
||||||
|
return new ResponseDTO(200, "SUCCESS", notifyConsent
|
||||||
|
? "초대가 취소되었습니다.<br>취소 알림이 발송되었습니다."
|
||||||
|
: "초대가 취소되었습니다.<br>(알림 수신 미동의로 메시지는 발송되지 않았습니다.)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Inactivate user
|
// 2. Inactivate user
|
||||||
@@ -40,25 +45,25 @@ public class UserManRestController {
|
|||||||
return new ResponseDTO(200, "SUCCESS", "사용자가 활성화되었습니다.");
|
return new ResponseDTO(200, "SUCCESS", "사용자가 활성화되었습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
//5. register new user
|
//5. register new user (휴대폰 번호 기반 초대)
|
||||||
@PostMapping("/invite")
|
@PostMapping("/invite")
|
||||||
public ResponseDTO sendInvitation(@RequestBody PortalUserDTO newUser) {
|
public ResponseDTO sendInvitation(@RequestBody PortalUserDTO newUser) {
|
||||||
String email = newUser.getEmailAddr();
|
String mobile = newUser.getMobileNumber();
|
||||||
|
|
||||||
// 이메일 형식 검증
|
// 휴대폰 번호 검증
|
||||||
if (email == null || email.trim().isEmpty()) {
|
if (mobile == null || mobile.trim().isEmpty()) {
|
||||||
return new ResponseDTO(400, "ERROR", "이메일 주소를 입력해주세요.");
|
return new ResponseDTO(400, "ERROR", "휴대폰 번호를 입력해주세요.");
|
||||||
|
}
|
||||||
|
if (!cellPhoneValidator.isValid(mobile, null)) {
|
||||||
|
return new ResponseDTO(400, "ERROR", "올바른 휴대폰 번호 형식을 입력해주세요.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 이메일 정규식 검증
|
boolean notifyConsent = newUser.isNotifyConsent();
|
||||||
String emailPattern = "^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$";
|
userManFacade.sendInvitation(SecurityUtil.getPortalAuthenticatedUser(), mobile, notifyConsent);
|
||||||
if (!email.matches(emailPattern)) {
|
|
||||||
return new ResponseDTO(400, "ERROR", "올바른 이메일 형식을 입력해주세요.");
|
|
||||||
}
|
|
||||||
|
|
||||||
userManFacade.sendInvitation(SecurityUtil.getPortalAuthenticatedUser(), email);
|
return new ResponseDTO(200, "SUCCESS", notifyConsent
|
||||||
|
? "초대 메시지가 발송되었습니다."
|
||||||
return new ResponseDTO(200, "SUCCESS", "요청 메일이 발송되었습니다.");
|
: "초대가 등록되었습니다.<br>(알림 수신 미동의로 메시지는 발송되지 않았습니다.)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/change-role")
|
@PostMapping("/change-role")
|
||||||
@@ -69,6 +74,20 @@ public class UserManRestController {
|
|||||||
return new ResponseDTO(200, "SUCCESS", "변경되었습니다. 확인 버튼을 누르시면 계정을 로그아웃 합니다.");
|
return new ResponseDTO(200, "SUCCESS", "변경되었습니다. 확인 버튼을 누르시면 계정을 로그아웃 합니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 관리자 -> 이용자 변경 (본인 제외)
|
||||||
|
@PostMapping("/revoke-manager")
|
||||||
|
public ResponseDTO revokeManager(@RequestBody PortalUserDTO user) {
|
||||||
|
userManFacade.revokeManager(SecurityUtil.getPortalAuthenticatedUser(), user.getId());
|
||||||
|
return new ResponseDTO(200, "SUCCESS", "이용자로 변경되었습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 소속 제외 -> 개인이용자로 전환
|
||||||
|
@PostMapping("/remove-from-org")
|
||||||
|
public ResponseDTO removeFromOrg(@RequestBody PortalUserDTO user) {
|
||||||
|
userManFacade.removeFromOrg(SecurityUtil.getPortalAuthenticatedUser(), user.getId());
|
||||||
|
return new ResponseDTO(200, "SUCCESS", "소속에서 제외되어 개인이용자로 변경되었습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
// 6. Resend invitation
|
// 6. Resend invitation
|
||||||
@PostMapping("/resend-invitation")
|
@PostMapping("/resend-invitation")
|
||||||
public ResponseDTO resendInvitation(@RequestBody PortalUserDTO user) {
|
public ResponseDTO resendInvitation(@RequestBody PortalUserDTO user) {
|
||||||
|
|||||||
+19
-27
@@ -73,8 +73,6 @@ public class UserRegisterController {
|
|||||||
public String getUserAgreement(@RequestParam(name = "invitation", required = false) String invitationToken, HttpSession session, Model model) {
|
public String getUserAgreement(@RequestParam(name = "invitation", required = false) String invitationToken, HttpSession session, Model model) {
|
||||||
boolean isInvited = false;
|
boolean isInvited = false;
|
||||||
String orgName = null;
|
String orgName = null;
|
||||||
String email = null;
|
|
||||||
String[] parts = null;
|
|
||||||
|
|
||||||
if (invitationToken != null) {
|
if (invitationToken != null) {
|
||||||
// 8글자 토큰 사용
|
// 8글자 토큰 사용
|
||||||
@@ -86,29 +84,22 @@ public class UserRegisterController {
|
|||||||
String orgId = invitation.get().getOrgId();
|
String orgId = invitation.get().getOrgId();
|
||||||
Optional<PortalOrg> org = portalOrgRepository.findById(orgId);
|
Optional<PortalOrg> org = portalOrgRepository.findById(orgId);
|
||||||
orgName = org.map(PortalOrg::getOrgName).orElse(null);
|
orgName = org.map(PortalOrg::getOrgName).orElse(null);
|
||||||
email = invitation.get().getInvitationEmail();
|
|
||||||
parts = email.split("@");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
model.addAttribute("registrationType", isInvited ? "corporate" : "personal");
|
model.addAttribute("registrationType", isInvited ? "corporate" : "personal");
|
||||||
model.addAttribute("isInvited", isInvited);
|
model.addAttribute("isInvited", isInvited);
|
||||||
model.addAttribute("orgName", orgName);
|
model.addAttribute("orgName", orgName);
|
||||||
model.addAttribute("email", email);
|
// 휴대폰 번호 기반 초대 전환: 이메일/로그인ID 는 가입자가 직접 입력한다
|
||||||
model.addAttribute("loginId", email);
|
model.addAttribute("email", "");
|
||||||
|
model.addAttribute("loginId", "");
|
||||||
if (parts != null) {
|
model.addAttribute("emailId", "");
|
||||||
model.addAttribute("emailId", parts[0]);
|
model.addAttribute("domain", "");
|
||||||
model.addAttribute("domain", parts[1]);
|
|
||||||
} else {
|
|
||||||
model.addAttribute("emailId", "");
|
|
||||||
model.addAttribute("domain", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
model.addAttribute("authTtl", portalProperties.getAuthTtl());
|
model.addAttribute("authTtl", portalProperties.getAuthTtl());
|
||||||
model.addAttribute("portalUser", new PortalUserRegistrationDTO());
|
model.addAttribute("portalUser", new PortalUserRegistrationDTO());
|
||||||
model.addAttribute("termsOfUse", agreementsFacade.getAgreement("TERMS_OF_USE"));
|
model.addAttribute("termsOfUse", agreementsFacade.getAgreement("TERMS_OF_USE"));
|
||||||
model.addAttribute("privacyCollect", agreementsFacade.getAgreement(isInvited ? "PRIVACY_COLLECT_ORG" : "PRIVACY_COLLECT_IND"));
|
model.addAttribute("privacyCollect", agreementsFacade.getAgreement("PRIVACY_COLLECT"));
|
||||||
model.addAttribute("notificationConsent", agreementsFacade.getAgreement("NOTIFICATION_CONSENT"));
|
model.addAttribute("notificationConsent", agreementsFacade.getAgreement("NOTIFICATION_CONSENT"));
|
||||||
|
|
||||||
return MAIN_USER_REGISTER;
|
return MAIN_USER_REGISTER;
|
||||||
@@ -259,9 +250,9 @@ public class UserRegisterController {
|
|||||||
return "redirect:/";
|
return "redirect:/";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 이메일로 사용자 정보 조회
|
// 휴대폰 번호로 사용자 정보 조회
|
||||||
String email = invitation.get().getInvitationEmail();
|
String mobile = invitation.get().getInvitationMobile();
|
||||||
Optional<PortalUser> portalUser = portalUserRepository.findPortalUserByEmailAddr(email);
|
Optional<PortalUser> portalUser = portalUserRepository.findAllByMobileNumber(mobile).stream().findFirst();
|
||||||
|
|
||||||
if (!portalUser.isPresent()) {
|
if (!portalUser.isPresent()) {
|
||||||
model.addAttribute("error", "사용자 정보를 찾을 수 없습니다.");
|
model.addAttribute("error", "사용자 정보를 찾을 수 없습니다.");
|
||||||
@@ -290,13 +281,14 @@ public class UserRegisterController {
|
|||||||
public String showDecisionProcessPage(HttpSession session, Model model) {
|
public String showDecisionProcessPage(HttpSession session, Model model) {
|
||||||
session.removeAttribute("decisionToken");
|
session.removeAttribute("decisionToken");
|
||||||
|
|
||||||
Optional<UserInvitation> invitation = userInvitationRepository.findFirstByInvitationEmailAndStatus(SecurityUtil.getPortalAuthenticatedUser().getLoginId(), UserInvitationEnums.InvitationStatus.PENDING);
|
String mobile = SecurityUtil.getPortalAuthenticatedUser().getMobileNumber();
|
||||||
|
Optional<UserInvitation> invitation = userInvitationRepository.findFirstByInvitationMobileAndStatus(mobile, UserInvitationEnums.InvitationStatus.PENDING);
|
||||||
|
|
||||||
if (!invitation.isPresent()) {
|
if (!invitation.isPresent()) {
|
||||||
model.addAttribute("error", "유효하지 않은 초대입니다");
|
model.addAttribute("error", "유효하지 않은 초대입니다");
|
||||||
return "redirect:/";
|
return "redirect:/";
|
||||||
}
|
}
|
||||||
if (!SecurityUtil.getPortalAuthenticatedUser().getLoginId().equals(invitation.get().getInvitationEmail())) {
|
if (mobile == null || !mobile.equals(invitation.get().getInvitationMobile())) {
|
||||||
model.addAttribute("error", "유효하지 않은 초대입니다");
|
model.addAttribute("error", "유효하지 않은 초대입니다");
|
||||||
return "redirect:/";
|
return "redirect:/";
|
||||||
}
|
}
|
||||||
@@ -312,7 +304,7 @@ public class UserRegisterController {
|
|||||||
model.addAttribute("orgName", org.get().getOrgName());
|
model.addAttribute("orgName", org.get().getOrgName());
|
||||||
model.addAttribute("invitationCode", invitation.get().getToken());
|
model.addAttribute("invitationCode", invitation.get().getToken());
|
||||||
model.addAttribute("termsOfUse", agreementsFacade.getAgreement("TERMS_OF_USE"));
|
model.addAttribute("termsOfUse", agreementsFacade.getAgreement("TERMS_OF_USE"));
|
||||||
model.addAttribute("privacyCollect", agreementsFacade.getAgreement("PRIVACY_COLLECT_ORG"));
|
model.addAttribute("privacyCollect", agreementsFacade.getAgreement("PRIVACY_COLLECT"));
|
||||||
model.addAttribute("registrationType", "corporate");
|
model.addAttribute("registrationType", "corporate");
|
||||||
|
|
||||||
return "apps/register/userDecisionProcess";
|
return "apps/register/userDecisionProcess";
|
||||||
@@ -331,7 +323,7 @@ public class UserRegisterController {
|
|||||||
|
|
||||||
agreementValidator.validate(agreement, bindingResult);
|
agreementValidator.validate(agreement, bindingResult);
|
||||||
|
|
||||||
Optional<UserInvitation> invitation = userInvitationRepository.findFirstByInvitationEmailAndStatus(SecurityUtil.getPortalAuthenticatedUser().getLoginId(), UserInvitationEnums.InvitationStatus.PENDING);
|
Optional<UserInvitation> invitation = userInvitationRepository.findFirstByInvitationMobileAndStatus(SecurityUtil.getPortalAuthenticatedUser().getMobileNumber(), UserInvitationEnums.InvitationStatus.PENDING);
|
||||||
|
|
||||||
if (action.equalsIgnoreCase("accept") && bindingResult.hasErrors()) {
|
if (action.equalsIgnoreCase("accept") && bindingResult.hasErrors()) {
|
||||||
|
|
||||||
@@ -341,7 +333,7 @@ public class UserRegisterController {
|
|||||||
model.addAttribute("userName", SecurityUtil.getPortalAuthenticatedUser().getUsername());
|
model.addAttribute("userName", SecurityUtil.getPortalAuthenticatedUser().getUsername());
|
||||||
model.addAttribute("orgName", org.get().getOrgName());
|
model.addAttribute("orgName", org.get().getOrgName());
|
||||||
model.addAttribute("termsOfUse", agreementsFacade.getAgreement("TERMS_OF_USE"));
|
model.addAttribute("termsOfUse", agreementsFacade.getAgreement("TERMS_OF_USE"));
|
||||||
model.addAttribute("privacyCollect", agreementsFacade.getAgreement("PRIVACY_COLLECT_ORG"));
|
model.addAttribute("privacyCollect", agreementsFacade.getAgreement("PRIVACY_COLLECT"));
|
||||||
model.addAttribute("registrationType", "corporate");
|
model.addAttribute("registrationType", "corporate");
|
||||||
|
|
||||||
return "apps/register/userDecisionProcess";
|
return "apps/register/userDecisionProcess";
|
||||||
@@ -396,7 +388,7 @@ public class UserRegisterController {
|
|||||||
model.addAttribute("agreement", agreement);
|
model.addAttribute("agreement", agreement);
|
||||||
|
|
||||||
model.addAttribute("termsOfUse", agreementsFacade.getAgreement("TERMS_OF_USE"));
|
model.addAttribute("termsOfUse", agreementsFacade.getAgreement("TERMS_OF_USE"));
|
||||||
model.addAttribute("privacyCollect", agreementsFacade.getAgreement("PRIVACY_COLLECT_IND"));
|
model.addAttribute("privacyCollect", agreementsFacade.getAgreement("PRIVACY_COLLECT"));
|
||||||
model.addAttribute("notificationConsent", agreementsFacade.getAgreement("NOTIFICATION_CONSENT"));
|
model.addAttribute("notificationConsent", agreementsFacade.getAgreement("NOTIFICATION_CONSENT"));
|
||||||
model.addAttribute("msgType", "sms");
|
model.addAttribute("msgType", "sms");
|
||||||
}
|
}
|
||||||
@@ -509,10 +501,10 @@ public class UserRegisterController {
|
|||||||
session.removeAttribute("invitationCodeAttempts");
|
session.removeAttribute("invitationCodeAttempts");
|
||||||
session.removeAttribute("invitationCodeLockoutUntil");
|
session.removeAttribute("invitationCodeLockoutUntil");
|
||||||
|
|
||||||
// 기존 사용자 여부 확인
|
// 기존 사용자 여부 확인 (휴대폰 번호 기반)
|
||||||
Optional<PortalUser> existingUser = portalUserRepository.findPortalUserByEmailAddr(invitation.getInvitationEmail());
|
boolean existingUser = !portalUserRepository.findAllByMobileNumber(invitation.getInvitationMobile()).isEmpty();
|
||||||
|
|
||||||
if (existingUser.isPresent()) {
|
if (existingUser) {
|
||||||
// 기존 사용자: 초대 수락 페이지로 이동
|
// 기존 사용자: 초대 수락 페이지로 이동
|
||||||
return "redirect:/signup/decision?invitation=" + normalizedCode;
|
return "redirect:/signup/decision?invitation=" + normalizedCode;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ public class PortalUserDTO implements Serializable {
|
|||||||
|
|
||||||
private String emailAddr;
|
private String emailAddr;
|
||||||
|
|
||||||
|
// 초대 시 알림 수신 동의 여부 (false 면 초대 레코드만 생성, 메시지 미발송)
|
||||||
|
private boolean notifyConsent;
|
||||||
|
|
||||||
private PortalOrgDTO portalOrg;
|
private PortalOrgDTO portalOrg;
|
||||||
|
|
||||||
private UserStatus userStatus;
|
private UserStatus userStatus;
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ public class OrgRegisterFacadeImpl implements OrgRegisterFacade {
|
|||||||
// 사용자 생성 및 기관 연결
|
// 사용자 생성 및 기관 연결
|
||||||
PortalUser newUser = portalUserService.createUserWithOrg(orgDTO, newOrg, "corporate");
|
PortalUser newUser = portalUserService.createUserWithOrg(orgDTO, newOrg, "corporate");
|
||||||
|
|
||||||
agreementsFacade.saveUserAgreements(newUser.getId(), AgreementType.PRIVACY_COLLECT_ORG);
|
agreementsFacade.saveUserAgreements(newUser.getId(), AgreementType.PRIVACY_COLLECT);
|
||||||
|
|
||||||
approvalService.createUserApproval(newUser);
|
approvalService.createUserApproval(newUser);
|
||||||
// 11.28 - 회원 가입단계가 아닌 로그인 단계로 이메일 인증 이동
|
// 11.28 - 회원 가입단계가 아닌 로그인 단계로 이메일 인증 이동
|
||||||
@@ -240,7 +240,7 @@ public class OrgRegisterFacadeImpl implements OrgRegisterFacade {
|
|||||||
|
|
||||||
portalUserRepository.save(existingUser);
|
portalUserRepository.save(existingUser);
|
||||||
agreementsFacade.deleteUserAgreements(existingUser.getId()); //기존 개인약관 동의 삭제
|
agreementsFacade.deleteUserAgreements(existingUser.getId()); //기존 개인약관 동의 삭제
|
||||||
agreementsFacade.saveUserAgreements(existingUser.getId(), AgreementType.PRIVACY_COLLECT_ORG);
|
agreementsFacade.saveUserAgreements(existingUser.getId(), AgreementType.PRIVACY_COLLECT);
|
||||||
|
|
||||||
approvalService.createUserApproval(existingUser);
|
approvalService.createUserApproval(existingUser);
|
||||||
|
|
||||||
@@ -274,7 +274,7 @@ public class OrgRegisterFacadeImpl implements OrgRegisterFacade {
|
|||||||
portalUserRepository.save(existingUser);
|
portalUserRepository.save(existingUser);
|
||||||
|
|
||||||
// 새로운 법인용 개인정보수집동의서 저장
|
// 새로운 법인용 개인정보수집동의서 저장
|
||||||
agreementsFacade.saveUserAgreements(existingUser.getId(), AgreementType.PRIVACY_COLLECT_ORG);
|
agreementsFacade.saveUserAgreements(existingUser.getId(), AgreementType.PRIVACY_COLLECT);
|
||||||
|
|
||||||
approvalService.createUserApproval(existingUser);
|
approvalService.createUserApproval(existingUser);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
|||||||
import com.eactive.apim.portal.common.util.DurationParser;
|
import com.eactive.apim.portal.common.util.DurationParser;
|
||||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||||
import com.eactive.apim.portal.common.util.StringMaskingUtil;
|
import com.eactive.apim.portal.common.util.StringMaskingUtil;
|
||||||
|
import com.eactive.apim.portal.common.validator.CellPhoneValidator;
|
||||||
import com.eactive.apim.portal.invitation.entity.UserInvitation;
|
import com.eactive.apim.portal.invitation.entity.UserInvitation;
|
||||||
import com.eactive.apim.portal.invitation.entity.UserInvitationEnums.InvitationStatus;
|
import com.eactive.apim.portal.invitation.entity.UserInvitationEnums.InvitationStatus;
|
||||||
import com.eactive.apim.portal.invitation.event.UserInvitationCancelEvent;
|
import com.eactive.apim.portal.invitation.event.UserInvitationCancelEvent;
|
||||||
@@ -52,6 +53,7 @@ public class UserManFacade {
|
|||||||
private final MessageHandlerService messageHandlerService;
|
private final MessageHandlerService messageHandlerService;
|
||||||
private final UserLogRepository userLogRepository;
|
private final UserLogRepository userLogRepository;
|
||||||
private final PortalPropertyService portalPropertyService;
|
private final PortalPropertyService portalPropertyService;
|
||||||
|
private final CellPhoneValidator cellPhoneValidator;
|
||||||
|
|
||||||
public Page<PortalUserDTO> getUsers(PortalOrgDTO userOrg, Pageable pageable) {
|
public Page<PortalUserDTO> getUsers(PortalOrgDTO userOrg, Pageable pageable) {
|
||||||
return portalUserRepository
|
return portalUserRepository
|
||||||
@@ -94,23 +96,35 @@ public class UserManFacade {
|
|||||||
dto.setMobileNumber(null);
|
dto.setMobileNumber(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendInvitation(PortalUser sender, String emailAddr) {
|
/**
|
||||||
// 이메일 형식 검증
|
* 법인 개발자 초대 (휴대폰 번호 기반).
|
||||||
if (emailAddr == null || emailAddr.trim().isEmpty()) {
|
*
|
||||||
throw new IllegalArgumentException("이메일 주소를 입력해주세요.");
|
* @param sender 초대를 보내는 법인 관리자
|
||||||
|
* @param mobileRaw 초대 대상 휴대폰 번호 (하이픈 유무 무관)
|
||||||
|
* @param notifyConsent 알림 수신 동의 여부. false 면 초대 레코드만 생성하고 메시지 발송은 스킵한다.
|
||||||
|
*/
|
||||||
|
public void sendInvitation(PortalUser sender, String mobileRaw, boolean notifyConsent) {
|
||||||
|
// 휴대폰 번호 형식 검증
|
||||||
|
if (mobileRaw == null || mobileRaw.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("휴대폰 번호를 입력해주세요.");
|
||||||
|
}
|
||||||
|
if (!cellPhoneValidator.isValid(mobileRaw, null)) {
|
||||||
|
throw new IllegalArgumentException("올바른 휴대폰 번호 형식을 입력해주세요.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 이메일 정규식 검증
|
// 저장 포맷(하이픈 포함, 예: 010-1234-5678)으로 정규화 — PortalUser.mobileNumber 저장 포맷과
|
||||||
String emailPattern = "^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$";
|
// 일치시켜야 결정적 암호화 equality 조회가 매칭된다.
|
||||||
if (!emailAddr.matches(emailPattern)) {
|
String mobile = normalizeMobile(mobileRaw);
|
||||||
throw new IllegalArgumentException("올바른 이메일 형식을 입력해주세요.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 중복 초대 확인
|
// 중복 초대 확인
|
||||||
checkDuplicateInvitation(sender.getPortalOrg().getId(), emailAddr);
|
checkDuplicateInvitation(sender.getPortalOrg().getId(), mobile);
|
||||||
|
|
||||||
//기존 사용자인지 확인
|
// 기존 사용자인지 휴대폰 번호로 확인 (mobileNumber 는 유니크가 아니므로 다건 가능)
|
||||||
Optional<PortalUser> existingUser = portalUserRepository.findPortalUserByEmailAddr(emailAddr);
|
List<PortalUser> matched = portalUserRepository.findAllByMobileNumber(mobile);
|
||||||
|
if (matched.size() > 1) {
|
||||||
|
throw new IllegalArgumentException("해당 휴대폰 번호로 가입된 계정이 여러 건 존재하여 초대할 수 없습니다. 관리자에게 문의해주세요.");
|
||||||
|
}
|
||||||
|
Optional<PortalUser> existingUser = matched.isEmpty() ? Optional.empty() : Optional.of(matched.get(0));
|
||||||
|
|
||||||
if (existingUser.isPresent()) {
|
if (existingUser.isPresent()) {
|
||||||
PortalUser user = existingUser.get();
|
PortalUser user = existingUser.get();
|
||||||
@@ -119,7 +133,12 @@ public class UserManFacade {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String token = createInvitation(sender, emailAddr);
|
String token = createInvitation(sender, mobile);
|
||||||
|
|
||||||
|
// 알림 수신 미동의 시 초대 레코드만 생성하고 메시지 발송은 스킵
|
||||||
|
if (!notifyConsent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// {"CRPT_NM":"%corpName%", "MNGR_NM":"%managerName%", "CUST_ID":"%loginId%", "AUTH_NO": "%authNumber%", "NAME": "%loginId%"}
|
// {"CRPT_NM":"%corpName%", "MNGR_NM":"%managerName%", "CUST_ID":"%loginId%", "AUTH_NO": "%authNumber%", "NAME": "%loginId%"}
|
||||||
MessageRecipient recipient;
|
MessageRecipient recipient;
|
||||||
@@ -129,7 +148,7 @@ public class UserManFacade {
|
|||||||
params.put("corpName", sender.getPortalOrg().getOrgName());
|
params.put("corpName", sender.getPortalOrg().getOrgName());
|
||||||
params.put("authNumber", token); // 8-character invitation code
|
params.put("authNumber", token); // 8-character invitation code
|
||||||
|
|
||||||
// 기존 사용자인 경우 회원 이름 사용, 비회원인 경우 이메일 사용
|
// 기존 사용자인 경우 회원 이름 사용, 비회원인 경우 휴대폰 번호 사용
|
||||||
if (existingUser.isPresent()) {
|
if (existingUser.isPresent()) {
|
||||||
PortalUser user = existingUser.get();
|
PortalUser user = existingUser.get();
|
||||||
params.put("loginId", user.getUserName());
|
params.put("loginId", user.getUserName());
|
||||||
@@ -137,26 +156,45 @@ public class UserManFacade {
|
|||||||
params.put("url", "signup/decision?invitation=" + token);
|
params.put("url", "signup/decision?invitation=" + token);
|
||||||
|
|
||||||
recipient = MessageRecipient.of(user);
|
recipient = MessageRecipient.of(user);
|
||||||
|
// MessageRecipient.of() 는 유선전화(phoneNumber)를 채우므로 휴대폰 번호로 보정
|
||||||
|
recipient.setPhone(user.getMobileNumber());
|
||||||
} else {
|
} else {
|
||||||
params.put("loginId", emailAddr);
|
params.put("loginId", mobile);
|
||||||
params.put("NAME", emailAddr);
|
params.put("NAME", mobile);
|
||||||
params.put("url", "signup/portalUser?invitation=" + token);
|
params.put("url", "signup/portalUser?invitation=" + token);
|
||||||
|
|
||||||
String emailUsername = emailAddr.contains("@") ? emailAddr.substring(0, emailAddr.indexOf("@")) : emailAddr;
|
|
||||||
recipient = MessageRecipient.builder()
|
recipient = MessageRecipient.builder()
|
||||||
.userId(emailAddr)
|
.userId(mobile)
|
||||||
.username(emailUsername)
|
.username(mobile)
|
||||||
|
.phone(mobile)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
messageHandlerService.publishEvent(UserInvitationEvent.KEY, recipient, params);
|
messageHandlerService.publishEvent(UserInvitationEvent.KEY, recipient, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String createInvitation(PortalUser sender, String emailAddr) {
|
/**
|
||||||
|
* 휴대폰 번호를 저장 포맷(하이픈 포함)으로 정규화한다.
|
||||||
|
* 010-XXXX-XXXX(11자리) / 010-XXX-XXXX(10자리). 형식 불명은 원본 반환(검증에서 차단됨).
|
||||||
|
*/
|
||||||
|
private String normalizeMobile(String raw) {
|
||||||
|
if (raw == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String digits = raw.replaceAll("[^0-9]", "");
|
||||||
|
if (digits.length() == 11) {
|
||||||
|
return digits.substring(0, 3) + "-" + digits.substring(3, 7) + "-" + digits.substring(7);
|
||||||
|
} else if (digits.length() == 10) {
|
||||||
|
return digits.substring(0, 3) + "-" + digits.substring(3, 6) + "-" + digits.substring(6);
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String createInvitation(PortalUser sender, String mobile) {
|
||||||
UserInvitation newInvitation = new UserInvitation();
|
UserInvitation newInvitation = new UserInvitation();
|
||||||
newInvitation.setAdminId(sender.getId());
|
newInvitation.setAdminId(sender.getId());
|
||||||
newInvitation.setOrgId(sender.getPortalOrg().getId());
|
newInvitation.setOrgId(sender.getPortalOrg().getId());
|
||||||
newInvitation.setInvitationEmail(emailAddr);
|
newInvitation.setInvitationMobile(mobile);
|
||||||
|
|
||||||
// Set additional required fields
|
// Set additional required fields
|
||||||
newInvitation.setInvitationDate(LocalDateTime.now());
|
newInvitation.setInvitationDate(LocalDateTime.now());
|
||||||
@@ -225,7 +263,51 @@ public class UserManFacade {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cancelInvitation(PortalAuthenticatedUser user, String invitationId) {
|
/**
|
||||||
|
* 법인 관리자 권한 회수 (관리자 → 이용자).
|
||||||
|
* 대상은 본인이 아닌 같은 기관의 ROLE_CORP_MANAGER 여야 한다.
|
||||||
|
*/
|
||||||
|
public void revokeManager(PortalAuthenticatedUser admin, String targetUserId) {
|
||||||
|
PortalUser targetUser = portalUserRepository.findById(targetUserId)
|
||||||
|
.orElseThrow(() -> new NotFoundException("ID [" + targetUserId + "] 사용자를 찾을 수 없습니다. "));
|
||||||
|
|
||||||
|
if (targetUser.getPortalOrg() == null
|
||||||
|
|| !targetUser.getPortalOrg().getId().equals(admin.getPortalOrg().getId())) {
|
||||||
|
throw new IllegalArgumentException("잘 못 된 접근입니다");
|
||||||
|
}
|
||||||
|
if (targetUser.getId().equals(admin.getId())) {
|
||||||
|
throw new IllegalArgumentException("본인의 권한은 변경할 수 없습니다.");
|
||||||
|
}
|
||||||
|
if (targetUser.getRoleCode() != RoleCode.ROLE_CORP_MANAGER) {
|
||||||
|
throw new IllegalArgumentException("관리자만 이용자로 변경할 수 있습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
targetUser.setRoleCode(RoleCode.ROLE_CORP_USER);
|
||||||
|
portalUserRepository.save(targetUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 소속 제외 (기관에서 제외 → 개인이용자).
|
||||||
|
* 대상은 본인이 아닌 같은 기관의 사용자여야 한다. portalOrg 를 비우고 ROLE_USER 로 전환한다.
|
||||||
|
*/
|
||||||
|
public void removeFromOrg(PortalAuthenticatedUser admin, String targetUserId) {
|
||||||
|
PortalUser targetUser = portalUserRepository.findById(targetUserId)
|
||||||
|
.orElseThrow(() -> new NotFoundException("ID [" + targetUserId + "] 사용자를 찾을 수 없습니다. "));
|
||||||
|
|
||||||
|
if (targetUser.getPortalOrg() == null
|
||||||
|
|| !targetUser.getPortalOrg().getId().equals(admin.getPortalOrg().getId())) {
|
||||||
|
throw new IllegalArgumentException("잘 못 된 접근입니다");
|
||||||
|
}
|
||||||
|
if (targetUser.getId().equals(admin.getId())) {
|
||||||
|
throw new IllegalArgumentException("본인은 소속에서 제외할 수 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
targetUser.setPortalOrg(null);
|
||||||
|
targetUser.setRoleCode(RoleCode.ROLE_USER);
|
||||||
|
portalUserRepository.save(targetUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelInvitation(PortalAuthenticatedUser user, String invitationId, boolean notifyConsent) {
|
||||||
UserInvitation invitation = userInvitationRepository.findById(invitationId)
|
UserInvitation invitation = userInvitationRepository.findById(invitationId)
|
||||||
.orElseThrow(() -> new NotFoundException("Invitation not found with id: " + invitationId));
|
.orElseThrow(() -> new NotFoundException("Invitation not found with id: " + invitationId));
|
||||||
|
|
||||||
@@ -237,17 +319,19 @@ public class UserManFacade {
|
|||||||
throw new IllegalStateException("Invitation is not in a cancelable state");
|
throw new IllegalStateException("Invitation is not in a cancelable state");
|
||||||
}
|
}
|
||||||
|
|
||||||
// invitationId 로 사용자 이름 조회, 실패시 id 를 이름으로 대체
|
invitation.setStatus(InvitationStatus.CANCELED);
|
||||||
String name = portalUserRepository.findById(invitation.getInvitationEmail())
|
userInvitationRepository.save(invitation);
|
||||||
.map(PortalUser::getUserName)
|
|
||||||
.orElse(invitation.getInvitationEmail());
|
|
||||||
|
|
||||||
// name 이 이메일 일 경우 @ 앞부분 만 사용 (안하면 암호화 해야함...)
|
// 알림 수신 미동의 시 취소 알림 메시지 발송은 스킵
|
||||||
if (name.contains("@")) {
|
if (!notifyConsent) {
|
||||||
name = name.substring(0, name.indexOf("@"));
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
invitation.setStatus(InvitationStatus.CANCELED);
|
// invitationMobile 로 사용자 이름 조회, 실패시 마스킹된 번호로 대체
|
||||||
|
String name = portalUserRepository.findAllByMobileNumber(invitation.getInvitationMobile())
|
||||||
|
.stream().findFirst()
|
||||||
|
.map(PortalUser::getUserName)
|
||||||
|
.orElse(StringMaskingUtil.maskMobileNumber(invitation.getInvitationMobile()));
|
||||||
|
|
||||||
messageHandlerService.publishEvent(UserInvitationCancelEvent.KEY,
|
messageHandlerService.publishEvent(UserInvitationCancelEvent.KEY,
|
||||||
MessageRecipient.builder()
|
MessageRecipient.builder()
|
||||||
@@ -260,8 +344,6 @@ public class UserManFacade {
|
|||||||
}}
|
}}
|
||||||
);
|
);
|
||||||
|
|
||||||
userInvitationRepository.save(invitation);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void inactivateUser(PortalAuthenticatedUser admin, String userId) {
|
public void inactivateUser(PortalAuthenticatedUser admin, String userId) {
|
||||||
@@ -298,8 +380,8 @@ public class UserManFacade {
|
|||||||
.stream()
|
.stream()
|
||||||
.map(invitation -> {
|
.map(invitation -> {
|
||||||
PortalUserDTO dto = portalUserMapper.pendingUserVO(invitation);
|
PortalUserDTO dto = portalUserMapper.pendingUserVO(invitation);
|
||||||
dto.setMaskedEmailAddr(StringMaskingUtil.maskEmail(invitation.getInvitationEmail()));
|
dto.setMaskedMobileNumber(StringMaskingUtil.maskMobileNumber(invitation.getInvitationMobile()));
|
||||||
dto.setEmailAddr(null); // Clear unmasked email
|
dto.setMobileNumber(null); // Clear unmasked mobile
|
||||||
return dto;
|
return dto;
|
||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
@@ -328,32 +410,32 @@ public class UserManFacade {
|
|||||||
existingInvitation.setStatus(InvitationStatus.CANCELED);
|
existingInvitation.setStatus(InvitationStatus.CANCELED);
|
||||||
userInvitationRepository.save(existingInvitation);
|
userInvitationRepository.save(existingInvitation);
|
||||||
|
|
||||||
// 새로운 초대 생성 및 발송
|
// 새로운 초대 생성 및 발송 (재발송 시 최초 동의값을 알 수 없으므로 발송함을 기본으로)
|
||||||
sendInvitation(sender, existingInvitation.getInvitationEmail());
|
sendInvitation(sender, existingInvitation.getInvitationMobile(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 중복 초대 확인
|
* 중복 초대 확인
|
||||||
* 같은 기관에서 같은 이메일로 이미 PENDING 상태인 초대가 있는지 확인
|
* 같은 기관에서 같은 휴대폰 번호로 이미 PENDING 상태인 초대가 있는지 확인
|
||||||
*/
|
*/
|
||||||
private void checkDuplicateInvitation(String orgId, String emailAddr) {
|
private void checkDuplicateInvitation(String orgId, String mobile) {
|
||||||
// 1. 같은 기관에서 PENDING 상태인 초대가 있는지 확인
|
// 1. 같은 기관에서 PENDING 상태인 초대가 있는지 확인
|
||||||
Optional<UserInvitation> pendingInvitation =
|
Optional<UserInvitation> pendingInvitation =
|
||||||
userInvitationRepository.findFirstByInvitationEmailAndOrgIdAndStatus(
|
userInvitationRepository.findFirstByInvitationMobileAndOrgIdAndStatus(
|
||||||
emailAddr, orgId, InvitationStatus.PENDING);
|
mobile, orgId, InvitationStatus.PENDING);
|
||||||
|
|
||||||
if (pendingInvitation.isPresent()) {
|
if (pendingInvitation.isPresent()) {
|
||||||
throw new IllegalArgumentException("이미 초대가 진행 중인 이메일 주소입니다.");
|
throw new IllegalArgumentException("이미 초대가 진행 중인 휴대폰 번호입니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 다른 기관에서 PENDING 상태인 초대가 있는지 확인
|
// 2. 다른 기관에서 PENDING 상태인 초대가 있는지 확인
|
||||||
Optional<UserInvitation> otherOrgPendingInvitation =
|
Optional<UserInvitation> otherOrgPendingInvitation =
|
||||||
userInvitationRepository.findFirstByInvitationEmailAndStatus(
|
userInvitationRepository.findFirstByInvitationMobileAndStatus(
|
||||||
emailAddr, InvitationStatus.PENDING);
|
mobile, InvitationStatus.PENDING);
|
||||||
|
|
||||||
if (otherOrgPendingInvitation.isPresent()
|
if (otherOrgPendingInvitation.isPresent()
|
||||||
&& !otherOrgPendingInvitation.get().getOrgId().equals(orgId)) {
|
&& !otherOrgPendingInvitation.get().getOrgId().equals(orgId)) {
|
||||||
throw new IllegalArgumentException("해당 이메일은 다른 기관에서 초대 진행 중입니다.");
|
throw new IllegalArgumentException("해당 휴대폰 번호는 다른 기관에서 초대 진행 중입니다.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ public class UserRegisterFacadeImpl implements UserRegisterFacade {
|
|||||||
return new ValidationResponse(false,"사용자 등록에 실패했습니다.");
|
return new ValidationResponse(false,"사용자 등록에 실패했습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
agreementsFacade.saveUserAgreements(newUser.getId(), AgreementType.PRIVACY_COLLECT_IND);
|
agreementsFacade.saveUserAgreements(newUser.getId(), AgreementType.PRIVACY_COLLECT);
|
||||||
// 11.13 - 회원 가입단계가 아닌 로그인 단계로 이메일 인증 이동
|
// 11.13 - 회원 가입단계가 아닌 로그인 단계로 이메일 인증 이동
|
||||||
// sendEmailActivation(newUser);
|
// sendEmailActivation(newUser);
|
||||||
return new ValidationResponse(true,"회원가입이 완료되었습니다.");
|
return new ValidationResponse(true,"회원가입이 완료되었습니다.");
|
||||||
@@ -195,7 +195,7 @@ public class UserRegisterFacadeImpl implements UserRegisterFacade {
|
|||||||
}
|
}
|
||||||
|
|
||||||
agreementsFacade.deleteUserAgreements(newUser.getId()); //이전에 동의한 내역은 삭제
|
agreementsFacade.deleteUserAgreements(newUser.getId()); //이전에 동의한 내역은 삭제
|
||||||
agreementsFacade.saveUserAgreements(newUser.getId(), AgreementType.PRIVACY_COLLECT_ORG);
|
agreementsFacade.saveUserAgreements(newUser.getId(), AgreementType.PRIVACY_COLLECT);
|
||||||
|
|
||||||
invitation.setStatus(UserInvitationEnums.InvitationStatus.COMPLETED);
|
invitation.setStatus(UserInvitationEnums.InvitationStatus.COMPLETED);
|
||||||
invitation.setCompleteDate(LocalDateTime.now());
|
invitation.setCompleteDate(LocalDateTime.now());
|
||||||
@@ -247,7 +247,8 @@ public class UserRegisterFacadeImpl implements UserRegisterFacade {
|
|||||||
try {
|
try {
|
||||||
if ("accept".equals(action)) {
|
if ("accept".equals(action)) {
|
||||||
// 수락 처리
|
// 수락 처리
|
||||||
PortalUser user = portalUserRepository.findPortalUserByEmailAddr(invitation.getInvitationEmail())
|
PortalUser user = portalUserRepository.findAllByMobileNumber(invitation.getInvitationMobile())
|
||||||
|
.stream().findFirst()
|
||||||
.orElseThrow(() -> new IllegalArgumentException("사용자 정보를 찾을 수 없습니다."));
|
.orElseThrow(() -> new IllegalArgumentException("사용자 정보를 찾을 수 없습니다."));
|
||||||
|
|
||||||
// 서비스 계층에 위임
|
// 서비스 계층에 위임
|
||||||
@@ -258,7 +259,7 @@ public class UserRegisterFacadeImpl implements UserRegisterFacade {
|
|||||||
userInvitationRepository.save(invitation);
|
userInvitationRepository.save(invitation);
|
||||||
|
|
||||||
agreementsFacade.deleteUserAgreements(user.getId());
|
agreementsFacade.deleteUserAgreements(user.getId());
|
||||||
agreementsFacade.saveUserAgreements(user.getId(), AgreementType.PRIVACY_COLLECT_ORG);
|
agreementsFacade.saveUserAgreements(user.getId(), AgreementType.PRIVACY_COLLECT);
|
||||||
|
|
||||||
ValidationResponse response = new ValidationResponse();
|
ValidationResponse response = new ValidationResponse();
|
||||||
response.setValid(true);
|
response.setValid(true);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public abstract class PortalUserMapper implements GenericMapper<PortalUserDTO, P
|
|||||||
|
|
||||||
public abstract PortalAuthenticatedUser portalUserToAuthenticatedUser(PortalUser portalUser);
|
public abstract PortalAuthenticatedUser portalUserToAuthenticatedUser(PortalUser portalUser);
|
||||||
|
|
||||||
@Mapping(target = "emailAddr", source = "invitationEmail")
|
@Mapping(target = "mobileNumber", source = "invitationMobile")
|
||||||
public abstract PortalUserDTO pendingUserVO(UserInvitation invitation);
|
public abstract PortalUserDTO pendingUserVO(UserInvitation invitation);
|
||||||
|
|
||||||
public PortalUserDTO toDTO(PortalUser portalUser) {
|
public PortalUserDTO toDTO(PortalUser portalUser) {
|
||||||
|
|||||||
+2
-2
@@ -96,8 +96,8 @@ public class PortalAuthenticationSuccessHandler implements AuthenticationSuccess
|
|||||||
// 초대 코드 확인 - ROLE_USER만 확인 (세션에 저장하여 메인 페이지에서 팝업으로 표시)
|
// 초대 코드 확인 - ROLE_USER만 확인 (세션에 저장하여 메인 페이지에서 팝업으로 표시)
|
||||||
if (user.getRoleCode() == PortalUserEnums.RoleCode.ROLE_USER) {
|
if (user.getRoleCode() == PortalUserEnums.RoleCode.ROLE_USER) {
|
||||||
Optional<UserInvitation> pendingInvitation =
|
Optional<UserInvitation> pendingInvitation =
|
||||||
userInvitationRepository.findFirstByInvitationEmailAndStatus(
|
userInvitationRepository.findFirstByInvitationMobileAndStatus(
|
||||||
user.getLoginId(), InvitationStatus.PENDING);
|
user.getMobileNumber(), InvitationStatus.PENDING);
|
||||||
|
|
||||||
if (pendingInvitation.isPresent()) {
|
if (pendingInvitation.isPresent()) {
|
||||||
UserInvitation invitation = pendingInvitation.get();
|
UserInvitation invitation = pendingInvitation.get();
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ spring:
|
|||||||
cachecontrol:
|
cachecontrol:
|
||||||
max-age: 86400
|
max-age: 86400
|
||||||
public: true
|
public: true
|
||||||
|
# 정적자원 콘텐츠 해시 버전닝: 운영은 항상 ON (파라미터로 비활성화 불가).
|
||||||
|
# 해시 URL + 장기 캐시(max-age 86400)로 배포 시 자동 캐시 무효화.
|
||||||
|
chain:
|
||||||
|
strategy:
|
||||||
|
content:
|
||||||
|
enabled: true
|
||||||
|
paths: /**
|
||||||
jpa:
|
jpa:
|
||||||
properties:
|
properties:
|
||||||
hibernate:
|
hibernate:
|
||||||
|
|||||||
@@ -33,6 +33,15 @@ spring:
|
|||||||
max-age: 0
|
max-age: 0
|
||||||
must-revalidate: true
|
must-revalidate: true
|
||||||
no-cache: true
|
no-cache: true
|
||||||
|
# 정적자원 콘텐츠 해시 버전닝: /css/main.css -> /css/main-<hash>.css 자동 치환.
|
||||||
|
# 내용이 바뀌면 URL이 바뀌어 강제 새로고침 없이 캐시가 무효화됨.
|
||||||
|
# 비-prod 는 app.resource-versioning.enabled 파라미터로 제어(기본 ON),
|
||||||
|
# false 로 두면 버전닝을 꺼서 sourceMap 디버깅이 가능. (prod 는 application-prod.yml 에서 항상 ON)
|
||||||
|
chain:
|
||||||
|
strategy:
|
||||||
|
content:
|
||||||
|
enabled: ${app.resource-versioning.enabled:true}
|
||||||
|
paths: /**
|
||||||
|
|
||||||
jta:
|
jta:
|
||||||
enabled: true
|
enabled: true
|
||||||
@@ -49,6 +58,14 @@ spring:
|
|||||||
cache: false
|
cache: false
|
||||||
main:
|
main:
|
||||||
allow-bean-definition-overriding: true
|
allow-bean-definition-overriding: true
|
||||||
|
|
||||||
|
# 정적자원 해시 버전닝 토글 (prod 제외 전 프로파일에 적용).
|
||||||
|
# false 로 변경 시 /css/main.css 가 해시 없이 그대로 서빙되어 sourceMap 디버깅 가능.
|
||||||
|
# prod 는 application-prod.yml 에서 항상 ON 으로 고정되어 이 값을 무시함.
|
||||||
|
app:
|
||||||
|
resource-versioning:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
security:
|
security:
|
||||||
basic:
|
basic:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|||||||
@@ -5739,11 +5739,11 @@ select.form-control {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 8px 16px;
|
padding: 0 8px;
|
||||||
height: 40px;
|
height: 30px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
font-size: 16px;
|
font-size: 13px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -5776,6 +5776,13 @@ select.form-control {
|
|||||||
.list-table-btn--secondary:hover {
|
.list-table-btn--secondary:hover {
|
||||||
background-color: rgb(187.8846153846, 197.2807692308, 234.8653846154);
|
background-color: rgb(187.8846153846, 197.2807692308, 234.8653846154);
|
||||||
}
|
}
|
||||||
|
.list-table-btn--danger {
|
||||||
|
background-color: #F8D7DA;
|
||||||
|
color: #B02A37;
|
||||||
|
}
|
||||||
|
.list-table-btn--danger:hover {
|
||||||
|
background-color: rgb(244.5521276596, 195.2978723404, 199.7755319149);
|
||||||
|
}
|
||||||
|
|
||||||
.table-pagination {
|
.table-pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -16644,16 +16651,15 @@ input[type=checkbox]:checked + .custom-checkbox {
|
|||||||
|
|
||||||
.user-current-badge {
|
.user-current-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
height: 40px;
|
height: 30px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 6px 12px;
|
padding: 0 8px;
|
||||||
background-color: rgba(167, 139, 250, 0.1);
|
background-color: rgba(167, 139, 250, 0.1);
|
||||||
color: #A78BFA;
|
color: #A78BFA;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
min-width: 116px;
|
|
||||||
}
|
}
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.user-current-badge {
|
.user-current-badge {
|
||||||
@@ -17383,6 +17389,16 @@ input[type=checkbox]:checked + .custom-checkbox {
|
|||||||
.dropdown-wrapper .dropdown-menu .dropdown-item + .dropdown-item {
|
.dropdown-wrapper .dropdown-menu .dropdown-item + .dropdown-item {
|
||||||
border-top: 1px solid #eee;
|
border-top: 1px solid #eee;
|
||||||
}
|
}
|
||||||
|
.dropdown-wrapper .dropdown-menu .dropdown-item--danger {
|
||||||
|
color: #B02A37;
|
||||||
|
}
|
||||||
|
.dropdown-wrapper .dropdown-menu .dropdown-item--danger:hover {
|
||||||
|
background: #fdeaec;
|
||||||
|
color: #B02A37;
|
||||||
|
}
|
||||||
|
.dropdown-wrapper .dropdown-menu .dropdown-item--danger:active {
|
||||||
|
background: #f8d7da;
|
||||||
|
}
|
||||||
.dropdown-wrapper.open .dropdown-toggle {
|
.dropdown-wrapper.open .dropdown-toggle {
|
||||||
background: rgba(0, 73, 180, 0.1);
|
background: rgba(0, 73, 180, 0.1);
|
||||||
color: #0049b4;
|
color: #0049b4;
|
||||||
@@ -17845,6 +17861,13 @@ body.commission-print-page .btn-primary:hover {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.terms-empty {
|
||||||
|
padding: 32px 24px;
|
||||||
|
text-align: center;
|
||||||
|
color: #8c959f;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.terms-selector {
|
.terms-selector {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background: #F6F9FB;
|
background: #F6F9FB;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -364,7 +364,7 @@ const customPopups = {
|
|||||||
/**
|
/**
|
||||||
* 사용자 초대 팝업 표시
|
* 사용자 초대 팝업 표시
|
||||||
* @param {Object} options - 팝업 옵션
|
* @param {Object} options - 팝업 옵션
|
||||||
* @param {Function} options.onConfirm - 확인 버튼 클릭 시 호출되는 콜백 (파라미터: email)
|
* @param {Function} options.onConfirm - 확인 버튼 클릭 시 호출되는 콜백 (파라미터: mobile, notifyConsent)
|
||||||
* @param {Function} options.onCancel - 취소 버튼 클릭 시 호출되는 콜백 (선택사항)
|
* @param {Function} options.onCancel - 취소 버튼 클릭 시 호출되는 콜백 (선택사항)
|
||||||
*/
|
*/
|
||||||
showUserInvite: function (options) {
|
showUserInvite: function (options) {
|
||||||
@@ -374,7 +374,8 @@ const customPopups = {
|
|||||||
const onCancel = options.onCancel;
|
const onCancel = options.onCancel;
|
||||||
|
|
||||||
// 입력 필드 및 에러 초기화
|
// 입력 필드 및 에러 초기화
|
||||||
$('#userInviteEmailInput').val('').removeClass('error');
|
$('#userInviteMobileInput').val('').removeClass('error');
|
||||||
|
$('#userInviteNotifyConsent').prop('checked', false);
|
||||||
$('#userInvitePopupError').removeClass('show').text('');
|
$('#userInvitePopupError').removeClass('show').text('');
|
||||||
|
|
||||||
// 팝업 표시 (modal 구조 사용)
|
// 팝업 표시 (modal 구조 사용)
|
||||||
@@ -389,28 +390,29 @@ const customPopups = {
|
|||||||
|
|
||||||
// 입력 필드에 포커스
|
// 입력 필드에 포커스
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
$('#userInviteEmailInput').focus();
|
$('#userInviteMobileInput').focus();
|
||||||
}, 350);
|
}, 350);
|
||||||
|
|
||||||
// 확인 버튼 이벤트 (기존 이벤트 제거 후 재등록)
|
// 확인 버튼 이벤트 (기존 이벤트 제거 후 재등록)
|
||||||
$('#userInvitePopupConfirmButton').off('click').on('click', function () {
|
$('#userInvitePopupConfirmButton').off('click').on('click', function () {
|
||||||
const email = $('#userInviteEmailInput').val().trim();
|
const mobile = $('#userInviteMobileInput').val().trim();
|
||||||
|
const notifyConsent = $('#userInviteNotifyConsent').is(':checked');
|
||||||
|
|
||||||
// 이메일 유효성 검사
|
// 휴대폰 번호 유효성 검사
|
||||||
if (!email) {
|
if (!mobile) {
|
||||||
customPopups.showUserInviteError('이메일 주소를 입력해주세요.');
|
customPopups.showUserInviteError('휴대폰 번호를 입력해주세요.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 이메일 형식 검증
|
// 휴대폰 번호 형식 검증 (하이픈 유무 모두 허용)
|
||||||
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
const mobilePattern = /^01[016-9]-?\d{3,4}-?\d{4}$/;
|
||||||
if (!emailPattern.test(email)) {
|
if (!mobilePattern.test(mobile)) {
|
||||||
customPopups.showUserInviteError('올바른 이메일 형식이 아닙니다.');
|
customPopups.showUserInviteError('올바른 휴대폰 번호 형식이 아닙니다.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof onConfirm === 'function') {
|
if (typeof onConfirm === 'function') {
|
||||||
onConfirm(email);
|
onConfirm(mobile, notifyConsent);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -431,14 +433,14 @@ const customPopups = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Enter 키 이벤트
|
// Enter 키 이벤트
|
||||||
$('#userInviteEmailInput').off('keypress').on('keypress', function (e) {
|
$('#userInviteMobileInput').off('keypress').on('keypress', function (e) {
|
||||||
if (e.which === 13) {
|
if (e.which === 13) {
|
||||||
$('#userInvitePopupConfirmButton').click();
|
$('#userInvitePopupConfirmButton').click();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 입력 시 에러 초기화
|
// 입력 시 에러 초기화
|
||||||
$('#userInviteEmailInput').off('input').on('input', function () {
|
$('#userInviteMobileInput').off('input').on('input', function () {
|
||||||
customPopups.clearUserInviteError();
|
customPopups.clearUserInviteError();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -460,14 +462,15 @@ const customPopups = {
|
|||||||
$('body').css('overflow', '');
|
$('body').css('overflow', '');
|
||||||
|
|
||||||
// 입력 필드 초기화
|
// 입력 필드 초기화
|
||||||
$('#userInviteEmailInput').val('').removeClass('error');
|
$('#userInviteMobileInput').val('').removeClass('error');
|
||||||
|
$('#userInviteNotifyConsent').prop('checked', false);
|
||||||
$('#userInvitePopupError').removeClass('show').text('');
|
$('#userInvitePopupError').removeClass('show').text('');
|
||||||
|
|
||||||
// 이벤트 리스너 제거
|
// 이벤트 리스너 제거
|
||||||
$('#userInvitePopupConfirmButton').off('click');
|
$('#userInvitePopupConfirmButton').off('click');
|
||||||
$('#userInvitePopupCancelButton').off('click');
|
$('#userInvitePopupCancelButton').off('click');
|
||||||
$('#userInvitePopupCloseButton').off('click');
|
$('#userInvitePopupCloseButton').off('click');
|
||||||
$('#userInviteEmailInput').off('keypress').off('input');
|
$('#userInviteMobileInput').off('keypress').off('input');
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -476,7 +479,7 @@ const customPopups = {
|
|||||||
*/
|
*/
|
||||||
showUserInviteError: function (message) {
|
showUserInviteError: function (message) {
|
||||||
$('#userInvitePopupError').text(message).addClass('show');
|
$('#userInvitePopupError').text(message).addClass('show');
|
||||||
$('#userInviteEmailInput').addClass('error');
|
$('#userInviteMobileInput').addClass('error');
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -484,7 +487,96 @@ const customPopups = {
|
|||||||
*/
|
*/
|
||||||
clearUserInviteError: function () {
|
clearUserInviteError: function () {
|
||||||
$('#userInvitePopupError').removeClass('show');
|
$('#userInvitePopupError').removeClass('show');
|
||||||
$('#userInviteEmailInput').removeClass('error');
|
$('#userInviteMobileInput').removeClass('error');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 초대취소 팝업 표시 (알림 수신 동의 체크박스 포함)
|
||||||
|
* @param {Object} options - 팝업 옵션
|
||||||
|
* @param {string} options.target - 취소 대상 표시 문자열 (마스킹된 이메일/휴대폰)
|
||||||
|
* @param {Function} options.onConfirm - 확인 버튼 클릭 시 호출되는 콜백 (파라미터: notifyConsent)
|
||||||
|
* @param {Function} options.onCancel - 취소(닫기) 버튼 클릭 시 호출되는 콜백 (선택사항)
|
||||||
|
*/
|
||||||
|
showCancelInvitation: function (options) {
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
const target = options.target || '';
|
||||||
|
const onConfirm = options.onConfirm;
|
||||||
|
const onCancel = options.onCancel;
|
||||||
|
|
||||||
|
// 대상 정보 및 체크박스 초기화
|
||||||
|
$('#cancelInvitationTarget').text(target);
|
||||||
|
$('#cancelInvitationNotifyConsent').prop('checked', false);
|
||||||
|
|
||||||
|
// 팝업 표시 (modal 구조 사용)
|
||||||
|
$('#cancelInvitationPopup').show();
|
||||||
|
setTimeout(function() {
|
||||||
|
$('#cancelInvitationModalBackdrop').addClass('show');
|
||||||
|
$('#cancelInvitationModal').addClass('show');
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// Body 스크롤 방지
|
||||||
|
$('body').css('overflow', 'hidden');
|
||||||
|
|
||||||
|
// 확인 버튼 이벤트 (기존 이벤트 제거 후 재등록)
|
||||||
|
$('#cancelInvitationPopupConfirmButton').off('click').on('click', function () {
|
||||||
|
const notifyConsent = $('#cancelInvitationNotifyConsent').is(':checked');
|
||||||
|
if (typeof onConfirm === 'function') {
|
||||||
|
onConfirm(notifyConsent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 닫기 버튼 이벤트
|
||||||
|
$('#cancelInvitationPopupCancelButton').off('click').on('click', function () {
|
||||||
|
customPopups.hideCancelInvitation();
|
||||||
|
if (typeof onCancel === 'function') {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// X 버튼 이벤트
|
||||||
|
$('#cancelInvitationPopupCloseButton').off('click').on('click', function () {
|
||||||
|
customPopups.hideCancelInvitation();
|
||||||
|
if (typeof onCancel === 'function') {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Escape 키 이벤트
|
||||||
|
$(document).off('keydown.cancelInvitation').on('keydown.cancelInvitation', function (e) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
customPopups.hideCancelInvitation();
|
||||||
|
if (typeof onCancel === 'function') {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 초대취소 팝업 숨기기
|
||||||
|
*/
|
||||||
|
hideCancelInvitation: function () {
|
||||||
|
// Modal 숨김 애니메이션
|
||||||
|
$('#cancelInvitationModalBackdrop').removeClass('show');
|
||||||
|
$('#cancelInvitationModal').removeClass('show');
|
||||||
|
|
||||||
|
// 애니메이션 완료 후 숨김
|
||||||
|
setTimeout(function() {
|
||||||
|
$('#cancelInvitationPopup').hide();
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
// Body 스크롤 복원
|
||||||
|
$('body').css('overflow', '');
|
||||||
|
|
||||||
|
// 체크박스 초기화
|
||||||
|
$('#cancelInvitationNotifyConsent').prop('checked', false);
|
||||||
|
|
||||||
|
// 이벤트 리스너 제거
|
||||||
|
$('#cancelInvitationPopupConfirmButton').off('click');
|
||||||
|
$('#cancelInvitationPopupCancelButton').off('click');
|
||||||
|
$('#cancelInvitationPopupCloseButton').off('click');
|
||||||
|
$(document).off('keydown.cancelInvitation');
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -459,11 +459,11 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: $spacing-sm $spacing-md;
|
padding: 0 $spacing-sm;
|
||||||
height: 40px;
|
height: 30px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
border-radius: $border-radius-md;
|
border-radius: $border-radius-sm;
|
||||||
font-size: $font-size-base;
|
font-size: 13px;
|
||||||
font-weight: $font-weight-regular;
|
font-weight: $font-weight-regular;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -504,6 +504,15 @@
|
|||||||
background-color: darken(#CDD4F0, 5%);
|
background-color: darken(#CDD4F0, 5%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--danger {
|
||||||
|
background-color: #F8D7DA;
|
||||||
|
color: #B02A37;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: darken(#F8D7DA, 5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -130,6 +130,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Empty state (해당 약관이 아직 시드되지 않은 경우)
|
||||||
|
.terms-empty {
|
||||||
|
padding: $spacing-xl $spacing-lg;
|
||||||
|
text-align: center;
|
||||||
|
color: #8c959f;
|
||||||
|
font-size: $font-size-base;
|
||||||
|
}
|
||||||
|
|
||||||
// Version Selector (Figma: 검색창 986-2208)
|
// Version Selector (Figma: 검색창 986-2208)
|
||||||
.terms-selector {
|
.terms-selector {
|
||||||
padding: $spacing-lg;
|
padding: $spacing-lg;
|
||||||
|
|||||||
@@ -101,16 +101,15 @@
|
|||||||
// Current user badge (used in actions column)
|
// Current user badge (used in actions column)
|
||||||
.user-current-badge {
|
.user-current-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
height: 40px;
|
height: 30px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 6px 12px;
|
padding: 0 $spacing-sm;
|
||||||
background-color: rgba($accent-purple, 0.1);
|
background-color: rgba($accent-purple, 0.1);
|
||||||
color: $accent-purple;
|
color: $accent-purple;
|
||||||
border-radius: $border-radius-sm;
|
border-radius: $border-radius-sm;
|
||||||
font-size: $font-size-sm;
|
font-size: 13px;
|
||||||
font-weight: $font-weight-medium;
|
font-weight: $font-weight-medium;
|
||||||
min-width: 116px;
|
|
||||||
|
|
||||||
// 모바일에서 숨김
|
// 모바일에서 숨김
|
||||||
@media (max-width: $breakpoint-sm) {
|
@media (max-width: $breakpoint-sm) {
|
||||||
@@ -1058,6 +1057,19 @@
|
|||||||
& + .dropdown-item {
|
& + .dropdown-item {
|
||||||
border-top: 1px solid #eee;
|
border-top: 1px solid #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--danger {
|
||||||
|
color: #B02A37;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fdeaec;
|
||||||
|
color: #B02A37;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #f8d7da;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,12 +24,16 @@
|
|||||||
<image id="image0_1130_6799" width="163" height="129" preserveAspectRatio="none" xlink:href="data:image/png;base64,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"/>
|
<image id="image0_1130_6799" width="163" height="129" preserveAspectRatio="none" xlink:href="data:image/png;base64,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"/>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
<h2 class="result-title">회원가입 신청이 완료되었습니다.</h2>
|
<h2 class="result-title" th:text="${registrationType == 'corporate'} ? '법인 신청이 완료 되었습니다.' : '회원가입 신청이 완료되었습니다.'">회원가입 신청이 완료되었습니다.</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Info Box -->
|
<!-- Info Box -->
|
||||||
<div class="result-info-box">
|
<div class="result-info-box">
|
||||||
<p class="info-text">
|
<p class="info-text" th:if="${registrationType == 'corporate'}">
|
||||||
|
관리자의 확인 및 승인 이후<br>
|
||||||
|
이용하실 수 있습니다.
|
||||||
|
</p>
|
||||||
|
<p class="info-text" th:unless="${registrationType == 'corporate'}">
|
||||||
API Portal 서비스를 이용하기 위해서<br>
|
API Portal 서비스를 이용하기 위해서<br>
|
||||||
<strong>이메일 인증</strong>을 완료해 주세요.
|
<strong>이메일 인증</strong>을 완료해 주세요.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<div class="list-table-header">
|
<div class="list-table-header">
|
||||||
<div class="header-cell" style="width: 90px;">NO.</div>
|
<div class="header-cell" style="width: 90px;">NO.</div>
|
||||||
<div class="header-cell" style="width: 90px;">성명</div>
|
<div class="header-cell" style="width: 90px;">성명</div>
|
||||||
<div class="header-cell" style="flex: 1; min-width: 180px;">이메일 아이디</div>
|
<div class="header-cell" style="flex: 1; min-width: 180px;">이메일 / 휴대폰</div>
|
||||||
<div class="header-cell" style="width: 120px;">권한</div>
|
<div class="header-cell" style="width: 120px;">권한</div>
|
||||||
<div class="header-cell" style="width: 120px;">계정상태</div>
|
<div class="header-cell" style="width: 120px;">계정상태</div>
|
||||||
<div class="header-cell" style="width: 320px;">설정</div>
|
<div class="header-cell" style="width: 320px;">설정</div>
|
||||||
@@ -71,18 +71,24 @@
|
|||||||
th:data-user-email="${user.maskedEmailAddr}">
|
th:data-user-email="${user.maskedEmailAddr}">
|
||||||
관리자 권한부여
|
관리자 권한부여
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="list-table-btn list-table-btn--default revoke_manager"
|
||||||
|
th:if="${user.userStatus.toString() == 'ACTIVE' && user.roleCode.toString() == 'ROLE_CORP_MANAGER'}"
|
||||||
|
th:data-user-id="${user.id}"
|
||||||
|
th:data-user-name="${user.maskedUserName}">
|
||||||
|
이용자로 변경
|
||||||
|
</button>
|
||||||
|
<button type="button" class="list-table-btn list-table-btn--danger remove_from_org"
|
||||||
|
th:if="${user.userStatus.toString() == 'ACTIVE' && (user.roleCode.toString() == 'ROLE_CORP_USER' || user.roleCode.toString() == 'ROLE_CORP_MANAGER')}"
|
||||||
|
th:data-user-id="${user.id}"
|
||||||
|
th:data-user-name="${user.maskedUserName}">
|
||||||
|
소속 제외
|
||||||
|
</button>
|
||||||
<button type="button" class="list-table-btn list-table-btn--primary activate_user"
|
<button type="button" class="list-table-btn list-table-btn--primary activate_user"
|
||||||
th:if="${user.userStatus.toString() != 'ACTIVE'}"
|
th:if="${user.userStatus.toString() != 'ACTIVE'}"
|
||||||
th:data-user-id="${user.id}"
|
th:data-user-id="${user.id}"
|
||||||
th:data-user-name="${user.maskedUserName}">
|
th:data-user-name="${user.maskedUserName}">
|
||||||
활성화
|
활성화
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="list-table-btn list-table-btn--primary inactivate_user"
|
|
||||||
th:if="${user.userStatus.toString() == 'ACTIVE'}"
|
|
||||||
th:data-user-id="${user.id}"
|
|
||||||
th:data-user-name="${user.maskedUserName}">
|
|
||||||
비활성화
|
|
||||||
</button>
|
|
||||||
<button type="button" class="list-table-btn list-table-btn--secondary cancel_invitation"
|
<button type="button" class="list-table-btn list-table-btn--secondary cancel_invitation"
|
||||||
th:if="${user.userStatus.toString() == 'PENDING'}"
|
th:if="${user.userStatus.toString() == 'PENDING'}"
|
||||||
th:data-user-id="${user.id}"
|
th:data-user-id="${user.id}"
|
||||||
@@ -108,18 +114,24 @@
|
|||||||
th:data-user-email="${user.maskedEmailAddr}">
|
th:data-user-email="${user.maskedEmailAddr}">
|
||||||
관리자 권한부여
|
관리자 권한부여
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="dropdown-item revoke_manager"
|
||||||
|
th:if="${user.userStatus.toString() == 'ACTIVE' && user.roleCode.toString() == 'ROLE_CORP_MANAGER'}"
|
||||||
|
th:data-user-id="${user.id}"
|
||||||
|
th:data-user-name="${user.maskedUserName}">
|
||||||
|
이용자로 변경
|
||||||
|
</button>
|
||||||
|
<button type="button" class="dropdown-item dropdown-item--danger remove_from_org"
|
||||||
|
th:if="${user.userStatus.toString() == 'ACTIVE' && (user.roleCode.toString() == 'ROLE_CORP_USER' || user.roleCode.toString() == 'ROLE_CORP_MANAGER')}"
|
||||||
|
th:data-user-id="${user.id}"
|
||||||
|
th:data-user-name="${user.maskedUserName}">
|
||||||
|
소속 제외
|
||||||
|
</button>
|
||||||
<button type="button" class="dropdown-item activate_user"
|
<button type="button" class="dropdown-item activate_user"
|
||||||
th:if="${user.userStatus.toString() != 'ACTIVE'}"
|
th:if="${user.userStatus.toString() != 'ACTIVE'}"
|
||||||
th:data-user-id="${user.id}"
|
th:data-user-id="${user.id}"
|
||||||
th:data-user-name="${user.maskedUserName}">
|
th:data-user-name="${user.maskedUserName}">
|
||||||
활성화
|
활성화
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="dropdown-item inactivate_user"
|
|
||||||
th:if="${user.userStatus.toString() == 'ACTIVE'}"
|
|
||||||
th:data-user-id="${user.id}"
|
|
||||||
th:data-user-name="${user.maskedUserName}">
|
|
||||||
비활성화
|
|
||||||
</button>
|
|
||||||
<button type="button" class="dropdown-item cancel_invitation"
|
<button type="button" class="dropdown-item cancel_invitation"
|
||||||
th:if="${user.userStatus.toString() == 'PENDING'}"
|
th:if="${user.userStatus.toString() == 'PENDING'}"
|
||||||
th:data-user-id="${user.id}"
|
th:data-user-id="${user.id}"
|
||||||
@@ -137,8 +149,8 @@
|
|||||||
<div class="list-table-row" th:each="user : ${pendingUsers}">
|
<div class="list-table-row" th:each="user : ${pendingUsers}">
|
||||||
<div class="row-cell" style="width: 90px;" data-label="NO">-</div>
|
<div class="row-cell" style="width: 90px;" data-label="NO">-</div>
|
||||||
<div class="row-cell" style="width: 90px;" data-label="성명">-</div>
|
<div class="row-cell" style="width: 90px;" data-label="성명">-</div>
|
||||||
<div class="row-cell" style="flex: 1; min-width: 180px;" data-label="이메일" th:text="${user.maskedEmailAddr}">
|
<div class="row-cell" style="flex: 1; min-width: 180px;" data-label="휴대폰" th:text="${user.maskedMobileNumber}">
|
||||||
pending@example.com
|
010-****-****
|
||||||
</div>
|
</div>
|
||||||
<div class="row-cell" style="width: 120px;" data-label="권한">이용자</div>
|
<div class="row-cell" style="width: 120px;" data-label="권한">이용자</div>
|
||||||
<div class="row-cell" style="width: 120px;" data-label="계정상태">초대중</div>
|
<div class="row-cell" style="width: 120px;" data-label="계정상태">초대중</div>
|
||||||
@@ -147,7 +159,7 @@
|
|||||||
<div class="actions-desktop">
|
<div class="actions-desktop">
|
||||||
<button type="button" class="list-table-btn list-table-btn--secondary cancel_invitation"
|
<button type="button" class="list-table-btn list-table-btn--secondary cancel_invitation"
|
||||||
th:data-user-id="${user.id}"
|
th:data-user-id="${user.id}"
|
||||||
th:data-user-email="${user.maskedEmailAddr}">
|
th:data-user-email="${user.maskedMobileNumber}">
|
||||||
초대취소
|
초대취소
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -164,7 +176,7 @@
|
|||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<button type="button" class="dropdown-item cancel_invitation"
|
<button type="button" class="dropdown-item cancel_invitation"
|
||||||
th:data-user-id="${user.id}"
|
th:data-user-id="${user.id}"
|
||||||
th:data-user-email="${user.maskedEmailAddr}">
|
th:data-user-email="${user.maskedMobileNumber}">
|
||||||
초대취소
|
초대취소
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -194,6 +206,8 @@
|
|||||||
<div th:replace="~{fragment/popup/userInvitePopup :: userInvitePopup}"></div>
|
<div th:replace="~{fragment/popup/userInvitePopup :: userInvitePopup}"></div>
|
||||||
<!-- Change Role Modal -->
|
<!-- Change Role Modal -->
|
||||||
<div th:replace="~{fragment/popup/changeRolePopup :: changeRolePopup}"></div>
|
<div th:replace="~{fragment/popup/changeRolePopup :: changeRolePopup}"></div>
|
||||||
|
<!-- Cancel Invitation Modal -->
|
||||||
|
<div th:replace="~{fragment/popup/cancelInvitationPopup :: cancelInvitationPopup}"></div>
|
||||||
</section>
|
</section>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
@@ -247,12 +261,12 @@
|
|||||||
// Function to open the user invite popup
|
// Function to open the user invite popup
|
||||||
function openUserInvitePopup() {
|
function openUserInvitePopup() {
|
||||||
customPopups.showUserInvite({
|
customPopups.showUserInvite({
|
||||||
onConfirm: function(email) {
|
onConfirm: function(mobile, notifyConsent) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/users/invite',
|
url: '/users/invite',
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
data: JSON.stringify({emailAddr: email})
|
data: JSON.stringify({mobileNumber: mobile, notifyConsent: notifyConsent})
|
||||||
}).done(function(response) {
|
}).done(function(response) {
|
||||||
customPopups.hideUserInvite();
|
customPopups.hideUserInvite();
|
||||||
customPopups.showAlert(response.msg, function() {
|
customPopups.showAlert(response.msg, function() {
|
||||||
@@ -315,6 +329,63 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 관리자 -> 이용자 변경 (본인 제외)
|
||||||
|
document.querySelectorAll('.revoke_manager').forEach(button => {
|
||||||
|
button.addEventListener('click', function(event) {
|
||||||
|
const btn = event.currentTarget;
|
||||||
|
const userId = btn.dataset.userId;
|
||||||
|
const userName = btn.dataset.userName;
|
||||||
|
|
||||||
|
customPopups.showConfirm(`<strong>${userName}</strong>님을 이용자로 변경하시겠습니까?`, function(selection) {
|
||||||
|
if (!selection) return;
|
||||||
|
$.ajax({
|
||||||
|
url: '/users/revoke-manager',
|
||||||
|
type: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({id: userId})
|
||||||
|
}).done(function(response) {
|
||||||
|
customPopups.showAlert(response.msg, function() {
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
}).fail(function(jqXHR, textStatus, errorThrown) {
|
||||||
|
let msg;
|
||||||
|
try { msg = JSON.parse(jqXHR.responseText).msg; } catch (e) {}
|
||||||
|
customPopups.showAlert(msg || ('권한 변경 중 오류가 발생했습니다: ' + errorThrown));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 소속 제외 -> 개인이용자 전환 (경고)
|
||||||
|
document.querySelectorAll('.remove_from_org').forEach(button => {
|
||||||
|
button.addEventListener('click', function(event) {
|
||||||
|
const btn = event.currentTarget;
|
||||||
|
const userId = btn.dataset.userId;
|
||||||
|
const userName = btn.dataset.userName;
|
||||||
|
|
||||||
|
customPopups.showConfirm(
|
||||||
|
`<strong>${userName}</strong>님을 소속에서 제외하시겠습니까?<br>제외 시 기관의 모든 권한이 회수되며 <strong>개인이용자</strong>로 전환됩니다.`,
|
||||||
|
function(selection) {
|
||||||
|
if (!selection) return;
|
||||||
|
$.ajax({
|
||||||
|
url: '/users/remove-from-org',
|
||||||
|
type: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({id: userId})
|
||||||
|
}).done(function(response) {
|
||||||
|
customPopups.showAlert(response.msg, function() {
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
}).fail(function(jqXHR, textStatus, errorThrown) {
|
||||||
|
let msg;
|
||||||
|
try { msg = JSON.parse(jqXHR.responseText).msg; } catch (e) {}
|
||||||
|
customPopups.showAlert(msg || ('소속 제외 중 오류가 발생했습니다: ' + errorThrown));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const activateButtons = document.querySelectorAll('.activate_user');
|
const activateButtons = document.querySelectorAll('.activate_user');
|
||||||
const inactivateButtons = document.querySelectorAll('.inactivate_user');
|
const inactivateButtons = document.querySelectorAll('.inactivate_user');
|
||||||
const cancelInvitationButtons = document.querySelectorAll('.cancel_invitation');
|
const cancelInvitationButtons = document.querySelectorAll('.cancel_invitation');
|
||||||
@@ -363,21 +434,24 @@
|
|||||||
const userId = button.dataset.userId;
|
const userId = button.dataset.userId;
|
||||||
const userEmail = button.dataset.userEmail;
|
const userEmail = button.dataset.userEmail;
|
||||||
|
|
||||||
customPopups.showConfirm(`${userEmail}에 대한 초대를 취소하시겠습니까?`, function(selection) {
|
customPopups.showCancelInvitation({
|
||||||
if (selection) {
|
target: userEmail,
|
||||||
|
onConfirm: function(notifyConsent) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/users/cancel-invitation',
|
url: '/users/cancel-invitation',
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
data: JSON.stringify({id: userId})
|
data: JSON.stringify({id: userId, notifyConsent: notifyConsent})
|
||||||
}).done(function(response) {
|
}).done(function(response) {
|
||||||
|
customPopups.hideCancelInvitation();
|
||||||
customPopups.showAlert(response.msg, function() {
|
customPopups.showAlert(response.msg, function() {
|
||||||
location.reload(); // Reload the page to reflect changes
|
location.reload(); // Reload the page to reflect changes
|
||||||
});
|
});
|
||||||
}).fail(function(jqXHR, textStatus, errorThrown) {
|
}).fail(function(jqXHR, textStatus, errorThrown) {
|
||||||
customPopups.showAlert('초대 취소 중 오류가 발생했습니다: ' + errorThrown);
|
customPopups.hideCancelInvitation();
|
||||||
}).always(function() {
|
let msg;
|
||||||
$('#customConfirm').hide();
|
try { msg = JSON.parse(jqXHR.responseText).msg; } catch (e) {}
|
||||||
|
customPopups.showAlert(msg || ('초대 취소 중 오류가 발생했습니다: ' + errorThrown));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a th:href="@{/agreements/terms}" class="footer-link">이용약관</a>
|
<a th:href="@{/agreements/terms}" class="footer-link">이용약관</a>
|
||||||
<span class="footer-separator">|</span>
|
<span class="footer-separator">|</span>
|
||||||
<a th:href="@{/agreements/terms(tab=privacy)}" class="footer-link">개인정보처리방침</a>
|
<a href="https://www.jejubank.co.kr/hmpg/csct/secuCenr/ptctPlcy/procsPlcy/ctnt.do"
|
||||||
|
target="_blank" rel="noopener noreferrer" class="footer-link">개인정보처리방침</a>
|
||||||
</div>
|
</div>
|
||||||
<p class="footer-copyright">Copyright © 2026 DJ Bank. All Rights Reserved.</p>
|
<p class="footer-copyright">Copyright © 2026 DJ Bank. All Rights Reserved.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
<section layout:fragment="title">
|
<section layout:fragment="title">
|
||||||
<div class="page-title-banner">
|
<div class="page-title-banner">
|
||||||
<img th:src="@{/img/img_title_bg.png}" class="title-image">
|
<img th:src="@{/img/img_title_bg.png}" class="title-image">
|
||||||
<h1 th:text="${agreementTitle}">이용 약관</h1>
|
<h1 th:text="${isTermsOfUse ? '이용약관' : (isPrivacyCollect ? '개인정보수집동의서' : '알림 수신 동의서')}">이용 약관</h1>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section layout:fragment="contentFragment">
|
<section layout:fragment="contentFragment">
|
||||||
<div class="terms-container">
|
<div class="terms-container">
|
||||||
<!-- Title Bar -->
|
<!-- Title Bar -->
|
||||||
<div class="common-title-bar">
|
<div class="common-title-bar">
|
||||||
<h2 class="common-title" th:text="${isTermsOfUse ? '이용약관' : '개인정보처리방침'}">이용약관</h2>
|
<h2 class="common-title" th:text="${isTermsOfUse ? '이용약관' : (isPrivacyCollect ? '개인정보수집동의서' : '알림 수신 동의서')}">이용약관</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab Navigation -->
|
<!-- Tab Navigation -->
|
||||||
@@ -22,15 +22,20 @@
|
|||||||
th:classappend="${isTermsOfUse ? 'active' : ''}">
|
th:classappend="${isTermsOfUse ? 'active' : ''}">
|
||||||
이용약관
|
이용약관
|
||||||
</a>
|
</a>
|
||||||
<a th:href="@{/agreements/terms(tab='privacy')}"
|
<a th:href="@{/agreements/terms(tab='privacy-collect')}"
|
||||||
class="tab-link"
|
class="tab-link"
|
||||||
th:classappend="${isPrivacyPolicy ? 'active' : ''}">
|
th:classappend="${isPrivacyCollect ? 'active' : ''}">
|
||||||
개인정보처리방침
|
개인정보수집동의서
|
||||||
|
</a>
|
||||||
|
<a th:href="@{/agreements/terms(tab='notification')}"
|
||||||
|
class="tab-link"
|
||||||
|
th:classappend="${isNotification ? 'active' : ''}">
|
||||||
|
알림 수신 동의서
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Version Selector -->
|
<!-- Version Selector -->
|
||||||
<div class="terms-selector">
|
<div class="terms-selector" th:if="${selectedAgreement != null}">
|
||||||
<form id="agreementForm" th:action="@{/agreements/terms}" method="get">
|
<form id="agreementForm" th:action="@{/agreements/terms}" method="get">
|
||||||
<input type="hidden" name="tab" th:value="${currentTab}">
|
<input type="hidden" name="tab" th:value="${currentTab}">
|
||||||
<select name="publishedOn" id="publishedOnSelect" class="terms-select" onchange="this.form.submit()">
|
<select name="publishedOn" id="publishedOnSelect" class="terms-select" onchange="this.form.submit()">
|
||||||
@@ -45,19 +50,24 @@
|
|||||||
|
|
||||||
<!-- Terms Content -->
|
<!-- Terms Content -->
|
||||||
<div class="terms-content">
|
<div class="terms-content">
|
||||||
<!-- Attachment Download -->
|
<th:block th:if="${selectedAgreement != null}">
|
||||||
<div class="terms-attachment" th:if="${selectedAgreement.attachedFileId != null}">
|
<!-- Attachment Download -->
|
||||||
<a th:href="@{/file/download(fileId=${selectedAgreement.attachedFileId}, fileSn=1)}"
|
<div class="terms-attachment" th:if="${selectedAgreement.attachedFileId != null}">
|
||||||
class="attachment-link">
|
<a th:href="@{/file/download(fileId=${selectedAgreement.attachedFileId}, fileSn=1)}"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
class="attachment-link">
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<polyline points="7 10 12 15 17 10"></polyline>
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
<polyline points="7 10 12 15 17 10"></polyline>
|
||||||
</svg>
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||||
<span th:text="${selectedAgreement.attachedFileName} ?: '첨부파일 다운로드'">첨부파일 다운로드</span>
|
</svg>
|
||||||
</a>
|
<span th:text="${selectedAgreement.attachedFileName} ?: '첨부파일 다운로드'">첨부파일 다운로드</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="content-body editor-content" th:utext="${selectedAgreement.contents}"></div>
|
||||||
|
</th:block>
|
||||||
|
<div class="terms-empty" th:if="${selectedAgreement == null}">
|
||||||
|
<p>등록된 약관이 없습니다.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="content-body editor-content" th:utext="${selectedAgreement.contents}"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<!-- views/fragment/popup/cancelInvitationPopup.html -->
|
||||||
|
<div th:fragment="cancelInvitationPopup" id="cancelInvitationPopup" style="display: none;">
|
||||||
|
<!-- Modal Backdrop -->
|
||||||
|
<div class="modal-backdrop" id="cancelInvitationModalBackdrop"></div>
|
||||||
|
|
||||||
|
<!-- Modal Wrapper -->
|
||||||
|
<div class="modal" id="cancelInvitationModal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
|
||||||
|
<!-- Modal Header -->
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title">초대취소</h3>
|
||||||
|
<button type="button" class="modal-close" id="cancelInvitationPopupCloseButton">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Body -->
|
||||||
|
<div class="modal-body">
|
||||||
|
<p id="cancelInvitationPopupMessage" style="text-align: center; margin-bottom: 24px; color: #1A1A2E; font-size: 16px;">
|
||||||
|
<strong id="cancelInvitationTarget" style="color: #4B9BFF;"></strong>에 대한<br>초대를 취소하시겠습니까?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- 알림 수신 동의 체크박스 -->
|
||||||
|
<div class="pop_consent_group" style="margin-top: 16px;">
|
||||||
|
<label class="pop_consent_label"
|
||||||
|
style="display: flex; align-items: flex-start; gap: 8px; font-size: 13px; color: #475569; line-height: 1.5; cursor: pointer;">
|
||||||
|
<input type="checkbox" id="cancelInvitationNotifyConsent" style="margin-top: 3px; flex-shrink: 0;">
|
||||||
|
<span>
|
||||||
|
초대 취소 사실을 수신자에게 알림 메시지로 전송하기 위해 수신자의
|
||||||
|
<a th:href="@{/agreements/terms(tab='notification')}" target="_blank" rel="noopener noreferrer"
|
||||||
|
style="color: #3ba4ed; text-decoration: underline;">알림 수신 동의서</a>
|
||||||
|
동의가 필요합니다. 동의하지 않을 경우 초대 취소는 진행되나 메시지가 발송되지 않습니다.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Footer -->
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary btn-submit" id="cancelInvitationPopupCancelButton">닫기</button>
|
||||||
|
<button type="button" class="btn btn-primary btn-submit" id="cancelInvitationPopupConfirmButton">초대취소</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -21,17 +21,32 @@
|
|||||||
<!-- Modal Body -->
|
<!-- Modal Body -->
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p id="userInvitePopupMessage" style="text-align: center; margin-bottom: 24px; color: #64748B;">
|
<p id="userInvitePopupMessage" style="text-align: center; margin-bottom: 24px; color: #64748B;">
|
||||||
추가할 이용자 e-mail 주소를 입력해 주세요.
|
추가할 이용자 휴대폰 번호를 입력해 주세요.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Email Input Field -->
|
<!-- Mobile Input Field -->
|
||||||
<div class="pop_input_group">
|
<div class="pop_input_group">
|
||||||
<input type="email"
|
<input type="tel"
|
||||||
id="userInviteEmailInput"
|
id="userInviteMobileInput"
|
||||||
class="pop_input_field"
|
class="pop_input_field"
|
||||||
placeholder="이용자 e-mail 주소">
|
placeholder="휴대폰 번호 ('-' 없이 입력)"
|
||||||
|
maxlength="13">
|
||||||
<div id="userInvitePopupError" class="error-message"></div>
|
<div id="userInvitePopupError" class="error-message"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 알림 수신 동의 체크박스 -->
|
||||||
|
<div class="pop_consent_group" style="margin-top: 16px;">
|
||||||
|
<label class="pop_consent_label"
|
||||||
|
style="display: flex; align-items: flex-start; gap: 8px; font-size: 13px; color: #475569; line-height: 1.5; cursor: pointer;">
|
||||||
|
<input type="checkbox" id="userInviteNotifyConsent" style="margin-top: 3px; flex-shrink: 0;">
|
||||||
|
<span>
|
||||||
|
입력한 휴대폰 번호로 초대 메시지를 보내기 위해 수신자의
|
||||||
|
<a th:href="@{/agreements/terms(tab='notification')}" target="_blank" rel="noopener noreferrer"
|
||||||
|
style="color: #3ba4ed; text-decoration: underline;">알림 수신 동의서</a>
|
||||||
|
동의가 필요합니다. 동의하지 않을 경우 초대는 진행되나 메시지가 발송되지 않습니다.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal Footer -->
|
<!-- Modal Footer -->
|
||||||
|
|||||||
Reference in New Issue
Block a user