From d9373b00f04b3e1f7e3bd12c3593ac86731f9ec0 Mon Sep 17 00:00:00 2001 From: Rinjae Date: Wed, 9 Sep 2026 16:00:13 +0900 Subject: [PATCH] =?UTF-8?q?Moment.js=20=EC=97=85=EA=B7=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=20=EB=B0=8F=20=ED=98=B8=ED=99=98=EC=84=B1=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80:=20-=20?= =?UTF-8?q?=EC=B5=9C=EC=8B=A0=20=EB=B2=84=EC=A0=84(2.30.1)=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=B0=8F=20?= =?UTF-8?q?CVE-2022-24785=20=EB=8C=80=EC=9D=91=20-=20=EB=B8=8C=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=EC=A0=80=20=EA=B8=B0=EB=B0=98=20date-range=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=20=EA=B2=80=EC=A6=9D=20=EB=B0=8F=20Playwrigh?= =?UTF-8?q?t=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20-=20?= =?UTF-8?q?=EC=9C=A4=EB=85=84/=EC=84=9C=EB=A8=B8=ED=83=80=EC=9E=84=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20CommonJS=20=EB=B3=B4=EC=95=88?= =?UTF-8?q?=20=ED=9A=8C=EA=B7=80=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 46 +++++ package.json | 2 + .../controller/UserRegisterController.java | 8 - .../user/facade/OrgRegisterFacadeImpl.java | 35 ---- .../user/facade/UserRegisterFacadeImpl.java | 33 ---- .../user/service/PortalUserAuthService.java | 10 -- .../portal/common/util/EncryptionUtil.java | 49 ------ src/main/resources/static/js/moment.LICENSE | 22 +++ src/main/resources/static/js/moment.min.js | 3 +- src/test/js/README.md | 35 ++++ src/test/js/fixtures/date-range.html | 28 +++ src/test/js/moment-compatibility.spec.js | 161 ++++++++++++++++++ src/test/js/playwright.config.js | 21 +++ 13 files changed, 317 insertions(+), 136 deletions(-) create mode 100644 src/main/resources/static/js/moment.LICENSE create mode 100644 src/test/js/README.md create mode 100644 src/test/js/fixtures/date-range.html create mode 100644 src/test/js/moment-compatibility.spec.js create mode 100644 src/test/js/playwright.config.js diff --git a/package-lock.json b/package-lock.json index 6691730..c07b845 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { + "@playwright/test": "1.63.0", "esbuild": "^0.25.0", "node-forge": "^1.3.1", "sass": "^1.69.5" @@ -766,6 +767,22 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@playwright/test": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", @@ -899,6 +916,35 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", diff --git a/package.json b/package.json index 11b86aa..be952c0 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "description": "SASS build system + forge custom bundle for EAPIM Portal", "scripts": { + "test:moment": "playwright test --config=src/test/js/playwright.config.js", "sass:build": "sass src/main/resources/static/sass/main.scss:src/main/resources/static/css/main.css --style=expanded", "sass:build:minified": "sass src/main/resources/static/sass/main.scss:src/main/resources/static/css/main.min.css --style=compressed", "sass:watch": "sass --watch src/main/resources/static/sass/main.scss:src/main/resources/static/css/main.css --style=expanded", @@ -11,6 +12,7 @@ "forge:build": "esbuild tools/forge-entry.js --bundle --minify --format=iife --global-name=forge --target=es5 --outfile=src/main/resources/static/js/lib/forge-crypto.min.js" }, "devDependencies": { + "@playwright/test": "1.63.0", "esbuild": "^0.25.0", "node-forge": "^1.3.1", "sass": "^1.69.5" diff --git a/src/main/java/com/eactive/apim/portal/apps/user/controller/UserRegisterController.java b/src/main/java/com/eactive/apim/portal/apps/user/controller/UserRegisterController.java index f3cd2d8..78ca8a7 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/controller/UserRegisterController.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/controller/UserRegisterController.java @@ -10,7 +10,6 @@ import com.eactive.apim.portal.apps.user.repository.PortalOrgRepository; import com.eactive.apim.portal.apps.user.service.PortalUserAuthService; import com.eactive.apim.portal.apps.user.service.PortalUserService; import com.eactive.apim.portal.apps.user.validator.AgreementValidator; -import com.eactive.apim.portal.common.util.EncryptionUtil; import com.eactive.apim.portal.common.util.PhoneNumberUtil; import com.eactive.apim.portal.common.util.SecurityUtil; import com.eactive.apim.portal.config.PortalProperties; @@ -22,21 +21,15 @@ import com.eactive.apim.portal.portaluser.entity.PortalUser; import com.eactive.apim.portal.portaluser.entity.PortalUserEnums; import com.eactive.apim.portal.portaluser.repository.PortalUserRepository; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; import java.util.Map; import java.util.Optional; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestBody; -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import javax.validation.Valid; import lombok.RequiredArgsConstructor; -import org.apache.xerces.impl.dv.util.Base64; import org.springframework.security.access.annotation.Secured; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -64,7 +57,6 @@ public class UserRegisterController { private final AgreementsFacade agreementsFacade; private final PortalProperties portalProperties; private final UserInvitationRepository userInvitationRepository; - private final EncryptionUtil encryptionUtil; private final AgreementValidator agreementValidator; private final PortalUserAuthService portalUserAuthService; diff --git a/src/main/java/com/eactive/apim/portal/apps/user/facade/OrgRegisterFacadeImpl.java b/src/main/java/com/eactive/apim/portal/apps/user/facade/OrgRegisterFacadeImpl.java index 2786224..e843029 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/facade/OrgRegisterFacadeImpl.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/facade/OrgRegisterFacadeImpl.java @@ -11,8 +11,6 @@ import com.eactive.apim.portal.apps.user.service.PortalOrgService; import com.eactive.apim.portal.apps.user.service.PortalUserService; import com.eactive.apim.portal.apps.user.service.UserRegistrationValidationService; import com.eactive.apim.portal.apps.user.validator.AgreementValidator; -import com.eactive.apim.portal.common.exception.SystemException; -import com.eactive.apim.portal.common.util.EncryptionUtil; import com.eactive.apim.portal.file.entity.FileInfo; import com.eactive.apim.portal.file.service.FileService; import com.eactive.apim.portal.file.service.FileTypeContext; @@ -20,26 +18,14 @@ 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.repository.PortalUserRepository; -import com.eactive.apim.portal.template.entity.MessageCode; -import com.eactive.apim.portal.template.service.MessageHandlerService; -import com.eactive.apim.portal.template.service.MessageRecipient; import lombok.RequiredArgsConstructor; -import org.apache.xerces.impl.dv.util.Base64; import org.springframework.http.ResponseEntity; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.time.format.DateTimeFormatter; -import java.util.HashMap; import java.util.List; import java.util.Optional; @@ -58,8 +44,6 @@ public class OrgRegisterFacadeImpl implements OrgRegisterFacade { private final PasswordEncoder passwordEncoder; private final AgreementValidator agreementValidator; private final ApprovalService approvalService; - private final MessageHandlerService messageHandlerService; - private final EncryptionUtil encryptionUtil; @Override @Transactional @@ -213,29 +197,10 @@ public class OrgRegisterFacadeImpl implements OrgRegisterFacade { agreementsFacade.saveUserAgreements(newUser.getId(), AgreementType.PRIVACY_COLLECT); approvalService.createUserApproval(newUser); - // 11.28 - 회원 가입단계가 아닌 로그인 단계로 이메일 인증 이동 -// sendActivationEmail(newUser); return new ValidationResponse(true, "법인 사용자 등록 신청이 완료되었습니다."); } - private void sendActivationEmail(PortalUser newUser) { - MessageRecipient recipient = new MessageRecipient(); - recipient.setUsername(newUser.getUserName()); - recipient.setUserId(newUser.getEmailAddr()); - recipient.setPhone(newUser.getMobileNumber()); - HashMap params = new HashMap<>(); - String tokenValue = newUser.getCreatedDate().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")) + ":" + newUser.getId(); - - try { - String encToken = encryptionUtil.encrypt(tokenValue); - params.put("token", Base64.encode(encToken.getBytes(StandardCharsets.UTF_8))); - messageHandlerService.publishEvent(MessageCode.USER_VERIFICATION_EMAIL, recipient, params); - } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { - throw new SystemException("암호화 모듈 오류"); - } - } - // 기존 사용자를 법인 사용자로 전환하는 메서드 private ValidationResponse convertExistingUserToCorporate( PortalUser existingUser, diff --git a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserRegisterFacadeImpl.java b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserRegisterFacadeImpl.java index 7b79d46..86b9c63 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserRegisterFacadeImpl.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserRegisterFacadeImpl.java @@ -2,7 +2,6 @@ package com.eactive.apim.portal.apps.user.facade; import com.eactive.apim.portal.agreements.entity.AgreementType; import com.eactive.apim.portal.apps.agreements.service.AgreementsFacade; -import com.eactive.apim.portal.apps.auth.service.AuthNumberGenerator; import com.eactive.apim.portal.apps.user.dto.PortalUserRegistrationDTO; import com.eactive.apim.portal.apps.user.dto.UserAgreementDTO; import com.eactive.apim.portal.apps.user.dto.ValidationResponse; @@ -11,15 +10,11 @@ import com.eactive.apim.portal.apps.user.service.PortalUserService; import com.eactive.apim.portal.apps.user.service.UserRegistrationValidationService; import com.eactive.apim.portal.apps.user.validator.AgreementValidator; import com.eactive.apim.portal.apps.user.validator.PasswordValidator; -import com.eactive.apim.portal.common.util.EncryptionUtil; import com.eactive.apim.portal.invitation.entity.UserInvitation; import com.eactive.apim.portal.invitation.entity.UserInvitationEnums; import com.eactive.apim.portal.invitation.repository.UserInvitationRepository; import com.eactive.apim.portal.portaluser.entity.PortalUser; import com.eactive.apim.portal.portaluser.repository.PortalUserRepository; -import com.eactive.apim.portal.template.entity.MessageCode; -import com.eactive.apim.portal.template.service.MessageHandlerService; -import com.eactive.apim.portal.template.service.MessageRecipient; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,7 +26,6 @@ import org.springframework.validation.BindingResult; import javax.servlet.http.HttpSession; import java.time.LocalDateTime; -import java.util.HashMap; import java.util.Optional; @Service @@ -49,9 +43,6 @@ public class UserRegisterFacadeImpl implements UserRegisterFacade { private final PasswordValidator passwordValidator; private final PasswordEncoder passwordEncoder; private final AgreementValidator agreementValidator; - private final MessageHandlerService messageHandlerService; - private final EncryptionUtil encryptionUtil; - private final AuthNumberGenerator authNumberGenerator; @Override @@ -159,8 +150,6 @@ public class UserRegisterFacadeImpl implements UserRegisterFacade { } agreementsFacade.saveUserAgreements(newUser.getId(), AgreementType.PRIVACY_COLLECT); - // 11.13 - 회원 가입단계가 아닌 로그인 단계로 이메일 인증 이동 - // sendEmailActivation(newUser); return new ValidationResponse(true,"회원가입이 완료되었습니다."); } @@ -229,28 +218,6 @@ public class UserRegisterFacadeImpl implements UserRegisterFacade { return false; } - private void sendEmailActivation(PortalUser registeredUser) { - MessageRecipient recipient = new MessageRecipient(); - recipient.setUsername(registeredUser.getUserName()); - recipient.setUserId(registeredUser.getEmailAddr()); - recipient.setPhone(registeredUser.getMobileNumber()); - - // 25.10.01 - 이메일 링크 방식으로 접근 불가이기에 SMS 인증방식과 동일하게 대체 - HashMap params = new HashMap<>(); - String tokenValue = String.valueOf(authNumberGenerator.generateAuthNumber()); - // String tokenValue = registeredUser.getCreatedDate().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")) + ":" + registeredUser.getId(); - params.put("token", tokenValue); - messageHandlerService.publishEvent(MessageCode.USER_VERIFICATION_EMAIL, recipient, params); - -// try { -// String encToken = encryptionUtil.encrypt(tokenValue); -// params.put("token", Base64.encode(encToken.getBytes(StandardCharsets.UTF_8))); -// messageHandlerService.publishEvent(UserEmailActivationEvent.KEY, recipient, params); -// } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { -// throw new SystemException("암호화 모듈 오류"); -// } - } - @Override @Transactional public ValidationResponse processInvitation(String action, UserInvitation invitation) { diff --git a/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserAuthService.java b/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserAuthService.java index 571a028..40ec762 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserAuthService.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserAuthService.java @@ -16,23 +16,14 @@ import com.eactive.apim.portal.portaluser.entity.PortalUserEnums; import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.RoleCode; import com.eactive.apim.portal.portaluser.event.UserPasswordResetEvent; import com.eactive.apim.portal.portaluser.repository.PortalUserRepository; -import com.eactive.apim.portal.template.entity.MessageCode; import com.eactive.apim.portal.template.entity.MessageRequest; import com.eactive.apim.portal.template.repository.MessageRequestRepository; import com.eactive.apim.portal.template.service.MessageHandlerService; import com.eactive.apim.portal.template.service.MessageRecipient; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.List; import java.util.stream.Collectors; -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; import lombok.RequiredArgsConstructor; -import org.apache.xerces.impl.dv.util.Base64; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; @@ -54,7 +45,6 @@ public class PortalUserAuthService implements UserDetailsService { private final PasswordEncoder passwordEncoder; private final MessageHandlerService messageHandlerService; private final MessageRequestRepository messageRequestRepository; - private final EncryptionUtil encryptionUtil; private final LoginFinalizer loginFinalizer; private final PasswordService passwordService; diff --git a/src/main/java/com/eactive/apim/portal/common/util/EncryptionUtil.java b/src/main/java/com/eactive/apim/portal/common/util/EncryptionUtil.java index bc02ff7..85029a2 100644 --- a/src/main/java/com/eactive/apim/portal/common/util/EncryptionUtil.java +++ b/src/main/java/com/eactive/apim/portal/common/util/EncryptionUtil.java @@ -1,18 +1,5 @@ package com.eactive.apim.portal.common.util; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.spec.SecretKeySpec; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -@Component("encryptionUtil") public class EncryptionUtil { public static String generateNewPassword() { @@ -53,40 +40,4 @@ public class EncryptionUtil { return newpassword.toString(); } - @Value("${encryption.key:kjbank_portal_application_1357902}") - private String secretKey; // Should be 16, 24, or 32 bytes long for AES-128, AES-192, or AES-256 - - private static final String ALGORITHM = "AES"; - - private SecretKeySpec createSecretKey() { - byte[] key = secretKey.getBytes(StandardCharsets.UTF_8); - return new SecretKeySpec(key, ALGORITHM); - } - - public String encrypt(String value) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { - if (value == null || value.isEmpty()) { - return value; - } - - SecretKeySpec key = createSecretKey(); - Cipher cipher = Cipher.getInstance(ALGORITHM); - cipher.init(Cipher.ENCRYPT_MODE, key); - - byte[] encryptedBytes = cipher.doFinal(value.getBytes()); - return Base64.getEncoder().encodeToString(encryptedBytes); - } - - public String decrypt(String encrypted) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { - if (encrypted == null || encrypted.isEmpty()) { - return encrypted; - } - - SecretKeySpec key = createSecretKey(); - Cipher cipher = Cipher.getInstance(ALGORITHM); - cipher.init(Cipher.DECRYPT_MODE, key); - - byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encrypted)); - return new String(decryptedBytes); - } - } diff --git a/src/main/resources/static/js/moment.LICENSE b/src/main/resources/static/js/moment.LICENSE new file mode 100644 index 0000000..8618b73 --- /dev/null +++ b/src/main/resources/static/js/moment.LICENSE @@ -0,0 +1,22 @@ +Copyright (c) JS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/main/resources/static/js/moment.min.js b/src/main/resources/static/js/moment.min.js index 5787a40..8b80f20 100644 --- a/src/main/resources/static/js/moment.min.js +++ b/src/main/resources/static/js/moment.min.js @@ -1 +1,2 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.moment=t()}(this,function(){"use strict";var e,i;function c(){return e.apply(null,arguments)}function o(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function u(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function l(e){return void 0===e}function h(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function d(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function f(e,t){var n,s=[];for(n=0;n>>0,s=0;sSe(e)?(r=e+1,o-Se(e)):(r=e,o),{year:r,dayOfYear:a}}function Ie(e,t,n){var s,i,r=Ve(e.year(),t,n),a=Math.floor((e.dayOfYear()-r-1)/7)+1;return a<1?s=a+Ae(i=e.year()-1,t,n):a>Ae(e.year(),t,n)?(s=a-Ae(e.year(),t,n),i=e.year()+1):(i=e.year(),s=a),{week:s,year:i}}function Ae(e,t,n){var s=Ve(e,t,n),i=Ve(e+1,t,n);return(Se(e)-s+i)/7}I("w",["ww",2],"wo","week"),I("W",["WW",2],"Wo","isoWeek"),C("week","w"),C("isoWeek","W"),F("week",5),F("isoWeek",5),ue("w",B),ue("ww",B,z),ue("W",B),ue("WW",B,z),fe(["w","ww","W","WW"],function(e,t,n,s){t[s.substr(0,1)]=D(e)});function je(e,t){return e.slice(t,7).concat(e.slice(0,t))}I("d",0,"do","day"),I("dd",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),I("ddd",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),I("dddd",0,0,function(e){return this.localeData().weekdays(this,e)}),I("e",0,0,"weekday"),I("E",0,0,"isoWeekday"),C("day","d"),C("weekday","e"),C("isoWeekday","E"),F("day",11),F("weekday",11),F("isoWeekday",11),ue("d",B),ue("e",B),ue("E",B),ue("dd",function(e,t){return t.weekdaysMinRegex(e)}),ue("ddd",function(e,t){return t.weekdaysShortRegex(e)}),ue("dddd",function(e,t){return t.weekdaysRegex(e)}),fe(["dd","ddd","dddd"],function(e,t,n,s){var i=n._locale.weekdaysParse(e,s,n._strict);null!=i?t.d=i:g(n).invalidWeekday=e}),fe(["d","e","E"],function(e,t,n,s){t[s]=D(e)});var Ze="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_");var ze="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_");var $e="Su_Mo_Tu_We_Th_Fr_Sa".split("_");var qe=ae;var Je=ae;var Be=ae;function Qe(){function e(e,t){return t.length-e.length}var t,n,s,i,r,a=[],o=[],u=[],l=[];for(t=0;t<7;t++)n=y([2e3,1]).day(t),s=this.weekdaysMin(n,""),i=this.weekdaysShort(n,""),r=this.weekdays(n,""),a.push(s),o.push(i),u.push(r),l.push(s),l.push(i),l.push(r);for(a.sort(e),o.sort(e),u.sort(e),l.sort(e),t=0;t<7;t++)o[t]=he(o[t]),u[t]=he(u[t]),l[t]=he(l[t]);this._weekdaysRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+o.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+a.join("|")+")","i")}function Xe(){return this.hours()%12||12}function Ke(e,t){I(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)})}function et(e,t){return t._meridiemParse}I("H",["HH",2],0,"hour"),I("h",["hh",2],0,Xe),I("k",["kk",2],0,function(){return this.hours()||24}),I("hmm",0,0,function(){return""+Xe.apply(this)+L(this.minutes(),2)}),I("hmmss",0,0,function(){return""+Xe.apply(this)+L(this.minutes(),2)+L(this.seconds(),2)}),I("Hmm",0,0,function(){return""+this.hours()+L(this.minutes(),2)}),I("Hmmss",0,0,function(){return""+this.hours()+L(this.minutes(),2)+L(this.seconds(),2)}),Ke("a",!0),Ke("A",!1),C("hour","h"),F("hour",13),ue("a",et),ue("A",et),ue("H",B),ue("h",B),ue("k",B),ue("HH",B,z),ue("hh",B,z),ue("kk",B,z),ue("hmm",Q),ue("hmmss",X),ue("Hmm",Q),ue("Hmmss",X),ce(["H","HH"],ge),ce(["k","kk"],function(e,t,n){var s=D(e);t[ge]=24===s?0:s}),ce(["a","A"],function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e}),ce(["h","hh"],function(e,t,n){t[ge]=D(e),g(n).bigHour=!0}),ce("hmm",function(e,t,n){var s=e.length-2;t[ge]=D(e.substr(0,s)),t[ve]=D(e.substr(s)),g(n).bigHour=!0}),ce("hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[ge]=D(e.substr(0,s)),t[ve]=D(e.substr(s,2)),t[pe]=D(e.substr(i)),g(n).bigHour=!0}),ce("Hmm",function(e,t,n){var s=e.length-2;t[ge]=D(e.substr(0,s)),t[ve]=D(e.substr(s))}),ce("Hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[ge]=D(e.substr(0,s)),t[ve]=D(e.substr(s,2)),t[pe]=D(e.substr(i))});var tt,nt=Te("Hours",!0),st={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Ce,monthsShort:He,week:{dow:0,doy:6},weekdays:Ze,weekdaysMin:$e,weekdaysShort:ze,meridiemParse:/[ap]\.?m?\.?/i},it={},rt={};function at(e){return e?e.toLowerCase().replace("_","-"):e}function ot(e){var t=null;if(!it[e]&&"undefined"!=typeof module&&module&&module.exports)try{t=tt._abbr,require("./locale/"+e),ut(t)}catch(e){}return it[e]}function ut(e,t){var n;return e&&((n=l(t)?ht(e):lt(e,t))?tt=n:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+e+" not found. Did you forget to load it?")),tt._abbr}function lt(e,t){if(null===t)return delete it[e],null;var n,s=st;if(t.abbr=e,null!=it[e])T("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),s=it[e]._config;else if(null!=t.parentLocale)if(null!=it[t.parentLocale])s=it[t.parentLocale]._config;else{if(null==(n=ot(t.parentLocale)))return rt[t.parentLocale]||(rt[t.parentLocale]=[]),rt[t.parentLocale].push({name:e,config:t}),null;s=n._config}return it[e]=new P(x(s,t)),rt[e]&&rt[e].forEach(function(e){lt(e.name,e.config)}),ut(e),it[e]}function ht(e){var t;if(e&&e._locale&&e._locale._abbr&&(e=e._locale._abbr),!e)return tt;if(!o(e)){if(t=ot(e))return t;e=[e]}return function(e){for(var t,n,s,i,r=0;r=t&&a(i,n,!0)>=t-1)break;t--}r++}return tt}(e)}function dt(e){var t,n=e._a;return n&&-2===g(e).overflow&&(t=n[_e]<0||11Pe(n[me],n[_e])?ye:n[ge]<0||24Ae(n,r,a)?g(e)._overflowWeeks=!0:null!=u?g(e)._overflowWeekday=!0:(o=Ee(n,s,i,r,a),e._a[me]=o.year,e._dayOfYear=o.dayOfYear)}(e),null!=e._dayOfYear&&(r=ct(e._a[me],s[me]),(e._dayOfYear>Se(r)||0===e._dayOfYear)&&(g(e)._overflowDayOfYear=!0),n=Ge(r,0,e._dayOfYear),e._a[_e]=n.getUTCMonth(),e._a[ye]=n.getUTCDate()),t=0;t<3&&null==e._a[t];++t)e._a[t]=a[t]=s[t];for(;t<7;t++)e._a[t]=a[t]=null==e._a[t]?2===t?1:0:e._a[t];24===e._a[ge]&&0===e._a[ve]&&0===e._a[pe]&&0===e._a[we]&&(e._nextDay=!0,e._a[ge]=0),e._d=(e._useUTC?Ge:function(e,t,n,s,i,r,a){var o;return e<100&&0<=e?(o=new Date(e+400,t,n,s,i,r,a),isFinite(o.getFullYear())&&o.setFullYear(e)):o=new Date(e,t,n,s,i,r,a),o}).apply(null,a),i=e._useUTC?e._d.getUTCDay():e._d.getDay(),null!=e._tzm&&e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),e._nextDay&&(e._a[ge]=24),e._w&&void 0!==e._w.d&&e._w.d!==i&&(g(e).weekdayMismatch=!0)}}var mt=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,_t=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,yt=/Z|[+-]\d\d(?::?\d\d)?/,gt=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],vt=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],pt=/^\/?Date\((\-?\d+)/i;function wt(e){var t,n,s,i,r,a,o=e._i,u=mt.exec(o)||_t.exec(o);if(u){for(g(e).iso=!0,t=0,n=gt.length;tn.valueOf():n.valueOf()this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},mn.isLocal=function(){return!!this.isValid()&&!this._isUTC},mn.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},mn.isUtc=Et,mn.isUTC=Et,mn.zoneAbbr=function(){return this._isUTC?"UTC":""},mn.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},mn.dates=n("dates accessor is deprecated. Use date instead.",un),mn.months=n("months accessor is deprecated. Use month instead",Ue),mn.years=n("years accessor is deprecated. Use year instead",Oe),mn.zone=n("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(e,t){return null!=e?("string"!=typeof e&&(e=-e),this.utcOffset(e,t),this):-this.utcOffset()}),mn.isDSTShifted=n("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){if(!l(this._isDSTShifted))return this._isDSTShifted;var e={};if(w(e,this),(e=Ot(e))._a){var t=e._isUTC?y(e._a):bt(e._a);this._isDSTShifted=this.isValid()&&0>>0,s=0;sWe(e)?(r=e+1,t-We(e)):(r=e,t);return{year:r,dayOfYear:n}}function Be(e,t,n){var s,i,r=qe(e.year(),t,n),r=Math.floor((e.dayOfYear()-r-1)/7)+1;return r<1?s=r+N(i=e.year()-1,t,n):r>N(e.year(),t,n)?(s=r-N(e.year(),t,n),i=e.year()+1):(i=e.year(),s=r),{week:s,year:i}}function N(e,t,n){var s=qe(e,t,n),t=qe(e+1,t,n);return(We(e)-s+t)/7}s("w",["ww",2],"wo","week"),s("W",["WW",2],"Wo","isoWeek"),h("w",n,u),h("ww",n,t),h("W",n,u),h("WW",n,t),Oe(["w","ww","W","WW"],function(e,t,n,s){t[s.substr(0,1)]=M(e)});function Je(e,t){return e.slice(t,7).concat(e.slice(0,t))}s("d",0,"do","day"),s("dd",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),s("ddd",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),s("dddd",0,0,function(e){return this.localeData().weekdays(this,e)}),s("e",0,0,"weekday"),s("E",0,0,"isoWeekday"),h("d",n),h("e",n),h("E",n),h("dd",function(e,t){return t.weekdaysMinRegex(e)}),h("ddd",function(e,t){return t.weekdaysShortRegex(e)}),h("dddd",function(e,t){return t.weekdaysRegex(e)}),Oe(["dd","ddd","dddd"],function(e,t,n,s){s=n._locale.weekdaysParse(e,s,n._strict);null!=s?t.d=s:p(n).invalidWeekday=e}),Oe(["d","e","E"],function(e,t,n,s){t[s]=M(e)});var Qe="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),Xe="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),Ke="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),et=i,tt=i,nt=i;function st(){function e(e,t){return t.length-e.length}for(var t,n,s,i=[],r=[],a=[],o=[],u=0;u<7;u++)s=l([2e3,1]).day(u),t=f(this.weekdaysMin(s,"")),n=f(this.weekdaysShort(s,"")),s=f(this.weekdays(s,"")),i.push(t),r.push(n),a.push(s),o.push(t),o.push(n),o.push(s);i.sort(e),r.sort(e),a.sort(e),o.sort(e),this._weekdaysRegex=new RegExp("^("+o.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+a.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+r.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+i.join("|")+")","i")}function it(){return this.hours()%12||12}function rt(e,t){s(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)})}function at(e,t){return t._meridiemParse}s("H",["HH",2],0,"hour"),s("h",["hh",2],0,it),s("k",["kk",2],0,function(){return this.hours()||24}),s("hmm",0,0,function(){return""+it.apply(this)+r(this.minutes(),2)}),s("hmmss",0,0,function(){return""+it.apply(this)+r(this.minutes(),2)+r(this.seconds(),2)}),s("Hmm",0,0,function(){return""+this.hours()+r(this.minutes(),2)}),s("Hmmss",0,0,function(){return""+this.hours()+r(this.minutes(),2)+r(this.seconds(),2)}),rt("a",!0),rt("A",!1),h("a",at),h("A",at),h("H",n,d),h("h",n,u),h("k",n,u),h("HH",n,t),h("hh",n,t),h("kk",n,t),h("hmm",me),h("hmmss",_e),h("Hmm",me),h("Hmmss",_e),v(["H","HH"],O),v(["k","kk"],function(e,t,n){e=M(e);t[O]=24===e?0:e}),v(["a","A"],function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e}),v(["h","hh"],function(e,t,n){t[O]=M(e),p(n).bigHour=!0}),v("hmm",function(e,t,n){var s=e.length-2;t[O]=M(e.substr(0,s)),t[b]=M(e.substr(s)),p(n).bigHour=!0}),v("hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[O]=M(e.substr(0,s)),t[b]=M(e.substr(s,2)),t[T]=M(e.substr(i)),p(n).bigHour=!0}),v("Hmm",function(e,t,n){var s=e.length-2;t[O]=M(e.substr(0,s)),t[b]=M(e.substr(s))}),v("Hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[O]=M(e.substr(0,s)),t[b]=M(e.substr(s,2)),t[T]=M(e.substr(i))});i=Re("Hours",!0);var ot,ut={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",w:"a week",ww:"%d weeks",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Fe,monthsShort:Le,week:{dow:0,doy:6},weekdays:Qe,weekdaysMin:Ke,weekdaysShort:Xe,meridiemParse:/[ap]\.?m?\.?/i},W={},lt={};function dt(e){return e&&e.toLowerCase().replace("_","-")}function ht(e){for(var t,n,s,i,r=0;r=t&&function(e,t){for(var n=Math.min(e.length,t.length),s=0;s=t-1)break;t--}r++}return ot}function ct(t){var e,n;if(void 0===W[t]&&"undefined"!=typeof module&&module&&module.exports&&(n=t)&&n.match("^[^/\\\\]*$"))try{e=ot._abbr,require("./locale/"+t),ft(e)}catch(e){W[t]=null}return W[t]}function ft(e,t){return e&&((t=g(t)?P(e):mt(e,t))?ot=t:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+e+" not found. Did you forget to load it?")),ot._abbr}function mt(e,t){if(null===t)return delete W[e],null;var n,s=ut;if(t.abbr=e,null!=W[e])Q("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),s=W[e]._config;else if(null!=t.parentLocale)if(null!=W[t.parentLocale])s=W[t.parentLocale]._config;else{if(null==(n=ct(t.parentLocale)))return lt[t.parentLocale]||(lt[t.parentLocale]=[]),lt[t.parentLocale].push({name:e,config:t}),null;s=n._config}return W[e]=new K(X(s,t)),lt[e]&<[e].forEach(function(e){mt(e.name,e.config)}),ft(e),W[e]}function P(e){var t;if(!(e=e&&e._locale&&e._locale._abbr?e._locale._abbr:e))return ot;if(!y(e)){if(t=ct(e))return t;e=[e]}return ht(e)}function _t(e){var t=e._a;return t&&-2===p(e).overflow&&(t=t[Y]<0||11He(t[D],t[Y])?S:t[O]<0||24N(r,u,l)?p(s)._overflowWeeks=!0:null!=d?p(s)._overflowWeekday=!0:(h=$e(r,a,o,u,l),s._a[D]=h.year,s._dayOfYear=h.dayOfYear)),null!=e._dayOfYear&&(i=bt(e._a[D],n[D]),(e._dayOfYear>We(i)||0===e._dayOfYear)&&(p(e)._overflowDayOfYear=!0),d=ze(i,0,e._dayOfYear),e._a[Y]=d.getUTCMonth(),e._a[S]=d.getUTCDate()),t=0;t<3&&null==e._a[t];++t)e._a[t]=c[t]=n[t];for(;t<7;t++)e._a[t]=c[t]=null==e._a[t]?2===t?1:0:e._a[t];24===e._a[O]&&0===e._a[b]&&0===e._a[T]&&0===e._a[Te]&&(e._nextDay=!0,e._a[O]=0),e._d=(e._useUTC?ze:Ze).apply(null,c),r=e._useUTC?e._d.getUTCDay():e._d.getDay(),null!=e._tzm&&e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),e._nextDay&&(e._a[O]=24),e._w&&void 0!==e._w.d&&e._w.d!==r&&(p(e).weekdayMismatch=!0)}}function xt(e){if(e._f===_.ISO_8601)Yt(e);else if(e._f===_.RFC_2822)Ot(e);else{e._a=[],p(e).empty=!0;for(var t,n,s,i,r,a=""+e._i,o=a.length,u=0,l=ae(e._f,e._locale).match(te)||[],d=l.length,h=0;he.valueOf():e.valueOf()"}),u.toJSON=function(){return this.isValid()?this.toISOString():null},u.toString=function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},u.unix=function(){return Math.floor(this.valueOf()/1e3)},u.valueOf=function(){return this._d.valueOf()-6e4*(this._offset||0)},u.creationData=function(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},u.eraName=function(){for(var e,t=this.localeData().eras(),n=0,s=t.length;nthis.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},u.isLocal=function(){return!!this.isValid()&&!this._isUTC},u.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},u.isUtc=At,u.isUTC=At,u.zoneAbbr=function(){return this._isUTC?"UTC":""},u.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},u.dates=e("dates accessor is deprecated. Use date instead.",ge),u.months=e("months accessor is deprecated. Use month instead",Ie),u.years=e("years accessor is deprecated. Use year instead",Pe),u.zone=e("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(e,t){return null!=e?(this.utcOffset(e="string"!=typeof e?-e:e,t),this):-this.utcOffset()}),u.isDSTShifted=e("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){var e,t;return g(this._isDSTShifted)&&(q(e={},this),(e=Nt(e))._a?(t=(e._isUTC?l:R)(e._a),this._isDSTShifted=this.isValid()&&0 + + + + Portal date range compatibility + + + + + +
+
+ + + + +
+
+
+
    +
    + + + + + + + diff --git a/src/test/js/moment-compatibility.spec.js b/src/test/js/moment-compatibility.spec.js new file mode 100644 index 0000000..94d391a --- /dev/null +++ b/src/test/js/moment-compatibility.spec.js @@ -0,0 +1,161 @@ +const { test, expect } = require('@playwright/test'); +const { readFileSync } = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); + +const staticRoot = path.resolve(__dirname, '../../main/resources/static'); +const momentSource = readFileSync(path.join(staticRoot, 'js/moment.min.js'), 'utf8'); +const fixture = readFileSync(path.join(__dirname, 'fixtures/date-range.html'), 'utf8'); +const assets = new Map([ + ['/css/daterangepicker.css', 'text/css'], + ['/plugins/jquery/jquery-3.7.1.min.js', 'text/javascript'], + ['/js/moment.min.js', 'text/javascript'], + ['/js/daterangepicker.js', 'text/javascript'], + ['/js/front2.js', 'text/javascript'], + ['/img/icon/icon_close.png', 'image/png'], + ['/img/icon/icon_dp_arrow_prev.png', 'image/png'], + ['/img/icon/icon_dp_arrow_next.png', 'image/png'] +]); + +async function openCalendar(page, start = '2024.02.28', end = '2024.03.02') { + // 애플리케이션 서버/DB/외부 CDN 없이 저장소의 실제 정적 파일을 제공한다. + await page.route('**/*', async route => { + const pathname = new URL(route.request().url()).pathname; + if (pathname === '/date-range') { + return route.fulfill({ + contentType: 'text/html', + body: fixture.replace('value="2024.02.28"', `value="${start}"`) + .replace('value="2024.03.02"', `value="${end}"`) + }); + } + if (assets.has(pathname)) { + return route.fulfill({ + contentType: assets.get(pathname), + path: path.join(staticRoot, pathname.slice(1)) + }); + } + throw new Error(`Unexpected fixture request: ${route.request().url()}`); + }); + await page.goto('http://portal.test/date-range'); + await expect(page.locator('[name="daterange"]')).toHaveValue(`${start} - ${end}`); +} + +function day(page, side, number) { + return page.locator(`.drp-calendar.${side} td.available:not(.off)`) + .filter({ hasText: new RegExp(`^${number}$`) }); +} + +async function expectRange(page, start, end) { + await expect(page.locator('[name="daterange"]')).toHaveValue(`${start} - ${end}`); + await expect(page.locator('[name="startDate"]')).toHaveValue(start); + await expect(page.locator('[name="endDate"]')).toHaveValue(end); +} + +test.beforeEach(async ({ page }) => { + // 공통 스크립트를 포함해 실행 중 JS 오류를 숨기지 않는다. + page.on('pageerror', error => { throw error; }); +}); + +test('ships the patched release and rejects traversal before CommonJS require', () => { + const requestedModules = []; + const context = { + exports: {}, + module: { exports: {} }, + require(name) { + requestedModules.push(name); + throw new Error('Locale module is not installed in this isolated test'); + } + }; + vm.runInNewContext(momentSource, context); + const moment = context.module.exports; + expect(moment.version).toBe('2.30.1'); + // 정상 로케일은 로딩을 시도해야 한다: require 분기가 실제 실행됨을 확인한다. + moment.locale('fr'); + expect(requestedModules).toContain('./locale/fr'); + requestedModules.length = 0; + moment.locale('../../package'); + moment.locale('..\\..\\package'); + expect(requestedModules).toEqual([]); + expect(moment.locale()).toBe('en'); +}); + +test('initializes the real portal calendar with its date format and Korean labels', async ({ page }) => { + await openCalendar(page); + expect(await page.evaluate(() => window.moment.version)).toBe('2.30.1'); + await page.getByRole('button', { name: '달력 열기' }).click(); + await expect(page.locator('.daterangepicker')).toBeVisible(); + await expect(page.locator('.drp-calendar.left .month')).toHaveText('2024.02'); + await expect(page.locator('.drp-calendar.right .month')).toHaveText('2024.03'); + await expect(page.locator('.applyBtn')).toHaveText('확인'); + await expect(page.locator('.cancelBtn')).toHaveText('취소'); +}); + +test('selects leap day across months and updates the submitted fields', async ({ page }) => { + await openCalendar(page); + await page.locator('[name="daterange"]').click(); + await day(page, 'left', 29).click(); + await day(page, 'right', 3).click(); + await page.locator('.applyBtn').click(); + await expect(page.locator('.daterangepicker')).toBeHidden(); + await expectRange(page, '2024.02.29', '2024.03.03'); +}); + +test('navigates across year end and preserves the selected year', async ({ page }) => { + await openCalendar(page, '2024.11.15', '2024.11.20'); + await page.locator('[name="daterange"]').click(); + await page.locator('.drp-calendar.right .next').click(); + await expect(page.locator('.drp-calendar.left .month')).toHaveText('2024.12'); + await expect(page.locator('.drp-calendar.right .month')).toHaveText('2025.01'); + await day(page, 'left', 31).click(); + await day(page, 'right', 2).click(); + await page.locator('.applyBtn').click(); + await expectRange(page, '2024.12.31', '2025.01.02'); +}); + +test('cancels a changed selection and restores the original dates', async ({ page }) => { + await openCalendar(page); + await page.locator('[name="daterange"]').click(); + await day(page, 'left', 29).click(); + await day(page, 'right', 4).click(); + await page.locator('.cancelBtn').click(); + await expectRange(page, '2024.02.28', '2024.03.02'); + await page.locator('[name="daterange"]').click(); + await expect(page.locator('.drp-calendar.left td.start-date')).toHaveText('28'); + await expect(page.locator('.drp-calendar.right td.end-date')).toHaveText('2'); +}); + +test('accepts a typed range across daylight saving and updates hidden fields', async ({ page }) => { + await openCalendar(page); + const input = page.locator('[name="daterange"]'); + await input.fill('2024.03.09 - 2024.03.11'); + // daterangepicker는 keyup에서 입력을 파싱하고 hide에서 제품 콜백을 호출한다. + await input.press('ArrowRight'); + await input.press('Tab'); + await expectRange(page, '2024.03.09', '2024.03.11'); +}); + +test('supports same-day selection', async ({ page }) => { + await openCalendar(page); + await page.locator('[name="daterange"]').click(); + await day(page, 'left', 29).click(); + await day(page, 'left', 29).click(); + await page.locator('.applyBtn').click(); + await expectRange(page, '2024.02.29', '2024.02.29'); +}); + +test('validates leap dates and retains month-end arithmetic', async ({ page }) => { + await openCalendar(page); + const result = await page.evaluate(() => { + const moment = window.moment; + return { + leap: moment('2024.02.29', 'YYYY.MM.DD', true).isValid(), + invalid: moment('2023.02.29', 'YYYY.MM.DD', true).isValid(), + malformed: moment('not-a-date', 'YYYY.MM.DD', true).isValid(), + monthEnd: moment('2024.01.31', 'YYYY.MM.DD').add(1, 'month').format('YYYY.MM.DD'), + dayAfter: moment('2024.02.29', 'YYYY.MM.DD').add(1, 'day').format('YYYY.MM.DD') + }; + }); + expect(result).toEqual({ + leap: true, invalid: false, malformed: false, monthEnd: '2024.02.29', dayAfter: '2024.03.01' + }); +}); diff --git a/src/test/js/playwright.config.js b/src/test/js/playwright.config.js new file mode 100644 index 0000000..b4fc53b --- /dev/null +++ b/src/test/js/playwright.config.js @@ -0,0 +1,21 @@ +const { defineConfig } = require('@playwright/test'); + +module.exports = defineConfig({ + testDir: __dirname, + testMatch: 'moment-compatibility.spec.js', + outputDir: '../../../build/playwright', + fullyParallel: true, + workers: 2, + reporter: 'list', + use: { + browserName: 'chromium', + locale: 'ko-KR', + viewport: { width: 1280, height: 800 }, + trace: 'retain-on-failure', + screenshot: 'only-on-failure' + }, + projects: [ + { name: 'seoul', use: { timezoneId: 'Asia/Seoul' } }, + { name: 'new-york', use: { timezoneId: 'America/New_York' } } + ] +});