diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/_AG.java b/smartservice_backend/src/main/java/com/handong/smartservice/_AG.java index 7793311..21c46d5 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/_AG.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/_AG.java @@ -195,6 +195,8 @@ 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/add-device-error.do") == 0 || uri.compareTo("/api/add-transaction.do") == 0) { return true; } diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/component/DeviceErrorCode.java b/smartservice_backend/src/main/java/com/handong/smartservice/component/DeviceErrorCode.java new file mode 100644 index 0000000..fed748f --- /dev/null +++ b/smartservice_backend/src/main/java/com/handong/smartservice/component/DeviceErrorCode.java @@ -0,0 +1,133 @@ +package com.handong.smartservice.component; + +import java.util.ArrayList; +import java.util.List; + +/** + * Decoder for the "자판기 이상 DATA (DC = 0x03)" telegram stored in device_error.error_code. + * Spec: 카드리더기_롯데_통신전문_추가사양_할부결제_요청 문서 10.3 (5 ~ 15 byte 가변) + * + * byte 1 bit0~7 : 대표이상 / 코인메카니즘 / 지폐식별기 / 물없음 / 온수저온 / 얼음품절 / 컵품절·컵걸림 / 배수부 가득참 + * byte 2 bit0~6 : System 이상 / 기타 이상 / Bucket 이상 / Door Open / 슬레이브 이상 / Reserved / Reserved + * byte 3~14 : 품절 컬럼 1 ~ 96 (byte 당 8 컬럼) + * byte 15 : 하위니블 = 품절 컬럼 97~99, 상위니블 = 컬럼 백의자리(0~9) + */ +public class DeviceErrorCode { + + public static final String STATE_FAULT = "이상"; + public static final String STATE_SOLD_OUT = "품절"; + + private static final String[] BYTE1_LABELS = { + "대표이상", + "코인메카니즘 관련 이상", + "지폐식별기 관련 이상", + "물없음", + "온수저온", + "얼음품절", + "컵품절 또는 컵걸림", + "배수부 가득참", + }; + + private static final String[] BYTE2_LABELS = { + "System 이상", + "기타 이상", + "Bucket 이상", + "Door Open", + "슬레이브 이상", + "Reserved(14)", + "Reserved(15)", + }; + + private static int[] toBytes(String error_code) { + if (error_code == null) + return new int[0]; + + String hex = error_code.trim(); + if (hex.length() < 2 || hex.length() % 2 != 0 || hex.matches(".*[^0-9a-fA-F].*")) + return new int[0]; + + int[] bytes = new int[hex.length() / 2]; + for (int i = 0; i < bytes.length; i++) + bytes[i] = Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16); + + return bytes; + } + + /** 대표이상을 제외한 단말기 이상 목록 */ + public static List getFaults(String error_code) { + int[] bytes = toBytes(error_code); + List faults = new ArrayList<>(); + if (bytes.length == 0) + return faults; + + for (int bit = 1; bit < 8; bit++) { + if ((bytes[0] & (1 << bit)) != 0) + faults.add(BYTE1_LABELS[bit]); + } + + if (bytes.length >= 2) { + for (int bit = 0; bit < 7; bit++) { + if ((bytes[1] & (1 << bit)) != 0) + faults.add(BYTE2_LABELS[bit]); + } + } + + return faults; + } + + /** 품절 컬럼번호 목록 */ + public static List getSoldOutColumns(String error_code) { + int[] bytes = toBytes(error_code); + List columns = new ArrayList<>(); + if (bytes.length == 0) + return columns; + + // The last byte carries the hundreds digit of the column number in its upper nibble + int hundreds = bytes.length >= 15 ? (bytes[14] >> 4) & 0x0f : 0; + int base = hundreds * 100; + + int last = Math.min(bytes.length, 14); + for (int index = 2; index < last; index++) { + for (int bit = 0; bit < 8; bit++) { + if ((bytes[index] & (1 << bit)) != 0) + columns.add(base + (index - 2) * 8 + bit + 1); + } + } + + if (bytes.length >= 15) { + for (int bit = 0; bit < 3; bit++) { + if ((bytes[14] & (1 << bit)) != 0) + columns.add(base + 97 + bit); + } + } + + return columns; + } + + public static boolean hasRepresentative(String error_code) { + int[] bytes = toBytes(error_code); + return bytes.length > 0 && (bytes[0] & 0x01) != 0; + } + + /** One line summary of the telegram, shown in the list screen. */ + public static String describe(String error_code) { + List parts = new ArrayList<>(getFaults(error_code)); + List columns = getSoldOutColumns(error_code); + + if (columns.size() > 0) { + StringBuilder sb = new StringBuilder("품절 (컬럼 "); + for (int i = 0; i < columns.size(); i++) { + if (i > 0) + sb.append(", "); + sb.append(columns.get(i)); + } + sb.append(")"); + parts.add(sb.toString()); + } + + if (parts.isEmpty()) + return hasRepresentative(error_code) ? "대표이상" : ""; + + return String.join(", ", parts); + } +} diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/component/SMSSender.java b/smartservice_backend/src/main/java/com/handong/smartservice/component/SMSSender.java index c1bf466..4b93754 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/component/SMSSender.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/component/SMSSender.java @@ -24,8 +24,8 @@ 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 = ""; - private static final String REMOTE_PASS = ""; + private static final String REMOTE_ID = "handong3883"; + private static final String REMOTE_PASS = "gksehd3885@"; 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"; diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/AccountController.java b/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/AccountController.java index 06f5c3e..2c1d6ec 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/AccountController.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/AccountController.java @@ -56,8 +56,9 @@ class AccountController { String user_id = body.get("user_id"); String email = body.get("email"); Long biz_group_id = _AG.toLong(body.get("biz_group_id")); + Long state = _AG.toLong(body.get("state")); - Map mapResult = accountService.getAccountList(offset, limit, date_start, date_end, is_excel, is_group_access, user_id, email, name, phone, biz_group_id); + Map mapResult = accountService.getAccountList(offset, limit, date_start, date_end, is_excel, is_group_access, user_id, email, name, phone, biz_group_id, state); if (is_excel) { List> list = (List>)mapResult.get("list"); diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/DashboardController.java b/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/DashboardController.java new file mode 100644 index 0000000..85df59b --- /dev/null +++ b/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/DashboardController.java @@ -0,0 +1,46 @@ +package com.handong.smartservice.controller.api; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.web.bind.annotation.*; + +import com.handong.smartservice._AG; +import com.handong.smartservice.component.CtResponse; +import com.handong.smartservice.service.DashboardService; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.util.Map; + +@RestController +@RequestMapping("/api") +class DashboardController { + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + private static final long DEFAULT_DAYS = 7L; + private static final long MAX_DAYS = 31L; + + private final DashboardService dashboardService; + + public DashboardController(DashboardService dashboardService) { + this.dashboardService = dashboardService; + } + + @RequestMapping(value = "/get-dashboard.do", method = RequestMethod.POST) + public CtResponse getDashboard(HttpServletRequest request, HttpServletResponse response, @RequestBody(required = false) Map body) { + logger.info("req: " + request.getRequestURI()); + + CtResponse result = new CtResponse(); + + Long days = body != null ? _AG.toLong(body.get("days")) : 0L; + if (days == null || days <= 0) + days = DEFAULT_DAYS; + if (days > MAX_DAYS) + days = MAX_DAYS; + + result.put("result", dashboardService.getDashboard(days)); + return result; + } +} diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/DeviceErrorController.java b/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/DeviceErrorController.java new file mode 100644 index 0000000..6ba383a --- /dev/null +++ b/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/DeviceErrorController.java @@ -0,0 +1,112 @@ +package com.handong.smartservice.controller.api; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.web.bind.annotation.*; + +import com.handong.smartservice._AG; +import com.handong.smartservice.component.CtExcelMaker; +import com.handong.smartservice.component.CtResponse; +import com.handong.smartservice.component.ErrorCode; +import com.handong.smartservice.service.DeviceErrorService; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api") +class DeviceErrorController { + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + private final DeviceErrorService deviceErrorService; + + public DeviceErrorController(DeviceErrorService deviceErrorService) { + this.deviceErrorService = deviceErrorService; + } + + @RequestMapping(value = "/get-device-error.do", method = RequestMethod.POST) + public CtResponse getDeviceErrorList(HttpServletRequest request, HttpServletResponse response, @RequestBody Map body) { + logger.info("req: " + request.getRequestURI()); + + CtResponse result = new CtResponse(); + + Boolean is_excel = _AG.toBoolean(body.get("is_excel")); + Long offset = _AG.toLong(body.get("offset")); + Long limit = _AG.toLong(body.get("limit")); + if (limit > 100) + limit = 100L; + + Boolean is_group_access = _AG.toBoolean(body.get("is_group_access")); + String date_start = body.get("date_start"); + String date_end = body.get("date_end"); + Long biz_group_id = _AG.toLong(body.get("biz_group_id")); + String device_name = body.get("device_name"); + String uid1 = body.get("uid1"); + Long uid1_type = _AG.toLong(body.get("uid1_type")); + String type = body.get("type"); + String error_code = body.get("error_code"); + Long error_type = _AG.toLong(body.get("error_type")); + + Map mapResult = deviceErrorService.getDeviceErrorList(is_excel, offset, limit, is_group_access, biz_group_id, date_start, date_end, + device_name, uid1, uid1_type, type, error_code, error_type); + + if (is_excel) { + List> list = (List>)mapResult.get("list"); + + for (Map item : list) { + item.replace("reg_time", _AG.timestampToStr(item.get("reg_time"))); + } + + // The vending machine type and the raw telegram are kept in the DB only, never shown + String[] arrHeader = {"발생시간", "무인기기명", "단말기 TID", "에러유형", "에러내용"}; + String[] arrColumn = {"reg_time", "device_name", "uid1", "error_type", "error_desc"}; + + CtExcelMaker.makeExcelResponse(request, response, "ERROR품절내역목록", arrHeader, arrColumn, list, null); + return null; + } + + result.put("result", mapResult); + return result; + } + + @RequestMapping(value = "/get-device-error-detail.do", method = RequestMethod.POST) + public CtResponse getDeviceErrorDetail(HttpServletRequest request, HttpServletResponse response, @RequestBody Map body) { + logger.info("req: " + request.getRequestURI()); + + CtResponse result = new CtResponse(); + + Long device_error_id = _AG.toLong(body.get("device_error_id")); + if (device_error_id == 0) { + result.setErrCode(ErrorCode.INVALID_PARAMETER); + return result; + } + + result.put("result", deviceErrorService.getDeviceErrorDetail(device_error_id)); + return result; + } + + @RequestMapping(value = "/add-device-error.do", method = RequestMethod.POST) + public CtResponse addDeviceError(HttpServletRequest request, HttpServletResponse response, @RequestBody Map body) { + logger.info("req: " + request.getRequestURI()); + + CtResponse result = new CtResponse(); + + String uid1 = body.get("uid1"); + Long uid1_type = _AG.toLong(body.get("uid1_type")); + String type = body.get("type"); + String error_code = body.get("error_code"); + Long error_type = _AG.toLong(body.get("error_type")); + + if (_AG.hasText(uid1) == false || uid1_type == null || _AG.hasText(type) == false) { + result.setErrCode(ErrorCode.INVALID_PARAMETER); + return result; + } + + result = deviceErrorService.addDeviceError(uid1, uid1_type, type, error_code, error_type); + return result; + } +} diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/SmsTestController.java b/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/SmsTestController.java new file mode 100644 index 0000000..a3c3284 --- /dev/null +++ b/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/SmsTestController.java @@ -0,0 +1,44 @@ +package com.handong.smartservice.controller.api; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.web.bind.annotation.*; + +import com.handong.smartservice.component.CtResponse; +import com.handong.smartservice.component.SMSSender; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * TEMPORARY: sends one fixed test message so the Munja114 integration can be verified + * from the sign in screen. The callback, the receiver and the body are fixed here on + * purpose - the endpoint is reachable without a token, so it must not be able to send + * an arbitrary message to an arbitrary number. Delete this class once the test is done. + */ +@RestController +@RequestMapping("/api") +class SmsTestController { + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + private static final String TEST_CALLBACK = "0314263883"; + private static final String TEST_PHONE = "01040334076"; + private static final String TEST_MESSAGE = "안녕하세요. 테스트입니다."; + + @RequestMapping(value = "/send-sms-test.do", method = RequestMethod.POST) + public CtResponse sendSmsTest(HttpServletRequest request) { + logger.info("req: " + request.getRequestURI()); + + SMSSender.Result sendResult = SMSSender.send(TEST_CALLBACK, TEST_PHONE, TEST_MESSAGE); + logger.info("send-sms-test: " + sendResult); + + CtResponse result = new CtResponse(); + result.put("code", sendResult.code); + result.put("msg", sendResult.msg); + result.put("sentCount", sendResult.sentCount); + result.put("remainCount", sendResult.remainCount); + result.put("callback", TEST_CALLBACK); + result.put("phone", TEST_PHONE); + return result; + } +} diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/TransactionController.java b/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/TransactionController.java index 6ace343..d54d9b3 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/TransactionController.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/controller/api/TransactionController.java @@ -74,8 +74,8 @@ class TransactionController { item.replace("order_time", _AG.timestampToStr(item.get("order_time"))); } - String[] arrHeader = {"거래시간", "단말기 TID", "금액", "결제유형", "승인번호", "컬럼번호", "상품코드", "상품명", "결제서비스업체"}; - String[] arrColumn = {"order_time", "uid1", "amount", "type", "approval", "column_no", "code", "goods_name", "pay_vendor"}; + String[] arrHeader = {"거래시간", "단말기 TID", "금액", "결제유형", "승인번호", "카드번호", "컬럼번호", "상품코드", "상품명", "결제서비스업체"}; + String[] arrColumn = {"order_time", "uid1", "amount", "type", "approval", "card_num", "column_no", "code", "goods_name", "pay_vendor"}; CtExcelMaker.makeExcelResponse(request, response, "거래내역목록", arrHeader, arrColumn, list, null); return null; diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/mapper/AccountMapper.java b/smartservice_backend/src/main/java/com/handong/smartservice/mapper/AccountMapper.java index b75fb66..8aaa84e 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/mapper/AccountMapper.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/mapper/AccountMapper.java @@ -13,7 +13,7 @@ public interface AccountMapper { String date_start, String date_end, Long gid, String user_id, String email, String name, String phone, Long biz_group_id); List> selectAccount2(Boolean isCount, Long offset, Long limit, - String date_start, String date_end, Boolean is_excel, Long gid, String user_id, String email, String name, String phone, Long biz_group_id); + String date_start, String date_end, Boolean is_excel, Long gid, String user_id, String email, String name, String phone, Long biz_group_id, Long state); int insertAccount(Map params); diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/mapper/DashboardMapper.java b/smartservice_backend/src/main/java/com/handong/smartservice/mapper/DashboardMapper.java new file mode 100644 index 0000000..ca46f16 --- /dev/null +++ b/smartservice_backend/src/main/java/com/handong/smartservice/mapper/DashboardMapper.java @@ -0,0 +1,25 @@ +package com.handong.smartservice.mapper; + +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; +import java.util.Map; + +/** + * Read only queries for the dashboard, all limited to a short period. + * is_all == true means the caller is a super admin and sees every group, + * otherwise the rows are limited to biz_group_id and its sub groups. + */ +@Mapper +public interface DashboardMapper { + + List> selectNoticeOfPeriod(String date_start, String date_end); + + List> selectSalesOfPeriod(Boolean is_all, Long biz_group_id, String date_start, String date_end); + + List> selectDeviceErrorOfPeriod(Boolean is_all, Long biz_group_id, String date_start, String date_end); + + List> selectAccountOfPeriod(Boolean is_all, Long biz_group_id, String date_start, String date_end); + + List> selectGoodsOfPeriod(Boolean is_all, Long biz_group_id, String date_start, String date_end); +} diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/mapper/DeviceErrorMapper.java b/smartservice_backend/src/main/java/com/handong/smartservice/mapper/DeviceErrorMapper.java new file mode 100644 index 0000000..a123356 --- /dev/null +++ b/smartservice_backend/src/main/java/com/handong/smartservice/mapper/DeviceErrorMapper.java @@ -0,0 +1,18 @@ +package com.handong.smartservice.mapper; + +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface DeviceErrorMapper { + + List> selectDeviceError(Boolean isCount, Boolean is_excel, Long offset, Long limit, Boolean is_group_access, + Long biz_group_id, String date_start, String date_end, String device_name, String uid1, Long uid1_type, String type, + String error_code, Long error_type); + + Map selectDeviceErrorById(Long device_error_id); + + int insertDeviceError(Map params); +} diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/service/AccountService.java b/smartservice_backend/src/main/java/com/handong/smartservice/service/AccountService.java index ee61078..dc1f24e 100644 --- a/smartservice_backend/src/main/java/com/handong/smartservice/service/AccountService.java +++ b/smartservice_backend/src/main/java/com/handong/smartservice/service/AccountService.java @@ -54,17 +54,17 @@ public class AccountService { return result; } - public Map getAccountList(Long offset, Long limit, String date_start, String date_end, Boolean is_excel, Boolean is_group_access, String user_id, String email, String name, String phone, Long biz_group_id) { + public Map getAccountList(Long offset, Long limit, String date_start, String date_end, Boolean is_excel, Boolean is_group_access, String user_id, String email, String name, String phone, Long biz_group_id, Long state) { Map mapResult = new HashMap<>(); //if (is_group_access) { List> listMap = (List>)accountMapper.selectAccount2(false, offset, limit, - date_start, date_end, is_excel, 0L, user_id, email, name, phone, biz_group_id); + date_start, date_end, is_excel, 0L, user_id, email, name, phone, biz_group_id, state); mapResult.put("list", listMap); Long totalCount = _AG.getNumberFromQuery("COUNT(*)", accountMapper.selectAccount2(true, offset, limit, - date_start, date_end, is_excel, 0L, user_id, email, name, phone, biz_group_id)); + date_start, date_end, is_excel, 0L, user_id, email, name, phone, biz_group_id, state)); mapResult.put("totalCount", totalCount); mapResult.put("offset", offset); return mapResult; @@ -150,7 +150,7 @@ public class AccountService { if (changed == 0) { result.setErrCode(ErrorCode.QUERY_ERROR); } - DbLogger.insert(2L, "계정 삭제, id: " + ids, 2L); + DbLogger.insert(2L, "계정 해지, id: " + ids, 2L); return result; } diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/service/DashboardService.java b/smartservice_backend/src/main/java/com/handong/smartservice/service/DashboardService.java new file mode 100644 index 0000000..1d8434e --- /dev/null +++ b/smartservice_backend/src/main/java/com/handong/smartservice/service/DashboardService.java @@ -0,0 +1,50 @@ +package com.handong.smartservice.service; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.stereotype.Service; + +import com.handong.smartservice.component.Permission; +import com.handong.smartservice.component.UserInfo; +import com.handong.smartservice.mapper.DashboardMapper; + +@Service +public class DashboardService { + + private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + private final DashboardMapper dashboardMapper; + + public DashboardService(DashboardMapper dashboardMapper) { + this.dashboardMapper = dashboardMapper; + } + + /** + * Every widget of the dashboard in one call, limited to the last {@code days} days. + * The scope comes from the signed in account, never from the request. + */ + public Map getDashboard(Long days) { + UserInfo userInfo = UserInfo.getCurr(); + + Boolean is_all = Permission.PERM_ALL.equals(userInfo.getPermission()); + Long biz_group_id = userInfo.getBizGroupId(); + + LocalDate today = LocalDate.now(); + String date_end = today.format(DATE_FORMAT); + String date_start = today.minusDays(days - 1).format(DATE_FORMAT); + + Map mapResult = new HashMap<>(); + mapResult.put("date_start", date_start); + mapResult.put("date_end", date_end); + mapResult.put("notice_list", dashboardMapper.selectNoticeOfPeriod(date_start, date_end)); + mapResult.put("sales_list", dashboardMapper.selectSalesOfPeriod(is_all, biz_group_id, date_start, date_end)); + mapResult.put("device_error_list", dashboardMapper.selectDeviceErrorOfPeriod(is_all, biz_group_id, date_start, date_end)); + mapResult.put("account_list", dashboardMapper.selectAccountOfPeriod(is_all, biz_group_id, date_start, date_end)); + mapResult.put("goods_list", dashboardMapper.selectGoodsOfPeriod(is_all, biz_group_id, date_start, date_end)); + + return mapResult; + } +} diff --git a/smartservice_backend/src/main/java/com/handong/smartservice/service/DeviceErrorService.java b/smartservice_backend/src/main/java/com/handong/smartservice/service/DeviceErrorService.java new file mode 100644 index 0000000..e4efd79 --- /dev/null +++ b/smartservice_backend/src/main/java/com/handong/smartservice/service/DeviceErrorService.java @@ -0,0 +1,156 @@ +package com.handong.smartservice.service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +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.mapper.BizGroupMapper; +import com.handong.smartservice.mapper.DeviceErrorMapper; +import com.handong.smartservice.mapper.GoodsMapper; +import com.handong.smartservice.mapper.TerminalMapper; + +@Service +public class DeviceErrorService { + + private final DeviceErrorMapper deviceErrorMapper; + private final GoodsMapper goodsMapper; + private final BizGroupMapper bizGroupMapper; + private final TerminalMapper terminalMapper; + + public DeviceErrorService(DeviceErrorMapper deviceErrorMapper, GoodsMapper goodsMapper, BizGroupMapper bizGroupMapper, TerminalMapper terminalMapper) { + this.deviceErrorMapper = deviceErrorMapper; + this.goodsMapper = goodsMapper; + this.bizGroupMapper = bizGroupMapper; + this.terminalMapper = terminalMapper; + } + + public Map getDeviceErrorList(Boolean is_excel, Long offset, Long limit, Boolean is_group_access, Long biz_group_id, + String date_start, String date_end, String device_name, String uid1, Long uid1_type, String type, + String error_code, Long error_type) { + Map mapResult = new HashMap<>(); + + List> listMap = (List>)deviceErrorMapper.selectDeviceError(false, is_excel, offset, limit, is_group_access, + biz_group_id, date_start, date_end, device_name, uid1, uid1_type, type, error_code, error_type); + + // The telegram is kept in the DB only, so the readable summary is built here + for (Map item : listMap) + item.put("error_desc", DeviceErrorCode.describe((String)item.get("error_code"))); + + mapResult.put("list", listMap); + + Long totalCount = _AG.getNumberFromQuery("COUNT(*)", deviceErrorMapper.selectDeviceError(true, is_excel, offset, limit, is_group_access, + biz_group_id, date_start, date_end, device_name, uid1, uid1_type, type, error_code, error_type)); + mapResult.put("totalCount", totalCount); + mapResult.put("offset", offset); + return mapResult; + } + + /** + * Parses the telegram of one record into a line per fault / sold out column, + * and names the goods that are registered on each sold out column. + */ + public Map getDeviceErrorDetail(Long device_error_id) { + Map mapResult = new HashMap<>(); + List> list = new ArrayList<>(); + + Map mapError = deviceErrorMapper.selectDeviceErrorById(device_error_id); + if (mapError == null) { + mapResult.put("list", list); + return mapResult; + } + + String error_code = (String)mapError.get("error_code"); + + for (String fault : DeviceErrorCode.getFaults(error_code)) { + Map item = new LinkedHashMap<>(); + item.put("detail", fault); + item.put("goods_code", null); + item.put("goods_name", null); + item.put("state", DeviceErrorCode.STATE_FAULT); + list.add(item); + } + + List columns = DeviceErrorCode.getSoldOutColumns(error_code); + if (columns.size() > 0) { + Map> mapGoods = getGoodsByColumn(toLong(mapError.get("device_id"))); + + for (Integer column : columns) { + Map goods = mapGoods.get(column.longValue()); + + Map item = new LinkedHashMap<>(); + item.put("detail", column + "번 컬럼 품절"); + item.put("goods_code", goods != null ? goods.get("code") : null); + item.put("goods_name", goods != null ? goods.get("name") : null); + item.put("state", DeviceErrorCode.STATE_SOLD_OUT); + list.add(item); + } + } + + mapResult.put("result_info", mapError); + mapResult.put("list", list); + return mapResult; + } + + // device_goods keeps the column number as text ("01", "02", ...), so it is normalized to a number + private Map> getGoodsByColumn(Long device_id) { + Map> mapGoods = new HashMap<>(); + if (device_id == 0) + return mapGoods; + + List> listGoods = goodsMapper.selectDeviceGoods(device_id, null, null); + if (listGoods == null) + return mapGoods; + + for (Map goods : listGoods) + mapGoods.put(_AG.toLong((String)goods.get("column_no")), goods); + + return mapGoods; + } + + private Long toLong(Object obj) { + return obj instanceof Number ? ((Number)obj).longValue() : 0L; + } + + public CtResponse addDeviceError(String uid1, Long uid1_type, String type, String error_code, Long error_type) { + CtResponse result = new CtResponse(); + Long biz_group_id = 0L; + Long device_id = 0L; + + // The kiosk only knows its own TID, so the group and the device are resolved from it + Map terminal = terminalMapper.selectTerminal3(uid1, uid1_type); + if (terminal != null) { + device_id = (Long)terminal.get("device_id"); + Map mapBizGroup = bizGroupMapper.selectBizGroupByDeviceId(device_id); + if (mapBizGroup != null) { + biz_group_id = (Long)mapBizGroup.get("biz_group_id"); + } + else { + Long terminal_id = (Long)terminal.get("terminal_id"); + Map mapBizGroup2 = bizGroupMapper.selectBizGroupByTerminalId(terminal_id); + if (mapBizGroup2 != null) { + biz_group_id = (Long)mapBizGroup2.get("biz_group_id"); + } + } + } + + Map params = new HashMap<>(); + params.put("uid1", uid1); + params.put("uid1_type", uid1_type); + params.put("type", type); + params.put("error_code", error_code); + params.put("error_type", error_type); + params.put("biz_group_id", biz_group_id); + params.put("device_id", device_id); + + deviceErrorMapper.insertDeviceError(params); + + return result; + } +} diff --git a/smartservice_backend/src/main/resources/mapper/AccountMapper.xml b/smartservice_backend/src/main/resources/mapper/AccountMapper.xml index e317891..3c83bd4 100644 --- a/smartservice_backend/src/main/resources/mapper/AccountMapper.xml +++ b/smartservice_backend/src/main/resources/mapper/AccountMapper.xml @@ -73,7 +73,10 @@ FROM account AS AC JOIN group_tree GT ON GT.biz_group_id = AC.biz_group_id - WHERE state != 2 + WHERE 1 = 1 + + AND AC.state = #{state} + AND #{date_start} <= DATE(AC.reg_time) @@ -96,7 +99,8 @@ AND AC.phone like CONCAT(#{phone}, '%') - ORDER BY gid DESC LIMIT #{limit} OFFSET #{offset} + /* the terminated accounts always go last */ + ORDER BY (AC.state = 2), AC.gid DESC LIMIT #{limit} OFFSET #{offset} diff --git a/smartservice_backend/src/main/resources/mapper/DashboardMapper.xml b/smartservice_backend/src/main/resources/mapper/DashboardMapper.xml new file mode 100644 index 0000000..17e984e --- /dev/null +++ b/smartservice_backend/src/main/resources/mapper/DashboardMapper.xml @@ -0,0 +1,98 @@ + + + + + + + + WITH target_group AS ( + 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 = #{biz_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 biz_group_id + FROM group_tree + ) + + + + + + + + + + + + + + diff --git a/smartservice_backend/src/main/resources/mapper/DeviceErrorMapper.xml b/smartservice_backend/src/main/resources/mapper/DeviceErrorMapper.xml new file mode 100644 index 0000000..64d6f31 --- /dev/null +++ b/smartservice_backend/src/main/resources/mapper/DeviceErrorMapper.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + INSERT INTO device_error (type, biz_group_id, device_id, uid1, uid1_type, error_code, error_type) + VALUES (#{type}, #{biz_group_id}, #{device_id}, #{uid1}, #{uid1_type}, #{error_code}, #{error_type}) + + + diff --git a/smartservice_backend/src/main/resources/mapper/TransactionMapper.xml b/smartservice_backend/src/main/resources/mapper/TransactionMapper.xml index b198bab..7150c1e 100644 --- a/smartservice_backend/src/main/resources/mapper/TransactionMapper.xml +++ b/smartservice_backend/src/main/resources/mapper/TransactionMapper.xml @@ -63,11 +63,17 @@ COUNT(*) - 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.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 FROM transactions AS T JOIN device D ON T.device_id = D.device_id + LEFT JOIN transactions ORG ON ORG.transaction_id = T.rtid WHERE diff --git a/smartservice_frontend/src/app/(admin)/Dashboard.tsx b/smartservice_frontend/src/app/(admin)/Dashboard.tsx new file mode 100644 index 0000000..48913a1 --- /dev/null +++ b/smartservice_frontend/src/app/(admin)/Dashboard.tsx @@ -0,0 +1,180 @@ +"use client" +import ComponentCard from '@/components/common/ComponentCard2'; +import PageBreadcrumb from "@/components/common/PageBreadCrumb2"; +import Badge from "@/components/ui/badge/Badge"; +import { BoxCubeIcon, DocsIcon, GroupIcon, PageIcon, PieChartIcon } from "@/icons"; + +import React, { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import api, { convertDateTime3, useAuthStore } from '@/lib/_AG'; + + +const DASHBOARD_DAYS = 7; + +type Column = { + title: string; + key: string; + align?: "left" | "center" | "right"; + renderItem?: (item: any) => React.ReactNode; +}; + +/** Small fixed table: the dashboard only shows one week, so there is no paging. */ +const DashTable = ({ columns, rows, emptyText }: { columns: Column[]; rows: any[]; emptyText: string }) => ( +
+ + + + { columns.map((col, index) => ( + + ))} + + + + { rows.length === 0 && + + + + } + { rows.map((row, rowIndex) => ( + + { columns.map((col, colIndex) => ( + + ))} + + ))} + +
+ {col.title} +
+ {emptyText} +
+ { col.renderItem ? col.renderItem(row) : (row[col.key] ?? "-") } +
+
+); + +const toNumber = (value: any) => Number(value ?? 0); +const toComma = (value: any) => toNumber(value).toLocaleString("ko-KR"); +const toDate = (value: any) => (value ? String(value).substring(0, 10) : "-"); + +export default function Dashboard() { + const router = useRouter(); + const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT + + const [ dateStart, setDateStart ] = useState(""); + const [ dateEnd, setDateEnd ] = useState(""); + const [ noticeList, setNoticeList ] = useState([]); + const [ salesList, setSalesList ] = useState([]); + const [ deviceErrorList, setDeviceErrorList ] = useState([]); + const [ accountList, setAccountList ] = useState([]); + const [ goodsList, setGoodsList ] = useState([]); + + function reqDashboard() { + api.post('/api/get-dashboard.do', { days: DASHBOARD_DAYS }).then((resp) => { + if (resp.data.errCode != 0) { + alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode); + return; + } + + const result = resp.data.result; + setDateStart(result.date_start ?? ""); + setDateEnd(result.date_end ?? ""); + setNoticeList(result.notice_list ?? []); + setSalesList(result.sales_list ?? []); + setDeviceErrorList(result.device_error_list ?? []); + setAccountList(result.account_list ?? []); + setGoodsList(result.goods_list ?? []); + }) + .catch((err) => console.error(err)); + } + + useEffect(() => { + if (tokenPayload == undefined) + return; + + reqDashboard(); + }, [tokenPayload]); + + + const salesColumns: Column[] = [ + { title: "날짜", key: "date", renderItem: (item) => item.date }, + { title: "승인", key: "approval_count", align: "right", renderItem: (item) => toComma(item.approval_count) + "건" }, + { title: "취소", key: "cancel_count", align: "right", renderItem: (item) => toComma(item.cancel_count) + "건" }, + { title: "전체", key: "total_count", align: "right", renderItem: (item) => toComma(item.total_count) + "건" }, + { title: "금액", key: "total_amount", align: "right", renderItem: (item) => toComma(item.total_amount) + "원" }, + ]; + + const deviceErrorColumns: Column[] = [ + { title: "날짜", key: "date", renderItem: (item) => item.date }, + { + title: "단말기 이상", key: "fault_count", align: "right", + renderItem: (item) => toNumber(item.fault_count) > 0 + ? {toComma(item.fault_count)}건 + : "0건", + }, + { + title: "품절", key: "sold_out_count", align: "right", + renderItem: (item) => toNumber(item.sold_out_count) > 0 + ? {toComma(item.sold_out_count)}건 + : "0건", + }, + { title: "합계", key: "total_count", align: "right", renderItem: (item) => toComma(item.total_count) + "건" }, + ]; + + const noticeColumns: Column[] = [ + { title: "등록일", key: "reg_time", renderItem: (item) => convertDateTime3(item.reg_time) }, + { title: "제목", key: "title", align: "left" }, + ]; + + const accountColumns: Column[] = [ + { title: "등록일", key: "reg_time", renderItem: (item) => toDate(item.reg_time) }, + { title: "아이디", key: "user_id" }, + { title: "이름", key: "nick_name", renderItem: (item) => item.nick_name ?? item.name ?? "-" }, + { title: "그룹(사업자)", key: "biz_group_name", align: "left" }, + ]; + + const goodsColumns: Column[] = [ + { title: "등록일", key: "reg_time", renderItem: (item) => toDate(item.reg_time) }, + { title: "상품코드", key: "code" }, + { title: "상품명", key: "name", align: "left" }, + { title: "가격", key: "price", align: "right", renderItem: (item) => toComma(item.price) + "원" }, + ]; + + const period = dateStart && dateEnd ? dateStart + " ~ " + dateEnd : ""; + + return ( +
+ + +
+
+ 최근 {DASHBOARD_DAYS}일간의 일별 현황입니다. { period && ({period}) } +
+ +
+ }> + + + + }> + + +
+ +
+ }> + + + + }> + + +
+ + }> + + +
+
+ ); +} diff --git a/smartservice_frontend/src/app/(admin)/account/Account.tsx b/smartservice_frontend/src/app/(admin)/account/Account.tsx index 8fc56fb..fc39945 100644 --- a/smartservice_frontend/src/app/(admin)/account/Account.tsx +++ b/smartservice_frontend/src/app/(admin)/account/Account.tsx @@ -23,6 +23,26 @@ import Select from "@/components/form/Select2"; import PageCountSelector from "@/components/PageCountSelector"; +const ACCOUNT_STATE_LABEL: { [key: string]: string } = { + "1": "정상", "2": "탈퇴", "3": "휴면", "4": "정지", "5": "잠김", +}; + +const ACCOUNT_STATE_OPTIONS = [ + { label: "정상", value: "1" }, + { label: "탈퇴", value: "2" }, + { label: "휴면", value: "3" }, + { label: "정지", value: "4" }, + { label: "잠김", value: "5" }, +]; + +const accountStateColor = (state: any) => { + const value = Number(state); + if (value === 1) return "success"; + if (value === 2) return "light"; + if (value === 4 || value === 5) return "error"; + return "warning"; +}; + const SEARCH_LABEL_WIDTH_PX = 80; //const COUNT_PER_PAGE = 10; const PAGE_NUM_COUNT = 5; @@ -49,6 +69,7 @@ export default function Account() { const [ user_id, setUserId ] = useState(""); const [ email, setEmail ] = useState(""); const [ nick_name, setNickName ] = useState(""); + const [ state, setState ] = useState(""); const [ date_start, setDateStart ] = useState(); const [ date_end, setDateEnd ] = useState(); @@ -70,6 +91,7 @@ export default function Account() { user_id: user_id, email: email, nick_name: nick_name, + state: state, date_start: date_start, date_end: date_end, biz_group_id: biz_group_id, @@ -200,8 +222,8 @@ export default function Account() { openModal(); }; - const handleRowClickDelete = (row: number) => { - if (confirm("'" + tableData[row].user_id + "' 계정을 삭제하시겠습니까?")) { + const handleRowClickTerminate = (row: number) => { + if (confirm("'" + tableData[row].user_id + "' 계정을 해지하시겠습니까?")) { reqRemoveAccount(tableData[row].gid); } }; @@ -251,8 +273,8 @@ export default function Account() { title: "상태", key: "state", renderItem: (item : any) => ( - - {item.state === 1 ? "정상" : "정지"} + + {ACCOUNT_STATE_LABEL[String(item.state)] ?? ""} ) }, @@ -268,13 +290,13 @@ export default function Account() { ), }, { - title: "삭제", + title: "해지", key: "_", renderItem: (item: any, row: number) => (
{handleRowClickDelete(row)}} /> + onClick={() => {handleRowClickTerminate(row)}} />
), }, @@ -323,7 +345,7 @@ export default function Account() { }>
-
+
setUserId(e.target.value)} /> @@ -333,6 +355,14 @@ export default function Account() { setNickName(e.target.value)} /> + + + + + + + +