대시보드, 기기에러, 계정관리 수정
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
+133
@@ -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<String> getFaults(String error_code) {
|
||||
int[] bytes = toBytes(error_code);
|
||||
List<String> 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<Integer> getSoldOutColumns(String error_code) {
|
||||
int[] bytes = toBytes(error_code);
|
||||
List<Integer> 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<String> parts = new ArrayList<>(getFaults(error_code));
|
||||
List<Integer> 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);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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";
|
||||
|
||||
+2
-1
@@ -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<String, Object> mapResult = accountService.getAccountList(offset, limit, date_start, date_end, is_excel, is_group_access, user_id, email, name, phone, biz_group_id);
|
||||
Map<String, Object> 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<Map<String, String>> list = (List<Map<String, String>>)mapResult.get("list");
|
||||
|
||||
+46
@@ -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<String, String> 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;
|
||||
}
|
||||
}
|
||||
+112
@@ -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<String, String> 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<String, Object> 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<Map<String, String>> list = (List<Map<String, String>>)mapResult.get("list");
|
||||
|
||||
for (Map<String, String> 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<String, String> 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<String, String> 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;
|
||||
}
|
||||
}
|
||||
+44
@@ -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;
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+1
-1
@@ -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<Map<String, Object>> 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<String, Object> params);
|
||||
|
||||
|
||||
+25
@@ -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<Map<String, Object>> selectNoticeOfPeriod(String date_start, String date_end);
|
||||
|
||||
List<Map<String, Object>> selectSalesOfPeriod(Boolean is_all, Long biz_group_id, String date_start, String date_end);
|
||||
|
||||
List<Map<String, Object>> selectDeviceErrorOfPeriod(Boolean is_all, Long biz_group_id, String date_start, String date_end);
|
||||
|
||||
List<Map<String, Object>> selectAccountOfPeriod(Boolean is_all, Long biz_group_id, String date_start, String date_end);
|
||||
|
||||
List<Map<String, Object>> selectGoodsOfPeriod(Boolean is_all, Long biz_group_id, String date_start, String date_end);
|
||||
}
|
||||
+18
@@ -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<Map<String, Object>> 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<String, Object> selectDeviceErrorById(Long device_error_id);
|
||||
|
||||
int insertDeviceError(Map<String, Object> params);
|
||||
}
|
||||
+4
-4
@@ -54,17 +54,17 @@ public class AccountService {
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<String, Object> 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<String, Object> 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<String, Object> mapResult = new HashMap<>();
|
||||
|
||||
//if (is_group_access) {
|
||||
|
||||
List<Map<String, Object>> listMap = (List<Map<String, Object>>)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;
|
||||
}
|
||||
|
||||
+50
@@ -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<String, Object> 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<String, Object> 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;
|
||||
}
|
||||
}
|
||||
+156
@@ -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<String, Object> 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<String, Object> mapResult = new HashMap<>();
|
||||
|
||||
List<Map<String, Object>> listMap = (List<Map<String, Object>>)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<String, Object> 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<String, Object> getDeviceErrorDetail(Long device_error_id) {
|
||||
Map<String, Object> mapResult = new HashMap<>();
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
|
||||
Map<String, Object> 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<String, Object> 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<Integer> columns = DeviceErrorCode.getSoldOutColumns(error_code);
|
||||
if (columns.size() > 0) {
|
||||
Map<Long, Map<String, Object>> mapGoods = getGoodsByColumn(toLong(mapError.get("device_id")));
|
||||
|
||||
for (Integer column : columns) {
|
||||
Map<String, Object> goods = mapGoods.get(column.longValue());
|
||||
|
||||
Map<String, Object> 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<Long, Map<String, Object>> getGoodsByColumn(Long device_id) {
|
||||
Map<Long, Map<String, Object>> mapGoods = new HashMap<>();
|
||||
if (device_id == 0)
|
||||
return mapGoods;
|
||||
|
||||
List<Map<String, Object>> listGoods = goodsMapper.selectDeviceGoods(device_id, null, null);
|
||||
if (listGoods == null)
|
||||
return mapGoods;
|
||||
|
||||
for (Map<String, Object> 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<String, Object> terminal = terminalMapper.selectTerminal3(uid1, uid1_type);
|
||||
if (terminal != null) {
|
||||
device_id = (Long)terminal.get("device_id");
|
||||
Map<String, Object> 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<String, Object> mapBizGroup2 = bizGroupMapper.selectBizGroupByTerminalId(terminal_id);
|
||||
if (mapBizGroup2 != null) {
|
||||
biz_group_id = (Long)mapBizGroup2.get("biz_group_id");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> 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;
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,10 @@
|
||||
</choose>
|
||||
FROM account AS AC
|
||||
JOIN group_tree GT ON GT.biz_group_id = AC.biz_group_id
|
||||
WHERE state != 2
|
||||
WHERE 1 = 1
|
||||
<if test='state != null and state != 0'>
|
||||
AND AC.state = #{state}
|
||||
</if>
|
||||
<if test='date_start != null and date_start != ""'>
|
||||
AND #{date_start} <= DATE(AC.reg_time)
|
||||
</if>
|
||||
@@ -96,7 +99,8 @@
|
||||
AND AC.phone like CONCAT(#{phone}, '%')
|
||||
</if>
|
||||
<if test='isCount == false and is_excel == false'>
|
||||
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}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<mapper namespace="com.handong.smartservice.mapper.DashboardMapper">
|
||||
|
||||
<sql id="targetGroupCte">
|
||||
<if test='is_all == false'>
|
||||
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
|
||||
)
|
||||
</if>
|
||||
</sql>
|
||||
|
||||
<select id="selectNoticeOfPeriod" resultType="map">
|
||||
SELECT N.notice_id, N.reg_time, N.title
|
||||
FROM notice AS N
|
||||
WHERE N.state != 2
|
||||
AND #{date_start} <= DATE(N.reg_time) AND DATE(N.reg_time) <= #{date_end}
|
||||
ORDER BY N.reg_time DESC
|
||||
</select>
|
||||
|
||||
<select id="selectSalesOfPeriod" resultType="map">
|
||||
<include refid="targetGroupCte" />
|
||||
SELECT
|
||||
DATE_FORMAT(T.reg_time, '%Y-%m-%d') AS date,
|
||||
COUNT(*) AS total_count,
|
||||
SUM(CASE WHEN T.type IN ('D1','I1','B1','TM1','EB1') THEN 1 ELSE 0 END) AS approval_count,
|
||||
SUM(CASE WHEN T.type IN ('D4','I4','B4','TM4','EB4') THEN 1 ELSE 0 END) AS cancel_count,
|
||||
SUM(T.amount) AS total_amount
|
||||
FROM transactions AS T
|
||||
WHERE #{date_start} <= DATE(T.reg_time) AND DATE(T.reg_time) <= #{date_end}
|
||||
<if test='is_all == false'>
|
||||
AND T.biz_group_id IN ( SELECT biz_group_id FROM target_group )
|
||||
</if>
|
||||
GROUP BY DATE_FORMAT(T.reg_time, '%Y-%m-%d')
|
||||
ORDER BY date DESC
|
||||
</select>
|
||||
|
||||
<select id="selectDeviceErrorOfPeriod" resultType="map">
|
||||
<include refid="targetGroupCte" />
|
||||
SELECT
|
||||
DATE_FORMAT(E.reg_time, '%Y-%m-%d') AS date,
|
||||
SUM(CASE WHEN E.error_type = 1 THEN 1 ELSE 0 END) AS fault_count,
|
||||
SUM(CASE WHEN E.error_type = 2 THEN 1 ELSE 0 END) AS sold_out_count,
|
||||
COUNT(*) AS total_count
|
||||
FROM device_error AS E
|
||||
WHERE #{date_start} <= DATE(E.reg_time) AND DATE(E.reg_time) <= #{date_end}
|
||||
<if test='is_all == false'>
|
||||
AND E.biz_group_id IN ( SELECT biz_group_id FROM target_group )
|
||||
</if>
|
||||
GROUP BY DATE_FORMAT(E.reg_time, '%Y-%m-%d')
|
||||
ORDER BY date DESC
|
||||
</select>
|
||||
|
||||
<select id="selectAccountOfPeriod" resultType="map">
|
||||
<include refid="targetGroupCte" />
|
||||
SELECT A.gid, A.reg_time, A.user_id, A.name, A.nick_name, BG.name AS biz_group_name
|
||||
FROM account AS A
|
||||
LEFT JOIN biz_group BG ON BG.biz_group_id = A.biz_group_id
|
||||
WHERE A.state != 2
|
||||
AND #{date_start} <= DATE(A.reg_time) AND DATE(A.reg_time) <= #{date_end}
|
||||
<if test='is_all == false'>
|
||||
AND A.biz_group_id IN ( SELECT biz_group_id FROM target_group )
|
||||
</if>
|
||||
ORDER BY A.reg_time DESC
|
||||
</select>
|
||||
|
||||
<select id="selectGoodsOfPeriod" resultType="map">
|
||||
<include refid="targetGroupCte" />
|
||||
SELECT G.goods_id, G.reg_time, G.code, G.name, G.price, BG.name AS biz_group_name
|
||||
FROM goods AS G
|
||||
LEFT JOIN biz_group BG ON BG.biz_group_id = G.biz_group_id
|
||||
WHERE G.state != 2
|
||||
AND #{date_start} <= DATE(G.reg_time) AND DATE(G.reg_time) <= #{date_end}
|
||||
<if test='is_all == false'>
|
||||
AND G.biz_group_id IN ( SELECT biz_group_id FROM target_group )
|
||||
</if>
|
||||
ORDER BY G.reg_time DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,77 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<mapper namespace="com.handong.smartservice.mapper.DeviceErrorMapper">
|
||||
|
||||
<select id="selectDeviceError" resultType="map">
|
||||
<if test='is_group_access == false'>
|
||||
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
|
||||
)
|
||||
</if>
|
||||
SELECT
|
||||
<choose>
|
||||
<when test='isCount == true'>
|
||||
COUNT(*)
|
||||
</when>
|
||||
<otherwise>
|
||||
E.device_error_id, E.reg_time, E.device_id, D.name AS device_name, E.type, E.uid1, E.uid1_type,
|
||||
E.error_code, E.error_type
|
||||
</otherwise>
|
||||
</choose>
|
||||
FROM device_error AS E
|
||||
LEFT JOIN device D ON E.device_id = D.device_id
|
||||
WHERE
|
||||
<choose>
|
||||
<when test='is_group_access == false'>
|
||||
E.biz_group_id IN ( SELECT biz_group_id FROM target_group )
|
||||
</when>
|
||||
<otherwise>
|
||||
E.biz_group_id = #{biz_group_id}
|
||||
</otherwise>
|
||||
</choose>
|
||||
<if test='date_start != null and date_start != ""'>AND #{date_start} <= DATE(E.reg_time)</if>
|
||||
<if test='date_end != null and date_end != ""'>AND DATE(E.reg_time) <= #{date_end}</if>
|
||||
<if test='device_name != null and device_name != ""'>AND D.name like CONCAT('%', #{device_name}, '%')</if>
|
||||
<if test='uid1 != null and uid1 != ""'>AND E.uid1 = #{uid1}</if>
|
||||
<if test='uid1_type != null and uid1_type != 0'>AND E.uid1_type = #{uid1_type}</if>
|
||||
<if test='type != null and type != ""'>AND E.type = #{type}</if>
|
||||
<if test='error_code != null and error_code != ""'>AND E.error_code like CONCAT('%', #{error_code}, '%')</if>
|
||||
<if test='error_type != null and error_type != 0'>AND E.error_type = #{error_type}</if>
|
||||
<if test='isCount == false and is_excel == false'>
|
||||
ORDER BY E.device_error_id DESC LIMIT #{limit} OFFSET #{offset}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<select id="selectDeviceErrorById" resultType="map">
|
||||
SELECT
|
||||
E.device_error_id, E.reg_time, E.biz_group_id, E.device_id, D.name AS device_name,
|
||||
E.type, E.uid1, E.uid1_type, E.error_code, E.error_type
|
||||
FROM device_error AS E
|
||||
LEFT JOIN device D ON E.device_id = D.device_id
|
||||
WHERE E.device_error_id = #{device_error_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})
|
||||
</insert>
|
||||
|
||||
</mapper>
|
||||
@@ -63,11 +63,17 @@
|
||||
COUNT(*)
|
||||
</when>
|
||||
<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.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
|
||||
</otherwise>
|
||||
</choose>
|
||||
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
|
||||
<choose>
|
||||
<when test='is_group_access == false'>
|
||||
|
||||
@@ -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 }) => (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[420px]">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 dark:border-gray-800">
|
||||
{ columns.map((col, index) => (
|
||||
<th key={index} className={"px-3 py-2.5 text-theme-sm font-medium text-gray-600 dark:text-gray-400 text-" + (col.align ?? "center")}>
|
||||
{col.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ rows.length === 0 &&
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="px-3 py-10 text-center text-theme-sm text-gray-500 dark:text-gray-400">
|
||||
{emptyText}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
{ rows.map((row, rowIndex) => (
|
||||
<tr key={rowIndex} className="border-b border-gray-100 last:border-0 dark:border-gray-800">
|
||||
{ columns.map((col, colIndex) => (
|
||||
<td key={colIndex} className={"px-3 py-2.5 text-theme-sm text-gray-800 dark:text-white/90 text-" + (col.align ?? "center")}>
|
||||
{ col.renderItem ? col.renderItem(row) : (row[col.key] ?? "-") }
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
|
||||
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<string>("");
|
||||
const [ dateEnd, setDateEnd ] = useState<string>("");
|
||||
const [ noticeList, setNoticeList ] = useState<any[]>([]);
|
||||
const [ salesList, setSalesList ] = useState<any[]>([]);
|
||||
const [ deviceErrorList, setDeviceErrorList ] = useState<any[]>([]);
|
||||
const [ accountList, setAccountList ] = useState<any[]>([]);
|
||||
const [ goodsList, setGoodsList ] = useState<any[]>([]);
|
||||
|
||||
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
|
||||
? <Badge size="sm" color="error">{toComma(item.fault_count)}건</Badge>
|
||||
: "0건",
|
||||
},
|
||||
{
|
||||
title: "품절", key: "sold_out_count", align: "right",
|
||||
renderItem: (item) => toNumber(item.sold_out_count) > 0
|
||||
? <Badge size="sm" color="warning">{toComma(item.sold_out_count)}건</Badge>
|
||||
: "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 (
|
||||
<div>
|
||||
<PageBreadcrumb pageTitle1="홈 (대시보드)" />
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-600 dark:border-gray-800 dark:bg-white/[0.03] dark:text-gray-400">
|
||||
최근 {DASHBOARD_DAYS}일간의 일별 현황입니다. { period && <span className="font-medium text-gray-800 dark:text-white/90">({period})</span> }
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 xl:grid-cols-2">
|
||||
<ComponentCard title="매출집계 (일별)" titleIcon={<PieChartIcon />}>
|
||||
<DashTable columns={salesColumns} rows={salesList} emptyText="기간 내 거래내역이 없습니다." />
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="무인기기 ERROR/품절내역 (일별)" titleIcon={<BoxCubeIcon />}>
|
||||
<DashTable columns={deviceErrorColumns} rows={deviceErrorList} emptyText="기간 내 이상/품절 내역이 없습니다." />
|
||||
</ComponentCard>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 xl:grid-cols-2">
|
||||
<ComponentCard title="추가된 계정" titleIcon={<GroupIcon />}>
|
||||
<DashTable columns={accountColumns} rows={accountList} emptyText="기간 내 추가된 계정이 없습니다." />
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="추가된 상품" titleIcon={<DocsIcon />}>
|
||||
<DashTable columns={goodsColumns} rows={goodsList} emptyText="기간 내 추가된 상품이 없습니다." />
|
||||
</ComponentCard>
|
||||
</div>
|
||||
|
||||
<ComponentCard title="공지사항" titleIcon={<PageIcon />}>
|
||||
<DashTable columns={noticeColumns} rows={noticeList} emptyText="기간 내 등록된 공지사항이 없습니다." />
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<string>("");
|
||||
const [ email, setEmail ] = useState<string>("");
|
||||
const [ nick_name, setNickName ] = useState<string>("");
|
||||
const [ state, setState ] = useState<string>("");
|
||||
const [ date_start, setDateStart ] = useState<string>();
|
||||
const [ date_end, setDateEnd ] = useState<string>();
|
||||
|
||||
@@ -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) => (
|
||||
<Badge size="sm" color={item.state === 1 ? "success" : item.state === 4 ? "error" : "warning"}>
|
||||
{item.state === 1 ? "정상" : "정지"}
|
||||
<Badge size="sm" color={accountStateColor(item.state)}>
|
||||
{ACCOUNT_STATE_LABEL[String(item.state)] ?? ""}
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
@@ -268,13 +290,13 @@ export default function Account() {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "삭제",
|
||||
title: "해지",
|
||||
key: "_",
|
||||
renderItem: (item: any, row: number) => (
|
||||
<div>
|
||||
<TrashBinIcon
|
||||
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
|
||||
onClick={() => {handleRowClickDelete(row)}} />
|
||||
onClick={() => {handleRowClickTerminate(row)}} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -323,7 +345,7 @@ export default function Account() {
|
||||
<ComponentCard title="계정 목록 조회" titleIcon={<TableIcon />}>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-4">
|
||||
<WithLabel label="아이디" label_width={SEARCH_LABEL_WIDTH_PX}>
|
||||
<Input type="text" placeholder="조회할 아이디를 입력하세요." value={user_id} onChange={(e) => setUserId(e.target.value)} />
|
||||
</WithLabel>
|
||||
@@ -333,6 +355,14 @@ export default function Account() {
|
||||
<WithLabel label="닉네임" label_width={SEARCH_LABEL_WIDTH_PX}>
|
||||
<Input type="text" placeholder="조회할 닉네임을 입력하세요." value={nick_name} onChange={(e) => setNickName(e.target.value)} />
|
||||
</WithLabel>
|
||||
<WithLabel label="상태" label_width={SEARCH_LABEL_WIDTH_PX}>
|
||||
<Select
|
||||
placeholder="전체"
|
||||
options={ACCOUNT_STATE_OPTIONS}
|
||||
defaultValue={state}
|
||||
onChange={(e) => setState(e.target.value)}
|
||||
className="dark:bg-dark-900" />
|
||||
</WithLabel>
|
||||
</div>
|
||||
<div className="space-y-2 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<WithLabel label="등록일" label_width={50}>
|
||||
|
||||
@@ -154,7 +154,10 @@ export default function AddModifyAccountModal({ multiState, onOk, isOpen, closeM
|
||||
placeholder="선택하세요"
|
||||
options={[
|
||||
{ label: "정상", value: "1" },
|
||||
{ label: "탈퇴", value: "2" },
|
||||
{ label: "휴면", value: "3" },
|
||||
{ label: "정지", value: "4" },
|
||||
{ label: "잠김", value: "5" },
|
||||
]}
|
||||
defaultValue={multiState.values.state} onChange={(e) => multiState.set("state", e.target.value)}
|
||||
className="dark:bg-dark-900" />
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
"use client"
|
||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
|
||||
import ComponentCard from '@/components/common/ComponentCard2';
|
||||
import BizGroupPanel from "@/components/BizGroupPanel";
|
||||
import CtTable1 from '@/components/tables/CtTable1';
|
||||
import Button from '@/components/ui/button/Button2';
|
||||
import { DocsIcon, TableIcon, FilterIcon } from "@/icons";
|
||||
import WithLabel from '@/components/form/WithLabel';
|
||||
import Input from '@/components/form/input/InputField2';
|
||||
import DatePicker from '@/components/form/date-picker2';
|
||||
|
||||
import React, { Key } from "react";
|
||||
import { useEffect, useState } from 'react';
|
||||
import api, { addDaysYMD, convertDateTime3, postFileDownload, todayYMD, useAuthStore, useUiLoadingStore } from '@/lib/_AG';
|
||||
import CtTree, { CtTreeNode, findNodeByKey, findRootNodeByKey } from "@/components/CtTree";
|
||||
import { reqBizGroupTree2, reqBizGroupTree3 } from "../biz-group/BizGroupContent";
|
||||
import Checkbox from "@/components/form/input/Checkbox";
|
||||
import CtSelectedTextField from "@/components/CtSelectedText";
|
||||
import CtDrawer from "@/components/CtDrawer";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import { useMultiState } from "@/hooks/useMultiState";
|
||||
import ErrorDetailModal, { ErrorDetailModalProps, DeviceErrorDetailItem } from "./ErrorDetailModal";
|
||||
import PageCountSelector from "@/components/PageCountSelector";
|
||||
import InputSelectField from "@/components/form/input/InputSelectField";
|
||||
import Select from "@/components/form/Select2";
|
||||
import { deviceErrorTypeLabel } from "@/lib/deviceErrorCode";
|
||||
|
||||
|
||||
const SEARCH_LABEL_WIDTH_PX = 80;
|
||||
//const COUNT_PER_PAGE = 10;
|
||||
const PAGE_NUM_COUNT = 5;
|
||||
|
||||
|
||||
export default function DeviceError() {
|
||||
const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT
|
||||
const isLoadingGlobal = useUiLoadingStore((s) => s.isLoading);
|
||||
|
||||
const [ tableData, setTableData ] = useState<any[]>([]);
|
||||
const [ tableOffset, setTableOffset ] = useState<number>(0);
|
||||
const [ tableTotalCount, setTableTotalCount ] = useState<number>(0);
|
||||
|
||||
const [ COUNT_PER_PAGE, setCOUNT_PER_PAGE ] = useState<number>(20);
|
||||
|
||||
const [ treeData, setTreeData ] = useState<CtTreeNode[]>([]);
|
||||
const [ selectedTreeKeys, setSelectedTreeKeys ] = useState<Key[]>([]);
|
||||
const [ expandedTreeKeys, setExpandedTreeKeys ] = useState<Key[]>([]);
|
||||
const [ isGroupAccess, setGroupAccess ] = useState(tokenPayload?.permission == 0xffffffff); //TempCode
|
||||
const [ isTreeOpen, setTreeOpen ] = useState(tokenPayload?.permission == 0xffffffff); //TempCode
|
||||
|
||||
// group search fields
|
||||
const [ name, setName ] = useState<string>("");
|
||||
const [ biz_reg_num, setBizRegNum ] = useState<string>("");
|
||||
|
||||
//search fields
|
||||
const [ device_name, setDeviceName ] = useState<string>("");
|
||||
const [ uid1, setUid1 ] = useState<string>("");
|
||||
const [ list_uid1, setListUid1 ] = useState<{label: string; value: string}[]>([]);
|
||||
const [ error_type, setErrorType ] = useState<string>("");
|
||||
const [ date_start, setDateStart ] = useState<string>(addDaysYMD(-7));
|
||||
const [ date_end, setDateEnd ] = useState<string>(todayYMD());
|
||||
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
const [ modalItems, setModalItems ] = useState<DeviceErrorDetailItem[]>([]);
|
||||
const modalData = useMultiState<ErrorDetailModalProps>({device_error_id: 0, reg_time: "", device_name: "", uid1: "", uid1_type: 0, type: "", error_type: 0, error_code: "", biz_group_name: ""});
|
||||
|
||||
const [ isMobileMode, setIsMobileMode ] = useState<boolean>(false);
|
||||
const [ isSearchFieldOpen, setIsSearchFieldOpen ] = useState<boolean>(false);
|
||||
|
||||
const [ listSelectedText, setListSelectedText ] = useState<string[]>([]);
|
||||
const changeListSelectedText = (index: number, text: string) => {
|
||||
setListSelectedText(prev => {
|
||||
const copy = [...prev];
|
||||
copy[index] = text;
|
||||
return copy;
|
||||
});
|
||||
}
|
||||
|
||||
//function part
|
||||
//
|
||||
function reqUid1List() {
|
||||
api.post('/api/list-uid1.do').then((resp) => {
|
||||
console.log(resp);
|
||||
|
||||
if (resp.data.errCode == 0) {
|
||||
if (resp.data.result.list.length == 0) {
|
||||
setListUid1([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setListUid1(
|
||||
resp.data.result.list.map((item: { uid1: string; name: string }) => ({
|
||||
label: item.uid1 + (item.name !== "" ? " (" + item.name + ")" : ""),
|
||||
value: item.uid1,
|
||||
}))
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(err))
|
||||
}
|
||||
|
||||
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 != 0) {
|
||||
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
|
||||
return;
|
||||
}
|
||||
|
||||
setModalItems(resp.data.result.list ?? []);
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
}
|
||||
|
||||
function reqDeviceErrorList(is_excel: boolean, biz_group_id: string, groupAccess: boolean) {
|
||||
if (!biz_group_id)
|
||||
return;
|
||||
|
||||
const params = {
|
||||
is_group_access: groupAccess,
|
||||
is_excel: is_excel,
|
||||
offset: tableOffset,
|
||||
limit: COUNT_PER_PAGE,
|
||||
device_name: device_name,
|
||||
uid1: uid1,
|
||||
error_type: error_type,
|
||||
date_start: date_start,
|
||||
date_end: date_end,
|
||||
biz_group_id: biz_group_id,
|
||||
}
|
||||
|
||||
if (is_excel == true) {
|
||||
postFileDownload('/api/get-device-error.do', params);
|
||||
return;
|
||||
}
|
||||
|
||||
api.post('/api/get-device-error.do', params).then((resp) => {
|
||||
console.log(resp);
|
||||
|
||||
if (resp.data.errCode === 0) {
|
||||
if (resp.data.result.list.length == 0) {
|
||||
alert("조회 결과가 없습니다.");
|
||||
setTableTotalCount(0);
|
||||
setTableData([]);
|
||||
return;
|
||||
}
|
||||
setTableTotalCount(resp.data.result.totalCount);
|
||||
setTableData(resp.data.result.list);
|
||||
}
|
||||
else {
|
||||
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
}
|
||||
|
||||
|
||||
// event processing part
|
||||
//
|
||||
// tree part //////////
|
||||
const handleClickGroupSearch = () => {
|
||||
if (tokenPayload?.permission == 0xffffffff) {
|
||||
if (name == "" && biz_reg_num == "") {
|
||||
alert("그룹(사업자)명 또는 사업자등록번호를 입력하셔야 합니다.");
|
||||
return;
|
||||
}
|
||||
if (name != "" && name.length < 2) {
|
||||
alert("그룹(사업자)명은 두글자 이상 입력해야 합니다.");
|
||||
return;
|
||||
}
|
||||
if (biz_reg_num != "" && biz_reg_num.length < 2) {
|
||||
alert("사업자등록번호는 두글자 이상 입력해야 합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
reqBizGroupTree2(true, "", "", "", "", name, biz_reg_num, treeData, setTreeData);
|
||||
}
|
||||
else {
|
||||
reqBizGroupTree3(String(tokenPayload?.biz_group_id), setTreeData, setSelectedTreeKeys, setExpandedTreeKeys, (resp: any) => {
|
||||
reqUid1List();
|
||||
|
||||
reqDeviceErrorList(false, String(resp.data.result.tree[0].key), isGroupAccess);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickNode = (key: Key) => {
|
||||
if (isGroupAccess == false) {
|
||||
const root = findRootNodeByKey(treeData, key as string);
|
||||
reqDeviceErrorList(false, root.key as string, isGroupAccess);
|
||||
return;
|
||||
}
|
||||
|
||||
setTableOffset(0);
|
||||
reqDeviceErrorList(false, key as string, isGroupAccess);
|
||||
}
|
||||
|
||||
const handleClickTreeExpanded = (key: Key) => {
|
||||
const node = findNodeByKey(treeData, key);
|
||||
if (node.children == undefined)
|
||||
reqBizGroupTree2(false, key as string, "", "", "", "", "2", treeData, setTreeData);
|
||||
}
|
||||
|
||||
const handleClickGroupAccess = (value: boolean) => {
|
||||
setGroupAccess(value);
|
||||
|
||||
if (tokenPayload?.permission == 0xffffffff) {
|
||||
if (selectedTreeKeys.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
reqDeviceErrorList(false, selectedTreeKeys[0] as string, value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value == false || selectedTreeKeys.length <= 0) {
|
||||
reqDeviceErrorList(false, String(tokenPayload?.biz_group_id), value);
|
||||
}
|
||||
else {
|
||||
reqDeviceErrorList(false, selectedTreeKeys[0] as string, value);
|
||||
}
|
||||
}
|
||||
// tree part //////////
|
||||
|
||||
const handleClickDownload = () => {
|
||||
reqDeviceErrorList(true, selectedTreeKeys[0] as string, isGroupAccess);
|
||||
};
|
||||
|
||||
const handleClickSearch = () => {
|
||||
if (selectedTreeKeys.length <= 0) {
|
||||
alert("조회하려면 사업자를 선택해야 합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
reqDeviceErrorList(false, selectedTreeKeys[0] as string, isGroupAccess);
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setTableOffset((page - 1) * COUNT_PER_PAGE);
|
||||
};
|
||||
|
||||
const handleRowClickErrorDetail = (row: number) => {
|
||||
// The group name is only shown as context, so a missing tree node must not block the detail
|
||||
const selectedKey = selectedTreeKeys[0];
|
||||
const node = selectedKey != undefined ? findRootNodeByKey(treeData, String(selectedKey)) : null;
|
||||
|
||||
modalData.setAll({...tableData[row],
|
||||
biz_group_name: node?.name ?? "",
|
||||
});
|
||||
|
||||
// The telegram is parsed on the server, which also names the goods of every sold out column
|
||||
setModalItems([]);
|
||||
reqDeviceErrorDetail(tableData[row].device_error_id);
|
||||
|
||||
openModal();
|
||||
};
|
||||
|
||||
const handleModalOk = () => {
|
||||
closeModal();
|
||||
}
|
||||
//
|
||||
const handleChangeDeviceName = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDeviceName(e.target.value);
|
||||
changeListSelectedText(0, e.target.value);
|
||||
}
|
||||
|
||||
const handleChangeUid1 = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUid1(e.target.value);
|
||||
changeListSelectedText(1, e.target.value);
|
||||
}
|
||||
|
||||
const handleChangeErrorType = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setErrorType(e.target.value);
|
||||
changeListSelectedText(2, e.target.options[e.target.selectedIndex].text);
|
||||
}
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (tokenPayload == undefined)
|
||||
return;
|
||||
|
||||
if (tokenPayload?.permission == 0xffffffff)
|
||||
return;
|
||||
|
||||
reqBizGroupTree3(String(tokenPayload?.biz_group_id), setTreeData, setSelectedTreeKeys, setExpandedTreeKeys, (resp: any) => {
|
||||
reqUid1List();
|
||||
|
||||
setGroupAccess(tokenPayload?.permission == 0xffffffff);
|
||||
setTreeOpen(tokenPayload?.permission == 0xffffffff);
|
||||
|
||||
reqDeviceErrorList(false, String(resp.data.result.tree[0].key), tokenPayload?.permission == 0xffffffff);
|
||||
});
|
||||
}, [tokenPayload]);
|
||||
|
||||
useEffect(() => {
|
||||
reqDeviceErrorList(false, selectedTreeKeys[0] as string, isGroupAccess);
|
||||
}, [tableOffset]);
|
||||
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "발생시간",
|
||||
key: "reg_time",
|
||||
renderItem: (item: any, row: number) => convertDateTime3(item.reg_time)
|
||||
},
|
||||
{
|
||||
title: "무인기기명",
|
||||
key: "device_name",
|
||||
},
|
||||
{
|
||||
title: "단말기 TID",
|
||||
key: "uid1",
|
||||
renderItem: (item: any, row: number) => item.uid1 ? (item.uid1 + " (" + (item.uid1_type == 1 ? "KICC" : "NICE") + ")") : "",
|
||||
viewMobile: true,
|
||||
},
|
||||
{
|
||||
title: "에러유형",
|
||||
key: "error_type",
|
||||
renderItem: (item: any, row: number) => deviceErrorTypeLabel(item.error_type),
|
||||
viewMobile: true,
|
||||
},
|
||||
{
|
||||
title: "에러내용",
|
||||
key: "error_desc",
|
||||
viewMobile: true,
|
||||
},
|
||||
{
|
||||
title: "상세내역",
|
||||
key: "_",
|
||||
renderItem: (item: any, row: number) => (
|
||||
<button className="inline-flex items-center px-1 py-0.5 justify-center gap-1 rounded font-medium text-theme-xs bg-error-50 text-error-600 dark:bg-error-500/15 dark:text-error-500"
|
||||
onClick={() => {handleRowClickErrorDetail(row)}}>
|
||||
상세정보
|
||||
</button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageBreadcrumb pageTitle1="무인기기 ERROR/품절내역" />
|
||||
|
||||
<div className="space-y-3">
|
||||
<BizGroupPanel
|
||||
title="사업자 그룹별로 관리"
|
||||
isOpen={isTreeOpen}
|
||||
setOpen={setTreeOpen}
|
||||
name={name}
|
||||
bizRegNum={biz_reg_num}
|
||||
onChangeName={setName}
|
||||
onChangeBizRegNum={setBizRegNum}
|
||||
onClickSearch={handleClickGroupSearch}
|
||||
isDisabled={isLoadingGlobal}
|
||||
searchLabelWidth={SEARCH_LABEL_WIDTH_PX}
|
||||
>
|
||||
<CtTree
|
||||
treeData={treeData}
|
||||
selectedKeys={selectedTreeKeys}
|
||||
setSelectedKeys={setSelectedTreeKeys}
|
||||
onClickNode={handleClickNode}
|
||||
expandedKeys={expandedTreeKeys}
|
||||
setExpandedKeys={setExpandedTreeKeys}
|
||||
onClickExpanded={handleClickTreeExpanded}
|
||||
/>
|
||||
</BizGroupPanel>
|
||||
|
||||
<ComponentCard title="ERROR/품절내역 목록" titleIcon={<TableIcon />}>
|
||||
|
||||
<CtDrawer title="상세 조회" isMobileMode={isMobileMode} setIsMobileMode={setIsMobileMode} isSearchFieldOpen={isSearchFieldOpen} setIsSearchFieldOpen={setIsSearchFieldOpen}>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-4">
|
||||
<WithLabel label="무인기기명" label_width={SEARCH_LABEL_WIDTH_PX}>
|
||||
<Input type="text" placeholder="무인기기명을 입력하세요." value={device_name} onChange={handleChangeDeviceName} />
|
||||
</WithLabel>
|
||||
<WithLabel label="단말기 TID" label_width={SEARCH_LABEL_WIDTH_PX}>
|
||||
<InputSelectField type="text" placeholder="TID를 입력하세요." value={uid1} options={list_uid1} onChange={handleChangeUid1} />
|
||||
</WithLabel>
|
||||
<WithLabel label="에러유형" label_width={SEARCH_LABEL_WIDTH_PX}>
|
||||
<Select
|
||||
placeholder="전체"
|
||||
options={[
|
||||
{ label: "단말기 이상", value: "1" },
|
||||
{ label: "품절", value: "2" },
|
||||
]}
|
||||
defaultValue={error_type} onChange={handleChangeErrorType}
|
||||
className="dark:bg-dark-900" />
|
||||
</WithLabel>
|
||||
</div>
|
||||
</div>
|
||||
</CtDrawer>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<WithLabel label="발생일" label_width={50}>
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-2">
|
||||
<DatePicker
|
||||
id="date_start-picker"
|
||||
placeholder="조회 시작일"
|
||||
onChange={(dates, currentDateString) => { setDateStart(currentDateString); }}
|
||||
defaultDate={date_start}
|
||||
/>
|
||||
<DatePicker
|
||||
id="date_end-picker"
|
||||
placeholder="조회 종료일"
|
||||
onChange={(dates, currentDateString) => { setDateEnd(currentDateString); }}
|
||||
defaultDate={date_end}
|
||||
/>
|
||||
</div>
|
||||
</WithLabel>
|
||||
<div className="flex items-center justify-end gap-5">
|
||||
<Button size="sm" variant="outline" startIcon={<DocsIcon />} onClick={handleClickDownload}>엑셀파일 다운로드</Button>
|
||||
{ isMobileMode && <Button size="sm" variant="outline" startIcon={<FilterIcon />} onClick={() => setIsSearchFieldOpen(true)}>필터</Button> }
|
||||
<Button size="sm" variant="primary" onClick={handleClickSearch} disabled={isLoadingGlobal}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-5">
|
||||
{ isTreeOpen && <Checkbox checked={isGroupAccess} onChange={(value) => handleClickGroupAccess(value)} label="그룹단위로 조회" /> }
|
||||
<PageCountSelector defaultValue={COUNT_PER_PAGE.toString()} onChange={(e) => setCOUNT_PER_PAGE(parseInt(e.target.value))} />
|
||||
</div>
|
||||
|
||||
{ isMobileMode &&
|
||||
<CtSelectedTextField label={"필터:"} listText={listSelectedText} setListText={setListSelectedText}></CtSelectedTextField>
|
||||
}
|
||||
|
||||
<CtTable1 columns={columns} bodyData={tableData}
|
||||
offset={tableOffset} totalCount={tableTotalCount} countPerPage={COUNT_PER_PAGE} pageNumCount={PAGE_NUM_COUNT}
|
||||
onClickPageChange={handlePageChange} />
|
||||
|
||||
</ComponentCard>
|
||||
|
||||
<ErrorDetailModal multiState={modalData} items={modalItems} onOk={handleModalOk} isOpen={isOpen} closeModal={closeModal} />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import CtModal from "@/components/CtModal";
|
||||
import Badge from "@/components/ui/badge/Badge";
|
||||
import { useMultiState } from "@/hooks/useMultiState";
|
||||
|
||||
|
||||
export interface ErrorDetailModalProps {
|
||||
device_error_id: number;
|
||||
reg_time: string;
|
||||
device_name: string;
|
||||
uid1: string;
|
||||
uid1_type: number;
|
||||
type: string;
|
||||
error_type: number;
|
||||
error_code: string;
|
||||
biz_group_name: string;
|
||||
}
|
||||
|
||||
/** One decoded line of the telegram, built by /api/get-device-error-detail.do */
|
||||
export interface DeviceErrorDetailItem {
|
||||
detail: string;
|
||||
goods_code: string | null;
|
||||
goods_name: string | null;
|
||||
state: string;
|
||||
}
|
||||
|
||||
|
||||
interface ViewErrorDetailModalProps {
|
||||
multiState: ReturnType<typeof useMultiState<ErrorDetailModalProps>>;
|
||||
items: DeviceErrorDetailItem[];
|
||||
onOk: () => void;
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const pad2 = (value: number) => String(value).padStart(2, "0");
|
||||
|
||||
const splitRegTime = (reg_time: string) => {
|
||||
if (!reg_time)
|
||||
return { date: "", time: "" };
|
||||
|
||||
const d = new Date(reg_time);
|
||||
if (isNaN(d.getTime()))
|
||||
return { date: reg_time, time: "" };
|
||||
|
||||
return {
|
||||
date: d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate()),
|
||||
time: pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds()),
|
||||
};
|
||||
};
|
||||
|
||||
const goodsText = (item: DeviceErrorDetailItem) => {
|
||||
if (!item.goods_name)
|
||||
return "-";
|
||||
|
||||
return item.goods_name + (item.goods_code ? " (" + item.goods_code + ")" : "");
|
||||
};
|
||||
|
||||
const HeaderField = ({ label, value }: { label: string; value: React.ReactNode }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="shrink-0 text-sm text-gray-500 dark:text-gray-400">{label}</span>
|
||||
<span className="min-w-0 flex-1 truncate rounded-lg border border-gray-200 bg-gray-50 px-3 py-1.5 text-sm text-gray-800 dark:border-gray-800 dark:bg-white/[0.03] dark:text-white/90">
|
||||
{value !== null && value !== undefined && value !== "" ? value : "-"}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function ErrorDetailModal({ multiState, items, onOk, isOpen, closeModal }: ViewErrorDetailModalProps) {
|
||||
const values = multiState.values;
|
||||
const { date, time } = splitRegTime(values.reg_time);
|
||||
|
||||
return (
|
||||
<CtModal
|
||||
title="자판기 상세내역"
|
||||
isOpen={isOpen}
|
||||
closeModal={closeModal}
|
||||
className="max-w-[700px] p-6 lg:p-10"
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<HeaderField label="발생일자" value={date} />
|
||||
<HeaderField label="발생시간" value={time} />
|
||||
<HeaderField label="무인기기명" value={values.device_name} />
|
||||
<HeaderField label="단말기 TID" value={values.uid1 ? values.uid1 + (values.uid1_type ? " (" + (values.uid1_type == 1 ? "KICC" : "NICE") + ")" : "") : ""} />
|
||||
<HeaderField label="그룹(사업자)" value={values.biz_group_name} />
|
||||
</div>
|
||||
|
||||
<div className="mt-4 overflow-hidden rounded-lg border border-gray-200 dark:border-gray-800">
|
||||
<div className="max-h-[320px] overflow-y-auto custom-scrollbar">
|
||||
<table className="w-full">
|
||||
<thead className="sticky top-0 bg-gray-50 dark:bg-white/[0.03]">
|
||||
<tr>
|
||||
<th className="border-b border-gray-200 px-4 py-2.5 text-center text-theme-sm font-medium text-gray-700 dark:border-gray-800 dark:text-gray-300">상세 내역</th>
|
||||
<th className="border-b border-gray-200 px-4 py-2.5 text-center text-theme-sm font-medium text-gray-700 dark:border-gray-800 dark:text-gray-300">상품명(상품코드)</th>
|
||||
<th className="w-28 border-b border-gray-200 px-4 py-2.5 text-center text-theme-sm font-medium text-gray-700 dark:border-gray-800 dark:text-gray-300">이상 유무</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ items.length === 0 &&
|
||||
<tr>
|
||||
<td colSpan={3} className="px-4 py-10 text-center text-theme-sm text-gray-500 dark:text-gray-400">
|
||||
이상 내역이 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
{ items.map((item, index) => (
|
||||
<tr key={index} className="border-b border-gray-100 last:border-0 dark:border-gray-800">
|
||||
<td className="px-4 py-2.5 text-center text-theme-sm text-gray-800 dark:text-white/90">{item.detail}</td>
|
||||
<td className="px-4 py-2.5 text-center text-theme-sm text-gray-600 dark:text-gray-400">{goodsText(item)}</td>
|
||||
<td className="px-4 py-2.5 text-center">
|
||||
<Badge size="sm" color={item.state === "품절" ? "warning" : "error"}>{item.state}</Badge>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</CtModal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import DeviceError from './DeviceError';
|
||||
import { Metadata } from "next";
|
||||
import React from "react";
|
||||
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "무인기기 ERROR/품절내역",
|
||||
description: "무인기기 ERROR/품절내역",
|
||||
};
|
||||
|
||||
export default function DeviceErrorPage() {
|
||||
return (
|
||||
<DeviceError />
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import { EcommerceMetrics } from "@/components/ecommerce/EcommerceMetrics";
|
||||
import React from "react";
|
||||
import MonthlyTarget from "@/components/ecommerce/MonthlyTarget";
|
||||
import MonthlySalesChart from "@/components/ecommerce/MonthlySalesChart";
|
||||
import StatisticsChart from "@/components/ecommerce/StatisticsChart";
|
||||
import RecentOrders from "@/components/ecommerce/RecentOrders";
|
||||
import DemographicCard from "@/components/ecommerce/DemographicCard";
|
||||
import Dashboard from "./Dashboard";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title:
|
||||
@@ -13,24 +8,8 @@ export const metadata: Metadata = {
|
||||
description: "한동정보통신 스마트서비스",
|
||||
};
|
||||
|
||||
export default function Dashboard() {
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<div className="grid grid-cols-12 gap-4 md:gap-6">
|
||||
<div className="col-span-12 space-y-6 xl:col-span-7">
|
||||
<EcommerceMetrics />
|
||||
|
||||
<MonthlySalesChart />
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 xl:col-span-5">
|
||||
<MonthlyTarget />
|
||||
</div>
|
||||
|
||||
<div className="col-span-12">
|
||||
<StatisticsChart />
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<Dashboard />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"use client"
|
||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
|
||||
import ComponentCard from '@/components/common/ComponentCard2';
|
||||
import { PageIcon } from "@/icons";
|
||||
|
||||
import React from "react";
|
||||
import { COMPANY_NAME, EFFECTIVE_DATE, SERVICE_NAME, TERMS_ARTICLES, TermsArticle } 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>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import Terms from './Terms';
|
||||
import { Metadata } from "next";
|
||||
import React from "react";
|
||||
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "이용약관",
|
||||
description: "이용약관",
|
||||
};
|
||||
|
||||
export default function TermsPage() {
|
||||
return (
|
||||
<Terms />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// 한동스마트서비스 이용약관
|
||||
// 이지샵(www.easyshop.co.kr) 서비스 이용약관의 구성을 참고하여, 무인기기 관리 서비스에 맞게
|
||||
// 조항을 재구성한 것이다. 시행일과 세부 요금/환불 정책은 운영 정책에 맞추어 조정한다.
|
||||
|
||||
export const COMPANY_NAME = "한동정보통신";
|
||||
export const SERVICE_NAME = "한동스마트서비스";
|
||||
export const EFFECTIVE_DATE = "2026년 9월 10일";
|
||||
|
||||
export type TermsArticle = {
|
||||
title: string;
|
||||
paragraphs?: string[]; // 번호 없는 본문
|
||||
items?: string[]; // 1. 2. 3. 형태의 항
|
||||
subItems?: { [index: number]: string[] }; // 해당 항 아래의 ① ② ③ 목
|
||||
};
|
||||
|
||||
export const TERMS_ARTICLES: TermsArticle[] = [
|
||||
{
|
||||
title: "제1조 (목적)",
|
||||
paragraphs: [
|
||||
`이 약관은 ${COMPANY_NAME}(이하 '회사'라 합니다)가 제공하는 ${SERVICE_NAME}(이하 '서비스'라 합니다)를 이용함에 있어 회사와 이용회원(이하 '회원'이라 합니다) 간의 권리·의무 및 책임사항, 기타 필요한 사항을 규정함을 목적으로 합니다.`,
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제2조 (용어의 정의)",
|
||||
paragraphs: ["이 약관에서 사용하는 용어의 정의는 다음과 같습니다."],
|
||||
items: [
|
||||
"'서비스'라 함은 회사가 웹 및 모바일 환경을 통하여 제공하는 무인기기 원격관리 서비스를 말하며, 무인기기 및 카드단말기 등록·관리, 거래내역 조회, 매출 집계, 상품 및 재고 관리, 기기 이상·품절 내역 조회, 알림 발송 등을 포함합니다.",
|
||||
"'회원'이라 함은 이 약관에 동의하고 회사와 이용계약을 체결하여 서비스를 이용하는 사업자 또는 그 사업자가 지정한 사용자를 말합니다.",
|
||||
"'무인기기'라 함은 회원이 운영하는 자동판매기 등 회사의 서비스에 등록된 기기를 말합니다.",
|
||||
"'단말기'라 함은 무인기기에 연결되어 결제를 처리하는 카드단말기를 말하며, 단말기 고유번호(TID)로 식별합니다.",
|
||||
"'계정'이라 함은 회원의 식별과 서비스 이용을 위하여 회사가 부여한 아이디와 비밀번호의 조합을 말합니다.",
|
||||
"'사업자 그룹'이라 함은 회원이 운영하는 사업장을 상위·하위 단위로 구성한 관리 단위를 말하며, 회원은 자신에게 부여된 그룹 범위 내의 정보만 조회할 수 있습니다.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제3조 (약관의 효력 및 변경)",
|
||||
items: [
|
||||
"이 약관은 서비스를 이용하고자 하는 자가 그 내용에 동의함으로써 효력이 발생합니다.",
|
||||
"회사는 관련 법령을 위배하지 않는 범위에서 이 약관을 개정할 수 있습니다.",
|
||||
"회사가 약관을 개정하는 경우에는 적용일자 및 개정사유를 명시하여 적용일자 15일 전부터 서비스 화면에 공지합니다. 다만 회원에게 불리한 변경인 경우에는 적용일자 30일 전부터 공지하고, 전자우편 또는 문자메시지 등으로 개별 통지합니다.",
|
||||
"회사가 전항의 공지 또는 통지를 하면서 적용일자까지 회원이 거부의사를 표시하지 아니하면 개정에 동의한 것으로 본다는 뜻을 명확히 고지하였음에도 회원이 명시적으로 거부의사를 표시하지 아니한 경우, 회원은 개정약관에 동의한 것으로 봅니다.",
|
||||
"개정약관에 동의하지 아니하는 회원은 이용계약을 해지할 수 있습니다.",
|
||||
"이 약관에 명시되지 아니한 사항은 전자상거래 등에서의 소비자보호에 관한 법률, 약관의 규제에 관한 법률, 정보통신망 이용촉진 및 정보보호 등에 관한 법률 등 관련 법령 또는 상관례에 따릅니다.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제4조 (이용계약의 성립 및 승낙의 제한)",
|
||||
items: [
|
||||
"이용계약은 서비스를 이용하고자 하는 자가 이 약관에 동의하고 회사가 정한 절차에 따라 이용을 신청한 후, 회사가 이를 승낙함으로써 성립합니다.",
|
||||
"회사는 다음 각 호에 해당하는 신청에 대하여는 승낙을 하지 아니하거나 사후에 이용계약을 해지할 수 있습니다.",
|
||||
"회원은 이용 신청 시 회사가 요구하는 사업자 정보와 기기 정보를 사실과 일치하도록 기재하여야 하며, 기재사항이 변경된 경우 지체 없이 이를 갱신하여야 합니다.",
|
||||
"회원이 정보를 사실과 다르게 기재함으로써 발생한 불이익에 대하여 회사는 책임을 지지 아니합니다.",
|
||||
],
|
||||
subItems: {
|
||||
1: [
|
||||
"타인의 명의 또는 사업자 정보를 도용하거나 허위로 기재한 경우",
|
||||
"이용 신청자가 이 약관에 따라 이전에 이용계약이 해지된 사실이 있는 경우",
|
||||
"서비스 이용 요금을 정당한 사유 없이 납부하지 아니한 경우",
|
||||
"회사의 설비 여유가 없거나 기술상 지장이 있는 경우",
|
||||
"기타 회사가 정한 이용 신청 요건을 충족하지 못한 경우",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "제5조 (서비스의 내용)",
|
||||
items: [
|
||||
"회사는 회원에게 다음 각 호의 서비스를 제공합니다.",
|
||||
"회사는 서비스의 품질 향상, 운영상·기술상의 필요에 따라 제공하는 서비스의 내용을 변경하거나 새로운 서비스를 추가할 수 있으며, 이 경우 변경 내용과 적용일자를 사전에 공지합니다.",
|
||||
],
|
||||
subItems: {
|
||||
0: [
|
||||
"무인기기 및 카드단말기 등록·조회·관리",
|
||||
"거래내역 조회 및 영수증 확인",
|
||||
"일별·기간별 매출 집계 및 통계 조회",
|
||||
"상품 정보, 컬럼별 상품 배치 및 재고 관리",
|
||||
"무인기기 이상(ERROR) 및 품절 내역 조회",
|
||||
"장애·품절 알림 등 문자메시지 발송",
|
||||
"사업자 그룹 및 사용자 계정 관리",
|
||||
"기타 회사가 정하는 부가 서비스",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "제6조 (서비스의 제공 시간 및 중단)",
|
||||
items: [
|
||||
"서비스는 연중무휴 1일 24시간 제공함을 원칙으로 합니다.",
|
||||
"회사는 시스템 점검, 설비의 보수·교체, 서비스 개선 등의 사유가 있는 경우 서비스의 전부 또는 일부를 일시 중단할 수 있으며, 이 경우 중단 사유와 기간을 사전에 공지합니다. 다만 긴급한 사유가 있는 경우에는 사후에 공지할 수 있습니다.",
|
||||
"천재지변, 정전, 통신망 장애, 카드사·부가통신사업자 등 제휴사의 시스템 장애 등 회사가 통제할 수 없는 사유로 서비스를 제공할 수 없는 경우 회사는 서비스 제공 의무를 면합니다.",
|
||||
"무인기기 또는 단말기의 통신 상태에 따라 거래내역 및 기기 상태 정보의 수집이 지연될 수 있으며, 이 경우 회사는 정상화를 위하여 필요한 조치를 취합니다.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제7조 (서비스 이용의 제한)",
|
||||
items: [
|
||||
"회사는 회원이 다음 각 호에 해당하는 경우 사전 통지 후 서비스 이용을 제한하거나 이용계약을 해지할 수 있습니다. 다만 긴급을 요하는 경우에는 조치 후 통지할 수 있습니다.",
|
||||
"회사가 전항에 따라 이용을 제한하는 경우, 회원은 그 사유가 발생한 날부터 10일 이내에 서면 또는 전자우편으로 이의를 제기할 수 있으며, 회사는 이를 접수한 날부터 10일 이내에 처리 결과를 회신합니다.",
|
||||
],
|
||||
subItems: {
|
||||
0: [
|
||||
"서비스의 안정적 운영을 방해할 목적으로 대량의 정보를 전송하거나 시스템에 부하를 유발하는 경우",
|
||||
"다른 회원의 계정 또는 비밀번호를 부정하게 사용하는 경우",
|
||||
"서비스를 통하여 취득한 정보를 회사의 사전 승낙 없이 제3자에게 제공하거나 영리 목적으로 이용하는 경우",
|
||||
"서비스를 이용하여 법령에 위반되는 행위를 하거나 이를 교사·방조하는 경우",
|
||||
"이 약관에서 정한 회원의 의무를 위반한 경우",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "제8조 (유료 서비스 및 이용요금)",
|
||||
items: [
|
||||
"회사가 제공하는 서비스는 무료 서비스와 유료 서비스로 구분되며, 유료 서비스의 종류와 요금은 회사와 회원 간의 개별 계약 또는 서비스 화면에 게시된 내용에 따릅니다.",
|
||||
"문자메시지 발송 등 사용량에 따라 과금되는 서비스의 요금은 실제 사용량을 기준으로 산정합니다.",
|
||||
"회사는 이용요금을 변경하는 경우 제3조에서 정한 방법에 따라 사전에 공지합니다.",
|
||||
"회원이 이용요금을 납부기일까지 납부하지 아니하는 경우 회사는 유료 서비스의 제공을 중단할 수 있습니다.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제9조 (계약의 해지 및 환불)",
|
||||
items: [
|
||||
"회원은 언제든지 회사가 정한 절차에 따라 이용계약의 해지를 신청할 수 있으며, 회사는 관련 법령이 정하는 바에 따라 이를 지체 없이 처리합니다.",
|
||||
"회원이 유료 서비스를 전혀 이용하지 아니한 경우, 결제일부터 7일 이내에 결제 취소를 요청할 수 있습니다.",
|
||||
"이미 제공된 서비스에 해당하는 요금은 환불되지 아니하며, 잔여 기간에 대한 요금은 회사가 정한 기준에 따라 일할 계산하여 환불합니다.",
|
||||
"환불은 회원이 결제한 수단과 동일한 방법으로 처리함을 원칙으로 하며, 동일한 방법으로 환불이 불가능한 경우 회사는 사전에 그 사유를 안내하고 다른 방법으로 환불합니다.",
|
||||
"회원의 약관 위반으로 회사가 이용계약을 해지하는 경우, 회사는 회원에게 발생한 손해에 대한 배상을 청구할 수 있습니다.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제10조 (과오금)",
|
||||
items: [
|
||||
"회사는 이용요금의 결제와 관련하여 과오금이 발생한 경우 결제와 동일한 방법으로 과오금 전액을 환불합니다. 동일한 방법으로 환불이 불가능한 경우에는 사전에 이를 안내합니다.",
|
||||
"회사의 귀책사유로 과오금이 발생한 경우 회사가 그 환불에 소요되는 비용을 부담하며, 회원의 귀책사유로 과오금이 발생한 경우에는 합리적인 범위에서 회원이 부담합니다.",
|
||||
"회사가 회원의 과오금 환불 요청을 거부하는 경우, 회사는 이용요금이 정당하게 부과되었음을 입증할 책임을 부담합니다.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제11조 (공지 및 정보의 제공)",
|
||||
items: [
|
||||
"회사는 서비스 운영과 관련한 공지사항을 서비스 화면에 게시하거나 전자우편, 문자메시지, 푸시 알림 등의 방법으로 회원에게 통지할 수 있습니다.",
|
||||
"회사는 무인기기의 장애·품절 등 운영에 필요한 정보를 회원이 등록한 연락처로 발송할 수 있습니다.",
|
||||
"광고성 정보는 사전에 수신에 동의한 회원에게만 발송하며, 회원은 언제든지 수신 동의를 철회할 수 있습니다.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제12조 (개인정보의 보호)",
|
||||
items: [
|
||||
"회사는 서비스 제공에 필요한 최소한의 범위에서 회원의 개인정보를 수집하며, 수집한 개인정보는 관련 법령과 회사의 개인정보처리방침에 따라 보호·관리합니다.",
|
||||
"회사는 회원의 동의 없이 개인정보를 제3자에게 제공하지 아니합니다. 다만 법령에 따라 요구되는 경우에는 그러하지 아니합니다.",
|
||||
"회원은 자신의 개인정보에 대한 열람·정정·삭제 및 처리정지를 언제든지 요구할 수 있습니다.",
|
||||
"개인정보의 처리에 관한 상세한 사항은 회사의 개인정보처리방침에 따릅니다.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제13조 (회원의 의무)",
|
||||
items: [
|
||||
"회원은 관계 법령, 이 약관, 서비스 이용안내 및 회사가 공지한 사항을 준수하여야 하며, 회사의 업무를 방해하는 행위를 하여서는 아니 됩니다.",
|
||||
"회원은 자신의 계정과 비밀번호를 관리할 책임이 있으며, 이를 제3자에게 이용하게 하여서는 아니 됩니다. 관리 소홀로 발생한 결과에 대한 책임은 회원에게 있습니다.",
|
||||
"회원은 계정 정보가 도용되거나 제3자가 무단으로 사용하고 있음을 인지한 경우 즉시 회사에 통지하고 회사의 안내에 따라야 합니다.",
|
||||
"회원은 서비스를 통하여 조회한 거래내역, 매출정보 등을 서비스 이용 목적 외의 용도로 사용하여서는 아니 됩니다.",
|
||||
"회원은 무인기기 및 단말기 정보를 사실과 일치하도록 등록·관리하여야 하며, 기기의 교체·폐기 등 변경사항을 지체 없이 반영하여야 합니다.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제14조 (회사의 의무)",
|
||||
items: [
|
||||
"회사는 관련 법령과 이 약관을 준수하며, 계속적이고 안정적으로 서비스를 제공하기 위하여 최선을 다합니다.",
|
||||
"회사는 회원의 개인정보 보호를 위한 보안 시스템을 갖추고 개인정보처리방침을 공시하고 준수합니다.",
|
||||
"회사는 서비스 이용과 관련하여 회원이 제기한 의견이나 불만이 정당하다고 인정되는 경우 이를 처리하며, 처리 과정과 결과를 회원에게 안내합니다.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제15조 (손해배상 및 면책)",
|
||||
items: [
|
||||
"회사는 다음 각 호의 사유로 회원 또는 제3자에게 발생한 손해에 대하여 책임을 지지 아니합니다.",
|
||||
"회사는 회원이 서비스를 이용하여 기대하는 수익을 얻지 못하였거나, 서비스를 통하여 조회한 정보를 신뢰함으로써 발생한 손해에 대하여 책임을 지지 아니합니다.",
|
||||
"회사는 회원 상호간 또는 회원과 제3자 간에 발생한 분쟁에 개입할 의무가 없으며, 회사의 귀책사유가 없는 한 이로 인한 손해를 배상할 책임이 없습니다.",
|
||||
"회원이 이 약관을 위반하여 회사에 손해를 입힌 경우, 회원은 회사에 그 손해를 배상하여야 합니다.",
|
||||
],
|
||||
subItems: {
|
||||
0: [
|
||||
"천재지변 또는 이에 준하는 불가항력으로 서비스를 제공할 수 없는 경우",
|
||||
"회원이 계정 또는 비밀번호의 관리를 소홀히 한 경우",
|
||||
"회사의 관리 영역이 아닌 통신망의 장애로 서비스 이용이 불가능한 경우",
|
||||
"무인기기 또는 단말기 자체의 고장·오작동으로 인한 경우",
|
||||
"기타 회사의 귀책사유가 없는 사유로 인한 경우",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "제16조 (분쟁의 해결)",
|
||||
items: [
|
||||
"회사와 회원은 서비스와 관련하여 분쟁이 발생한 경우 원만한 해결을 위하여 성실히 협의합니다.",
|
||||
"협의가 이루어지지 아니한 경우, 회사의 본점 소재지를 관할하는 법원을 제1심 관할법원으로 합니다.",
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -27,6 +27,49 @@ import PageCountSelector from "@/components/PageCountSelector";
|
||||
import InputSelectField from "@/components/form/input/InputSelectField";
|
||||
|
||||
|
||||
const PAY_TYPE_LABEL: { [key: string]: string } = {
|
||||
D1: "신용승인", D4: "신용취소",
|
||||
I1: "페이승인", I4: "페이취소",
|
||||
B1: "현금",
|
||||
TM1: "티머니승인", TM4: "티머니취소",
|
||||
EB1: "캐시비승인", EB4: "캐시비취소",
|
||||
};
|
||||
|
||||
// A credit cancel carries rtid, so the list also brings down the original approval
|
||||
const isCreditCancel = (item: any) => item.type === "D4" && item.rtid;
|
||||
|
||||
// Only the first 6 digits are stored: pad to 4-4-4-4 and mask the unknown digits
|
||||
const formatCardNum = (card_num: any) => {
|
||||
const digits = String(card_num ?? "").replace(/[^0-9]/g, "");
|
||||
if (digits === "")
|
||||
return "";
|
||||
|
||||
const filled = (digits + "****************").substring(0, 16);
|
||||
return (filled.match(/.{1,4}/g) ?? []).join("-");
|
||||
};
|
||||
|
||||
const toAmount = (amount: any) => (amount === null || amount === undefined ? "" : Number(amount).toLocaleString("ko-KR") + "원");
|
||||
|
||||
const payCardName = (type: string, pay_name: string) => (
|
||||
type === "TM1" || type === "TM4" ? "티머니" :
|
||||
type === "EB1" || type === "EB4" ? "캐시비" :
|
||||
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;
|
||||
const PAGE_NUM_COUNT = 5;
|
||||
@@ -240,13 +283,22 @@ export default function Transactions() {
|
||||
};
|
||||
|
||||
const handleRowClickReceipt = (row: number) => {
|
||||
const node = findRootNodeByKey(treeData, String(tokenPayload?.biz_group_id));
|
||||
// The receipt belongs to the group being listed, not to the signed-in account:
|
||||
// a super admin browses other groups, so its own biz_group_id is not in the tree.
|
||||
const selectedKey = selectedTreeKeys[0];
|
||||
const node = selectedKey != undefined ? findRootNodeByKey(treeData, String(selectedKey)) : null;
|
||||
|
||||
if (node == null) {
|
||||
alert("사업자 정보를 찾을 수 없습니다. 좌측에서 사업자를 선택한 후 다시 시도하세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
modalData.setAll({...tableData[row],
|
||||
biz_group_name: node.name,
|
||||
biz_reg_num: node.biz_reg_num,
|
||||
ceo: node.ceo,
|
||||
phone: node.phone,
|
||||
address: node.address,
|
||||
biz_group_name: node.name ?? "",
|
||||
biz_reg_num: node.biz_reg_num ?? "",
|
||||
ceo: node.ceo ?? "",
|
||||
phone: node.phone ?? "",
|
||||
address: node.address ?? "",
|
||||
});
|
||||
openModal();
|
||||
};
|
||||
@@ -374,28 +426,30 @@ export default function Transactions() {
|
||||
{
|
||||
title: "거래시간",
|
||||
key: "order_time",
|
||||
renderItem: (item: any, row: number) => convertDateTime6(item.order_time),
|
||||
renderItem: (item: any, row: number) => cell(item, convertDateTime6(item.order_time), convertDateTime6(item.org_order_time)),
|
||||
viewMobile: true,
|
||||
},
|
||||
{
|
||||
title: "무인기기명",
|
||||
key: "device_name",
|
||||
renderItem: (item: any) => cell(item, item.device_name),
|
||||
},
|
||||
{
|
||||
title: "단말기 TID",
|
||||
key: "uid1",
|
||||
renderItem: (item: any) => cell(item, item.uid1),
|
||||
viewMobile: true,
|
||||
},
|
||||
{
|
||||
title: "금액",
|
||||
key: "amount",
|
||||
renderItem: (item : any) => (item.amount.toLocaleString("ko-KR") + "원"),
|
||||
renderItem: (item : any) => cell(item, toAmount(item.amount), toAmount(item.org_amount)),
|
||||
viewMobile: true,
|
||||
},
|
||||
{
|
||||
title: "결제유형",
|
||||
key: "type",
|
||||
renderItem: (item : any) => (
|
||||
renderItem: (item : any) => cell(item,
|
||||
<Badge size="sm" color={
|
||||
item.type === "D1" ? "primary" :
|
||||
item.type === "I1" ? "info" :
|
||||
@@ -404,46 +458,40 @@ export default function Transactions() {
|
||||
item.type === "EB1" ? "success" :
|
||||
"error"
|
||||
}>
|
||||
{item.type === "D1" ? "신용승인" :
|
||||
item.type === "D4" ? "신용취소" :
|
||||
item.type === "I1" ? "페이승인" :
|
||||
item.type === "I4" ? "페이취소" :
|
||||
item.type === "B1" ? "현금" :
|
||||
item.type === "TM1" ? "티머니승인" :
|
||||
item.type === "TM4" ? "티머니취소" :
|
||||
item.type === "EB1" ? "캐시비승인" :
|
||||
item.type === "EB4" ? "캐시비취소" :
|
||||
""}
|
||||
</Badge>
|
||||
|
||||
{PAY_TYPE_LABEL[item.type] ?? ""}
|
||||
</Badge>,
|
||||
PAY_TYPE_LABEL[item.org_type] ?? ""
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "승인번호",
|
||||
key: "approval",
|
||||
renderItem: (item: any) => cell(item, item.approval, item.org_approval),
|
||||
},
|
||||
{
|
||||
title: "카드번호",
|
||||
key: "card_num",
|
||||
renderItem: (item: any) => cell(item, formatCardNum(item.card_num), formatCardNum(item.org_card_num)),
|
||||
},
|
||||
{
|
||||
title: "컬럼번호",
|
||||
key: "column_no",
|
||||
renderItem: (item: any) => cell(item, item.column_no, item.org_column_no),
|
||||
},
|
||||
{
|
||||
title: "상품코드",
|
||||
key: "code",
|
||||
renderItem: (item: any) => cell(item, item.code, item.org_code),
|
||||
},
|
||||
{
|
||||
title: "상품명",
|
||||
key: "goods_name",
|
||||
renderItem: (item: any) => cell(item, item.goods_name, item.org_goods_name),
|
||||
},
|
||||
{
|
||||
title: "결제카드",
|
||||
key: "pay_name",
|
||||
renderItem: (item : any) => (
|
||||
item.type === "TM1" ? "티머니" :
|
||||
item.type === "TM4" ? "티머니" :
|
||||
item.type === "EB1" ? "캐시비" :
|
||||
item.type === "EB4" ? "캐시비" :
|
||||
item.pay_name
|
||||
)
|
||||
renderItem: (item : any) => cell(item, payCardName(item.type, item.pay_name), payCardName(item.org_type, item.org_pay_name))
|
||||
},
|
||||
{
|
||||
title: "영수증",
|
||||
|
||||
@@ -20,6 +20,28 @@ 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
|
||||
@@ -124,6 +146,12 @@ 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>*/}
|
||||
|
||||
|
||||
@@ -73,6 +73,11 @@ const othersItems: NavItem[] = [
|
||||
name: "무인기기 관리",
|
||||
path: "/device",
|
||||
},
|
||||
{
|
||||
icon: <DocsIcon />,
|
||||
name: "무인기기 ERROR/품절내역",
|
||||
path: "/device-error",
|
||||
},
|
||||
{
|
||||
icon: <PieChartIcon />,
|
||||
name: "매출집계",
|
||||
@@ -94,6 +99,7 @@ const othersItems: NavItem[] = [
|
||||
subItems: [
|
||||
{ name: "공지사항", path: "/notice" },
|
||||
{ name: "FAQ", path: "/faq" },
|
||||
{ name: "이용약관", path: "/terms" },
|
||||
//{ name: "자료실", path: "/resource-board" },
|
||||
//{ name: "VOC", path: "/voc" },
|
||||
],
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// Labels for device_error.error_type.
|
||||
// The telegram itself (device_error.error_code) is parsed on the server:
|
||||
// see component/DeviceErrorCode.java and /api/get-device-error-detail.do
|
||||
|
||||
export const DEVICE_ERROR_TYPE_FAULT = 1; // 단말기 이상
|
||||
export const DEVICE_ERROR_TYPE_SOLD_OUT = 2; // 품절
|
||||
|
||||
export const deviceErrorTypeLabel = (error_type: number | string | null | undefined) => {
|
||||
const value = Number(error_type);
|
||||
if (value === DEVICE_ERROR_TYPE_FAULT) return "단말기 이상";
|
||||
if (value === DEVICE_ERROR_TYPE_SOLD_OUT) return "품절";
|
||||
return "";
|
||||
};
|
||||
Reference in New Issue
Block a user