개발환경 변경 및 설명 추가
This commit is contained in:
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
//...
|
||||
}
|
||||
|
||||
+14
-6
@@ -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);
|
||||
|
||||
+35
@@ -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);
|
||||
}
|
||||
}
|
||||
+10
@@ -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<String, String> 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<String, String> body) {
|
||||
logger.info("req: " + request.getRequestURI());
|
||||
|
||||
+41
-1
@@ -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<String, String> 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<String, String> 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<String, String> 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);
|
||||
|
||||
+4
-1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+13
-9
@@ -62,12 +62,10 @@ class DeviceController {
|
||||
|
||||
for (Map<String, String> 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<String, String> 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<String, String> body) {
|
||||
logger.info("req: " + request.getRequestURI());
|
||||
|
||||
Long device_id = _AG.toLong(body.get("device_id"));
|
||||
|
||||
Map<String, Object> mapResult = deviceService.configDevice(device_id);
|
||||
result.put("result", mapResult);
|
||||
return result;
|
||||
|
||||
return deviceService.clearDeviceEmpty(device_id);
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/error-device.do", method = RequestMethod.POST)
|
||||
|
||||
+17
-2
@@ -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<String, Object> 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;
|
||||
}
|
||||
|
||||
|
||||
-44
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Long> 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);
|
||||
}
|
||||
|
||||
+4
@@ -14,5 +14,9 @@ public interface DeviceErrorMapper {
|
||||
|
||||
Map<String, Object> selectDeviceErrorById(Long device_error_id);
|
||||
|
||||
Long selectLatestSoldOutErrorId(Long device_id);
|
||||
|
||||
int selectGroupInTree(Long root_group_id, Long biz_group_id);
|
||||
|
||||
int insertDeviceError(Map<String, Object> params);
|
||||
}
|
||||
|
||||
@@ -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<Long> ids, int state);
|
||||
|
||||
|
||||
+2
@@ -24,5 +24,7 @@ public interface TransactionMapper {
|
||||
|
||||
Map<String, Object> selectTransactionSum(Boolean is_group_access, Long biz_group_id, String date_start, String date_end);
|
||||
|
||||
Long selectOrgTransactionId(String uid1, String approval);
|
||||
|
||||
int insertTransaction(Map<String, Object> params);
|
||||
}
|
||||
|
||||
+101
-1
@@ -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<String, Object> mapAccount = (Map<String, Object>)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<String, Object> mapAccount = (Map<String, Object>)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();
|
||||
|
||||
|
||||
+89
-5
@@ -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<String, Object> 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<String, Object> 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();
|
||||
|
||||
+21
@@ -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<String, Object> 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<Long, Map<String, Object>> getGoodsByColumn(Long device_id) {
|
||||
Map<Long, Map<String, Object>> mapGoods = new HashMap<>();
|
||||
|
||||
+14
@@ -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<String, Object> selectDeviceByError(Long biz_group_id) {
|
||||
Map<String, Object> mapResult = new HashMap<>();
|
||||
|
||||
|
||||
+5
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
COUNT(*)
|
||||
</when>
|
||||
<otherwise>
|
||||
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
|
||||
</otherwise>
|
||||
</choose>
|
||||
FROM account AS AC
|
||||
@@ -141,6 +141,28 @@
|
||||
WHERE gid = #{gid}
|
||||
</update>
|
||||
|
||||
<!-- user_id is not unique in the schema, so every ID assignment checks the accounts that are not terminated -->
|
||||
<select id="countActiveUserId" resultType="int">
|
||||
SELECT COUNT(*)
|
||||
FROM account
|
||||
WHERE state != 2 AND user_id = #{user_id} AND gid != #{exclude_gid}
|
||||
</select>
|
||||
|
||||
<!-- Leaves the initialized state (6): the temporary ID and password are replaced by the user's own,
|
||||
and the terms of service agreement that the same page requires is stamped -->
|
||||
<update id="updateInitAccount">
|
||||
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>
|
||||
|
||||
<!-- Password reset: only the password changes, the account keeps its state -->
|
||||
<update id="updateResetPassword">
|
||||
UPDATE account
|
||||
SET user_pw = #{user_pw}, user_pw_time = NOW()
|
||||
WHERE gid = #{gid} AND state != 2
|
||||
</update>
|
||||
|
||||
<update id="changeState">
|
||||
<if test='ids != null and ids.size > 0'>
|
||||
UPDATE account
|
||||
|
||||
@@ -69,6 +69,38 @@
|
||||
WHERE E.device_error_id = #{device_error_id}
|
||||
</select>
|
||||
|
||||
<!-- The latest sold out record (error_type 2) of a device, behind the sold out badge of the device list -->
|
||||
<select id="selectLatestSoldOutErrorId" resultType="java.lang.Long">
|
||||
SELECT device_error_id
|
||||
FROM device_error
|
||||
WHERE device_id = #{device_id} AND error_type = 2
|
||||
ORDER BY device_error_id DESC
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<!-- 1 when biz_group_id is root_group_id or one of its descendants, same walk as the list queries -->
|
||||
<select id="selectGroupInTree" resultType="int">
|
||||
WITH RECURSIVE group_tree AS (
|
||||
SELECT
|
||||
BG1.biz_group_id,
|
||||
1 AS depth
|
||||
FROM biz_group BG1
|
||||
WHERE BG1.state != 2 AND BG1.biz_group_id = #{root_group_id}
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
BG2.biz_group_id,
|
||||
GT.depth + 1
|
||||
FROM biz_group BG2
|
||||
INNER JOIN group_tree GT ON BG2.pid = GT.biz_group_id
|
||||
WHERE BG2.state != 2 AND GT.depth < 10
|
||||
)
|
||||
SELECT COUNT(*)
|
||||
FROM group_tree
|
||||
WHERE biz_group_id = #{biz_group_id}
|
||||
</select>
|
||||
|
||||
<insert id="insertDeviceError" parameterType="map">
|
||||
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})
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
COUNT(*)
|
||||
</when>
|
||||
<otherwise>
|
||||
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
|
||||
</otherwise>
|
||||
</choose>
|
||||
FROM device AS D
|
||||
@@ -89,10 +89,17 @@
|
||||
AND T.uid1 = #{uid1}
|
||||
</if>
|
||||
<if test='conn_state != null and conn_state == "1"'>
|
||||
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)
|
||||
</if>
|
||||
<if test='conn_state != null and conn_state == "4"'>
|
||||
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
|
||||
</if>
|
||||
<!-- Sold out is shown instead of the connection state, so it is its own filter and excluded from the others -->
|
||||
<if test='conn_state != null and (conn_state == "1" or conn_state == "4")'>
|
||||
AND IFNULL(D.is_empty, 0) = 0
|
||||
</if>
|
||||
<if test='conn_state != null and conn_state == "5"'>
|
||||
AND D.is_empty = 1
|
||||
</if>
|
||||
<if test='date_start != null and date_start != ""'>
|
||||
AND #{date_start} <= DATE(D.reg_time)
|
||||
@@ -129,7 +136,7 @@
|
||||
COUNT(*)
|
||||
</when>
|
||||
<otherwise>
|
||||
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
|
||||
</otherwise>
|
||||
</choose>
|
||||
FROM device AS D
|
||||
@@ -144,10 +151,17 @@
|
||||
AND T.uid1 = #{uid1}
|
||||
</if>
|
||||
<if test='conn_state != null and conn_state == "1"'>
|
||||
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)
|
||||
</if>
|
||||
<if test='conn_state != null and conn_state == "4"'>
|
||||
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
|
||||
</if>
|
||||
<!-- Sold out is shown instead of the connection state, so it is its own filter and excluded from the others -->
|
||||
<if test='conn_state != null and (conn_state == "1" or conn_state == "4")'>
|
||||
AND IFNULL(D.is_empty, 0) = 0
|
||||
</if>
|
||||
<if test='conn_state != null and conn_state == "5"'>
|
||||
AND D.is_empty = 1
|
||||
</if>
|
||||
<if test='date_start != null and date_start != ""'>
|
||||
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>
|
||||
|
||||
<update id="updateDeviceEmptyClear">
|
||||
UPDATE device
|
||||
SET is_empty = NULL
|
||||
WHERE device_id = #{device_id}
|
||||
</update>
|
||||
|
||||
<update id="changeStateDevice">
|
||||
<if test='ids != null and ids.size > 0'>
|
||||
UPDATE device
|
||||
|
||||
@@ -65,10 +65,8 @@
|
||||
<otherwise>
|
||||
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
|
||||
</otherwise>
|
||||
</choose>
|
||||
FROM transactions AS T
|
||||
@@ -388,6 +386,15 @@
|
||||
HAVING card_count != 0 OR cash_count != 0 OR tmoney_count != 0 OR cbee_count != 0
|
||||
</select>
|
||||
|
||||
<!-- The original credit approval of a cancel: same terminal and same approval number, latest one first -->
|
||||
<select id="selectOrgTransactionId" resultType="java.lang.Long">
|
||||
SELECT transaction_id
|
||||
FROM transactions
|
||||
WHERE type = 'D1' AND uid1 = #{uid1} AND approval = #{approval}
|
||||
ORDER BY transaction_id DESC
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<insert id="insertTransaction">
|
||||
INSERT INTO transactions
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
@@ -406,6 +413,7 @@
|
||||
<if test='goods_name != null and goods_name != ""'>goods_name,</if>
|
||||
<if test='price != null and price != 0'>price,</if>
|
||||
<if test='pay_name != null and pay_name != ""'>pay_name,</if>
|
||||
<if test='rtid != null'>rtid,</if>
|
||||
<if test='pay_vendor != null and pay_vendor != ""'>pay_vendor</if>
|
||||
</trim>
|
||||
VALUES
|
||||
@@ -425,6 +433,7 @@
|
||||
<if test='goods_name != null and goods_name != ""'>#{goods_name},</if>
|
||||
<if test='price != null and price != 0'>#{price},</if>
|
||||
<if test='pay_name != null and pay_name != ""'>#{pay_name},</if>
|
||||
<if test='rtid != null'>#{rtid},</if>
|
||||
<if test='pay_vendor != null and pay_vendor != ""'>#{pay_vendor}</if>
|
||||
</trim>
|
||||
</insert>
|
||||
|
||||
@@ -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 ? "" :
|
||||
<div>
|
||||
<TrashBinIcon
|
||||
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
|
||||
|
||||
@@ -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';
|
||||
@@ -41,6 +44,8 @@ export default function AddModifyAccountModal({ multiState, onOk, isOpen, closeM
|
||||
const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT
|
||||
|
||||
const [ user_pw_confirm, setUserPwConfirm ] = useState<string>("");
|
||||
|
||||
const { isOpen: isOpenReset, openModal: openResetModal, closeModal: closeResetModal } = useModal();
|
||||
|
||||
|
||||
const handleClickAddOrModify = () => {
|
||||
@@ -102,6 +107,7 @@ export default function AddModifyAccountModal({ multiState, onOk, isOpen, closeM
|
||||
|
||||
return (
|
||||
// <div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
||||
<>
|
||||
<CtModal
|
||||
title={multiState.values.isModify ? "계정 정보 수정": "계정 등록"}
|
||||
description={multiState.values.isModify ? "수정할 계정 정보를 입력하세요." : "새로 등록할 계정 정보를 입력하세요."}
|
||||
@@ -154,15 +160,24 @@ export default function AddModifyAccountModal({ multiState, onOk, isOpen, closeM
|
||||
placeholder="선택하세요"
|
||||
options={[
|
||||
{ label: "정상", value: "1" },
|
||||
{ label: "탈퇴", value: "2" },
|
||||
{ label: "해지", value: "2" },
|
||||
{ label: "휴면", value: "3" },
|
||||
{ label: "정지", value: "4" },
|
||||
{ label: "잠김", value: "5" },
|
||||
{ label: "초기화", value: "6" },
|
||||
]}
|
||||
defaultValue={multiState.values.state} onChange={(e) => multiState.set("state", e.target.value)}
|
||||
className="dark:bg-dark-900" />
|
||||
</div>
|
||||
{ multiState.values.isModify &&
|
||||
<div>
|
||||
<Label>비밀번호 초기화</Label>
|
||||
<Button size="sm" variant="outline" onClick={openResetModal}>비밀번호 초기화</Button>
|
||||
</div>
|
||||
}
|
||||
</CtModal>
|
||||
<ResetPasswordModal gid={multiState.values.gid} phone={multiState.values.phone} isOpen={isOpenReset} closeModal={closeResetModal} />
|
||||
</>
|
||||
// </div>
|
||||
);
|
||||
};
|
||||
@@ -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<typeof useMultiState<BizGroupModalProps>>;
|
||||
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
|
||||
<Input type="text" value={multiState.values.biz_reg_num} onChange={(e) => handleChangeBizRegNum(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>전화번호</Label>
|
||||
<Label>휴대폰번호</Label>
|
||||
<Input type="text" value={multiState.values.phone} onChange={(e) => multiState.set("phone", e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
@@ -123,6 +150,12 @@ export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, close
|
||||
<Label>주소</Label>
|
||||
<Input type="text" value={multiState.values.address} onChange={(e) => multiState.set("address", e.target.value)} />
|
||||
</div>
|
||||
{ !multiState.values.isModify &&
|
||||
<div>
|
||||
<Checkbox checked={multiState.values.issue_account} onChange={(value) => multiState.set("issue_account", value)}
|
||||
label="계정발급 (사업자번호를 아이디로, 임시비밀번호를 휴대폰번호로 문자 발송)" />
|
||||
</div>
|
||||
}
|
||||
</CtModal>
|
||||
// </div>
|
||||
);
|
||||
|
||||
@@ -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<BizGroupModalProps>({isModify: false, pid: "", name: "", biz_reg_num: "", phone: "", email: "", ceo: "", address: "" });
|
||||
const modalData = useMultiState<BizGroupModalProps>({isModify: false, pid: "", name: "", biz_reg_num: "", phone: "", email: "", ceo: "", address: "", issue_account: true });
|
||||
|
||||
|
||||
|
||||
@@ -419,7 +419,7 @@ export default function BizGroupContent({
|
||||
<Input type="text" value={formInfo.email} onChange={(e) => handleChangeFormInfo("email", e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>전화번호</Label>
|
||||
<Label>휴대폰번호</Label>
|
||||
<Input type="text" value={formInfo.phone} onChange={(e) => handleChangeFormInfo("phone", e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -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
|
||||
<Input type="text" value={multiState.values.biz_reg_num} onChange={(e) => multiState.set("biz_reg_num", e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>전화번호</Label>
|
||||
<Label>휴대폰번호</Label>
|
||||
<Input type="text" value={multiState.values.phone} onChange={(e) => multiState.set("phone", e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
@@ -103,6 +129,12 @@ export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, close
|
||||
<Label>주소</Label>
|
||||
<Input type="text" value={multiState.values.address} onChange={(e) => multiState.set("address", e.target.value)} />
|
||||
</div>
|
||||
{ !multiState.values.isModify &&
|
||||
<div>
|
||||
<Checkbox checked={multiState.values.issue_account} onChange={(value) => multiState.set("issue_account", value)}
|
||||
label="계정발급 (사업자번호를 아이디로, 임시비밀번호를 휴대폰번호로 문자 발송)" />
|
||||
</div>
|
||||
}
|
||||
</CtModal>
|
||||
// </div>
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function BizGroup() {
|
||||
|
||||
// modal
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
const modalData = useMultiState<BizGroupModalProps>( {isModify: false, pid: "", name: "", biz_reg_num: "", phone: "", email: "", ceo: "", address: "" });
|
||||
const modalData = useMultiState<BizGroupModalProps>( {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() {
|
||||
</WithLabel>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
||||
<WithLabel label="전화번호" label_width={SEARCH_LABEL_WIDTH_PX}>
|
||||
<Input type="text" placeholder="조회할 전화번호를 입력하세요." value={phone} onChange={(e) => setPhone(e.target.value)} />
|
||||
<WithLabel label="휴대폰번호" label_width={SEARCH_LABEL_WIDTH_PX}>
|
||||
<Input type="text" placeholder="조회할 휴대폰번호를 입력하세요." value={phone} onChange={(e) => setPhone(e.target.value)} />
|
||||
</WithLabel>
|
||||
<WithLabel label="주소" label_width={SEARCH_LABEL_WIDTH_PX}>
|
||||
<Input type="text" placeholder="조회할 주소를 입력하세요." value={address} onChange={(e) => setAddress(e.target.value)} />
|
||||
|
||||
+14
@@ -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<string>("");
|
||||
|
||||
const { isOpen: isOpenReset, openModal: openResetModal, closeModal: closeResetModal } = useModal();
|
||||
|
||||
|
||||
const handleClickAddOrModify = () => {
|
||||
@@ -105,6 +110,7 @@ export default function AddModifyCustomerAccountModal({ multiState, onOk, isOpen
|
||||
|
||||
return (
|
||||
// <div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
||||
<>
|
||||
<CtModal
|
||||
title={multiState.values.isModify ? "계정 정보 수정": "계정 등록"}
|
||||
description={multiState.values.isModify ? "수정할 계정 정보를 입력하세요." : "새로 등록할 계정 정보를 입력하세요."}
|
||||
@@ -164,7 +170,15 @@ export default function AddModifyCustomerAccountModal({ multiState, onOk, isOpen
|
||||
defaultValue={multiState.values.state} onChange={(e) => multiState.set("state", e.target.value)}
|
||||
className="dark:bg-dark-900" />
|
||||
</div>
|
||||
{ multiState.values.isModify &&
|
||||
<div>
|
||||
<Label>비밀번호 초기화</Label>
|
||||
<Button size="sm" variant="outline" onClick={openResetModal}>비밀번호 초기화</Button>
|
||||
</div>
|
||||
}
|
||||
</CtModal>
|
||||
<ResetPasswordModal gid={multiState.values.gid} phone={multiState.values.phone} isOpen={isOpenReset} closeModal={closeResetModal} />
|
||||
</>
|
||||
// </div>
|
||||
);
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
<div>
|
||||
<Button size="sm" variant="outline" onClick={handleReboot}>카드단말기 리부팅</Button>
|
||||
<Button size="sm" variant="outline" onClick={handleResetTime}>장애시간 초기화(알람끄기)</Button>
|
||||
<Button size="sm" variant="outline" onClick={handleClearEmpty}>품절표시 끄기</Button>
|
||||
{/**
|
||||
<Button size="sm" variant="outline" onClick={handleTest1}>1</Button>
|
||||
<Button size="sm" variant="outline" onClick={handleTest2}>2</Button>
|
||||
|
||||
@@ -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<TemplateModalProps>({ biz_group_id: 0, optGoodsList: [] });
|
||||
|
||||
const { isOpen: isOpenSoldOut, openModal: openModalSoldOut, closeModal: closeModalSoldOut } = useModal();
|
||||
const [ soldOutItems, setSoldOutItems ] = useState<DeviceErrorDetailItem[]>([]);
|
||||
const modalDataSoldOut = useMultiState<ErrorDetailModalProps>({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 (
|
||||
<button type="button" className="cursor-pointer hover:opacity-80" onClick={() => handleClickSoldOut(item)}>
|
||||
<Badge size="sm" color={color}>
|
||||
{text}
|
||||
</Badge>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge size="sm" color={color}>
|
||||
{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() {
|
||||
{/*<ManageGoodsModal multiState={modalDataManageGoods} onOk={handleManageGoodsModalOk} isOpen={isOpenManageGoods} closeModal={closeModalManageGoods} />*/}
|
||||
<ManageTemplateModal multiState={modalDataManageTemplate} onOk={handleManageTemplateModalOk} isOpen={isOpenManageTemplate} closeModal={closeModalManageTemplate} />
|
||||
|
||||
<ErrorDetailModal multiState={modalDataSoldOut} items={soldOutItems} onOk={closeModalSoldOut} isOpen={isOpenSoldOut} closeModal={closeModalSoldOut} />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function AddModifyNoticeModal({ isOpen, openModal, closeModal, mu
|
||||
<Label>상태</Label>
|
||||
<select value={multiState.values.state} onChange={(e) => multiState.set("state", e.target.value)}>
|
||||
<option value={1}>정상</option>
|
||||
<option value={2}>탈퇴</option>
|
||||
<option value={2}>해지</option>
|
||||
<option value={3}>휴면</option>
|
||||
<option value={4}>정지</option>
|
||||
</select>
|
||||
|
||||
@@ -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 }) => (
|
||||
<section className="border-b border-gray-100 py-5 last:border-0 dark:border-gray-800">
|
||||
<h3 className="mb-3 text-base font-semibold text-gray-800 dark:text-white/90">{article.title}</h3>
|
||||
|
||||
{ article.paragraphs?.map((text, index) => (
|
||||
<p key={index} className="mb-3 text-sm leading-relaxed text-gray-700 dark:text-gray-300">{text}</p>
|
||||
))}
|
||||
|
||||
{ article.items && article.items.length > 0 &&
|
||||
<ol className="space-y-2">
|
||||
{ article.items.map((text, index) => (
|
||||
<li key={index} className="text-sm leading-relaxed text-gray-700 dark:text-gray-300">
|
||||
<span className="mr-1.5 text-gray-500 dark:text-gray-400">{index + 1}.</span>
|
||||
{text}
|
||||
|
||||
{ article.subItems?.[index] &&
|
||||
<ul className="mt-2 space-y-1.5 pl-5">
|
||||
{ article.subItems[index].map((sub, subIndex) => (
|
||||
<li key={subIndex} className="text-sm leading-relaxed text-gray-600 dark:text-gray-400">
|
||||
<span className="mr-1.5">{CIRCLED[subIndex] ?? "-"}</span>
|
||||
{sub}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
}
|
||||
</section>
|
||||
);
|
||||
|
||||
export default function Terms() {
|
||||
return (
|
||||
<div>
|
||||
<PageBreadcrumb pageTitle1="게시판" pageTitle2="이용약관" />
|
||||
|
||||
<ComponentCard title={SERVICE_NAME + " 이용약관"} titleIcon={<PageIcon />}>
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 text-sm text-gray-600 dark:border-gray-800 dark:bg-white/[0.03] dark:text-gray-400">
|
||||
본 약관은 {COMPANY_NAME}(이하 ‘회사’)가 제공하는 {SERVICE_NAME}의 이용조건과 절차, 회사와 회원의 권리·의무를 정합니다.
|
||||
<br />
|
||||
시행일: {EFFECTIVE_DATE}
|
||||
</div>
|
||||
|
||||
<div className="mt-2">
|
||||
{ TERMS_ARTICLES.map((article, index) => (
|
||||
<Article key={index} article={article} />
|
||||
))}
|
||||
|
||||
<section className="pt-5">
|
||||
<h3 className="mb-3 text-base font-semibold text-gray-800 dark:text-white/90">부칙</h3>
|
||||
<p className="text-sm leading-relaxed text-gray-700 dark:text-gray-300">
|
||||
이 약관은 {EFFECTIVE_DATE}부터 시행합니다.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
<TermsContent />
|
||||
</ComponentCard>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<HTMLDivElement>(null);
|
||||
|
||||
@@ -61,14 +72,22 @@ export default function ReceiptModal({ multiState, onOk, isOpen, closeModal }: V
|
||||
<div className="p-5" ref={elementRef}>
|
||||
<div className="receipt w-[80mm] bg-white text-gray-900 px-5 py-5 mx-auto border border-gray-300 shadow-md text-[13px] leading-relaxed">
|
||||
|
||||
<h2 className="text-center text-lg font-bold mb-1">[영수증]</h2>
|
||||
<h2 className="text-center text-lg font-bold mb-1">{isCancel ? "[취소 영수증]" : "[영수증]"}</h2>
|
||||
|
||||
<div className="text-center text-[11px] mb-5">
|
||||
<div className="text-left text-[11px] mb-5">
|
||||
상호: {multiState.values.biz_group_name}<br />
|
||||
주소: {multiState.values.address}<br />
|
||||
사업자등록번호: {multiState.values.biz_reg_num} | 대표자: {multiState.values.ceo}<br />
|
||||
사업자등록번호: {multiState.values.biz_reg_num}<br />
|
||||
대표자: {multiState.values.ceo}<br />
|
||||
TEL: {multiState.values.phone}<br />
|
||||
{convertDateTime6(multiState.values.order_time)}
|
||||
{isCancel ?
|
||||
<>
|
||||
취소일시: {convertDateTime6(multiState.values.order_time)}<br />
|
||||
원승인일시: {convertDateTime6(multiState.values.org_order_time)}
|
||||
</>
|
||||
:
|
||||
<>거래일시: {convertDateTime6(multiState.values.order_time)}</>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-dashed border-black my-2.5" />
|
||||
@@ -83,9 +102,9 @@ export default function ReceiptModal({ multiState, onOk, isOpen, closeModal }: V
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="py-1">{multiState.values.goods_name ? multiState.values.goods_name : "물품"}</td>
|
||||
<td className="py-1">{goodsName ? goodsName : "물품"}</td>
|
||||
<td className="py-1 text-center">1</td>
|
||||
<td className="py-1 text-right">₩ {multiState.values.amount.toLocaleString("ko-KR")}</td>
|
||||
<td className="py-1 text-right">{sign}₩ {amount.toLocaleString("ko-KR")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -96,15 +115,15 @@ export default function ReceiptModal({ multiState, onOk, isOpen, closeModal }: V
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="py-1">합계 (총 1개)</td>
|
||||
<td className="py-1 text-right text-[15px] font-bold border-t-2 border-b-2 border-black">₩ {price.toLocaleString("ko-KR")}</td>
|
||||
<td className="py-1 text-right text-[15px] font-bold">{sign}₩ {price.toLocaleString("ko-KR")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-1">부가세(10%)</td>
|
||||
<td className="py-1 text-right">₩ {tax}</td>
|
||||
<td className="py-1 text-right">{sign}₩ {tax.toLocaleString("ko-KR")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-1 text-[15px] font-bold border-t-2 border-b-2 border-black">최종 결제금액</td>
|
||||
<td className="py-1 text-right text-[15px] font-bold border-t-2 border-b-2 border-black">₩ {multiState.values.amount.toLocaleString("ko-KR")}</td>
|
||||
<td className="py-1 text-[15px] font-bold border-t-2 border-b-2 border-black">{isCancel ? "최종 취소금액" : "최종 결제금액"}</td>
|
||||
<td className="py-1 text-right text-[15px] font-bold border-t-2 border-b-2 border-black">{sign}₩ {amount.toLocaleString("ko-KR")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -117,16 +136,16 @@ export default function ReceiptModal({ multiState, onOk, isOpen, closeModal }: V
|
||||
<td className="py-1">결제수단: 신용카드 (일시불)</td>
|
||||
<td className="py-1 text-right"></td>
|
||||
</tr>
|
||||
{/*<tr>
|
||||
<td className="py-1">카드번호: 1234-****-****-1234</td>
|
||||
<tr>
|
||||
<td className="py-1">카드번호: {multiState.values.card_num}</td>
|
||||
<td className="py-1 text-right"></td>
|
||||
</tr>*/}
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-1">카드사: {multiState.values.pay_name}</td>
|
||||
<td className="py-1 text-right"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-1">승인번호: {multiState.values.approval}</td>
|
||||
<td className="py-1">{isCancel ? "원승인번호" : "승인번호"}: {multiState.values.approval}</td>
|
||||
<td className="py-1 text-right"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<div>{main}</div>
|
||||
<div className="mt-1 text-gray-400 dark:text-gray-500">{sub !== undefined && sub !== null && sub !== "" ? sub : " "}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const SEARCH_LABEL_WIDTH_PX = 80;
|
||||
//const COUNT_PER_PAGE = 10;
|
||||
@@ -111,7 +98,7 @@ export default function Transactions() {
|
||||
const [ date_end, setDateEnd ] = useState<string>(todayYMD());
|
||||
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
const modalData = useMultiState<ReceiptModalProps>({order_time: "", goods_name: "", amount: 0, pay_name: "", approval: "", biz_group_name: "", biz_reg_num: "", ceo: "", phone: "", address: ""});
|
||||
const modalData = useMultiState<ReceiptModalProps>({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<boolean>(false);
|
||||
const [ isSearchFieldOpen, setIsSearchFieldOpen ] = useState<boolean>(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) => (
|
||||
<Badge size="sm" color={
|
||||
item.type === "D1" ? "primary" :
|
||||
item.type === "I1" ? "info" :
|
||||
@@ -459,47 +447,46 @@ export default function Transactions() {
|
||||
"error"
|
||||
}>
|
||||
{PAY_TYPE_LABEL[item.type] ?? ""}
|
||||
</Badge>,
|
||||
PAY_TYPE_LABEL[item.org_type] ?? ""
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
{
|
||||
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" ?
|
||||
<div>
|
||||
<DocsIcon
|
||||
<DocsIcon
|
||||
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
|
||||
onClick={() => {handleRowClickReceipt(row)}} />
|
||||
</div>
|
||||
@@ -524,6 +511,11 @@ export default function Transactions() {
|
||||
onClick={() => {handleRowClickDelete(row)}}>
|
||||
결제취소
|
||||
</button>
|
||||
: isCreditCancel(item) ?
|
||||
<div className="text-theme-xs font-medium text-blue-light-700 dark:text-blue-light-400">
|
||||
<div>[원승인일시]</div>
|
||||
<div>{convertDateTime6(item.org_order_time)}</div>
|
||||
</div>
|
||||
: ""
|
||||
),
|
||||
},
|
||||
|
||||
@@ -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 <ChangeInfoForm />;
|
||||
}
|
||||
@@ -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<string>("");
|
||||
|
||||
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 (
|
||||
<CtModal
|
||||
title="비밀번호 초기화"
|
||||
okBtnText="임시 비밀번호 전송"
|
||||
onClickOk={handleClickSend}
|
||||
isOpen={isOpen}
|
||||
closeModal={closeModal}
|
||||
className="max-w-[500px] p-6 lg:p-10"
|
||||
>
|
||||
<div className="whitespace-pre-line text-sm text-gray-700 dark:text-gray-400">
|
||||
{"비밀번호 초기화는 사용자의 휴대폰에 임시 비밀번호를 문자로 전송합니다.\n사용자의 휴대폰 번호를 입력하세요."}
|
||||
</div>
|
||||
<div>
|
||||
<Label>휴대폰 번호</Label>
|
||||
<Input type="text" placeholder="휴대폰 번호를 입력하세요." value={inputPhone} onChange={(e) => setInputPhone(e.target.value)}
|
||||
onKeyDown={(e) => {if (e.key === "Enter") handleClickSend()}} />
|
||||
</div>
|
||||
</CtModal>
|
||||
);
|
||||
};
|
||||
@@ -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 }) => (
|
||||
<section className="border-b border-gray-100 py-5 last:border-0 dark:border-gray-800">
|
||||
<h3 className="mb-3 text-base font-semibold text-gray-800 dark:text-white/90">{article.title}</h3>
|
||||
|
||||
{ article.paragraphs?.map((text, index) => (
|
||||
<p key={index} className="mb-3 text-sm leading-relaxed text-gray-700 dark:text-gray-300">{text}</p>
|
||||
))}
|
||||
|
||||
{ article.items && article.items.length > 0 &&
|
||||
<ol className="space-y-2">
|
||||
{ article.items.map((text, index) => (
|
||||
<li key={index} className="text-sm leading-relaxed text-gray-700 dark:text-gray-300">
|
||||
<span className="mr-1.5 text-gray-500 dark:text-gray-400">{index + 1}.</span>
|
||||
{text}
|
||||
|
||||
{ article.subItems?.[index] &&
|
||||
<ul className="mt-2 space-y-1.5 pl-5">
|
||||
{ article.subItems[index].map((sub, subIndex) => (
|
||||
<li key={subIndex} className="text-sm leading-relaxed text-gray-600 dark:text-gray-400">
|
||||
<span className="mr-1.5">{CIRCLED[subIndex] ?? "-"}</span>
|
||||
{sub}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
}
|
||||
</section>
|
||||
);
|
||||
|
||||
/** The terms of service text, shared by the terms page and the account change page */
|
||||
export default function TermsContent() {
|
||||
return (
|
||||
<>
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 text-sm text-gray-600 dark:border-gray-800 dark:bg-white/[0.03] dark:text-gray-400">
|
||||
본 약관은 {COMPANY_NAME}(이하 ‘회사’)가 제공하는 {SERVICE_NAME}의 이용조건과 절차, 회사와 회원의 권리·의무를 정합니다.
|
||||
<br />
|
||||
시행일: {EFFECTIVE_DATE}
|
||||
</div>
|
||||
|
||||
<div className="mt-2">
|
||||
{ TERMS_ARTICLES.map((article, index) => (
|
||||
<Article key={index} article={article} />
|
||||
))}
|
||||
|
||||
<section className="pt-5">
|
||||
<h3 className="mb-3 text-base font-semibold text-gray-800 dark:text-white/90">부칙</h3>
|
||||
<p className="text-sm leading-relaxed text-gray-700 dark:text-gray-300">
|
||||
이 약관은 {EFFECTIVE_DATE}부터 시행합니다.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="flex flex-col flex-1 lg:w-1/2 w-full">
|
||||
<div className="flex flex-col justify-center flex-1 w-full max-w-xl mx-auto">
|
||||
<div>
|
||||
<div className="mb-5 sm:mb-8">
|
||||
<h1 className="mb-2 font-semibold text-gray-800 text-title-sm dark:text-white/90 sm:text-title-md">
|
||||
계정정보 변경
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
임시비밀번호로 로그인했습니다. 사용할 아이디와 비밀번호를 입력하세요.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label>
|
||||
아이디 <span className="text-error-500">*</span>
|
||||
</Label>
|
||||
<Input placeholder="사용할 아이디를 입력하세요" type="text" value={new_id} onChange={(e) => setNewId(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
비밀번호 <span className="text-error-500">*</span>
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="영문, 숫자 포함 8자 이상"
|
||||
value={new_pw} onChange={(e) => setNewPw(e.target.value)}
|
||||
/>
|
||||
<span
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute z-30 -translate-y-1/2 cursor-pointer right-4 top-1/2"
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeIcon className="fill-gray-500 dark:fill-gray-400" />
|
||||
) : (
|
||||
<EyeCloseIcon className="fill-gray-500 dark:fill-gray-400" />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
비밀번호 확인 <span className="text-error-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="비밀번호를 다시 입력하세요"
|
||||
value={new_pw_confirm} onChange={(e) => setNewPwConfirm(e.target.value)}
|
||||
onKeyDown={(e) => {if (e.key === "Enter") handleChange()}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
이용약관 <span className="text-error-500">*</span>
|
||||
</Label>
|
||||
<div className="max-h-[220px] overflow-y-auto custom-scrollbar rounded-lg border border-gray-200 p-4 dark:border-gray-800">
|
||||
<TermsContent />
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<Checkbox checked={isAgreed} onChange={setIsAgreed} label="이용약관에 동의합니다." />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button className="w-full" size="sm" onClick={handleChange}>
|
||||
변경
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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() {
|
||||
로그인
|
||||
</Button>
|
||||
</div>
|
||||
{/* TempCode: SMS integration test, remove when done */}
|
||||
<div>
|
||||
<Button className="w-full" size="sm" variant="outline" onClick={handleSendSmsTest} disabled={isSendingSms}>
|
||||
{isSendingSms ? "문자 전송 중..." : "문자전송 테스트"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/*</form>*/}
|
||||
|
||||
|
||||
@@ -66,6 +66,22 @@ export const useAuthStore = create<AuthState>((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<InitAccountState>((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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user