수수료 관리 기능 제거:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled

- 관련 Controller, DTO 클래스 삭제
- commissionManage.html 및 스타일 파일 제거
This commit is contained in:
Rinjae
2026-07-03 18:30:34 +09:00
parent 4f9fbefbcc
commit b02e598d62
19 changed files with 1 additions and 2323 deletions
@@ -1,26 +0,0 @@
package com.eactive.apim.portal.apps.commission.constant;
/**
* Application constants.
*/
public final class Constants {
public static final String SYSTEM_ACCOUNT = "system";
public static final String APIM_FOR_OBP_KEY = "x-obp-trust-system";
public static final String APIM_CODE_FOR_OBP_KEY = "x-obp-partnercode";
public static final String SMS_URL = "/api/messaging/sms";
public static final String COMMISSION_URL = "/api/billing/billing/findBillingForCondition";
public static final String COMMISSION_PRINT_URL = "/api/billing/billing/findBillingForCondition/print";
public static final String OBP_ORGANIZATION_URL = "/api/customer/findCustomerByBusinessManRegistrationNo/";
public static String LANGUAGE = "ko";
private Constants() {
}
}
@@ -1,320 +0,0 @@
package com.eactive.apim.portal.apps.commission.controller;
import com.eactive.apim.portal.apps.commission.constant.Constants;
import com.eactive.apim.portal.apps.commission.dto.CommissionDTO;
import com.eactive.apim.portal.apps.commission.dto.CommissionSearch;
import com.eactive.apim.portal.apps.commission.exception.ErrorUtil;
import com.eactive.apim.portal.apps.commission.service.ExternalService;
import com.eactive.apim.portal.common.util.SecurityUtil;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping(value = "/commission")
@Secured("ROLE_APP")
@RequiredArgsConstructor
public class CommissionController {
private final Logger log = LoggerFactory.getLogger(CommissionController.class);
private final ExternalService externalService;
private final PortalPropertyService portalPropertyService;
private final ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@GetMapping("/manage")
public String commissionManagePage(Model model) {
// 기본 정보 설정
model.addAttribute("organization", SecurityUtil.getUserOrg());
model.addAttribute("noOrgCode", StringUtils.isEmpty(SecurityUtil.getUserOrg().getOrgCode()));
return "apps/commission/commissionManage";
}
@GetMapping("/print/{year}/{month}")
public String commissionPrintPage(
@PathVariable("year") String year,
@PathVariable("month") String month,
Model model) {
CommissionSearch search = new CommissionSearch();
search.setPartnerCode(SecurityUtil.getUserOrg().getOrgCode());
search.setYear(year);
search.setMonth(month);
CommissionResult result = fetchCommissionData(search, Constants.COMMISSION_PRINT_URL);
if (result.hasError()) {
model.addAttribute("error", result.getError());
return "apps/commission/commissionPrint";
}
// Model에 데이터 바인딩
model.addAttribute("commission", result.getData());
model.addAttribute("organization", SecurityUtil.getUserOrg().getOrgName());
model.addAttribute("year", year);
model.addAttribute("month", month);
model.addAttribute("toDay", java.time.LocalDate.now().toString());
return "apps/commission/commissionPrint";
}
// ==================== API Endpoints ====================
@PostMapping("/print/check")
public ResponseEntity<?> checkPrintData(@RequestBody CommissionSearch search) {
search.setPartnerCode(SecurityUtil.getUserOrg().getOrgCode());
if (StringUtils.isEmpty(search.getPartnerCode())) {
return ErrorUtil.create("fail.commission.needPartnerCode");
}
CommissionResult result = fetchCommissionData(search, Constants.COMMISSION_PRINT_URL);
if (result.hasError()) {
return ErrorUtil.create("fail.commission.print", result.getError());
}
return ResponseEntity.ok().body(result.getData());
}
@PostMapping()
public ResponseEntity<?> list(@RequestBody CommissionSearch search) {
search.setPartnerCode(SecurityUtil.getUserOrg().getOrgCode());
if (StringUtils.isEmpty(search.getPartnerCode())) {
return ErrorUtil.create("fail.commission.needPartnerCode");
}
CommissionResult result = fetchCommissionData(search, Constants.COMMISSION_URL);
if (result.hasError()) {
return ErrorUtil.create("fail.commission.list", result.getError());
}
return ResponseEntity.ok().body(result.getData());
}
// ==================== Private Methods ====================
/**
* 외부 API를 호출하여 수수료 데이터를 조회합니다.
*
* @param search 검색 조건
* @param apiPath API 경로 (Constants.COMMISSION_URL 또는 Constants.COMMISSION_PRINT_URL)
* @return 조회 결과
*/
private CommissionResult fetchCommissionData(CommissionSearch search, String apiPath) {
if (StringUtils.isEmpty(search.getPartnerCode())) {
return CommissionResult.error("파트너 코드가 없습니다.");
}
try {
String obmUrl = portalPropertyService.getPortalPropertiesAsMap("Portal").getOrDefault("obp.obm.url", "");
String url = obmUrl + apiPath;
// 첫 번째 API 호출
String result = externalService.getResponseHttpPost(url, toJson(search));
if (StringUtils.isEmpty(result)) {
return CommissionResult.error("외부 서버 연결에 실패하였습니다.");
}
HashMap<String, Object> returnObj = objectMapper.readValue(result, HashMap.class);
// 에러 응답 체크
String errorMessage = extractErrorMessage(returnObj);
if (errorMessage != null) {
return CommissionResult.error(errorMessage);
}
// Together 관련 추가 처리 (000002-01)
if (StringUtils.pathEquals(search.getPartnerCode(), "000002-01")) {
log.debug("Found Together : " + search.getPartnerCode());
CommissionSearch togetherSearch = new CommissionSearch();
togetherSearch.setPartnerCode("000002-02");
togetherSearch.setYear(search.getYear());
togetherSearch.setMonth(search.getMonth());
String togetherResult = externalService.getResponseHttpPost(url, toJson(togetherSearch));
if (StringUtils.isEmpty(togetherResult)) {
return CommissionResult.error("외부 서버 연결에 실패하였습니다.");
}
HashMap<String, Object> togetherReturnObj = objectMapper.readValue(togetherResult, HashMap.class);
log.debug("togetherLendingReturnObj : " + togetherReturnObj.toString());
String togetherErrorMessage = extractErrorMessage(togetherReturnObj);
if (togetherErrorMessage != null) {
return CommissionResult.error(togetherErrorMessage);
}
HashMap<String, Object> mergeMap = mergeCommissionData(returnObj, togetherReturnObj);
result = objectMapper.writeValueAsString(mergeMap);
}
CommissionDTO commissionDTO = objectMapper.readValue(result, CommissionDTO.class);
return CommissionResult.success(commissionDTO);
} catch (Exception e) {
log.warn(e.getMessage(), e);
return CommissionResult.error("수수료 조회에 실패하였습니다.");
}
}
/**
* 응답에서 에러 메시지를 추출합니다.
*/
private String extractErrorMessage(HashMap<String, Object> response) {
if (!StringUtils.isEmpty(response.get("error"))) {
HashMap<String, Object> error = (HashMap) response.get("error");
return error.get("message").toString();
}
return null;
}
private String toJson(CommissionSearch search) {
try {
Map<String, String> jsonMap = new HashMap<>();
jsonMap.put("partnerCode", search.getPartnerCode());
// 월을 두 자리로 패딩 (예: 1 -> 01, 12 -> 12)
String paddedMonth = String.format("%02d", Integer.parseInt(search.getMonth()));
jsonMap.put("targetOnMonth", search.getYear() + paddedMonth);
return objectMapper.writeValueAsString(jsonMap);
} catch (Exception e) {
log.warn("Failed to serialize CommissionSearch to JSON", e);
return "{}";
}
}
/**
* Safely parse double value from HashMap
* Handles both numeric types and string representations
*/
private double safeGetDouble(HashMap<String, Object> map, String key) {
Object value = map.get(key);
if (value == null) {
return 0.0;
}
try {
if (value instanceof Number) {
return ((Number) value).doubleValue();
} else if (value instanceof String) {
return Double.parseDouble((String) value);
}
return 0.0;
} catch (NumberFormatException e) {
log.warn("Failed to parse double value for key: {} with value: {}", key, value);
return 0.0;
}
}
public HashMap<String, Object> mergeCommissionData(HashMap<String, Object> o1, HashMap<String, Object> o2) {
HashMap<String, Object> mergeMap = new HashMap<String, Object>();
String o1Status = MapUtils.getString(o1, "status");
String o2Status = MapUtils.getString(o2, "status");
String resultStatus = null;
if (Integer.parseInt(o1Status) <= Integer.parseInt(o2Status)) {
resultStatus = o1Status;
} else {
resultStatus = o2Status;
}
double apiTotalValueSum = safeGetDouble(o1, "apiTotalValueSum") + safeGetDouble(o2, "apiTotalValueSum");
double apiSum = safeGetDouble(o1, "apiSum") + safeGetDouble(o2, "apiSum");
double apiTotalValueSum2 = safeGetDouble(o1, "apiTotalValueSum2") + safeGetDouble(o2, "apiTotalValueSum2");
double apiSum2 = safeGetDouble(o1, "apiSum2") + safeGetDouble(o2, "apiSum2");
double productTotalValueCountSum = safeGetDouble(o1, "productTotalValueCountSum") + safeGetDouble(o2, "productTotalValueCountSum");
double productTotalValueAmountSum = safeGetDouble(o1, "productTotalValueAmountSum") + safeGetDouble(o2, "productTotalValueAmountSum");
double productSum = safeGetDouble(o1, "productSum") + safeGetDouble(o2, "productSum");
double productTotalValueCountSum2 = safeGetDouble(o1, "productTotalValueCountSum2") + safeGetDouble(o2, "productTotalValueCountSum2");
double productTotalValueAmountSum2 = safeGetDouble(o1, "productTotalValueAmountSum2") + safeGetDouble(o2, "productTotalValueAmountSum2");
double productSum2 = safeGetDouble(o1, "productSum2") + safeGetDouble(o2, "productSum2");
double totalValueCountSum = safeGetDouble(o1, "totalValueCountSum") + safeGetDouble(o2, "totalValueCountSum");
double totalValueCountSum2 = safeGetDouble(o1, "totalValueCountSum2") + safeGetDouble(o2, "totalValueCountSum2");
double totalValueAmountSum = safeGetDouble(o1, "totalValueAmountSum") + safeGetDouble(o2, "totalValueAmountSum");
double totalValueAmountSum2 = safeGetDouble(o1, "totalValueAmountSum2") + safeGetDouble(o2, "totalValueAmountSum2");
double sum = safeGetDouble(o1, "sum") + safeGetDouble(o2, "sum");
double sum2 = safeGetDouble(o1, "sum2") + safeGetDouble(o2, "sum2");
List<Map> o1List = (ArrayList) o1.get("list");
List<Map> o2pList = (ArrayList) o2.get("list");
o1List.addAll(o2pList);
mergeMap.put("status", resultStatus);
mergeMap.put("apiTotalValueSum", String.format("%.2f", apiTotalValueSum));
mergeMap.put("apiSum", String.format("%.2f", apiSum));
mergeMap.put("apiTotalValueSum2", String.format("%.2f", apiTotalValueSum2));
mergeMap.put("apiSum2", String.format("%.2f", apiSum2));
mergeMap.put("productTotalValueCountSum", String.format("%.2f", productTotalValueCountSum));
mergeMap.put("productTotalValueAmountSum", String.format("%.2f", productTotalValueAmountSum));
mergeMap.put("productSum", String.format("%.2f", productSum));
mergeMap.put("productTotalValueCountSum2", String.format("%.2f", productTotalValueCountSum2));
mergeMap.put("productTotalValueAmountSum2", String.format("%.2f", productTotalValueAmountSum2));
mergeMap.put("productSum2", String.format("%.2f", productSum2));
mergeMap.put("totalValueCountSum", String.format("%.2f", totalValueCountSum));
mergeMap.put("totalValueCountSum2", String.format("%.2f", totalValueCountSum2));
mergeMap.put("totalValueAmountSum", String.format("%.2f", totalValueAmountSum));
mergeMap.put("totalValueAmountSum2", String.format("%.2f", totalValueAmountSum2));
mergeMap.put("sum", String.format("%.2f", sum));
mergeMap.put("sum2", String.format("%.2f", sum2));
mergeMap.put("list", o1List);
return mergeMap;
}
// ==================== Inner Classes ====================
/**
* 수수료 조회 결과를 담는 내부 클래스
*/
private static class CommissionResult {
private CommissionDTO data;
private String error;
static CommissionResult success(CommissionDTO data) {
CommissionResult result = new CommissionResult();
result.data = data;
return result;
}
static CommissionResult error(String message) {
CommissionResult result = new CommissionResult();
result.error = message;
return result;
}
boolean hasError() {
return error != null;
}
CommissionDTO getData() {
return data;
}
String getError() {
return error;
}
}
}
@@ -1,28 +0,0 @@
package com.eactive.apim.portal.apps.commission.dto;
import java.util.List;
import lombok.Data;
@Data
public class CommissionDTO {
private String status;
private String apiTotalValueSum;
private String apiSum;
private String apiTotalValueSum2;
private String apiSum2;
private String productTotalValueCountSum;
private String productTotalValueAmountSum;
private String productSum;
private String productTotalValueCountSum2;
private String productTotalValueAmountSum2;
private String productSum2;
private String totalValueCountSum;
private String totalValueCountSum2;
private String totalValueAmountSum;
private String totalValueAmountSum2;
private String sum;
private String sum2;
private List<CommissionList> list;
private String documentFormatNo;
}
@@ -1,32 +0,0 @@
package com.eactive.apim.portal.apps.commission.dto;
import lombok.Data;
/**
* Created by jskim on 17. 01. 26.
*
* @author jskim
*/
@Data
public class CommissionList {
private String paymentTargetName;
private String paymentTargetType;
private String value;
private String minimumAmount;
private String maximumAmount;
private String totalCount;
private String totalCount2;
private String totalValue;
private String totalValue2;
private String minimumAmount2;
private String maximumAmount2;
private String paymentBaseName;
private String paymentTimingName;
private String valueDivisionName;
private String sum;
private String sum2;
private String detailFeeTypeName;
private String value2;
private String changeReason2;
}
@@ -1,17 +0,0 @@
package com.eactive.apim.portal.apps.commission.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
public class CommissionSearch {
@ApiModelProperty(value = "대상년")
private String year;
@ApiModelProperty(value = "대상월")
private String month;
private String partnerCode;
}
@@ -1,27 +0,0 @@
package com.eactive.apim.portal.apps.commission.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
/**
* Created by ybsong on 16. 12. 7.
*
* @author ybsong
*/
public class ErrorUtil {
public static ResponseEntity<ErrorVM> create(HttpStatus status, String message, String description) {
return new ResponseEntity<>(new ErrorVM(message, description), status);
}
public static ResponseEntity<ErrorVM> create(HttpStatus status, String message) {
return create(status, message, null);
}
public static ResponseEntity<ErrorVM> create(String message, String description) {
return create(HttpStatus.BAD_REQUEST, message, description);
}
public static ResponseEntity<ErrorVM> create(String message) {
return create(message, null);
}
}
@@ -1,43 +0,0 @@
package com.eactive.apim.portal.apps.commission.exception;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
/**
* View Model for transferring error message with a list of field errors.
*/
@Data
public class ErrorVM implements Serializable {
private static final long serialVersionUID = 1L;
private final String message;
private final String description;
private List<FieldErrorVM> fieldErrors;
public ErrorVM(String message) {
this(message, null);
}
public ErrorVM(String message, String description) {
this.message = message;
this.description = description;
}
public ErrorVM(String message, String description, List<FieldErrorVM> fieldErrors) {
this.message = message;
this.description = description;
this.fieldErrors = fieldErrors;
}
public void add(String objectName, String field, String message) {
if (fieldErrors == null) {
fieldErrors = new ArrayList<>();
}
fieldErrors.add(new FieldErrorVM(objectName, field, message));
}
}
@@ -1,33 +0,0 @@
package com.eactive.apim.portal.apps.commission.exception;
import java.io.Serializable;
public class FieldErrorVM implements Serializable {
private static final long serialVersionUID = 1L;
private final String objectName;
private final String field;
private final String message;
public FieldErrorVM(String dto, String field, String message) {
this.objectName = dto;
this.field = field;
this.message = message;
}
public String getObjectName() {
return objectName;
}
public String getField() {
return field;
}
public String getMessage() {
return message;
}
}
@@ -1,80 +0,0 @@
package com.eactive.apim.portal.apps.commission.service;
import com.eactive.apim.portal.apps.commission.constant.Constants;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import java.util.Map;
import javax.inject.Inject;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
/**
* 외부 API 호출 서비스
* RestTemplate 기반으로 외부 시스템과 HTTP 통신을 수행합니다.
*
* @author ybsong
* @since 2017-03-02
*/
@Service
@Transactional
@RequiredArgsConstructor
public class ExternalService {
private final Logger log = LoggerFactory.getLogger(ExternalService.class);
private final RestTemplate restTemplate;
private final PortalPropertyService portalPropertyService;
/**
* HTTP POST 요청을 통해 외부 API를 호출하고 응답을 받습니다.
*
* @param url 호출할 API URL
* @param payload 요청 본문 (JSON 문자열)
* @return API 응답 본문 (문자열), 오류 발생 시 null
*/
public String getResponseHttpPost(String url, String payload) {
try {
Map<String, String> portalProperties = portalPropertyService.getPortalPropertiesAsMap("Portal");
// HTTP 헤더 설정
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set(Constants.APIM_FOR_OBP_KEY, portalProperties.getOrDefault("obp.api_key","APIMPT-0002-QVBJTVBULTAwMDI="));
headers.set(Constants.APIM_CODE_FOR_OBP_KEY, portalProperties.getOrDefault("obp.partner_code","100001-01"));
// HTTP 요청 엔티티 생성 (헤더 + 본문)
HttpEntity<String> requestEntity = new HttpEntity<>(payload, headers);
// POST 요청 실행
ResponseEntity<String> response = restTemplate.postForEntity(url, requestEntity, String.class);
// 응답 본문 반환
return response.getBody();
} catch (HttpStatusCodeException e) {
// 4xx, 5xx 에러더라도 응답 본문이 있으면 반환 (에러 메시지 포함)
String responseBody = e.getResponseBodyAsString();
log.warn("HTTP error from external API: url={}, status={}, body={}", url, e.getStatusCode(), responseBody);
if (responseBody != null && !responseBody.isEmpty()) {
return responseBody;
}
return null;
} catch (RestClientException e) {
log.error("Failed to call external API: url={}, error={}", url, e.getMessage(), e);
return null;
} catch (Exception e) {
log.error("Unexpected error during HTTP POST: url={}", url, e);
return null;
}
}
}
@@ -6,7 +6,6 @@ import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import com.eactive.apim.portal.config.PortalProperties;
import com.eactive.apim.portal.apps.session.service.UserSessionService;
@@ -16,9 +15,6 @@ public class GlobalControllerAdvice {
@Autowired
private PageService pageService;
@Autowired
private PortalPropertyService portalPropertyService;
@Autowired
private PortalProperties portalProperties;
@@ -37,17 +33,6 @@ public class GlobalControllerAdvice {
return pageService.getPageName(currentPath);
}
@ModelAttribute("designSurveyEnabled")
public boolean isDesignSurveyEnabled() {
String value = portalPropertyService.getOrCreateProperty(
"Portal",
"ui.design-survey",
"false",
"네비게이션 바 디자인 설문 활성화 여부 (true/false)"
);
return "true".equalsIgnoreCase(value);
}
@ModelAttribute("showTestAuthNotice")
public boolean showTestAuthNotice() {
return portalProperties.isTestAuthNoticeEnabled();
-3
View File
@@ -378,9 +378,6 @@ page:
myapikey_modify_step3:
name: "앱 수정 요청 완료"
path: "/myapikey/modify/step3"
commission:
name: "과금 관리"
path: "/commission/manage"
api_statistics:
name: "이용 통계"
path: "/statistics/api"
-406
View File
@@ -663,75 +663,6 @@ hr {
--shadow-xl: 0 16px 40px rgba(75, 155, 255, 0.2);
}
.design-survey-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 48px;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
z-index: 400;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.design-survey-bar .survey-container {
display: flex;
align-items: center;
gap: 16px;
}
.design-survey-bar .survey-label {
color: #ffffff;
font-size: 14px;
font-weight: 500;
}
.design-survey-bar .survey-buttons {
display: flex;
gap: 8px;
}
.design-survey-bar .survey-btn {
padding: 6px 16px;
border: 2px solid rgba(255, 255, 255, 0.5);
border-radius: 20px;
background: transparent;
color: #ffffff;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.design-survey-bar .survey-btn:hover {
background: rgba(255, 255, 255, 0.2);
border-color: #ffffff;
}
.design-survey-bar .survey-btn.active {
background: #ffffff;
color: #667eea;
border-color: #ffffff;
}
@media (max-width: 768px) {
.design-survey-bar {
height: 40px;
}
.design-survey-bar .survey-label {
display: none;
}
.design-survey-bar .survey-btn {
padding: 4px 12px;
font-size: 12px;
}
}
body.design-survey-active .global-header {
margin-top: 48px;
}
@media (max-width: 768px) {
body.design-survey-active .global-header {
margin-top: 40px;
}
}
.blind {
position: absolute;
width: 1px;
@@ -17467,343 +17398,6 @@ input[type=checkbox]:checked + .custom-checkbox {
transform: translateY(4px);
}
.commission-container {
max-width: 1400px;
margin: 0 auto;
}
.wrap {
max-width: 1400px;
margin: 0 auto;
background: #FFFFFF;
padding: 24px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(75, 155, 255, 0.1);
}
.searchInfo {
display: inline-block;
margin-right: 8px;
margin-bottom: 16px;
}
.searchBox {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.searchBox.row-two {
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.searchBox label {
font-weight: 500;
color: #1A1A2E;
min-width: 80px;
}
.searchBox .form-control {
padding: 6px 12px;
border: 1px solid #E2E8F0;
border-radius: 6px;
font-size: 14px;
min-width: 100px;
}
.form-inline {
display: flex;
align-items: center;
gap: 10px;
}
.form-inline .period-selects {
display: flex;
align-items: center;
gap: 10px;
}
.form-inline .period-selects .period-unit {
margin-right: 10px;
}
.btnBox {
margin-left: auto;
}
.btnBox .btn + .btn {
margin-left: 5px;
}
#printBtn {
display: none;
}
.commission-notice {
color: #dc3545;
margin-top: 30px;
line-height: 1.6;
}
.voffset3 {
margin-top: 30px;
}
.color_red {
color: #FF6B6B;
}
.color_blue {
color: #007bff;
}
.color_darkRed {
color: #8b0000;
}
.table-responsive {
overflow-x: auto;
margin-top: 30px;
}
.table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.table th {
background: #F8FAFC;
font-weight: 500;
color: #1A1A2E;
padding: 16px;
text-align: center;
border: 1px solid #E2E8F0;
}
.table td {
padding: 16px;
text-align: center;
border: 1px solid #E2E8F0;
}
.table-bordered {
border: 1px solid #E2E8F0;
}
.table-bordered th,
.table-bordered td {
border: 1px solid #E2E8F0;
}
.border-type {
border: 2px solid #dee2e6;
}
.commission-table-wrapper {
margin-top: 30px;
}
.commission-table-wrapper .table {
border: 2px solid #dee2e6;
}
.summary-row {
background-color: #fff3cd !important;
}
.summary-row td {
font-weight: 600;
}
.summary-row .summary-amount {
font-size: 16px;
color: #007bff;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.text-left {
text-align: left;
}
.bg-warning {
background-color: #fff3cd !important;
}
.glyphicon-search::before {
content: "🔍";
}
.info-table {
border-top: 10px solid #64748B;
width: 100%;
border-bottom: 2px solid #1A1A2E;
margin-bottom: 40px;
}
.info-table th {
text-align: left;
padding: 16px 24px;
font-weight: 600;
background: none;
border: none;
}
.info-table td {
padding: 16px 24px;
border: none;
}
.commission-table {
width: 100%;
margin-top: 40px;
}
.commission-table th,
.commission-table td {
border: 1px solid #999 !important;
padding: 16px 24px;
}
.commission-table td {
text-align: right;
}
.bg_ygreen {
background-color: aquamarine;
text-align: center;
}
.bg_skyBlue {
background-color: skyblue;
text-align: center;
}
.bg_yellow {
background-color: yellow;
text-align: center;
}
@media print {
body {
padding: 0;
background: #FFFFFF;
}
.searchBox,
.searchInfo,
#printButton,
#backButton,
#printBtn {
display: none !important;
}
@page {
size: auto;
margin: 0 0cm;
}
html {
margin: 0 0cm;
}
}
@media screen and (max-width: 1024px) {
.wrap {
padding: 16px;
}
.searchBox {
padding: 16px;
}
.searchBox.row-two {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.form-inline {
width: 100%;
flex-direction: column;
align-items: flex-start;
}
.form-inline label {
min-width: auto;
margin-bottom: 4px;
}
.form-inline .form-control {
width: 100%;
}
.form-inline--period .period-selects {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
width: 100%;
}
.form-inline--period .period-selects .form-control {
flex: 1;
min-width: 0;
}
.form-inline--period .period-selects .period-unit {
flex-shrink: 0;
font-size: 14px;
color: #1A1A2E;
}
.btnBox {
margin-left: 0;
width: 100%;
}
.btnBox .btn {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.table-responsive {
overflow-x: scroll;
-webkit-overflow-scrolling: touch;
}
}
body.commission-print-page {
font-size: 12px;
width: 600px;
margin: 90px;
font-family: "Spoqa Han Sans Neo", "SpoqaHanSans", "Noto Sans KR", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
body.commission-print-page div {
position: relative;
}
body.commission-print-page table {
border-collapse: collapse;
}
body.commission-print-page table th,
body.commission-print-page table td {
padding: 5px 10px;
}
body.commission-print-page table tr {
font-size: 12px;
}
body.commission-print-page h1 {
text-align: center;
}
body.commission-print-page h1 img {
width: 300px;
}
body.commission-print-page h2 {
margin-top: 40px;
float: left;
}
body.commission-print-page p {
line-height: 1.6;
}
body.commission-print-page p[style*="float: right"] {
margin-top: 25px;
}
body.commission-print-page .btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
body.commission-print-page .btn-primary {
background: #667eea;
color: white;
}
body.commission-print-page .btn-primary:hover {
background: #5a67d8;
}
.terms-page {
min-height: calc(100vh - 140px);
background: #F8FAFC;
File diff suppressed because one or more lines are too long
@@ -33,80 +33,6 @@
--shadow-xl: 0 16px 40px rgba(75, 155, 255, 0.2);
}
// ===========================
// Design Survey Bar
// ===========================
.design-survey-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 48px;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
z-index: 400;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
.survey-container {
display: flex;
align-items: center;
gap: 16px;
}
.survey-label {
color: #ffffff;
font-size: 14px;
font-weight: 500;
}
.survey-buttons {
display: flex;
gap: 8px;
}
.survey-btn {
padding: 6px 16px;
border: 2px solid rgba(255, 255, 255, 0.5);
border-radius: 20px;
background: transparent;
color: #ffffff;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: rgba(255, 255, 255, 0.2);
border-color: #ffffff;
}
&.active {
background: #ffffff;
color: #667eea;
border-color: #ffffff;
}
}
@media (max-width: 768px) {
height: 40px;
.survey-label { display: none; }
.survey-btn { padding: 4px 12px; font-size: 12px; }
}
}
// Body offset when survey is active
body.design-survey-active {
.global-header { margin-top: 48px; }
@media (max-width: 768px) {
.global-header { margin-top: 40px; }
}
}
// 디자인 변형 스타일은 JavaScript에서 동적으로 적용됩니다.
// header_container.html의 DESIGN_OPTIONS 참조
// Blind text for screen readers
.blind {
position: absolute;
-1
View File
@@ -63,7 +63,6 @@
@use 'pages/org-register' as *;
@use 'pages/partnership' as *;
@use 'pages/user-management' as *;
@use 'pages/commission' as *;
@use 'pages/terms-agreements' as *;
@use 'pages/service' as *;
@use 'pages/api-statistics' as *;
@@ -1,412 +0,0 @@
@use '../abstracts/variables' as *;
@use '../abstracts/color-functions' as *;
@use '../abstracts/mixins' as *;
// Commission Pages Styles
// 수수료 관리 및 청구서 출력 페이지 스타일
// ==================== Commission Management Page ====================
// Commission Container
.commission-container {
max-width: 1400px;
margin: 0 auto;
}
.wrap {
max-width: 1400px;
margin: 0 auto;
background: $white;
padding: $spacing-lg;
border-radius: $border-radius-md;
box-shadow: $shadow-md;
}
.searchInfo {
display: inline-block;
margin-right: $spacing-sm;
margin-bottom: $spacing-md;
}
// Search Box
.searchBox {
background: #f8f9fa;
padding: 20px;
border-radius: $border-radius-md;
margin-bottom: 20px;
&.row-two {
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
label {
font-weight: $font-weight-medium;
color: $text-dark;
min-width: 80px;
}
.form-control {
padding: 6px 12px;
border: 1px solid $border-gray;
border-radius: $border-radius-sm;
font-size: $font-size-sm;
min-width: 100px;
}
}
.form-inline {
display: flex;
align-items: center;
gap: 10px;
// 조회기간 [년] [월] 컨테이너 - PC 기본 스타일
.period-selects {
display: flex;
align-items: center;
gap: 10px;
.period-unit {
margin-right: 10px;
}
}
}
.btnBox {
margin-left: auto;
.btn + .btn {
margin-left: 5px;
}
}
// Print Button (initially hidden)
#printBtn {
display: none;
}
// Notice Text
.commission-notice {
color: #dc3545;
margin-top: 30px;
line-height: 1.6;
}
.voffset3 {
margin-top: 30px;
}
// Color Utilities
.color_red {
color: $accent-orange;
}
.color_blue {
color: #007bff;
}
.color_darkRed {
color: #8b0000;
}
// Table Styles
.table-responsive {
overflow-x: auto;
margin-top: 30px;
}
.table {
width: 100%;
border-collapse: collapse;
font-size: $font-size-sm;
th {
background: $gray-bg;
font-weight: $font-weight-medium;
color: $text-dark;
padding: $spacing-md;
text-align: center;
border: 1px solid $border-gray;
}
td {
padding: $spacing-md;
text-align: center;
border: 1px solid $border-gray;
}
}
.table-bordered {
border: 1px solid $border-gray;
th,
td {
border: 1px solid $border-gray;
}
}
.border-type {
border: 2px solid #dee2e6;
}
// Commission Tables
.commission-table-wrapper {
margin-top: 30px;
.table {
border: 2px solid #dee2e6;
}
}
// Summary Row
.summary-row {
background-color: #fff3cd !important;
td {
font-weight: $font-weight-semibold;
}
.summary-amount {
font-size: 16px;
color: #007bff;
}
}
// Text Alignment Utilities
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.text-left {
text-align: left;
}
// Background Utilities
.bg-warning {
background-color: #fff3cd !important;
}
// Icon
.glyphicon-search::before {
content: "🔍";
}
// ==================== Commission Print Page ====================
.info-table {
border-top: 10px solid $text-gray;
width: 100%;
border-bottom: 2px solid $text-dark;
margin-bottom: $spacing-2xl;
th {
text-align: left;
padding: $spacing-md $spacing-lg;
font-weight: $font-weight-semibold;
background: none;
border: none;
}
td {
padding: $spacing-md $spacing-lg;
border: none;
}
}
.commission-table {
width: 100%;
margin-top: $spacing-2xl;
th,
td {
border: 1px solid #999 !important;
padding: $spacing-md $spacing-lg;
}
td {
text-align: right;
}
}
.bg_ygreen {
background-color: aquamarine;
text-align: center;
}
.bg_skyBlue {
background-color: skyblue;
text-align: center;
}
.bg_yellow {
background-color: yellow;
text-align: center;
}
// Print-specific styles
@media print {
body {
padding: 0;
background: $white;
}
.searchBox,
.searchInfo,
#printButton,
#backButton,
#printBtn {
display: none !important;
}
@page {
size: auto;
margin: 0 -0cm;
}
html {
margin: 0 0cm;
}
}
// Responsive Design
@media screen and (max-width: $breakpoint-md) {
.wrap {
padding: $spacing-md;
}
.searchBox {
padding: $spacing-md;
&.row-two {
flex-direction: column;
align-items: flex-start;
gap: $spacing-md;
}
}
.form-inline {
width: 100%;
flex-direction: column;
align-items: flex-start;
label {
min-width: auto;
margin-bottom: $spacing-xs;
}
.form-control {
width: 100%;
}
// 조회기간: [년] [월] 한 줄 유지
&--period {
.period-selects {
display: flex;
flex-direction: row;
align-items: center;
gap: $spacing-sm;
width: 100%;
.form-control {
flex: 1;
min-width: 0;
}
.period-unit {
flex-shrink: 0;
font-size: $font-size-sm;
color: $text-dark;
}
}
}
}
.btnBox {
margin-left: 0;
width: 100%;
.btn {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
}
.table-responsive {
overflow-x: scroll;
-webkit-overflow-scrolling: touch;
}
}
// Commission Print Page Specific
// Note: commissionPrint.html uses body class for scoping
body.commission-print-page {
font-size: 12px;
width: 600px;
margin: 90px;
font-family: $font-family-primary;
div {
position: relative;
}
table {
border-collapse: collapse;
th,
td {
padding: 5px 10px;
}
tr {
font-size: 12px;
}
}
h1 {
text-align: center;
img {
width: 300px;
}
}
h2 {
margin-top: $spacing-2xl;
float: left;
}
p {
line-height: 1.6;
&[style*="float: right"] {
margin-top: 25px;
}
}
// Print buttons
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-primary {
background: #667eea;
color: white;
&:hover {
background: #5a67d8;
}
}
}
@@ -1,496 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/djbank_title_layout}">
<head>
<title>수수료 관리</title>
</head>
<body>
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/img_title_bg.png}" class="title-image">
<h1>기관 수수료 관리</h1>
</div>
</section>
<th:block layout:fragment="contentFragment">
<section class="org-register-container">
<div class="searchBox row-two">
<span class="form-inline form-inline--period">
<label for="forYear">조회기간</label>
<span class="period-selects">
<select id="forYear" class="form-control">
<option value="">- 선택 -</option>
</select> <span class="period-unit"></span>
<select id="forMonth" class="form-control">
<option value="">- 선택 -</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
<option value="10">10</option>
<option value="11">11</option>
<option value="12">12</option>
</select> <span class="period-unit"></span>
</span>
</span>
<span class="btnBox">
<span id="searchedPeriod" class="searched-period" style="display:none; margin-right:10px; font-weight:bold; color:#28a745;"></span>
<button type="button" class="btn btn-primary" onclick="loadData()">
<span class="fa fa-search"></span><span>조회</span>
</button>
<button type="button" id="printBtn" class="btn btn-secondary" onclick="printCommission()">
<span>청구서 출력</span>
</button>
</span>
</div>
<div class="commission-container">
<div class="col-md-12">
<p class="commission-notice">
이용기관은 매월 1~5일 전월 수수료 조회 가능하며, 10일 확정금액으로 정산됩니다. <br/>
금액이상시 담당자 연락 및 조정요청 하시기 바랍니다. (매월 1~5일 수수료 조회, 6~9일 수수료 조정 및 확정, 10일 정산)
</p>
<!-- API 수수료 테이블 -->
<div class="table-responsive">
<table class="table table-bordered border-type">
<colgroup>
<col style="width:6%"/>
<col style="width:16%"/>
<col style="width:8%"/>
<col style="width:8%"/>
<col style="width:8%"/>
<col style="width:8%"/>
<col style="width:8%"/>
<col style="width:8%"/>
<col style="width:8%"/>
<col style="width:8%"/>
<col/>
</colgroup>
<thead>
<tr>
<th rowspan="2">구분</th>
<th rowspan="2">API 명</th>
<th colspan="4">조정 전</th>
<th colspan="4">조정 후</th>
<th rowspan="2">조정사유</th>
</tr>
<tr>
<th>처리<br/>건수</th>
<th>적용<br/>수수료(원)</th>
<th>최저<br/>수수료</th>
<th>출금<br/>예상금액</th>
<th>조정<br/>건수</th>
<th>조정<br/>수수료(원)</th>
<th>최저<br/>수수료</th>
<th>출금<br/>예상금액</th>
</tr>
</thead>
<tbody id="apiTableBody">
<!-- 동적 생성 -->
</tbody>
</table>
<!-- 판매 수수료 테이블 -->
<table class="table table-bordered border-type voffset3">
<colgroup>
<col style="width: 6%;">
<col style="width: 8%;">
<col style="width: 8%;">
<col style="width: 5%;">
<col style="width: 5%;">
<col style="width: 7%;">
<col style="width: 6%;">
<col style="width: 6%;">
<col style="width: 7%;">
<col style="width: 7%;">
<col style="width: 6%;">
<col style="width: 6%;">
<col style="width: 7%;">
<col style="width: 7%;">
<col>
</colgroup>
<thead>
<tr>
<th rowspan="2">구분</th>
<th rowspan="2">과금대상<br/>업무</th>
<th rowspan="2">수수료<br/>유형</th>
<th rowspan="2">과금<br/>기준</th>
<th rowspan="2">값구분</th>
<th rowspan="2">판매액</th>
<th colspan="4">조정 전</th>
<th colspan="4">조정 후</th>
<th rowspan="2">조정사유</th>
</tr>
<tr>
<th>적용<br/>수수료</th>
<th>처리<br/>건수</th>
<th>최저<br/>수수료</th>
<th>출금<br/>예상수수료</th>
<th>적용<br/>수수료</th>
<th>처리<br/>건수</th>
<th>최저<br/>수수료</th>
<th>출금<br/>예상수수료</th>
</tr>
</thead>
<tbody id="saleTableBody">
<!-- 동적 생성 -->
</tbody>
</table>
<!-- 합계 테이블 -->
<table class="table table-bordered border-type voffset3">
<colgroup>
<col/>
<col style="width: 13%"/>
<col style="width: 13%"/>
<col style="width: 13%"/>
<col style="width: 13%"/>
<col style="width: 13%"/>
<col style="width: 13%"/>
</colgroup>
<thead>
<tr>
<th rowspan="2">구분</th>
<th colspan="3">조정 전</th>
<th colspan="3">조정 후</th>
</tr>
<tr>
<th>판매액</th>
<th>처리건수(건)</th>
<th>출금예상금액</th>
<th>판매액</th>
<th>처리건수(건)</th>
<th>출금예상금액</th>
</tr>
</thead>
<tbody>
<tr class="summary-row">
<td class="text-center"><strong id="totalTitle">합계</strong></td>
<td class="text-right"><strong id="totalSalesAmount1">-</strong></td>
<td class="text-right"><strong id="totalCount1">-</strong></td>
<td class="text-right"><strong class="summary-amount" id="totalAmount1">-</strong></td>
<td class="text-right"><strong id="totalSalesAmount2">-</strong></td>
<td class="text-right"><strong id="totalCount2">-</strong></td>
<td class="text-right"><strong class="summary-amount" id="totalAmount2">-</strong></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
</th:block>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
// 페이지 로드 시 초기화 및 자동 조회
document.addEventListener('DOMContentLoaded', function() {
var noOrgCode = /*[[${noOrgCode}]]*/ false;
if (noOrgCode) {
customPopups.showAlert('제휴기관코드가 등록되지 않았습니다.<br/>관리자에게 문의하세요.', function() {
history.back();
});
return;
}
initializeDateSelects();
// 페이지 로딩 후 자동 조회
loadData();
});
function initializeDateSelects() {
const now = new Date();
const currentYear = now.getFullYear();
const currentMonth = now.getMonth() + 1; // 0-indexed
// 전월 계산
let lastMonth = currentMonth - 1;
let lastMonthYear = currentYear;
if (lastMonth === 0) {
lastMonth = 12;
lastMonthYear = currentYear - 1;
}
// 연도 옵션 동적 생성 (최근 3년)
const yearSelect = document.getElementById('forYear');
for (let year = currentYear; year >= currentYear - 2; year--) {
const option = document.createElement('option');
option.value = year;
option.textContent = year;
if (year === lastMonthYear) {
option.selected = true;
}
yearSelect.appendChild(option);
}
// 월 기본값 설정 (전월)
const monthSelect = document.getElementById('forMonth');
monthSelect.value = lastMonth.toString();
}
function showSearchedPeriod(year, month) {
const searchedPeriodEl = document.getElementById('searchedPeriod');
searchedPeriodEl.textContent = year + '년 ' + month + '월 조회완료';
searchedPeriodEl.style.display = 'inline';
}
function hideSearchedPeriod() {
const searchedPeriodEl = document.getElementById('searchedPeriod');
searchedPeriodEl.style.display = 'none';
}
function formatNumber(num) {
return new Intl.NumberFormat('ko-KR').format(num);
}
function loadData() {
const year = document.getElementById('forYear').value;
const month = document.getElementById('forMonth').value;
if (!year || !month) {
customPopups.showAlert('조회 기간을 선택해주세요.');
return;
}
// 서버에서 데이터 조회 API 호출
fetch('/commission', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ ''
},
body: JSON.stringify({
year: year,
month: month
})
})
.then(response => {
return response.json().then(data => {
if (!response.ok) {
// 서버에서 반환한 에러 메시지 추출
const errorMessage = data.description || data.message || '데이터 조회에 실패했습니다.';
throw new Error(errorMessage);
}
return data;
});
})
.then(data => renderCommissionData(data, year, month))
.catch(error => {
console.error('Error:', error);
hideSearchedPeriod();
customPopups.showAlert(error.message || '데이터 조회 중 오류가 발생했습니다.');
});
}
function renderCommissionData(data, year, month) {
// API 수수료 항목 필터링
const apiCommissions = data.list.filter(item => item.paymentTargetType === '01');
const saleCommissions = data.list.filter(item => item.paymentTargetType !== '01');
// API 수수료 테이블 렌더링
const apiTableBody = document.getElementById('apiTableBody');
apiTableBody.innerHTML = '';
let apiTotal1 = 0;
let apiTotal2 = 0;
let apiCount1 = 0;
let apiCount2 = 0;
apiCommissions.forEach((item, index) => {
if (index === 0) {
apiTableBody.innerHTML += `
<tr>
<td rowspan="${apiCommissions.length}" class="text-center">API<br/>수수료</td>
<td class="text-left">${item.paymentTargetName}</td>
<td class="text-right">${formatNumber(item.totalCount || 0)}</td>
<td class="text-right">${formatNumber(item.value || 0)}</td>
<td class="text-right">${formatNumber(item.minimumAmount || 0)}</td>
<td class="text-right"><span class="color_darkRed">${formatNumber(item.sum || 0)}</span></td>
<td class="text-right"><strong class="color_blue">${formatNumber(item.totalCount2 || 0)}</strong></td>
<td class="text-right">${formatNumber(item.value2 || 0)}</td>
<td class="text-right">${formatNumber(item.minimumAmount2 || 0)}</td>
<td class="text-right"><span class="color_darkRed">${formatNumber(item.sum2 || 0)}</span></td>
<td>${item.changeReason2 || ''}</td>
</tr>
`;
} else {
apiTableBody.innerHTML += `
<tr>
<td class="text-left">${item.paymentTargetName}</td>
<td class="text-right">${formatNumber(item.totalCount || 0)}</td>
<td class="text-right">${formatNumber(item.value || 0)}</td>
<td class="text-right">${formatNumber(item.minimumAmount || 0)}</td>
<td class="text-right"><span class="color_darkRed">${formatNumber(item.sum || 0)}</span></td>
<td class="text-right"><strong class="color_blue">${formatNumber(item.totalCount2 || 0)}</strong></td>
<td class="text-right">${formatNumber(item.value2 || 0)}</td>
<td class="text-right">${formatNumber(item.minimumAmount2 || 0)}</td>
<td class="text-right"><span class="color_darkRed">${formatNumber(item.sum2 || 0)}</span></td>
<td>${item.changeReason2 || ''}</td>
</tr>
`;
}
apiTotal1 += parseFloat(item.sum || 0);
apiTotal2 += parseFloat(item.sum2 || 0);
apiCount1 += parseInt(item.totalCount || 0);
apiCount2 += parseInt(item.totalCount2 || 0);
});
// API 소계
apiTableBody.innerHTML += `
<tr>
<td colspan="2" class="text-center">${year}${month}월 API 수수료 소계</td>
<td class="text-right">${formatNumber(apiCount1)}</td>
<td class="text-right"></td>
<td class="text-right"></td>
<td class="text-right"><strong class="color_blue">${formatNumber(apiTotal1)}</strong></td>
<td class="text-right">${formatNumber(apiCount2)}</td>
<td class="text-right"></td>
<td class="text-right"></td>
<td class="text-right"><strong class="color_blue">${formatNumber(apiTotal2)}</strong></td>
<td></td>
</tr>
`;
// 판매 수수료 테이블 렌더링
const saleTableBody = document.getElementById('saleTableBody');
saleTableBody.innerHTML = '';
let saleTotal1 = 0;
let saleTotal2 = 0;
let saleCount1 = 0;
let saleCount2 = 0;
let saleSalesAmount = 0;
saleCommissions.forEach((item, index) => {
const isPercentageFee = item.paymentBaseName === '금액';
if (index === 0) {
saleTableBody.innerHTML += `
<tr>
<td rowspan="${saleCommissions.length}" class="text-center">판매<br/>수수료</td>
<td class="text-left">${item.paymentTargetName}</td>
<td>${item.detailFeeTypeName || ''}</td>
<td>${item.paymentBaseName || ''}</td>
<td>${item.valueDivisionName || ''}</td>
<td class="text-right">${item.totalValue ? formatNumber(item.totalValue) : ''}</td>
<td class="text-right">${isPercentageFee ? (parseFloat(item.value || 0) * 100) + '%' : formatNumber(item.value || 0)}</td>
<td class="text-right">${formatNumber(item.totalCount || 0)}</td>
<td class="text-right">${formatNumber(item.minimumAmount || 0)}</td>
<td class="text-right"><span class="color_darkRed">${formatNumber(item.sum || 0)}</span></td>
<td class="text-right">${isPercentageFee ? (parseFloat(item.value2 || 0) * 100) + '%' : formatNumber(item.value2 || 0)}</td>
<td class="text-right">${formatNumber(item.totalCount2 || 0)}</td>
<td class="text-right">${formatNumber(item.minimumAmount2 || 0)}</td>
<td class="text-right"><span class="color_darkRed">${formatNumber(item.sum2 || 0)}</span></td>
<td>${item.changeReason2 || ''}</td>
</tr>
`;
} else {
saleTableBody.innerHTML += `
<tr>
<td class="text-left">${item.paymentTargetName}</td>
<td>${item.detailFeeTypeName || ''}</td>
<td>${item.paymentBaseName || ''}</td>
<td>${item.valueDivisionName || ''}</td>
<td class="text-right">${item.totalValue ? formatNumber(item.totalValue) : ''}</td>
<td class="text-right">${isPercentageFee ? (parseFloat(item.value || 0) * 100) + '%' : formatNumber(item.value || 0)}</td>
<td class="text-right">${formatNumber(item.totalCount || 0)}</td>
<td class="text-right">${formatNumber(item.minimumAmount || 0)}</td>
<td class="text-right"><span class="color_darkRed">${formatNumber(item.sum || 0)}</span></td>
<td class="text-right">${isPercentageFee ? (parseFloat(item.value2 || 0) * 100) + '%' : formatNumber(item.value2 || 0)}</td>
<td class="text-right">${formatNumber(item.totalCount2 || 0)}</td>
<td class="text-right">${formatNumber(item.minimumAmount2 || 0)}</td>
<td class="text-right"><span class="color_darkRed">${formatNumber(item.sum2 || 0)}</span></td>
<td>${item.changeReason2 || ''}</td>
</tr>
`;
}
saleTotal1 += parseFloat(item.sum || 0);
saleTotal2 += parseFloat(item.sum2 || 0);
saleCount1 += parseInt(item.totalCount || 0);
saleCount2 += parseInt(item.totalCount2 || 0);
saleSalesAmount += parseFloat(item.totalValue || 0);
});
// 판매 소계
saleTableBody.innerHTML += `
<tr>
<td colspan="5" class="text-center">${year}${month}월 판매 수수료 소계</td>
<td class="text-right">${formatNumber(saleSalesAmount)}</td>
<td class="text-right"></td>
<td class="text-right">${formatNumber(saleCount1)}</td>
<td></td>
<td class="text-right"><strong class="color_blue">${formatNumber(saleTotal1)}</strong></td>
<td class="text-right"></td>
<td class="text-right">${formatNumber(saleCount2)}</td>
<td></td>
<td class="text-right"><strong class="color_blue">${formatNumber(saleTotal2)}</strong></td>
<td></td>
</tr>
`;
// 합계 업데이트
document.getElementById('totalTitle').textContent = `${year}${month}월 합계`;
document.getElementById('totalSalesAmount1').textContent = formatNumber(saleSalesAmount);
document.getElementById('totalCount1').textContent = formatNumber(apiCount1 + saleCount1);
document.getElementById('totalAmount1').textContent = formatNumber(apiTotal1 + saleTotal1);
document.getElementById('totalSalesAmount2').textContent = formatNumber(saleSalesAmount);
document.getElementById('totalCount2').textContent = formatNumber(apiCount2 + saleCount2);
document.getElementById('totalAmount2').textContent = formatNumber(apiTotal2 + saleTotal2);
// 출력 버튼 표시
document.getElementById('printBtn').style.display = 'inline-block';
// 조회 기간 표시
showSearchedPeriod(year, month);
}
function printCommission() {
const year = document.getElementById('forYear').value;
const month = document.getElementById('forMonth').value;
if (!year || !month) {
customPopups.showAlert('조회 기간을 선택해주세요.');
return;
}
// 먼저 청구서 출력용 API로 데이터 조회하여 에러 여부 확인
fetch('/commission/print/check', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ ''
},
body: JSON.stringify({
year: year,
month: month
})
})
.then(response => {
return response.json().then(data => {
if (!response.ok) {
const errorMessage = data.description || data.message || '데이터 조회에 실패했습니다.';
throw new Error(errorMessage);
}
return data;
});
})
.then(data => {
// 에러가 없으면 청구서 출력 페이지 열기
const printUrl = '/commission/print/' + year + '/' + month;
window.open(printUrl, '_blank', 'width=800,height=900,scrollbars=yes,resizable=yes');
})
.catch(error => {
console.error('Error:', error);
customPopups.showAlert(error.message || '청구서 조회 중 오류가 발생했습니다.');
});
}
</script>
</th:block>
</body>
</html>
@@ -1,197 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>수수료 청구서</title>
<!-- <link rel="stylesheet" th:href="@{/content/css/vendor.css}">-->
<!-- <link rel="stylesheet" th:href="@{/css/main.css}">-->
<style>
html,body{font-size:12px;}
body{width:600px; margin:90px}
table{border-collapse: collapse;}
table th, td{padding:5px 10px;}
table tr{font-size:12px;}
.info-table{ border-top: 10px solid gray; width: 100%; border-bottom: 2px solid #333;}
.info-table th{text-align:left;}
.commission-table{width:100%;}
.commission-table th,.commission-table td{border:1px solid #999 !important}
.commission-table td{text-align:right}
.bg_ygreen{background-color:aquamarine; text-align:center}
.bg_skyBlue{background-color:skyblue; text-align:center}
.bg_yellow{background-color:yellow; text-align:center}
DIV { position: relative; }
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5a67d8;
}
@media print {
@page {
size: auto;
margin: 0 -0cm;
}
html {
margin: 0 0cm;
}
.control {
display: none !important;
}
}
</style>
</head>
<body class="commission-print-page">
<div class="control">
<input id="printButton" type="button" class="btn btn-primary" value="청구서 인쇄" onclick="window.print()"/>
</div>
<div id="PrintDIV">
<h1 style="text-align:center">
<img style="height:30px;" th:src="@{/img/logo/logo-djb.png}" alt="DJBank 오픈뱅킹" />
</h1>
<p style="text-align:center">우61470 광주광역시 동구 제봉로 225 전화 (062)239-6722 FAX (062)239-6719 오픈뱅크 플랫폼 담당자</p>
<table class="info-table">
<colgroup>
<col style="width:15%;" />
<col/>
</colgroup>
<tbody>
<tr>
<th>문서번호</th>
<td><span th:text="|전자금융(OBP) ${year}-${month}-001호|">전자금융(OBP) 2024-03-001호</span></td>
</tr>
<tr>
<th>발 급 일</th>
<td><span th:text="${toDay}">2024년 3월 15일</span></td>
</tr>
<tr>
<th>수 신</th>
<td><span th:text="${organization}">ABC 핀테크 주식회사</span></td>
</tr>
<tr>
<th>참 조</th>
<td></td>
</tr>
<tr>
<th>제 목</th>
<td><span th:text="|${year}년 ${month}월 오픈뱅크플랫폼 API 서비스 이용수수료 청구|">2024년 3월 오픈뱅크플랫폼 API 서비스 이용수수료 청구</span></td>
</tr>
</tbody>
</table>
<div>
<p>1. 귀 사의 무궁한 발전을 기원합니다.</p>
<p>2. 귀 사와 체결된 서비스 이용 계약에 따라 동 업무처리와 관련하여 발생된 수수료를 다음과 같이 청구하오니 <br/>협조하여 주시기 바랍니다.</p>
</div>
<h2 style="float:left">1. 청구내역</h2>
<p style="float: right;margin-top: 25px;">(단위:건수/원)</p>
<table class="commission-table">
<colgroup>
<col style="width:35%"/>
<col style="width:20%"/>
<col style="width:15%"/>
<col style="width:15%"/>
<col style="width:15%"/>
</colgroup>
<thead>
<tr>
<th class="bg_ygreen">청구내용</th>
<th class="bg_ygreen">대출금액</th>
<th class="bg_ygreen">처리건수</th>
<th class="bg_ygreen">수수료</th>
<th class="bg_ygreen">수수료합계</th>
</tr>
</thead>
<tbody>
<th:block th:if="${commission != null and commission.list != null}">
<!-- API 수수료 -->
<tr th:each="item : ${commission.list}" th:if="${item.paymentTargetType == '01'}">
<td style="text-align:left" th:text="${item.paymentTargetName}">계좌잔액조회</td>
<td></td>
<td th:text="${#numbers.formatInteger(item.totalValue2, 0, 'COMMA')}">20,543</td>
<td th:text="|₩${#numbers.formatInteger(item.value2, 0, 'COMMA')}|">₩50</td>
<td th:text="|₩${#numbers.formatInteger(item.sum2, 0, 'COMMA')}|">₩1,027,150</td>
</tr>
<!-- API 소계 -->
<th:block th:if="${commission.apiSum2 != null}">
<tr>
<th class="bg_skyBlue" colspan="4">소계</th>
<td class="bg_skyBlue" th:text="|₩${#numbers.formatInteger(commission.apiSum2, 0, 'COMMA')}|">₩3,306,780</td>
</tr>
</th:block>
<!-- 판매 수수료 -->
<tr th:each="item : ${commission.list}" th:if="${item.paymentTargetType != '01'}">
<td style="text-align:left" th:text="${item.paymentTargetName}">개인대출 상품 A</td>
<td th:text="${item.paymentBaseName == '금액' ? '₩' + #numbers.formatInteger(item.totalValue2, 0, 'COMMA') : ''}">₩1,050,000,000</td>
<td th:text="${item.paymentBaseName == '건' ? #numbers.formatInteger(item.totalCount2, 0, 'COMMA') : ''}">55</td>
<td th:text="|₩${#numbers.formatInteger(item.value2, 0, 'COMMA')}|">₩5,000</td>
<td th:text="|₩${#numbers.formatInteger(item.sum2, 0, 'COMMA')}|">₩275,000</td>
</tr>
<!-- 판매 소계 -->
<th:block th:if="${commission.productSum2 != null}">
<tr>
<th class="bg_skyBlue" colspan="4">소계</th>
<td class="bg_skyBlue" th:text="|₩${#numbers.formatInteger(commission.productSum2, 0, 'COMMA')}|">₩533,000</td>
</tr>
</th:block>
<!-- 합계 -->
<th:block th:if="${commission.sum2 != null}">
<tr>
<th class="bg_yellow" colspan="4">합계</th>
<td class="bg_yellow" th:text="|₩${#numbers.formatInteger(commission.sum2, 0, 'COMMA')}|">₩3,839,780</td>
</tr>
</th:block>
</th:block>
</tbody>
</table>
<p>* 수수료는 Max(처리건수*금액, 월최저수수료) 금액으로 출금됩니다</p>
<br>
<h2>2. 수수료정산</h2>
<p>① 정산방법 : 수수료 결제계좌에서 자동출금</p>
<p>② 처리일자 : 매월10일 (휴,공휴일인 경우 익영업일). 끝.</p>
<div style="text-align:center;padding:20px 0">
<h1>
<span style="letter-spacing:0.8;">주식회사 광주은행</span><br/>
<span style="letter-spacing:6.5;">디지털</span> <span style="letter-spacing:6.5;">사업부장</span>
<small style="position: absolute; bottom: 39px; margin-left: 12px; font-weight: normal; font-size: 13px;">(직인생략)</small>
</h1>
</div>
</div>
<script th:if="${error}" th:inline="javascript">
document.addEventListener('DOMContentLoaded', function() {
var errorMessage = /*[[${error}]]*/ '';
if (errorMessage) {
alert(errorMessage);
// 에러 발생 시 인쇄 버튼 비활성화
var printButton = document.getElementById('printButton');
if (printButton) {
printButton.disabled = true;
printButton.value = '조회 오류';
}
}
});
</script>
</body>
</html>
@@ -3,16 +3,6 @@
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<body>
<th:block th:fragment="headerFragment(headerClass)">
<!-- Design Survey Bar -->
<div th:if="${designSurveyEnabled}" class="design-survey-bar" id="designSurveyBar">
<div class="survey-container">
<span class="survey-label">네비게이션 디자인을 선택해주세요:</span>
<div class="survey-buttons" id="surveyButtons">
<!-- 버튼은 JavaScript에서 동적으로 생성됩니다 -->
</div>
</div>
</div>
<!-- Global Header Container -->
<header class="global-header" th:classappend="${headerClass}">
<div class="container">
@@ -228,7 +218,6 @@
<li sec:authorize="hasRole('ROLE_USER_MANAGER')"><a th:href="@{/users}">이용자 관리</a></li>
<li sec:authorize="hasRole('ROLE_APP')"><a th:href="@{/myapikey}">인증 키 관리</a></li>
<li sec:authorize="hasRole('ROLE_APP')"><a th:href="@{/statistics/api}">이용 통계</a></li>
<li sec:authorize="hasRole('ROLE_APP')"><a th:href="@{/commission/manage}">과금 관리</a></li>
<li><a th:href="@{/mypage}">내 정보 관리</a></li>
<li><a th:href="@{/change_password}">비밀번호 변경</a></li>
</ul>
@@ -504,107 +493,6 @@
}
});
// ============================================================
// Design Survey Configuration
// 새 디자인 옵션 추가: DESIGN_OPTIONS 배열에 항목 추가
// 예: { id: 'D', label: 'D', styles: { '.logo-text': { 'font-size': '16px' } } }
// ============================================================
const DESIGN_OPTIONS = [
{
id: 'A',
label: 'A (현재)',
isDefault: true,
styles: {} // 기본 스타일 (변경 없음)
},
{
id: 'B',
label: 'B',
styles: {
'.logo-text': { 'font-size': '18px' },
'.nav-link': { 'margin': '0 20px' }
}
},
{
id: 'C',
label: 'C',
styles: {
'.nav-link': { 'margin': '0 20px', 'font-weight': 'bold' }
}
}
// 새 디자인 추가 예시:
// {
// id: 'D',
// label: 'D',
// styles: {
// '.logo-text': { 'font-size': '16px', 'color': '#333' },
// '.nav-link': { 'padding': '10px 24px' }
// }
// }
];
// Design Survey Logic
const surveyBar = document.getElementById('designSurveyBar');
if (surveyBar) {
const body = document.body;
const buttonContainer = document.getElementById('surveyButtons');
const STORAGE_KEY = 'design-survey-selection';
let styleElement = null;
body.classList.add('design-survey-active');
// 버튼 동적 생성
DESIGN_OPTIONS.forEach(option => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'survey-btn';
btn.dataset.design = option.id;
btn.textContent = option.label;
buttonContainer.appendChild(btn);
});
const surveyButtons = buttonContainer.querySelectorAll('.survey-btn');
const defaultOption = DESIGN_OPTIONS.find(o => o.isDefault) || DESIGN_OPTIONS[0];
const savedDesign = localStorage.getItem(STORAGE_KEY) || defaultOption.id;
applyDesign(savedDesign);
surveyButtons.forEach(btn => {
btn.classList.toggle('active', btn.dataset.design === savedDesign);
});
surveyButtons.forEach(btn => {
btn.addEventListener('click', function() {
surveyButtons.forEach(b => b.classList.remove('active'));
this.classList.add('active');
applyDesign(this.dataset.design);
localStorage.setItem(STORAGE_KEY, this.dataset.design);
});
});
function applyDesign(designId) {
// 기존 동적 스타일 제거
if (styleElement) {
styleElement.remove();
styleElement = null;
}
const option = DESIGN_OPTIONS.find(o => o.id === designId);
if (!option || Object.keys(option.styles).length === 0) return;
// 동적 스타일 생성
let css = '';
for (const [selector, props] of Object.entries(option.styles)) {
const propsStr = Object.entries(props)
.map(([prop, val]) => `${prop}: ${val} !important`)
.join('; ');
css += `${selector} { ${propsStr}; }\n`;
}
styleElement = document.createElement('style');
styleElement.id = 'design-survey-styles';
styleElement.textContent = css;
document.head.appendChild(styleElement);
}
}
});
</script>
</th:block>