From ef277abe7008f1b8538b4d26c25be7647417a980 Mon Sep 17 00:00:00 2001 From: comicgum Date: Wed, 16 Sep 2026 13:35:29 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B0=9C=EB=B0=9C=ED=99=98=EA=B2=BD=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EC=84=A4=EB=AA=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- smartservice_backend/Dockerfile | 11 ++ smartservice_backend/docker-compose.yaml | 16 ++ .../java/com/handong/smartservice/_AG.java | 2 +- .../smartservice/component/ErrorCode.java | 6 + .../smartservice/component/SMSSender.java | 20 ++- .../smartservice/component/TempPassword.java | 35 ++++ .../controller/api/AccountController.java | 10 ++ .../controller/api/AuthController.java | 42 ++++- .../controller/api/BizGroupController.java | 5 +- .../controller/api/DeviceController.java | 22 ++- .../controller/api/DeviceErrorController.java | 19 +- .../controller/api/SmsTestController.java | 44 ----- .../smartservice/mapper/AccountMapper.java | 6 + .../mapper/DeviceErrorMapper.java | 4 + .../smartservice/mapper/DeviceMapper.java | 2 + .../mapper/TransactionMapper.java | 2 + .../smartservice/service/AccountService.java | 102 ++++++++++- .../smartservice/service/BizGroupService.java | 94 +++++++++- .../service/DeviceErrorService.java | 21 +++ .../smartservice/service/DeviceService.java | 14 ++ .../service/TransactionService.java | 5 + .../src/main/resources/application-prod.yaml | 8 + .../src/main/resources/application.yaml | 15 +- .../main/resources/mapper/AccountMapper.xml | 24 ++- .../resources/mapper/DeviceErrorMapper.xml | 32 ++++ .../main/resources/mapper/DeviceMapper.xml | 34 +++- .../resources/mapper/TransactionMapper.xml | 17 +- .../src/app/(admin)/account/Account.tsx | 12 +- .../(admin)/account/AddModifyAccountModal.tsx | 17 +- .../biz-group/AddModifyBizGroupModal.tsx | 41 ++++- .../app/(admin)/biz-group/BizGroupContent.tsx | 4 +- .../biz-group2/AddModifyBizGroupModal.tsx | 40 ++++- .../src/app/(admin)/biz-group2/BizGroup.tsx | 9 +- .../AddModifyCustomerAccountModal.tsx | 14 ++ .../customer-account/CustomerAccount.tsx | 3 +- .../app/(admin)/device-error/DeviceError.tsx | 4 + .../(admin)/device/AddModifyDeviceModal.tsx | 24 +++ .../src/app/(admin)/device/Device.tsx | 57 +++++- .../(admin)/notice/AddModifyNoticeModal.tsx | 2 +- .../src/app/(admin)/terms/Terms.tsx | 56 +----- .../app/(admin)/transactions/ReceiptModal.tsx | 51 ++++-- .../app/(admin)/transactions/Transactions.tsx | 50 +++--- .../(auth)/change-info/page.tsx | 11 ++ .../src/components/ResetPasswordModal.tsx | 65 +++++++ .../src/components/TermsContent.tsx | 64 +++++++ .../src/components/auth/ChangeInfoForm.tsx | 167 ++++++++++++++++++ .../src/components/auth/SignInForm.tsx | 37 +--- smartservice_frontend/src/lib/_AG.ts | 16 ++ 개발환경 설정 및 실행방법.txt | 56 ++++++ 49 files changed, 1176 insertions(+), 236 deletions(-) create mode 100644 smartservice_backend/Dockerfile create mode 100644 smartservice_backend/docker-compose.yaml create mode 100644 smartservice_backend/src/main/java/com/handong/smartservice/component/TempPassword.java delete mode 100644 smartservice_backend/src/main/java/com/handong/smartservice/controller/api/SmsTestController.java create mode 100644 smartservice_backend/src/main/resources/application-prod.yaml create mode 100644 smartservice_frontend/src/app/(full-width-pages)/(auth)/change-info/page.tsx create mode 100644 smartservice_frontend/src/components/ResetPasswordModal.tsx create mode 100644 smartservice_frontend/src/components/TermsContent.tsx create mode 100644 smartservice_frontend/src/components/auth/ChangeInfoForm.tsx create mode 100644 개발환경 설정 및 실행방법.txt diff --git a/smartservice_backend/Dockerfile b/smartservice_backend/Dockerfile new file mode 100644 index 0000000..5b92a23 --- /dev/null +++ b/smartservice_backend/Dockerfile @@ -0,0 +1,11 @@ +# docker build -t hd1-was:1.0.8 . +# docker tag hd1-was:1.0.8 192.168.219.191:19101/hd1-was:1.0.8 +# docker push 192.168.219.191:19101/hd1-was:1.0.8 +# or +# .\_docker_push.bat 1.0.8 + +FROM eclipse-temurin:21-jdk +WORKDIR /app +COPY ./build/libs/*.jar app.jar +EXPOSE 18080 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/smartservice_backend/docker-compose.yaml b/smartservice_backend/docker-compose.yaml new file mode 100644 index 0000000..d02add2 --- /dev/null +++ b/smartservice_backend/docker-compose.yaml @@ -0,0 +1,16 @@ +# docker pull 49.165.181.28:19101/hd1-was:1.0.8 +docker stop hd1-was +docker rm hd1-was +docker-compose up -d +# or +# _docker_pull_restart.sh 1.0.8 + +services: + hd1: + image: 49.165.181.28:19101/hd1-was:1.0.8 + container_name: hd1-was + ports: + - "28080:18080" + environment: + - TZ=Asia/Seoul + restart: unless-stopped diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/_AG.java b/smartservice_backend/src/main/java/com/handong/smartservice/_AG.java index 21c46d5..cd3009e 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/_AG.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/_AG.java @@ -195,7 +195,7 @@ public class _AG { uri.compareTo("/api/terminal-state.do") == 0 || uri.compareTo("/api/sign-in.do") == 0 || uri.compareTo("/api/sval2.do") == 0 || - uri.compareTo("/api/send-sms-test.do") == 0 || //TempCode: SMS integration test, remove when done + uri.compareTo("/api/change-init-account.do") == 0 || // checks the temporary ID and password itself uri.compareTo("/api/add-device-error.do") == 0 || uri.compareTo("/api/add-transaction.do") == 0) { return true; diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/component/ErrorCode.java b/smartservice_backend/src/main/java/com/handong/smartservice/component/ErrorCode.java index dc003ec..12e6f26 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/component/ErrorCode.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/component/ErrorCode.java @@ -5,5 +5,11 @@ public class ErrorCode extends CtErrorCode { static public final int INVALID_REFRESH = -16; static public final int INVALID_BIZ_REG_NUM = -17; + + static public final int NEED_ACCOUNT_CHANGE = -18; // signed in with a temporary password, the ID and password must be changed first + static public final int INVALID_MOBILE = -19; + static public final int SMS_SEND_FAILED = -20; + static public final int ACCOUNT_ALREADY_EXIST = -21; + static public final int INVALID_NEW_ACCOUNT = -22; // the new ID or password breaks the rules //... } diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/component/SMSSender.java b/smartservice_backend/src/main/java/com/handong/smartservice/component/SMSSender.java index 4b93754..47bdd52 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/component/SMSSender.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/component/SMSSender.java @@ -10,6 +10,8 @@ import java.time.Duration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; /** * Munja114 (www.munja114.co.kr) SMS/LMS URL integration. @@ -20,12 +22,18 @@ import org.slf4j.LoggerFactory; * * The response body is a pipe separated string: code|msg|nums|cols|etc1|etc2 */ +@Component public class SMSSender { private static final Logger logger = LoggerFactory.getLogger(SMSSender.class); - // Munja114 account, issued and authorized through the Munja114 customer center - private static final String REMOTE_ID = "handong3883"; - private static final String REMOTE_PASS = "gksehd3885@"; + // Munja114 account, issued and authorized through the Munja114 customer center (sms.remote-id / sms.remote-pass) + private final String remoteId; + private final String remotePass; + + public SMSSender(@Value("${sms.remote-id}") String remoteId, @Value("${sms.remote-pass}") String remotePass) { + this.remoteId = remoteId; + this.remotePass = remotePass; + } private static final String URL_SMS = "https://www.munja114.co.kr/Remote/RemoteSms.html"; private static final String URL_LMS = "https://www.munja114.co.kr/Remote/RemoteMms.html"; @@ -101,7 +109,7 @@ public class SMSSender { * @param phone receiver number, comma separated for multiple receivers * @param msg message body, sent as SMS when it fits in 90 EUC-KR bytes, otherwise as LMS */ - public static Result send(String callback, String phone, String msg) { + public Result send(String callback, String phone, String msg) { if (!hasText(callback) || !hasText(phone) || !hasText(msg)) { logger.warn("send: missing parameter, callback={}, phone={}", callback, phone); return new Result(CODE_INVALID_PARAMETER, "invalid parameter", 0, 0, ""); @@ -120,8 +128,8 @@ public class SMSSender { } StringBuilder body = new StringBuilder(); - appendParam(body, "remote_id", REMOTE_ID); - appendParam(body, "remote_pass", REMOTE_PASS); + appendParam(body, "remote_id", remoteId); + appendParam(body, "remote_pass", remotePass); appendParam(body, "remote_num", String.valueOf(receiverCount)); appendParam(body, "remote_reserve", "0"); appendParam(body, "remote_phone", phoneList); diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/component/TempPassword.java b/smartservice_backend/src/main/java/com/handong/smartservice/component/TempPassword.java new file mode 100644 index 0000000..a248d78 --- /dev/null +++ b/smartservice_backend/src/main/java/com/handong/smartservice/component/TempPassword.java @@ -0,0 +1,35 @@ +package com.handong.smartservice.component; + +import java.security.SecureRandom; + +/** Temporary password of an issued or reset account, short enough to be typed from a text message. */ +public class TempPassword { + + // Look-alike characters (0, O, 1, l, I) are left out + private static final String LETTERS = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"; + private static final String DIGITS = "23456789"; + private static final int LENGTH = 8; + + private static final SecureRandom secureRandom = new SecureRandom(); + + /** Letters and digits only, always with at least one of each. */ + public static String create() { + String all = LETTERS + DIGITS; + + char[] pw = new char[LENGTH]; + pw[0] = LETTERS.charAt(secureRandom.nextInt(LETTERS.length())); + pw[1] = DIGITS.charAt(secureRandom.nextInt(DIGITS.length())); + for (int i = 2; i < LENGTH; i++) + pw[i] = all.charAt(secureRandom.nextInt(all.length())); + + // Shuffle so the letter and the digit are not always in front + for (int i = LENGTH - 1; i > 0; i--) { + int j = secureRandom.nextInt(i + 1); + char tmp = pw[i]; + pw[i] = pw[j]; + pw[j] = tmp; + } + + return new String(pw); + } +} diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/AccountController.java b/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/AccountController.java index 2c1d6ec..6e23d09 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/AccountController.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/AccountController.java @@ -36,6 +36,16 @@ class AccountController { this.accountService = accountService; } + @RequestMapping(value = "/reset-account-password.do", method = RequestMethod.POST) + public CtResponse resetAccountPassword(HttpServletRequest request, HttpServletResponse response, @RequestBody Map body) { + logger.info("req: " + request.getRequestURI()); + + Long gid = _AG.toLong(body.get("gid")); + String phone = body.get("phone"); + + return accountService.resetPassword(gid, phone); + } + @RequestMapping(value = "/get-account.do", method = RequestMethod.POST) public CtResponse getAccountList(HttpServletRequest request, HttpServletResponse response, @RequestBody Map body) { logger.info("req: " + request.getRequestURI()); diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/AuthController.java b/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/AuthController.java index 87343ca..a8bfd49 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/AuthController.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/AuthController.java @@ -18,6 +18,7 @@ import com.handong.smartservice.component.CtResponse; import com.handong.smartservice.component.ErrorCode; import com.handong.smartservice.component.UserInfo; import com.handong.smartservice.service.AccountService; +import com.handong.smartservice.service.BizGroupService; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -48,7 +49,7 @@ class AuthController { public CtResponse signIn(HttpServletRequest request, HttpServletResponse response, @RequestBody Map body) { logger.info("req: " + request.getRequestURI()); - String user_id = body.get("id"); + String user_id = normalizeUserId(body.get("id")); String user_pw = body.get("pw"); logger.info("user_id = " + user_id); //TestComment @@ -83,6 +84,16 @@ class AuthController { return result; } + // An issued account signs in with the temporary password only to change it: no token is given + if (((Number)mapAccount.get("state")).longValue() == BizGroupService.ACCOUNT_STATE_INIT) { + CtResponse needChange = new CtResponse(); + needChange.setErrCode(ErrorCode.NEED_ACCOUNT_CHANGE); + return needChange; + } + + // The account row goes back to the client, so the password hash must not travel with it + mapAccount.remove("user_pw"); + //mapAccount.put("sval1", jwtProvider.createAccessToken(gid, user_id, name, permission, biz_group_id)); mapAccount.put("sval1", jwtProvider.createAccessToken(gid, _AG.createUserInfoMap(user_id, name, permission, biz_group_id))); String refreshToken = jwtProvider.createRefreshToken(gid); @@ -107,6 +118,28 @@ class AuthController { return result; } + @RequestMapping(value = "/change-init-account.do", method = RequestMethod.POST) + public CtResponse changeInitAccount(HttpServletRequest request, HttpServletResponse response, @RequestBody Map body) { + logger.info("req: " + request.getRequestURI()); + + String user_id = normalizeUserId(body.get("id")); + String user_pw = body.get("pw"); + String new_id = body.get("new_id") == null ? null : body.get("new_id").trim(); + String new_pw = body.get("new_pw"); + boolean agree = _AG.toBoolean(body.get("agree")); + + return accountService.changeInitAccount(user_id, user_pw, new_id, new_pw, agree); + } + + // An issued ID is the business number, which people often type with dashes + private String normalizeUserId(String user_id) { + if (user_id == null) + return null; + + user_id = user_id.trim(); + return user_id.matches("^[0-9-]+$") ? user_id.replaceAll("-", "") : user_id; + } + @RequestMapping(value = "/sign-out.do", method = RequestMethod.POST) public CtResponse signOut(HttpServletRequest request, HttpServletResponse response, @RequestBody Map body) { @@ -155,6 +188,12 @@ class AuthController { return ResponseEntity.ok(result); } + // A session must not outlive an account that was put back to the temporary state + if (((Number)mapAccount.get("state")).longValue() == BizGroupService.ACCOUNT_STATE_INIT) { + result.setErrCode(ErrorCode.NEED_ACCOUNT_CHANGE); + return ResponseEntity.ok(result); + } + String user_id = (String)mapAccount.get("user_id"); String name = (String)mapAccount.get("user_id"); Long permission = (Long)mapAccount.get("permission"); @@ -162,6 +201,7 @@ class AuthController { //String newAccessToken = jwtProvider.createAccessToken(gid, user_id, name, permission, biz_group_id); String newAccessToken = jwtProvider.createAccessToken(gid, _AG.createUserInfoMap(user_id, name, permission, biz_group_id)); + mapAccount.remove("user_pw"); mapAccount.put("sval1", newAccessToken); return ResponseEntity.ok(result); diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/BizGroupController.java b/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/BizGroupController.java index 4183d26..6a52d85 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/BizGroupController.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/BizGroupController.java @@ -103,7 +103,10 @@ class BizGroupController { return result; } - result = bizGroupService.addOrModifyBizGroup(isModify, biz_group_id, pid, top_group_id, name, biz_reg_num, email, phone, ceo, address); + // Only a new group can issue an account; the modal checks it by default + boolean issue_account = _AG.toBoolean(body.get("issue_account")); + + result = bizGroupService.addOrModifyBizGroup(isModify, biz_group_id, pid, top_group_id, name, biz_reg_num, email, phone, ceo, address, issue_account); return result; } diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/DeviceController.java b/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/DeviceController.java index 8b8cc78..f365a78 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/DeviceController.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/DeviceController.java @@ -62,12 +62,10 @@ class DeviceController { for (Map item : list) { item.replace("reg_time", _AG.timestampToStr(item.get("reg_time"))); - item.replace("connect_time", _AG.timestampToStr(item.get("connect_time"))); - item.replace("disconnect_time", _AG.timestampToStr(item.get("disconnect_time"))); } - String[] arrHeader = {"등록일", "그룹", "장치명", "단말기TID", "가동시간", "장애발생시간", "관리자"}; - String[] arrColumn = {"reg_time", "biz_group_name", "name", "uid1", "connect_time", "disconnect_time", "manager_name"}; + String[] arrHeader = {"등록일", "그룹", "장치명", "단말기TID", "관리자"}; + String[] arrColumn = {"reg_time", "biz_group_name", "name", "uid1", "manager_name"}; CtExcelMaker.makeExcelResponse(request, response, "무인기기목록", arrHeader, arrColumn, list, null); return null; @@ -128,13 +126,19 @@ class DeviceController { public CtResponse configDevice(HttpServletRequest request, HttpServletResponse response, @RequestBody Map body) { logger.info("req: " + request.getRequestURI()); - CtResponse result = new CtResponse(); + Long device_id = _AG.toLong(body.get("device_id")); + + // Return the service response as is, so its errCode reaches the client + return deviceService.configDevice(device_id); + } + + @RequestMapping(value = "/clear-device-empty.do", method = RequestMethod.POST) + public CtResponse clearDeviceEmpty(HttpServletRequest request, HttpServletResponse response, @RequestBody Map body) { + logger.info("req: " + request.getRequestURI()); Long device_id = _AG.toLong(body.get("device_id")); - - Map mapResult = deviceService.configDevice(device_id); - result.put("result", mapResult); - return result; + + return deviceService.clearDeviceEmpty(device_id); } @RequestMapping(value = "/error-device.do", method = RequestMethod.POST) diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/DeviceErrorController.java b/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/DeviceErrorController.java index 6ba383a..1d94862 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/DeviceErrorController.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/DeviceErrorController.java @@ -80,12 +80,27 @@ class DeviceErrorController { CtResponse result = new CtResponse(); Long device_error_id = _AG.toLong(body.get("device_error_id")); - if (device_error_id == 0) { + Long device_id = _AG.toLong(body.get("device_id")); + + // Either one record, or (from the device list) the latest sold out record of a device + Map mapResult; + if (device_error_id != 0) { + mapResult = deviceErrorService.getDeviceErrorDetail(device_error_id); + } + else if (device_id != 0) { + mapResult = deviceErrorService.getLatestSoldOutDetail(device_id); + } + else { result.setErrCode(ErrorCode.INVALID_PARAMETER); return result; } - result.put("result", deviceErrorService.getDeviceErrorDetail(device_error_id)); + if (mapResult == null) { + result.setErrCode(ErrorCode.PERMISSION_DENIED); + return result; + } + + result.put("result", mapResult); return result; } diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/SmsTestController.java b/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/SmsTestController.java deleted file mode 100644 index a3c3284..0000000 --- a/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/SmsTestController.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.handong.smartservice.controller.api; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.web.bind.annotation.*; - -import com.handong.smartservice.component.CtResponse; -import com.handong.smartservice.component.SMSSender; - -import jakarta.servlet.http.HttpServletRequest; - -/** - * TEMPORARY: sends one fixed test message so the Munja114 integration can be verified - * from the sign in screen. The callback, the receiver and the body are fixed here on - * purpose - the endpoint is reachable without a token, so it must not be able to send - * an arbitrary message to an arbitrary number. Delete this class once the test is done. - */ -@RestController -@RequestMapping("/api") -class SmsTestController { - private final Logger logger = LoggerFactory.getLogger(this.getClass()); - - private static final String TEST_CALLBACK = "0314263883"; - private static final String TEST_PHONE = "01040334076"; - private static final String TEST_MESSAGE = "안녕하세요. 테스트입니다."; - - @RequestMapping(value = "/send-sms-test.do", method = RequestMethod.POST) - public CtResponse sendSmsTest(HttpServletRequest request) { - logger.info("req: " + request.getRequestURI()); - - SMSSender.Result sendResult = SMSSender.send(TEST_CALLBACK, TEST_PHONE, TEST_MESSAGE); - logger.info("send-sms-test: " + sendResult); - - CtResponse result = new CtResponse(); - result.put("code", sendResult.code); - result.put("msg", sendResult.msg); - result.put("sentCount", sendResult.sentCount); - result.put("remainCount", sendResult.remainCount); - result.put("callback", TEST_CALLBACK); - result.put("phone", TEST_PHONE); - return result; - } -} diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/mapper/AccountMapper.java b/smartservice_backend/src/main/java/com/handong/smartservice/mapper/AccountMapper.java index 8aaa84e..85f4523 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/mapper/AccountMapper.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/mapper/AccountMapper.java @@ -20,4 +20,10 @@ public interface AccountMapper { int updateAccount(Long gid, String user_pw, String name, String phone, String email, Long state, Long biz_group_id, Long permission); int changeState(List ids, int state); + + int countActiveUserId(String user_id, Long exclude_gid); + + int updateInitAccount(Long gid, String user_id, String user_pw); + + int updateResetPassword(Long gid, String user_pw); } diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/mapper/DeviceErrorMapper.java b/smartservice_backend/src/main/java/com/handong/smartservice/mapper/DeviceErrorMapper.java index a123356..e782774 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/mapper/DeviceErrorMapper.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/mapper/DeviceErrorMapper.java @@ -14,5 +14,9 @@ public interface DeviceErrorMapper { Map selectDeviceErrorById(Long device_error_id); + Long selectLatestSoldOutErrorId(Long device_id); + + int selectGroupInTree(Long root_group_id, Long biz_group_id); + int insertDeviceError(Map params); } diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/mapper/DeviceMapper.java b/smartservice_backend/src/main/java/com/handong/smartservice/mapper/DeviceMapper.java index 38d704a..9c9c4de 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/mapper/DeviceMapper.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/mapper/DeviceMapper.java @@ -24,6 +24,8 @@ public interface DeviceMapper { int updateDevice(Long device_id, Long terminal_id, String name, String uid1, String manager_name, Long gid); int updateDeviceTerminalTime(Long device_id, LocalDateTime connect_time, LocalDateTime disconnect_time); + + int updateDeviceEmptyClear(Long device_id); int changeStateDevice(List ids, int state); diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/mapper/TransactionMapper.java b/smartservice_backend/src/main/java/com/handong/smartservice/mapper/TransactionMapper.java index 4c2542b..5340d2f 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/mapper/TransactionMapper.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/mapper/TransactionMapper.java @@ -24,5 +24,7 @@ public interface TransactionMapper { Map selectTransactionSum(Boolean is_group_access, Long biz_group_id, String date_start, String date_end); + Long selectOrgTransactionId(String uid1, String approval); + int insertTransaction(Map params); } diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/service/AccountService.java b/smartservice_backend/src/main/java/com/handong/smartservice/service/AccountService.java index dc1f24e..0a7a8dc 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/service/AccountService.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/service/AccountService.java @@ -14,8 +14,11 @@ import com.handong.smartservice.component.CtResponse; import com.handong.smartservice.component.DbLogger; import com.handong.smartservice.component.ErrorCode; import com.handong.smartservice.component.Permission; +import com.handong.smartservice.component.SMSSender; +import com.handong.smartservice.component.TempPassword; import com.handong.smartservice.component.UserInfo; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.password.PasswordEncoder; @@ -30,10 +33,15 @@ public class AccountService { private final AccountMapper accountMapper; private final PasswordEncoder passwordEncoder; + private final SMSSender smsSender; - public AccountService(AccountMapper accountMapper, PasswordEncoder passwordEncoder) { + @Value("${sms.callback}") String smsCallback; + @Value("${account.site-url}") String siteUrl; + + public AccountService(AccountMapper accountMapper, PasswordEncoder passwordEncoder, SMSSender smsSender) { this.accountMapper = accountMapper; this.passwordEncoder = passwordEncoder; + this.smsSender = smsSender; } public CtResponse getAccount(Long gid, String user_id) { @@ -139,6 +147,98 @@ public class AccountService { return result; } + /** + * Replaces the temporary ID and password of an issued account (state 6) with the user's own. + * The temporary credentials are checked again here, because the caller has no token yet. + */ + public CtResponse changeInitAccount(String user_id, String user_pw, String new_id, String new_pw, boolean agree) { + CtResponse result = new CtResponse(); + + // Without the terms of service agreement the account stays initialized, so the site cannot be used + if (agree == false) { + result.setErrCode(ErrorCode.INVALID_PARAMETER); + return result; + } + + CtResponse resAccount = getAccount(0L, user_id); + Map mapAccount = (Map)resAccount.get("result"); + + if (mapAccount == null + || passwordEncoder.matches(user_pw, (String)mapAccount.get("user_pw")) == false + || ((Number)mapAccount.get("state")).longValue() != BizGroupService.ACCOUNT_STATE_INIT) { + result.setErrCode(ErrorCode.INVALID_USER); + return result; + } + + // ID: 4 to 50 letters, digits or . _ -, password: 8 to 50 characters with both a letter and a digit + if (new_id == null || new_id.matches("^[A-Za-z0-9._-]{4,50}$") == false + || new_pw == null || new_pw.length() < 8 || new_pw.length() > 50 + || new_pw.matches(".*[A-Za-z].*") == false || new_pw.matches(".*[0-9].*") == false + || passwordEncoder.matches(new_pw, (String)mapAccount.get("user_pw"))) { + result.setErrCode(ErrorCode.INVALID_NEW_ACCOUNT); + return result; + } + + Long gid = ((Number)mapAccount.get("gid")).longValue(); + if (accountMapper.countActiveUserId(new_id, gid) > 0) { + result.setErrCode(ErrorCode.ACCOUNT_ALREADY_EXIST); + return result; + } + + if (accountMapper.updateInitAccount(gid, new_id, passwordEncoder.encode(new_pw)) == 0) { + result.setErrCode(ErrorCode.QUERY_ERROR); + return result; + } + DbLogger.insert(2L, "임시계정 정보변경(이용약관 동의), id: " + user_id + " -> " + new_id, gid); + + return result; + } + + /** + * Replaces the password with a random one and texts it to the given mobile number. + * The account keeps its state, so the new password stays valid until it is changed again. + */ + public CtResponse resetPassword(Long gid, String phone) { + CtResponse result = new CtResponse(); + + String mobile = phone == null ? "" : phone.replaceAll("[^0-9]", ""); + if (gid == 0 || mobile.matches("^01[016789][0-9]{7,8}$") == false) { + result.setErrCode(ErrorCode.INVALID_MOBILE); + return result; + } + + CtResponse resAccount = getAccount(gid, null); + Map mapAccount = (Map)resAccount.get("result"); + if (mapAccount == null) { + result.setErrCode(ErrorCode.INVALID_USER); + return result; + } + + String newPw = TempPassword.create(); + if (accountMapper.updateResetPassword(gid, passwordEncoder.encode(newPw)) == 0) { + result.setErrCode(ErrorCode.QUERY_ERROR); + return result; + } + + String msg = "[스마트서비스] 비밀번호가 초기화되었습니다.\n" + + "사이트: " + siteUrl + "\n" + + "ID: " + mapAccount.get("user_id") + "\n" + + "비밀번호: " + newPw + "\n" + + "로그인 후 비밀번호를 변경해 주세요."; + + SMSSender.Result sendResult = smsSender.send(smsCallback, mobile, msg); + if (sendResult.isSuccess() == false) { + result.setErrCode(ErrorCode.SMS_SEND_FAILED); + result.put("sms_code", sendResult.code); + return result; + } + + UserInfo userInfo = UserInfo.getCurr(); + DbLogger.insert(2L, "비밀번호 초기화, id: " + mapAccount.get("user_id"), userInfo.getGid()); + + return result; + } + public CtResponse removeAccount(String ids) { CtResponse result = new CtResponse(); diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/service/BizGroupService.java b/smartservice_backend/src/main/java/com/handong/smartservice/service/BizGroupService.java index 87b153e..0d7563f 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/service/BizGroupService.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/service/BizGroupService.java @@ -13,8 +13,11 @@ import org.apache.ibatis.jdbc.SQL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.interceptor.TransactionAspectSupport; import com.handong.smartservice._AG; import com.handong.smartservice.component.CtResponse; @@ -22,7 +25,10 @@ import com.handong.smartservice.component.DbLogger; import com.handong.smartservice.component.ErrorCode; import com.handong.smartservice.component.Permission; import com.handong.smartservice.component.RcTreeBizGroup; +import com.handong.smartservice.component.SMSSender; +import com.handong.smartservice.component.TempPassword; import com.handong.smartservice.component.UserInfo; +import com.handong.smartservice.mapper.AccountMapper; import com.handong.smartservice.mapper.BizGroupMapper; import com.handong.smartservice.mapper.OperatingHistoryMapper; @@ -31,7 +37,16 @@ import com.handong.smartservice.mapper.OperatingHistoryMapper; public class BizGroupService { private final Logger logger = LoggerFactory.getLogger(this.getClass()); + // Account state of an issued account that still carries the temporary ID and password + public static final long ACCOUNT_STATE_INIT = 6L; + @Autowired BizGroupMapper bizGroupMapper; + @Autowired AccountMapper accountMapper; + @Autowired PasswordEncoder passwordEncoder; + @Autowired SMSSender smsSender; + + @Value("${sms.callback}") String smsCallback; + @Value("${account.site-url}") String siteUrl; public Map getBizGroupList(Long offset, Long limit, String date_start, String date_end, @@ -66,9 +81,29 @@ public class BizGroupService { return mapResult; } - public CtResponse addOrModifyBizGroup(boolean isModify, Long biz_group_id, Long pid, Long top_group_id, String name, String biz_reg_num, String email, String phone, String ceo, String address) { + @Transactional + public CtResponse addOrModifyBizGroup(boolean isModify, Long biz_group_id, Long pid, Long top_group_id, String name, String biz_reg_num, String email, String phone, String ceo, String address, + boolean issue_account) { CtResponse result = new CtResponse(); + // Everything the issued account needs is checked before the group is written + boolean isIssue = !isModify && issue_account; + String mobile = phone == null ? "" : phone.replaceAll("[^0-9]", ""); + if (isIssue) { + if (biz_reg_num == null || biz_reg_num.isEmpty()) { + result.setErrCode(ErrorCode.INVALID_BIZ_REG_NUM); + return result; + } + if (mobile.matches("^01[016789][0-9]{7,8}$") == false) { + result.setErrCode(ErrorCode.INVALID_MOBILE); + return result; + } + if (accountMapper.countActiveUserId(biz_reg_num, 0L) > 0) { + result.setErrCode(ErrorCode.ACCOUNT_ALREADY_EXIST); + return result; + } + } + //if (bizGroupMapper.selectExistBizGroup(biz_group_id, biz_reg_num) != null) { // result.setErrCode(ErrorCode.ALREADY_EXIST); // return result; @@ -96,16 +131,65 @@ public class BizGroupService { if (bizGroupMapper.insertBizGroup(params) == 0) { result.setErrCode(ErrorCode.INVALID_PARAMETER); + return result; } - else { - DbLogger.insert(2L, "사업자그룹 추가, 사업자명: " + name + ", 사업자번호: " + biz_reg_num, 2L); - result.put("result", Map.of("biz_group_id", params.get("biz_group_id"))); + + if (isIssue) { + Long new_biz_group_id = ((Number)params.get("biz_group_id")).longValue(); + + // A failed message rolls the group and the account back, so the registration can simply be retried + String smsCode = issueAccount(new_biz_group_id, name, biz_reg_num, mobile, ceo); + if (smsCode != null) { + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + result.setErrCode(ErrorCode.SMS_SEND_FAILED); + result.put("sms_code", smsCode); + return result; + } } + + DbLogger.insert(2L, "사업자그룹 추가, 사업자명: " + name + ", 사업자번호: " + biz_reg_num + (isIssue ? ", 계정발급" : ""), 2L); + result.put("result", Map.of("biz_group_id", params.get("biz_group_id"))); } - + return result; } + /** + * Creates the account of a new business group with a temporary password and texts the + * sign in information to the group's mobile number. + * + * @return null when the message was sent, otherwise the gateway result code + */ + private String issueAccount(Long biz_group_id, String name, String biz_reg_num, String mobile, String ceo) { + String tempPw = TempPassword.create(); + + Permission permission = new Permission(); + permission.setGroup(true); + permission.setUid1(true); + permission.setCancel(true); + + Map params = new HashMap<>(); + params.put("user_id", biz_reg_num); + params.put("user_pw", passwordEncoder.encode(tempPw)); + params.put("name", ceo != null && ceo.isEmpty() == false ? ceo : name); + params.put("phone", mobile); + params.put("email", null); + params.put("state", ACCOUNT_STATE_INIT); + params.put("biz_group_id", biz_group_id); + params.put("permission", permission.get()); + accountMapper.insertAccount(params); + + String msg = "[스마트서비스] 계정이 발급되었습니다.\n" + + "사이트: " + siteUrl + "\n" + + "ID: " + biz_reg_num + "\n" + + "임시비밀번호: " + tempPw + "\n" + + "로그인 후 아이디와 비밀번호를 변경해 주세요."; + + SMSSender.Result sendResult = smsSender.send(smsCallback, mobile, msg); + return sendResult.isSuccess() ? null : sendResult.code; + } + + @Transactional public CtResponse removeBizGroup(String ids) { CtResponse result = new CtResponse(); diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/service/DeviceErrorService.java b/smartservice_backend/src/main/java/com/handong/smartservice/service/DeviceErrorService.java index e4efd79..89cfab0 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/service/DeviceErrorService.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/service/DeviceErrorService.java @@ -11,6 +11,8 @@ import org.springframework.stereotype.Service; import com.handong.smartservice._AG; import com.handong.smartservice.component.CtResponse; import com.handong.smartservice.component.DeviceErrorCode; +import com.handong.smartservice.component.Permission; +import com.handong.smartservice.component.UserInfo; import com.handong.smartservice.mapper.BizGroupMapper; import com.handong.smartservice.mapper.DeviceErrorMapper; import com.handong.smartservice.mapper.GoodsMapper; @@ -66,6 +68,10 @@ public class DeviceErrorService { return mapResult; } + // null tells the caller the record belongs to a group outside the caller's tree + if (canAccessGroup(toLong(mapError.get("biz_group_id"))) == false) + return null; + String error_code = (String)mapError.get("error_code"); for (String fault : DeviceErrorCode.getFaults(error_code)) { @@ -98,6 +104,21 @@ public class DeviceErrorService { return mapResult; } + // A super admin sees every group; anyone else only their own group and its descendants + private boolean canAccessGroup(Long biz_group_id) { + UserInfo userInfo = UserInfo.getCurr(); + if (Permission.PERM_ALL.equals(userInfo.getPermission())) + return true; + + return deviceErrorMapper.selectGroupInTree(userInfo.getBizGroupId(), biz_group_id) > 0; + } + + /** Detail of the latest sold out record of a device; an empty list when it has none, null when not allowed */ + public Map getLatestSoldOutDetail(Long device_id) { + Long device_error_id = deviceErrorMapper.selectLatestSoldOutErrorId(device_id); + return getDeviceErrorDetail(device_error_id != null ? device_error_id : 0L); + } + // device_goods keeps the column number as text ("01", "02", ...), so it is normalized to a number private Map> getGoodsByColumn(Long device_id) { Map> mapGoods = new HashMap<>(); diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/service/DeviceService.java b/smartservice_backend/src/main/java/com/handong/smartservice/service/DeviceService.java index 5889265..1826d55 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/service/DeviceService.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/service/DeviceService.java @@ -215,6 +215,20 @@ public class DeviceService { return result; } + public CtResponse clearDeviceEmpty(Long device_id) { + CtResponse result = new CtResponse(); + UserInfo userInfo = UserInfo.getCurr(); + + int changed = deviceMapper.updateDeviceEmptyClear(device_id); + if (changed == 0) { + result.setErrCode(ErrorCode.QUERY_ERROR); + return result; + } + DbLogger.insert(2L, "무인기기 품절표시 끄기, device_id: " + device_id, userInfo.getGid()); + + return result; + } + public Map selectDeviceByError(Long biz_group_id) { Map mapResult = new HashMap<>(); diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/service/TransactionService.java b/smartservice_backend/src/main/java/com/handong/smartservice/service/TransactionService.java index 6830a3f..7035b39 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/service/TransactionService.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/service/TransactionService.java @@ -120,6 +120,11 @@ public class TransactionService { params.put("device_id", device_id); if (type.compareTo("D4") == 0) { + // A credit cancel carries the approval number of its original approval + if (approval != null && !approval.isEmpty()) { + params.put("rtid", transactionMapper.selectOrgTransactionId(uid1, approval)); + } + goodsMapper.updateDeviceGoodsPlusInventory(approval); } else if (goods != null) { diff --git a/smartservice_backend/src/main/resources/application-prod.yaml b/smartservice_backend/src/main/resources/application-prod.yaml new file mode 100644 index 0000000..1abc426 --- /dev/null +++ b/smartservice_backend/src/main/resources/application-prod.yaml @@ -0,0 +1,8 @@ +# Production overrides. Everything not listed here comes from application.yaml. +# Run with: java -jar smartservice.jar --spring.profiles.active=prod +# (or SPRING_PROFILES_ACTIVE=prod, or ./gradlew bootRun --args='--spring.profiles.active=prod') +spring: + datasource: + url: jdbc:mysql://PROD_DB_HOST:3306/PROD_DB_NAME?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull + username: PROD_DB_USER + password: PROD_DB_PASSWORD diff --git a/smartservice_backend/src/main/resources/application.yaml b/smartservice_backend/src/main/resources/application.yaml index 9a9aff4..2ffc849 100644 --- a/smartservice_backend/src/main/resources/application.yaml +++ b/smartservice_backend/src/main/resources/application.yaml @@ -14,8 +14,8 @@ spring: port: 6379 password: secret datasource: - #url: jdbc:mysql://localhost:3306/smartservice?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull - url: jdbc:mysql://sensemeka.ddns.net:19106/hdsmartsvc?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull + #url: jdbc:mysql://sensemeka.ddns.net:19106/hdsmartsvc?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull + url: jdbc:mysql://localhost:3306/hdsmartsvc?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull username: smartsvc2 password: Gknehd!@wjdqhxhdtls driver-class-name: com.mysql.jdbc.Driver @@ -30,6 +30,17 @@ mybatis: configuration: map-underscore-to-camel-case: true +sms: + # Munja114 remote account + remote-id: "handong3883" + remote-pass: "gksehd3885@" + # sender number registered with Munja114 + callback: "0314263883" + +account: + # site link texted with an issued account + site-url: "http://www.parone.co.kr" + jwt: secret: c2VjcmV0L12AAsWtleS12ZXJ5LXN232DSJDSKDJWNsllY3VyZS1zZWNyZXQta2V5LXZlcnktc2VjdXJl diff --git a/smartservice_backend/src/main/resources/mapper/AccountMapper.xml b/smartservice_backend/src/main/resources/mapper/AccountMapper.xml index 3c83bd4..e1396d7 100644 --- a/smartservice_backend/src/main/resources/mapper/AccountMapper.xml +++ b/smartservice_backend/src/main/resources/mapper/AccountMapper.xml @@ -68,7 +68,7 @@ COUNT(*) - AC.gid, AC.user_id, AC.reg_time, AC.user_pw, AC.biz_group_id, GT.name AS biz_group_name, AC.user_pw_time, AC.name, AC.phone, AC.email, AC.state, AC.permission + AC.gid, AC.user_id, AC.reg_time, AC.biz_group_id, GT.name AS biz_group_name, AC.user_pw_time, AC.terms_agree_time, AC.name, AC.phone, AC.email, AC.state, AC.permission FROM account AS AC @@ -141,6 +141,28 @@ WHERE gid = #{gid} + + + + + + UPDATE account + SET user_id = #{user_id}, user_pw = #{user_pw}, user_pw_time = NOW(), terms_agree_time = NOW(), state = 1 + WHERE gid = #{gid} AND state = 6 + + + + + UPDATE account + SET user_pw = #{user_pw}, user_pw_time = NOW() + WHERE gid = #{gid} AND state != 2 + + UPDATE account diff --git a/smartservice_backend/src/main/resources/mapper/DeviceErrorMapper.xml b/smartservice_backend/src/main/resources/mapper/DeviceErrorMapper.xml index 64d6f31..358ef85 100644 --- a/smartservice_backend/src/main/resources/mapper/DeviceErrorMapper.xml +++ b/smartservice_backend/src/main/resources/mapper/DeviceErrorMapper.xml @@ -69,6 +69,38 @@ WHERE E.device_error_id = #{device_error_id} + + + + + + INSERT INTO device_error (type, biz_group_id, device_id, uid1, uid1_type, error_code, error_type) VALUES (#{type}, #{biz_group_id}, #{device_id}, #{uid1}, #{uid1_type}, #{error_code}, #{error_type}) diff --git a/smartservice_backend/src/main/resources/mapper/DeviceMapper.xml b/smartservice_backend/src/main/resources/mapper/DeviceMapper.xml index 4e9f7c6..479b0be 100644 --- a/smartservice_backend/src/main/resources/mapper/DeviceMapper.xml +++ b/smartservice_backend/src/main/resources/mapper/DeviceMapper.xml @@ -66,7 +66,7 @@ COUNT(*) - D.device_id, T.terminal_id, BG.biz_group_id, BG.name AS biz_group_name, D.reg_time, D.name, T.uid1, T.type, D.manager_name, D.state, T.connect_time, T.disconnect_time + D.device_id, T.terminal_id, BG.biz_group_id, BG.name AS biz_group_name, D.reg_time, D.name, T.uid1, T.type, D.manager_name, D.state, D.is_empty, T.connect_time, T.disconnect_time FROM device AS D @@ -89,10 +89,17 @@ AND T.uid1 = #{uid1} - AND T.connect_time IS NOT NULL AND T.disconnect_time IS NOT NULL AND T.connect_time <= T.disconnect_time + AND T.connect_time IS NOT NULL AND (T.disconnect_time IS NULL OR T.disconnect_time < T.connect_time) - AND T.connect_time IS NOT NULL AND T.disconnect_time IS NOT NULL AND T.connect_time > T.disconnect_time + AND T.connect_time IS NOT NULL AND T.disconnect_time IS NOT NULL AND T.disconnect_time > T.connect_time + + + + AND IFNULL(D.is_empty, 0) = 0 + + + AND D.is_empty = 1 AND #{date_start} <= DATE(D.reg_time) @@ -129,7 +136,7 @@ COUNT(*) - D.device_id, T.terminal_id, GT.biz_group_id, GT.name AS biz_group_name, D.reg_time, D.name, T.uid1, T.type, D.manager_name, D.state, T.connect_time, T.disconnect_time + D.device_id, T.terminal_id, GT.biz_group_id, GT.name AS biz_group_name, D.reg_time, D.name, T.uid1, T.type, D.manager_name, D.state, D.is_empty, T.connect_time, T.disconnect_time FROM device AS D @@ -144,10 +151,17 @@ AND T.uid1 = #{uid1} - AND T.connect_time IS NOT NULL AND T.disconnect_time IS NOT NULL AND T.connect_time >= T.disconnect_time + AND T.connect_time IS NOT NULL AND (T.disconnect_time IS NULL OR T.disconnect_time < T.connect_time) - AND T.connect_time IS NOT NULL AND T.disconnect_time IS NOT NULL AND T.connect_time < T.disconnect_time + AND T.connect_time IS NOT NULL AND T.disconnect_time IS NOT NULL AND T.disconnect_time > T.connect_time + + + + AND IFNULL(D.is_empty, 0) = 0 + + + AND D.is_empty = 1 AND #{date_start} <= DATE(D.reg_time) @@ -179,7 +193,7 @@ INNER JOIN group_tree gt ON bg.pid = gt.biz_group_id ) SELECT - D.device_id, T.terminal_id, GT.biz_group_id, GT.name AS biz_group_name, D.reg_time, D.name, T.uid1, T.type, D.manager_name, D.state, T.connect_time, T.disconnect_time + D.device_id, T.terminal_id, GT.biz_group_id, GT.name AS biz_group_name, D.reg_time, D.name, T.uid1, T.type, D.manager_name, D.state, D.is_empty, T.connect_time, T.disconnect_time FROM device AS D JOIN device_biz_group AS DBG ON DBG.device_id = D.device_id JOIN group_tree GT ON DBG.biz_group_id = GT.biz_group_id @@ -246,6 +260,12 @@ WHERE D.device_id = #{device_id} + + UPDATE device + SET is_empty = NULL + WHERE device_id = #{device_id} + + UPDATE device diff --git a/smartservice_backend/src/main/resources/mapper/TransactionMapper.xml b/smartservice_backend/src/main/resources/mapper/TransactionMapper.xml index 7150c1e..04a3686 100644 --- a/smartservice_backend/src/main/resources/mapper/TransactionMapper.xml +++ b/smartservice_backend/src/main/resources/mapper/TransactionMapper.xml @@ -65,10 +65,8 @@ T.reg_time, T.order_time, D.name AS device_name, T.amount, T.type, T.approval, T.uid1, T.pay_name, T.pay_vendor, T.column_no, T.code, T.goods_name, T.pay_unique_num, T.order_time, T.card_num, T.rtid, - /* the original approval, shown under a credit cancel row */ - ORG.reg_time AS org_reg_time, ORG.order_time AS org_order_time, ORG.type AS org_type, ORG.amount AS org_amount, - ORG.approval AS org_approval, ORG.card_num AS org_card_num, ORG.pay_name AS org_pay_name, ORG.pay_vendor AS org_pay_vendor, - ORG.column_no AS org_column_no, ORG.code AS org_code, ORG.goods_name AS org_goods_name + /* the original approval, used for the cancel column and the cancel receipt of a credit cancel row */ + ORG.order_time AS org_order_time, ORG.goods_name AS org_goods_name FROM transactions AS T @@ -388,6 +386,15 @@ HAVING card_count != 0 OR cash_count != 0 OR tmoney_count != 0 OR cbee_count != 0 + + + INSERT INTO transactions @@ -406,6 +413,7 @@ goods_name, price, pay_name, + rtid, pay_vendor VALUES @@ -425,6 +433,7 @@ #{goods_name}, #{price}, #{pay_name}, + #{rtid}, #{pay_vendor} diff --git a/smartservice_frontend/src/app/(admin)/account/Account.tsx b/smartservice_frontend/src/app/(admin)/account/Account.tsx index fc39945..343a194 100644 --- a/smartservice_frontend/src/app/(admin)/account/Account.tsx +++ b/smartservice_frontend/src/app/(admin)/account/Account.tsx @@ -24,15 +24,16 @@ import PageCountSelector from "@/components/PageCountSelector"; const ACCOUNT_STATE_LABEL: { [key: string]: string } = { - "1": "정상", "2": "탈퇴", "3": "휴면", "4": "정지", "5": "잠김", + "1": "정상", "2": "해지", "3": "휴면", "4": "정지", "5": "잠김", "6": "초기화", }; const ACCOUNT_STATE_OPTIONS = [ { label: "정상", value: "1" }, - { label: "탈퇴", value: "2" }, + { label: "해지", value: "2" }, { label: "휴면", value: "3" }, { label: "정지", value: "4" }, { label: "잠김", value: "5" }, + { label: "초기화", value: "6" }, ]; const accountStateColor = (state: any) => { @@ -40,6 +41,7 @@ const accountStateColor = (state: any) => { if (value === 1) return "success"; if (value === 2) return "light"; if (value === 4 || value === 5) return "error"; + if (value === 6) return "info"; return "warning"; }; @@ -215,7 +217,9 @@ export default function Account() { const handleRowClickModify = (row: number) => { const node = findNodeByKey(treeData, selectedTreeKeys[0]); - modalData.setAll({...tableData[row], + // The password is never listed, so a modification always starts with an empty one + modalData.setAll({...tableData[row], + user_pw: "", biz_group_name: node.name, biz_reg_num: node.biz_reg_num, isModify: true}); @@ -292,7 +296,9 @@ export default function Account() { { title: "해지", key: "_", + // An already terminated account has nothing left to terminate renderItem: (item: any, row: number) => ( + Number(item.state) === 2 ? "" :
s.tokenPayload); //Bruce, JWT const [ user_pw_confirm, setUserPwConfirm ] = useState(""); + + const { isOpen: isOpenReset, openModal: openResetModal, closeModal: closeResetModal } = useModal(); const handleClickAddOrModify = () => { @@ -102,6 +107,7 @@ export default function AddModifyAccountModal({ multiState, onOk, isOpen, closeM return ( //
+ <> multiState.set("state", e.target.value)} className="dark:bg-dark-900" />
+ { multiState.values.isModify && +
+ + +
+ } + + //
); }; \ No newline at end of file diff --git a/smartservice_frontend/src/app/(admin)/biz-group/AddModifyBizGroupModal.tsx b/smartservice_frontend/src/app/(admin)/biz-group/AddModifyBizGroupModal.tsx index 53f6b05..10fa14d 100644 --- a/smartservice_frontend/src/app/(admin)/biz-group/AddModifyBizGroupModal.tsx +++ b/smartservice_frontend/src/app/(admin)/biz-group/AddModifyBizGroupModal.tsx @@ -3,6 +3,7 @@ import React, { useState } from "react"; import Label from '@/components/form/Label2'; import Input from '@/components/form/input/InputField2'; import CtModal from "@/components/CtModal"; +import Checkbox from "@/components/form/input/Checkbox"; import { useMultiState } from "@/hooks/useMultiState"; import api, { formatBusinessNumber, isValidBusinessNumber } from '@/lib/_AG'; @@ -17,8 +18,23 @@ export interface BizGroupModalProps { email: string; ceo: string; address: string; + issue_account: boolean; // new group only: create an account and text the temporary password } +// Error codes of an account issued together with a new group +const alertIssueError = (errCode: number) => { + if (errCode == -19) + alert("계정발급을 하려면 올바른 휴대폰번호를 입력해야 합니다."); + else if (errCode == -20) + alert("문자 발송에 실패해 사업자 등록이 취소되었습니다. 잠시 후 다시 시도하세요."); + else if (errCode == -21) + alert("이 사업자번호로 발급된 계정이 이미 있습니다."); + else + return false; + + return true; +}; + interface AddModifyBizGroupModalProps { multiState: ReturnType>; onOk: (insertedId: number) => void; @@ -37,7 +53,18 @@ export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, close // alert("사업자등록번호를 입력하셔야 합니다."); // return; //} - if (multiState.values.biz_reg_num != "" && !isValidBusinessNumber(multiState.values.biz_reg_num)) { + if (!multiState.values.isModify && multiState.values.issue_account) { + // The issued ID is the business number and the message goes to the mobile number + if (multiState.values.biz_reg_num == "" || !isValidBusinessNumber(multiState.values.biz_reg_num)) { + alert("계정발급을 하려면 올바른 사업자번호를 입력해야 합니다."); + return; + } + if (!/^01[016789][0-9]{7,8}$/.test(multiState.values.phone.replace(/[^0-9]/g, ""))) { + alert("계정발급을 하려면 올바른 휴대폰번호를 입력해야 합니다."); + return; + } + } + else if (multiState.values.biz_reg_num != "" && !isValidBusinessNumber(multiState.values.biz_reg_num)) { if (!confirm("잘못된 형식의 사업자등록번호입니다. 그래도 등록하시겠습니까?")) return; } @@ -67,7 +94,7 @@ export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, close console.log(resp); if (resp.data.errCode == 0) { - alert("추가되었습니다."); + alert(multiState.values.issue_account ? "추가되었습니다. 발급된 계정 정보를 문자로 보냈습니다." : "추가되었습니다."); onOk(resp.data.result.biz_group_id); } else if (resp.data.errCode == -11) { @@ -76,7 +103,7 @@ export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, close else if (resp.data.errCode == -17) { alert("잘못된 형식의 사업자번호입니다."); } - else { + else if (alertIssueError(resp.data.errCode) == false) { alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode); } }) @@ -108,7 +135,7 @@ export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, close handleChangeBizRegNum(e.target.value)} />
- + multiState.set("phone", e.target.value)} />
@@ -123,6 +150,12 @@ export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, close multiState.set("address", e.target.value)} />
+ { !multiState.values.isModify && +
+ multiState.set("issue_account", value)} + label="계정발급 (사업자번호를 아이디로, 임시비밀번호를 휴대폰번호로 문자 발송)" /> +
+ } // ); diff --git a/smartservice_frontend/src/app/(admin)/biz-group/BizGroupContent.tsx b/smartservice_frontend/src/app/(admin)/biz-group/BizGroupContent.tsx index e44588f..0649c94 100644 --- a/smartservice_frontend/src/app/(admin)/biz-group/BizGroupContent.tsx +++ b/smartservice_frontend/src/app/(admin)/biz-group/BizGroupContent.tsx @@ -145,7 +145,7 @@ export default function BizGroupContent({ const handleChangeFormInfo = (field: string, value?: string | number) => { setFormInfo(prev => ({ ...prev, [field]: value ?? "" })); }; // modal values - const modalData = useMultiState({isModify: false, pid: "", name: "", biz_reg_num: "", phone: "", email: "", ceo: "", address: "" }); + const modalData = useMultiState({isModify: false, pid: "", name: "", biz_reg_num: "", phone: "", email: "", ceo: "", address: "", issue_account: true }); @@ -419,7 +419,7 @@ export default function BizGroupContent({ handleChangeFormInfo("email", e.target.value)} />
- + handleChangeFormInfo("phone", e.target.value)} />
diff --git a/smartservice_frontend/src/app/(admin)/biz-group2/AddModifyBizGroupModal.tsx b/smartservice_frontend/src/app/(admin)/biz-group2/AddModifyBizGroupModal.tsx index 5ff4644..5a33d2f 100644 --- a/smartservice_frontend/src/app/(admin)/biz-group2/AddModifyBizGroupModal.tsx +++ b/smartservice_frontend/src/app/(admin)/biz-group2/AddModifyBizGroupModal.tsx @@ -3,8 +3,9 @@ import React, { useState } from "react"; import Label from '@/components/form/Label2'; import Input from '@/components/form/input/InputField2'; import CtModal from "@/components/CtModal"; +import Checkbox from "@/components/form/input/Checkbox"; import { useMultiState } from "@/hooks/useMultiState"; -import api from '@/lib/_AG'; +import api, { isValidBusinessNumber } from '@/lib/_AG'; export interface BizGroupModalProps { @@ -17,6 +18,7 @@ export interface BizGroupModalProps { email: string; ceo: string; address: string; + issue_account: boolean; // new group only: create an account and text the temporary password } interface AddModifyBizGroupModalProps { @@ -37,7 +39,19 @@ export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, close // alert("사업자등록번호를 입력하셔야 합니다."); // return; //} - + + if (!multiState.values.isModify && multiState.values.issue_account) { + // The issued ID is the business number and the message goes to the mobile number + if (multiState.values.biz_reg_num == "" || !isValidBusinessNumber(multiState.values.biz_reg_num)) { + alert("계정발급을 하려면 올바른 사업자번호를 입력해야 합니다."); + return; + } + if (!/^01[016789][0-9]{7,8}$/.test(multiState.values.phone.replace(/[^0-9]/g, ""))) { + alert("계정발급을 하려면 올바른 휴대폰번호를 입력해야 합니다."); + return; + } + } + if (multiState.values.isModify) { api.post('/api/modify-biz-group.do', multiState.values).then((resp) => { console.log(resp); @@ -57,9 +71,21 @@ export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, close console.log(resp); if (resp.data.errCode == 0) { - alert("추가되었습니다."); + alert(multiState.values.issue_account ? "추가되었습니다. 발급된 계정 정보를 문자로 보냈습니다." : "추가되었습니다."); onOk(); } + else if (resp.data.errCode == -17) { + alert("잘못된 형식의 사업자번호입니다."); + } + else if (resp.data.errCode == -19) { + alert("계정발급을 하려면 올바른 휴대폰번호를 입력해야 합니다."); + } + else if (resp.data.errCode == -20) { + alert("문자 발송에 실패해 사업자 등록이 취소되었습니다. 잠시 후 다시 시도하세요."); + } + else if (resp.data.errCode == -21) { + alert("이 사업자번호로 발급된 계정이 이미 있습니다."); + } else { alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode); } @@ -88,7 +114,7 @@ export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, close multiState.set("biz_reg_num", e.target.value)} />
- + multiState.set("phone", e.target.value)} />
@@ -103,6 +129,12 @@ export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, close multiState.set("address", e.target.value)} />
+ { !multiState.values.isModify && +
+ multiState.set("issue_account", value)} + label="계정발급 (사업자번호를 아이디로, 임시비밀번호를 휴대폰번호로 문자 발송)" /> +
+ } // ); diff --git a/smartservice_frontend/src/app/(admin)/biz-group2/BizGroup.tsx b/smartservice_frontend/src/app/(admin)/biz-group2/BizGroup.tsx index ab3fbcd..0a9ff43 100644 --- a/smartservice_frontend/src/app/(admin)/biz-group2/BizGroup.tsx +++ b/smartservice_frontend/src/app/(admin)/biz-group2/BizGroup.tsx @@ -49,7 +49,7 @@ export default function BizGroup() { // modal const { isOpen, openModal, closeModal } = useModal(); - const modalData = useMultiState( {isModify: false, pid: "", name: "", biz_reg_num: "", phone: "", email: "", ceo: "", address: "" }); + const modalData = useMultiState( {isModify: false, pid: "", name: "", biz_reg_num: "", phone: "", email: "", ceo: "", address: "", issue_account: true }); // function part @@ -141,6 +141,7 @@ export default function BizGroup() { email: "", ceo: "", address: "", + issue_account: true, }); } else { @@ -212,7 +213,7 @@ export default function BizGroup() { key: "email", }, { - title: "전화번호", + title: "휴대폰번호", key: "phone", }, { @@ -272,8 +273,8 @@ export default function BizGroup() {
- - setPhone(e.target.value)} /> + + setPhone(e.target.value)} /> setAddress(e.target.value)} /> diff --git a/smartservice_frontend/src/app/(admin)/customer-account/AddModifyCustomerAccountModal.tsx b/smartservice_frontend/src/app/(admin)/customer-account/AddModifyCustomerAccountModal.tsx index 135f426..c4fb26f 100644 --- a/smartservice_frontend/src/app/(admin)/customer-account/AddModifyCustomerAccountModal.tsx +++ b/smartservice_frontend/src/app/(admin)/customer-account/AddModifyCustomerAccountModal.tsx @@ -1,6 +1,9 @@ "use client"; import React, { useState, useRef, useEffect } from "react"; import CtModal from "@/components/CtModal"; +import ResetPasswordModal from "@/components/ResetPasswordModal"; +import Button from "@/components/ui/button/Button2"; +import { useModal } from "@/hooks/useModal"; import Label from '@/components/form/Label2'; import Input from '@/components/form/input/InputField2'; import Select from '@/components/form/Select2'; @@ -42,6 +45,8 @@ export default function AddModifyCustomerAccountModal({ multiState, onOk, isOpen const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT const [ user_pw_confirm, setUserPwConfirm ] = useState(""); + + const { isOpen: isOpenReset, openModal: openResetModal, closeModal: closeResetModal } = useModal(); const handleClickAddOrModify = () => { @@ -105,6 +110,7 @@ export default function AddModifyCustomerAccountModal({ multiState, onOk, isOpen return ( //
+ <> multiState.set("state", e.target.value)} className="dark:bg-dark-900" />
+ { multiState.values.isModify && +
+ + +
+ } + + //
); }; \ No newline at end of file diff --git a/smartservice_frontend/src/app/(admin)/customer-account/CustomerAccount.tsx b/smartservice_frontend/src/app/(admin)/customer-account/CustomerAccount.tsx index 6dabf61..71595ba 100644 --- a/smartservice_frontend/src/app/(admin)/customer-account/CustomerAccount.tsx +++ b/smartservice_frontend/src/app/(admin)/customer-account/CustomerAccount.tsx @@ -276,7 +276,8 @@ export default function CustomerAccount() { }; const handleRowClickModify = (row: number) => { - modalData.setAll({...tableData[row], isModify: true}); + // The password is never listed, so a modification always starts with an empty one + modalData.setAll({...tableData[row], user_pw: "", isModify: true}); openModal(); }; diff --git a/smartservice_frontend/src/app/(admin)/device-error/DeviceError.tsx b/smartservice_frontend/src/app/(admin)/device-error/DeviceError.tsx index ae835d8..6b77790 100644 --- a/smartservice_frontend/src/app/(admin)/device-error/DeviceError.tsx +++ b/smartservice_frontend/src/app/(admin)/device-error/DeviceError.tsx @@ -100,6 +100,10 @@ export default function DeviceError() { function reqDeviceErrorDetail(device_error_id: number) { api.post('/api/get-device-error-detail.do', { device_error_id: device_error_id }).then((resp) => { + if (resp.data.errCode == -13) { + alert("조회 권한이 없습니다."); + return; + } if (resp.data.errCode != 0) { alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode); return; diff --git a/smartservice_frontend/src/app/(admin)/device/AddModifyDeviceModal.tsx b/smartservice_frontend/src/app/(admin)/device/AddModifyDeviceModal.tsx index cd136d7..ae72f32 100644 --- a/smartservice_frontend/src/app/(admin)/device/AddModifyDeviceModal.tsx +++ b/smartservice_frontend/src/app/(admin)/device/AddModifyDeviceModal.tsx @@ -66,6 +66,29 @@ export default function AddModifyDeviceModal({ multiState, onOk, isOpen, closeMo alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode); } }) + .catch((err) => { + console.error(err); + alert("초기화에 실패했습니다."); + }) + } + + const handleClearEmpty = () => { + if (confirm("품절표시를 끄시겠습니까?") == false) + return; + + const params = { + device_id: multiState.values.device_id + } + + api.post('/api/clear-device-empty.do', params).then((resp) => { + if (resp.data.errCode == 0) { + alert("품절표시를 껐습니다."); + onOk(); + } + else { + alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode); + } + }) .catch((err) => console.error(err)) } @@ -188,6 +211,7 @@ export default function AddModifyDeviceModal({ multiState, onOk, isOpen, closeMo
+ {/** diff --git a/smartservice_frontend/src/app/(admin)/device/Device.tsx b/smartservice_frontend/src/app/(admin)/device/Device.tsx index a8effac..b17288e 100644 --- a/smartservice_frontend/src/app/(admin)/device/Device.tsx +++ b/smartservice_frontend/src/app/(admin)/device/Device.tsx @@ -21,6 +21,7 @@ import { useModal } from "@/hooks/useModal"; import AddModifyDeviceModal, { DeviceModalProps } from './AddModifyDeviceModal'; import ManageGoodsModal, { GoodsModalProps } from './ManageGoodsModal'; import ManageTemplateModal, { TemplateModalProps } from './ManageTemplateModal'; +import ErrorDetailModal, { ErrorDetailModalProps, DeviceErrorDetailItem } from '../device-error/ErrorDetailModal'; import { useMultiState } from "@/hooks/useMultiState"; import CtTree, { CtTreeNode, expandAndSelectTreeNode, getAllNodeKeys, findParentKeys, findNodeByKey, treeToList, addNodeByKey, deleteNodeByKey, findRootNodeByKey } from "@/components/CtTree"; @@ -93,6 +94,10 @@ export default function Device() { const { isOpen: isOpenManageTemplate, openModal: openModalManageTemplate, closeModal: closeModalManageTemplate } = useModal(); const modalDataManageTemplate = useMultiState({ biz_group_id: 0, optGoodsList: [] }); + + const { isOpen: isOpenSoldOut, openModal: openModalSoldOut, closeModal: closeModalSoldOut } = useModal(); + const [ soldOutItems, setSoldOutItems ] = useState([]); + const modalDataSoldOut = useMultiState({device_error_id: 0, reg_time: "", device_name: "", uid1: "", uid1_type: 0, type: "", error_type: 0, error_code: "", biz_group_name: ""}); // function part @@ -547,6 +552,33 @@ export default function Device() { }, [tableOffset]); + // Opens the same detail popup as the device error screen, for the latest sold out record of the device + const handleClickSoldOut = (item: any) => { + api.post('/api/get-device-error-detail.do', { device_id: item.device_id }).then((resp) => { + if (resp.data.errCode == -13) { + alert("조회 권한이 없습니다."); + return; + } + if (resp.data.errCode != 0) { + alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode); + return; + } + + const info = resp.data.result.result_info; + if (!info) { + alert("품절 내역이 없습니다."); + return; + } + + modalDataSoldOut.setAll({...info, + biz_group_name: item.biz_group_name ?? "", + }); + setSoldOutItems(resp.data.result.list ?? []); + openModalSoldOut(); + }) + .catch((err) => console.error(err)); + }; + // UI part // const columns = [ @@ -577,10 +609,14 @@ export default function Device() { const connectTime = item.connect_time ? new Date(item.connect_time.replace(' ', 'T')) : null; const disconnectTime = item.disconnect_time ? new Date(item.disconnect_time.replace(' ', 'T')) : null; - let color: "success" | "error" | "info" = "info"; + let color: "success" | "error" | "info" | "warning" = "info"; let text = ""; - if (connectTime && (!disconnectTime || disconnectTime < connectTime)) { + // Sold out (device.is_empty = 1) wins over the connection state + if (Number(item.is_empty) === 1) { + color = "warning"; + text = "품절"; + } else if (connectTime && (!disconnectTime || disconnectTime < connectTime)) { color = "success"; text = "가동중"; } else if (connectTime && disconnectTime && disconnectTime > connectTime) { @@ -588,6 +624,16 @@ export default function Device() { text = "장애"; } + if (text === "품절") { + return ( + + ); + } + return ( {text} @@ -596,11 +642,11 @@ export default function Device() { }, viewMobile: true, }, - { + /*{ title: "가동시간", key: "connect_time", renderItem: (item: any, row: number) => convertDateTime3(item.connect_time) - }, + },*/ { title: "장애발생시간", key: "disconnect_time", @@ -692,6 +738,7 @@ export default function Device() { { label: "전체", value: "" }, { label: "가동중", value: "1" }, { label: "장애", value: "4" }, + { label: "품절", value: "5" }, ]} defaultValue={conn_state} onChange={handleChangeConnState} className="dark:bg-dark-900" /> @@ -753,6 +800,8 @@ export default function Device() { {/**/} + +
); diff --git a/smartservice_frontend/src/app/(admin)/notice/AddModifyNoticeModal.tsx b/smartservice_frontend/src/app/(admin)/notice/AddModifyNoticeModal.tsx index afda1de..ad4f8e0 100644 --- a/smartservice_frontend/src/app/(admin)/notice/AddModifyNoticeModal.tsx +++ b/smartservice_frontend/src/app/(admin)/notice/AddModifyNoticeModal.tsx @@ -107,7 +107,7 @@ export default function AddModifyNoticeModal({ isOpen, openModal, closeModal, mu diff --git a/smartservice_frontend/src/app/(admin)/terms/Terms.tsx b/smartservice_frontend/src/app/(admin)/terms/Terms.tsx index 221cf51..4513787 100644 --- a/smartservice_frontend/src/app/(admin)/terms/Terms.tsx +++ b/smartservice_frontend/src/app/(admin)/terms/Terms.tsx @@ -1,70 +1,20 @@ "use client" import PageBreadcrumb from "@/components/common/PageBreadCrumb2"; import ComponentCard from '@/components/common/ComponentCard2'; +import TermsContent from "@/components/TermsContent"; import { PageIcon } from "@/icons"; import React from "react"; -import { COMPANY_NAME, EFFECTIVE_DATE, SERVICE_NAME, TERMS_ARTICLES, TermsArticle } from "./termsData"; +import { SERVICE_NAME } from "./termsData"; -const CIRCLED = ["①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨", "⑩"]; - -const Article = ({ article }: { article: TermsArticle }) => ( -
-

{article.title}

- - { article.paragraphs?.map((text, index) => ( -

{text}

- ))} - - { article.items && article.items.length > 0 && -
    - { article.items.map((text, index) => ( -
  1. - {index + 1}. - {text} - - { article.subItems?.[index] && -
      - { article.subItems[index].map((sub, subIndex) => ( -
    • - {CIRCLED[subIndex] ?? "-"} - {sub} -
    • - ))} -
    - } -
  2. - ))} -
- } -
-); - export default function Terms() { return (
}> -
- 본 약관은 {COMPANY_NAME}(이하 ‘회사’)가 제공하는 {SERVICE_NAME}의 이용조건과 절차, 회사와 회원의 권리·의무를 정합니다. -
- 시행일: {EFFECTIVE_DATE} -
- -
- { TERMS_ARTICLES.map((article, index) => ( -
- ))} - -
-

부칙

-

- 이 약관은 {EFFECTIVE_DATE}부터 시행합니다. -

-
-
+
); diff --git a/smartservice_frontend/src/app/(admin)/transactions/ReceiptModal.tsx b/smartservice_frontend/src/app/(admin)/transactions/ReceiptModal.tsx index b59c587..05cd9e1 100644 --- a/smartservice_frontend/src/app/(admin)/transactions/ReceiptModal.tsx +++ b/smartservice_frontend/src/app/(admin)/transactions/ReceiptModal.tsx @@ -13,7 +13,11 @@ export interface ReceiptModalProps { goods_name: string; amount: number; pay_name: string; + card_num: string; approval: string; + type: string; + org_order_time: string; + org_goods_name: string; biz_group_name: string; biz_reg_num: string; ceo: string; @@ -30,8 +34,15 @@ interface ViewReceiptModalProps { } export default function ReceiptModal({ multiState, onOk, isOpen, closeModal }: ViewReceiptModalProps) { - const tax = multiState.values.amount / 10; - const price = multiState.values.amount - tax; + // A credit cancel (D4) prints as a cancel receipt; its approval number is the original one + const isCancel = multiState.values.type === "D4"; + const amount = Math.abs(Number(multiState.values.amount) || 0); + const sign = isCancel ? "-" : ""; + // A cancel row is stored without goods, so fall back to the original approval's goods name + const goodsName = multiState.values.goods_name || (isCancel ? multiState.values.org_goods_name : ""); + + const tax = amount / 10; + const price = amount - tax; const elementRef = useRef(null); @@ -61,14 +72,22 @@ export default function ReceiptModal({ multiState, onOk, isOpen, closeModal }: V
-

[영수증]

+

{isCancel ? "[취소 영수증]" : "[영수증]"}

-
+
상호: {multiState.values.biz_group_name}
주소: {multiState.values.address}
- 사업자등록번호: {multiState.values.biz_reg_num} | 대표자: {multiState.values.ceo}
+ 사업자등록번호: {multiState.values.biz_reg_num}
+ 대표자: {multiState.values.ceo}
TEL: {multiState.values.phone}
- {convertDateTime6(multiState.values.order_time)} + {isCancel ? + <> + 취소일시: {convertDateTime6(multiState.values.order_time)}
+ 원승인일시: {convertDateTime6(multiState.values.org_order_time)} + + : + <>거래일시: {convertDateTime6(multiState.values.order_time)} + }
@@ -83,9 +102,9 @@ export default function ReceiptModal({ multiState, onOk, isOpen, closeModal }: V - {multiState.values.goods_name ? multiState.values.goods_name : "물품"} + {goodsName ? goodsName : "물품"} 1 - ₩ {multiState.values.amount.toLocaleString("ko-KR")} + {sign}₩ {amount.toLocaleString("ko-KR")} @@ -96,15 +115,15 @@ export default function ReceiptModal({ multiState, onOk, isOpen, closeModal }: V 합계 (총 1개) - ₩ {price.toLocaleString("ko-KR")} + {sign}₩ {price.toLocaleString("ko-KR")} 부가세(10%) - ₩ {tax} + {sign}₩ {tax.toLocaleString("ko-KR")} - 최종 결제금액 - ₩ {multiState.values.amount.toLocaleString("ko-KR")} + {isCancel ? "최종 취소금액" : "최종 결제금액"} + {sign}₩ {amount.toLocaleString("ko-KR")} @@ -117,16 +136,16 @@ export default function ReceiptModal({ multiState, onOk, isOpen, closeModal }: V 결제수단: 신용카드 (일시불) - {/* - 카드번호: 1234-****-****-1234 + + 카드번호: {multiState.values.card_num} - */} + 카드사: {multiState.values.pay_name} - 승인번호: {multiState.values.approval} + {isCancel ? "원승인번호" : "승인번호"}: {multiState.values.approval} diff --git a/smartservice_frontend/src/app/(admin)/transactions/Transactions.tsx b/smartservice_frontend/src/app/(admin)/transactions/Transactions.tsx index c9a18bb..04d46e7 100644 --- a/smartservice_frontend/src/app/(admin)/transactions/Transactions.tsx +++ b/smartservice_frontend/src/app/(admin)/transactions/Transactions.tsx @@ -56,19 +56,6 @@ const payCardName = (type: string, pay_name: string) => ( pay_name ); -/** A credit cancel is drawn as two lines, the dim second line being the original approval */ -const cell = (item: any, main: React.ReactNode, sub?: React.ReactNode) => { - if (!isCreditCancel(item)) - return main; - - return ( - <> -
{main}
-
{sub !== undefined && sub !== null && sub !== "" ? sub : " "}
- - ); -}; - const SEARCH_LABEL_WIDTH_PX = 80; //const COUNT_PER_PAGE = 10; @@ -111,7 +98,7 @@ export default function Transactions() { const [ date_end, setDateEnd ] = useState(todayYMD()); const { isOpen, openModal, closeModal } = useModal(); - const modalData = useMultiState({order_time: "", goods_name: "", amount: 0, pay_name: "", approval: "", biz_group_name: "", biz_reg_num: "", ceo: "", phone: "", address: ""}); + const modalData = useMultiState({order_time: "", goods_name: "", amount: 0, pay_name: "", card_num: "", approval: "", type: "", org_order_time: "", org_goods_name: "", biz_group_name: "", biz_reg_num: "", ceo: "", phone: "", address: ""}); const [ isMobileMode, setIsMobileMode ] = useState(false); const [ isSearchFieldOpen, setIsSearchFieldOpen ] = useState(false); @@ -294,6 +281,7 @@ export default function Transactions() { } modalData.setAll({...tableData[row], + card_num: formatCardNum(tableData[row].card_num), biz_group_name: node.name ?? "", biz_reg_num: node.biz_reg_num ?? "", ceo: node.ceo ?? "", @@ -426,30 +414,30 @@ export default function Transactions() { { title: "거래시간", key: "order_time", - renderItem: (item: any, row: number) => cell(item, convertDateTime6(item.order_time), convertDateTime6(item.org_order_time)), + renderItem: (item: any, row: number) => convertDateTime6(item.order_time), viewMobile: true, }, { title: "무인기기명", key: "device_name", - renderItem: (item: any) => cell(item, item.device_name), + renderItem: (item: any) => item.device_name, }, { title: "단말기 TID", key: "uid1", - renderItem: (item: any) => cell(item, item.uid1), + renderItem: (item: any) => item.uid1, viewMobile: true, }, { title: "금액", key: "amount", - renderItem: (item : any) => cell(item, toAmount(item.amount), toAmount(item.org_amount)), + renderItem: (item : any) => toAmount(item.amount), viewMobile: true, }, { title: "결제유형", key: "type", - renderItem: (item : any) => cell(item, + renderItem: (item : any) => ( {PAY_TYPE_LABEL[item.type] ?? ""} - , - PAY_TYPE_LABEL[item.org_type] ?? "" + ) }, { title: "승인번호", key: "approval", - renderItem: (item: any) => cell(item, item.approval, item.org_approval), + renderItem: (item: any) => item.approval, }, { title: "카드번호", key: "card_num", - renderItem: (item: any) => cell(item, formatCardNum(item.card_num), formatCardNum(item.org_card_num)), + renderItem: (item: any) => formatCardNum(item.card_num), }, { title: "컬럼번호", key: "column_no", - renderItem: (item: any) => cell(item, item.column_no, item.org_column_no), + renderItem: (item: any) => item.column_no, }, { title: "상품코드", key: "code", - renderItem: (item: any) => cell(item, item.code, item.org_code), + renderItem: (item: any) => item.code, }, { title: "상품명", key: "goods_name", - renderItem: (item: any) => cell(item, item.goods_name, item.org_goods_name), + renderItem: (item: any) => item.goods_name, }, { title: "결제카드", key: "pay_name", - renderItem: (item : any) => cell(item, payCardName(item.type, item.pay_name), payCardName(item.org_type, item.org_pay_name)) + renderItem: (item : any) => payCardName(item.type, item.pay_name) }, { title: "영수증", key: "_", renderItem: (item: any, row: number) => ( - item.type == "D1" ? + item.type == "D1" || item.type == "D4" ?
- {handleRowClickReceipt(row)}} />
@@ -524,6 +511,11 @@ export default function Transactions() { onClick={() => {handleRowClickDelete(row)}}> 결제취소 + : isCreditCancel(item) ? +
+
[원승인일시]
+
{convertDateTime6(item.org_order_time)}
+
: "" ), }, diff --git a/smartservice_frontend/src/app/(full-width-pages)/(auth)/change-info/page.tsx b/smartservice_frontend/src/app/(full-width-pages)/(auth)/change-info/page.tsx new file mode 100644 index 0000000..37bc8fb --- /dev/null +++ b/smartservice_frontend/src/app/(full-width-pages)/(auth)/change-info/page.tsx @@ -0,0 +1,11 @@ +import ChangeInfoForm from "@/components/auth/ChangeInfoForm"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "계정정보 변경", + description: "계정정보 변경", +}; + +export default function ChangeInfo() { + return ; +} diff --git a/smartservice_frontend/src/components/ResetPasswordModal.tsx b/smartservice_frontend/src/components/ResetPasswordModal.tsx new file mode 100644 index 0000000..0a55ecd --- /dev/null +++ b/smartservice_frontend/src/components/ResetPasswordModal.tsx @@ -0,0 +1,65 @@ +"use client"; +import React, { useEffect, useState } from "react"; +import CtModal from "@/components/CtModal"; +import Label from '@/components/form/Label2'; +import Input from '@/components/form/input/InputField2'; +import api from '@/lib/_AG'; + + +interface ResetPasswordModalProps { + gid: string; + phone: string; // the account's mobile number, shown as the default + isOpen: boolean; + closeModal: () => void; +} + +/** Sends a new temporary password to the account holder's mobile number */ +export default function ResetPasswordModal({ gid, phone, isOpen, closeModal }: ResetPasswordModalProps) { + const [ inputPhone, setInputPhone ] = useState(""); + + useEffect(() => { + if (isOpen) + setInputPhone(phone ?? ""); + }, [isOpen, phone]); + + const handleClickSend = () => { + if (inputPhone.trim() == "") { + alert("휴대폰 번호를 입력하세요"); + return; + } + + api.post('/api/reset-account-password.do', { gid: gid, phone: inputPhone }).then((resp) => { + if (resp.data.errCode == 0) { + alert("임시 비밀번호 전송완료"); + closeModal(); + } + else { + alert("전송에 실패했습니다."); + } + }) + .catch((err) => { + console.error(err); + alert("전송에 실패했습니다."); + }); + }; + + return ( + +
+ {"비밀번호 초기화는 사용자의 휴대폰에 임시 비밀번호를 문자로 전송합니다.\n사용자의 휴대폰 번호를 입력하세요."} +
+
+ + setInputPhone(e.target.value)} + onKeyDown={(e) => {if (e.key === "Enter") handleClickSend()}} /> +
+
+ ); +}; diff --git a/smartservice_frontend/src/components/TermsContent.tsx b/smartservice_frontend/src/components/TermsContent.tsx new file mode 100644 index 0000000..2f2744e --- /dev/null +++ b/smartservice_frontend/src/components/TermsContent.tsx @@ -0,0 +1,64 @@ +"use client"; +import React from "react"; +import { COMPANY_NAME, EFFECTIVE_DATE, SERVICE_NAME, TERMS_ARTICLES, TermsArticle } from "@/app/(admin)/terms/termsData"; + + +const CIRCLED = ["①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨", "⑩"]; + +const Article = ({ article }: { article: TermsArticle }) => ( +
+

{article.title}

+ + { article.paragraphs?.map((text, index) => ( +

{text}

+ ))} + + { article.items && article.items.length > 0 && +
    + { article.items.map((text, index) => ( +
  1. + {index + 1}. + {text} + + { article.subItems?.[index] && +
      + { article.subItems[index].map((sub, subIndex) => ( +
    • + {CIRCLED[subIndex] ?? "-"} + {sub} +
    • + ))} +
    + } +
  2. + ))} +
+ } +
+); + +/** The terms of service text, shared by the terms page and the account change page */ +export default function TermsContent() { + return ( + <> +
+ 본 약관은 {COMPANY_NAME}(이하 ‘회사’)가 제공하는 {SERVICE_NAME}의 이용조건과 절차, 회사와 회원의 권리·의무를 정합니다. +
+ 시행일: {EFFECTIVE_DATE} +
+ +
+ { TERMS_ARTICLES.map((article, index) => ( +
+ ))} + +
+

부칙

+

+ 이 약관은 {EFFECTIVE_DATE}부터 시행합니다. +

+
+
+ + ); +} diff --git a/smartservice_frontend/src/components/auth/ChangeInfoForm.tsx b/smartservice_frontend/src/components/auth/ChangeInfoForm.tsx new file mode 100644 index 0000000..a6076dc --- /dev/null +++ b/smartservice_frontend/src/components/auth/ChangeInfoForm.tsx @@ -0,0 +1,167 @@ +"use client"; +import Input from "@/components/form/input/InputField2"; +import Label from "@/components/form/Label"; +import Button from "@/components/ui/button/Button"; +import { EyeCloseIcon, EyeIcon } from "@/icons"; +import React, { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import Checkbox from "@/components/form/input/Checkbox"; +import TermsContent from "@/components/TermsContent"; +import api, { useInitAccountStore } from '@/lib/_AG'; + + +/** Replaces the temporary ID and password of an issued account, then sends the user back to sign in */ +export default function ChangeInfoForm() { + const router = useRouter(); + const tempId = useInitAccountStore((s) => s.user_id); + const tempPw = useInitAccountStore((s) => s.user_pw); + + const [new_id, setNewId] = useState(""); + const [new_pw, setNewPw] = useState(""); + const [new_pw_confirm, setNewPwConfirm] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [isAgreed, setIsAgreed] = useState(false); + + // Only reachable right after signing in with a temporary password + useEffect(() => { + if (tempId == "" || tempPw == "") + router.replace("/signin"); + }, [tempId, tempPw, router]); + + const handleChange = () => { + if (new_id == "") { + alert("아이디를 입력하셔야 합니다."); + return; + } + if (!/^[A-Za-z0-9._-]{4,50}$/.test(new_id)) { + alert("아이디는 영문, 숫자, . _ - 로 4자 이상 입력해야 합니다."); + return; + } + if (new_pw == "") { + alert("비밀번호를 입력하셔야 합니다."); + return; + } + if (new_pw.length < 8 || !/[A-Za-z]/.test(new_pw) || !/[0-9]/.test(new_pw)) { + alert("비밀번호는 영문과 숫자를 포함해 8자 이상 입력해야 합니다."); + return; + } + if (new_pw_confirm == "") { + alert("비밀번호 확인을 입력하셔야 합니다."); + return; + } + if (new_pw !== new_pw_confirm) { + alert("비밀번호 확인이 일치하지 않습니다."); + return; + } + if (!isAgreed) { + alert("이용약관에 동의해야 서비스를 이용할 수 있습니다."); + return; + } + + const params = { + id: tempId, + pw: tempPw, + new_id: new_id, + new_pw: new_pw, + agree: true, + }; + + api.post('/api/change-init-account.do', params).then((resp) => { + if (resp.data.errCode == 0) { + useInitAccountStore.getState().clearInitAccount(); + alert("정보가 변경되었습니다. 변경된 아이디와 비밀번호로 다시 로그인하세요."); + router.replace("/signin"); + } + else if (resp.data.errCode == -21) { + alert("이미 사용 중인 아이디입니다."); + } + else if (resp.data.errCode == -22) { + alert("아이디 또는 비밀번호 형식이 올바르지 않습니다. 임시비밀번호와 같은 비밀번호는 사용할 수 없습니다."); + } + else if (resp.data.errCode == -9) { + useInitAccountStore.getState().clearInitAccount(); + alert("계정 정보를 확인할 수 없습니다. 다시 로그인하세요."); + router.replace("/signin"); + } + else { + alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode); + } + }) + .catch((err) => console.error(err)); + }; + + return ( +
+
+
+
+

+ 계정정보 변경 +

+

+ 임시비밀번호로 로그인했습니다. 사용할 아이디와 비밀번호를 입력하세요. +

+
+ +
+
+ + setNewId(e.target.value)} /> +
+
+ +
+ setNewPw(e.target.value)} + /> + setShowPassword(!showPassword)} + className="absolute z-30 -translate-y-1/2 cursor-pointer right-4 top-1/2" + > + {showPassword ? ( + + ) : ( + + )} + +
+
+
+ + setNewPwConfirm(e.target.value)} + onKeyDown={(e) => {if (e.key === "Enter") handleChange()}} + /> +
+
+ +
+ +
+
+ +
+
+
+ +
+
+
+
+
+ ); +} diff --git a/smartservice_frontend/src/components/auth/SignInForm.tsx b/smartservice_frontend/src/components/auth/SignInForm.tsx index 8d38f88..19749d7 100644 --- a/smartservice_frontend/src/components/auth/SignInForm.tsx +++ b/smartservice_frontend/src/components/auth/SignInForm.tsx @@ -9,7 +9,7 @@ import { ChevronLeftIcon, EyeCloseIcon, EyeIcon } from "@/icons"; import Link from "next/link"; import React, { useState } from "react"; import { useRouter } from "next/navigation"; -import api, { useAuthStore } from '@/lib/_AG'; +import api, { useAuthStore, useInitAccountStore } from '@/lib/_AG'; export default function SignInForm() { @@ -20,28 +20,6 @@ export default function SignInForm() { const [user_pw, setUserPw] = useState(""); const [showPassword, setShowPassword] = useState(false); const [isChecked, setIsChecked] = useState(false); - const [isSendingSms, setIsSendingSms] = useState(false); //TempCode: SMS integration test - - // TempCode: SMS integration test, remove when done - const handleSendSmsTest = () => { - setIsSendingSms(true); - - api.post('/api/send-sms-test.do') - .then((resp) => { - console.log(resp); - - const r = resp.data; - if (r.code === "0000") - alert("문자를 전송했습니다.\n수신번호: " + r.phone + "\n잔여건수: " + r.remainCount); - else - alert("문자 전송에 실패했습니다.\n코드: " + r.code + "\n메시지: " + r.msg); - }) - .catch((err) => { - console.error(err); - alert("문자 전송 요청에 실패했습니다."); - }) - .finally(() => setIsSendingSms(false)); - }; const handleLogin = () => { //router.push("/"); //TestCode @@ -63,11 +41,16 @@ export default function SignInForm() { api.post('/api/sign-in.do', params).then((resp) => { console.log(resp); - + if (resp.data.errCode == 0) { useAuthStore.getState().setAccessToken(resp.data.result.sval1); router.push("/"); } + else if (resp.data.errCode == -18) { + // Signed in with the temporary password of an issued account: the ID and password are changed first + useInitAccountStore.getState().setInitAccount(user_id, user_pw); + router.push("/change-info"); + } else if (resp.data.errCode == -9) { alert("유효하지 않은 아이디 또는 암호입니다."); } @@ -146,12 +129,6 @@ export default function SignInForm() { 로그인
- {/* TempCode: SMS integration test, remove when done */} -
- -
{/**/} diff --git a/smartservice_frontend/src/lib/_AG.ts b/smartservice_frontend/src/lib/_AG.ts index dd63240..b0bc708 100644 --- a/smartservice_frontend/src/lib/_AG.ts +++ b/smartservice_frontend/src/lib/_AG.ts @@ -66,6 +66,22 @@ export const useAuthStore = create((set: any) => ({ setAuthStatus: (status: AuthStatus) => set({ authStatus: status }), })); +// The temporary ID and password typed on the sign in page, handed to the account change page. +// Kept in memory only: a reload loses it and sends the user back to sign in. +type InitAccountState = { + user_id: string; + user_pw: string; + setInitAccount: (user_id: string, user_pw: string) => void; + clearInitAccount: () => void; +}; + +export const useInitAccountStore = create((set) => ({ + user_id: "", + user_pw: "", + setInitAccount: (user_id: string, user_pw: string) => set({ user_id, user_pw }), + clearInitAccount: () => set({ user_id: "", user_pw: "" }), +})); + type UiLoadingState = { pendingCount: number; treePendingCount: number; diff --git a/개발환경 설정 및 실행방법.txt b/개발환경 설정 및 실행방법.txt new file mode 100644 index 0000000..717f6ff --- /dev/null +++ b/개발환경 설정 및 실행방법.txt @@ -0,0 +1,56 @@ + + + + +1. DB 백업 및 복원 + +MariaDB 또는 MySQL 모두 가능 + + - hdsmartsvc.sql 파일을 PC에 설치된 DB에 복원 + + - 다음의 SQL을 실행해서 API서버의 DB접속계정과 동일한 DB계정 생성. + + - 또는 API의 서버의 로컬 DB계정을 수정해서 테스트 해도 됨. + +use mysql; +create user 'smartsvc2'@'%' identified by 'Gknehd!@wjdqhxhdtls'; +grant all privileges on hdsmartsvc.* to 'smartsvc2'@'%'; +GRANT DELETE, INSERT, SELECT, UPDATE ON hdsmartsvc.* to 'smartsvc2'@'%'; +flush privileges; + + + + +2. API 서버 (smartservice_backend) + +개발환경은 jdk-21.0.2 + +https://jdk.java.net/archive/ 에서 다운로드 + + + - PC의 환경변수에 JAVA_HOME 지정 + + - PATH 환경변수에 JAVA_HOME\bin 추가 + + - smartservice_backend 디렉토리에서 다음의 명령을 수행하면 API 서버가 실행됨. +.\gradlew bootrun + + +3. 프론트엔드 (smartservice_frontend) + +개발환경은 node.js/typescript/next.js + +PC 에 node.js 설치 + + - smartservice_frontend 디렉토리에서 다음의 명령을 수행. 최초 한번만 하면 됨. +npm install + + - 같은 디렉토리에서 다음의 명령으로 프론트엔드 서버 실행 + +npm run dev + + +백엔드서버와 프론트엔드 서버가 에러 없이 실행되면 웹브라우저에서 다음의 주소로 접속 및 테스트 가능 + +http://localhostL30000 +