요금제 추가

This commit is contained in:
2026-07-31 14:17:57 +09:00
parent 5feb7845fd
commit b035537430
29 changed files with 530 additions and 479 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,28 +0,0 @@
package net.jwsi.jcms.vpp.RatePlanDetailTime;
import lombok.extern.slf4j.Slf4j;
import net.jwsi.jcms.base.BaseController;
import net.jwsi.jcms.exception.JsonDataException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@Controller
@Slf4j
@RequestMapping("/cms/ratePlanDetailTime")
public class RatePlanDetailTimeController extends BaseController<RatePlanDetailTime,Integer,RatePlanDetailTimeRepository,RatePlanDetailTimeService> {
public RatePlanDetailTimeController(RatePlanDetailTimeService service) {
super(service);
}
@Override
protected void chkSelectList(List<RatePlanDetailTime> list) throws JsonDataException {
}
@Override
protected void chkSelect(RatePlanDetailTime data) throws JsonDataException {
}
}
@@ -1,55 +0,0 @@
package net.jwsi.jcms.vpp.RatePlanDetailTime;
import lombok.extern.slf4j.Slf4j;
import net.jwsi.jcms.base.BaseService;
import net.jwsi.jcms.base.EnumSqlType;
import org.springframework.data.domain.Page;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@Slf4j
public class RatePlanDetailTimeService extends BaseService<RatePlanDetailTime,Integer,RatePlanDetailTimeRepository> {
public RatePlanDetailTimeService(RatePlanDetailTimeRepository repository) {
super(repository);
}
@Override
protected Integer getUserId() {
return null;
}
@Override
protected String getUserName(Integer id) {
return null;
}
@Override
protected RatePlanDetailTime newSearchParam(RatePlanDetailTime ratePlanDetailTime) {
return null;
}
@Override
protected EnumSqlType useSqlTyp() {
return null;
}
@Override
protected Specification<RatePlanDetailTime> getSpecification(RatePlanDetailTime ratePlanDetailTime) {
return null;
}
@Override
protected Page<RatePlanDetailTime> _selectPage(RatePlanDetailTime ratePlanDetailTime) {
return null;
}
@Override
protected List<RatePlanDetailTime> _selectList(RatePlanDetailTime ratePlanDetailTime) {
return null;
}
}
@@ -1,4 +0,0 @@
package net.jwsi.jcms.vpp.RatePlanDetailTime;
public class SeasonTimeDetailDto {
}
@@ -5,14 +5,12 @@ import net.jwsi.jcms.vpp.chgr.ChgrResponseDto;
import net.jwsi.jcms.vpp.chgrCurrState.ChgrCurrState;
import net.jwsi.jcms.vpp.chgrHist.ChgrHistResponseDto;
import net.jwsi.jcms.vpp.provider.ProviderDto;
import net.jwsi.jcms.vpp.rehchgingList.RechgingList;
import net.jwsi.jcms.vpp.rehchgingList.RechgingListDto;
import net.jwsi.jcms.vpp.ratePlan.RatePlanResponseDto;
import net.jwsi.jcms.vpp.rehchgingList.RechgingListDto;
import net.jwsi.jcms.vpp.station.StationDto;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -82,11 +80,22 @@ public class ApiClient {
public ApiResponse<List<RatePlanResponseDto>> fetchRatePlan(){
Map<String, String> requestBody = Map.of(
"type","all",
"pageSize", "10000"
);
return externalApiClient.post()
.uri("/api/ratePlan/list")
.body(Collections.emptyMap())
.body(requestBody)
.retrieve()
.body(new ParameterizedTypeReference<ApiResponse<List<RatePlanResponseDto>>>() {
});
}
public ApiResponse<?> registerRatePlan(RatePlanResponseDto requestDto) {
return externalApiClient.post()
.uri("/api/ratePlan/register")
.body(requestDto)
.retrieve()
.body(ApiResponse.class);
}
}
@@ -1,28 +0,0 @@
package net.jwsi.jcms.vpp.chargerRatePlan;
import lombok.extern.slf4j.Slf4j;
import net.jwsi.jcms.base.BaseController;
import net.jwsi.jcms.exception.JsonDataException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@Controller
@Slf4j
@RequestMapping("/cms/chargerRatePlan")
public class ChargerRatePlanController extends BaseController<ChargerRatePlan,Integer,ChargerRatePlanRepository,ChargerRatePlanService> {
public ChargerRatePlanController(ChargerRatePlanService service) {
super(service);
}
@Override
protected void chkSelectList(List<ChargerRatePlan> list) throws JsonDataException {
}
@Override
protected void chkSelect(ChargerRatePlan data) throws JsonDataException {
}
}
@@ -1,53 +0,0 @@
package net.jwsi.jcms.vpp.chargerRatePlan;
import lombok.extern.slf4j.Slf4j;
import net.jwsi.jcms.base.BaseService;
import net.jwsi.jcms.base.EnumSqlType;
import org.springframework.data.domain.Page;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@Slf4j
public class ChargerRatePlanService extends BaseService<ChargerRatePlan,Integer,ChargerRatePlanRepository> {
public ChargerRatePlanService(ChargerRatePlanRepository repository) {
super(repository);
}
@Override
protected Integer getUserId() {
return null;
}
@Override
protected String getUserName(Integer id) {
return null;
}
@Override
protected ChargerRatePlan newSearchParam(ChargerRatePlan chargerRatePlan) {
return null;
}
@Override
protected EnumSqlType useSqlTyp() {
return null;
}
@Override
protected Specification<ChargerRatePlan> getSpecification(ChargerRatePlan chargerRatePlan) {
return null;
}
@Override
protected Page<ChargerRatePlan> _selectPage(ChargerRatePlan chargerRatePlan) {
return null;
}
@Override
protected List<ChargerRatePlan> _selectList(ChargerRatePlan chargerRatePlan) {
return null;
}
}
@@ -14,7 +14,7 @@ import java.util.Map;
@Controller
@Slf4j
@RequestMapping("/cms/chgr")
@RequestMapping("/cms/chgr/")
public class ChgrController extends BaseController<Chgr,String,ChgrRepository,ChgrService> {
private final String path = "/system/chgr/";
@@ -12,7 +12,7 @@ import java.util.List;
import java.util.Map;
@Controller
@RequestMapping("/cms/chgrCurrState")
@RequestMapping("/cms/chgrCurrState/")
public class ChgrCurrStateController extends BaseController<ChgrCurrState,ChgrCurrStateId,ChgrCurrStateRepository,ChgrCurrStateService> {
private final ChgrCurrStateService chgrCurrStateService;
@@ -10,7 +10,7 @@ import java.util.List;
import java.util.Map;
@Controller
@RequestMapping("/cms/chgrHist")
@RequestMapping("/cms/chgrHist/")
@Slf4j
public class ChgrHistController extends BaseController<ChgrHist,Integer,ChgrHistRepository,ChgrHistService> {
@@ -50,22 +50,15 @@ public class ChgrHistService extends BaseService<ChgrHist, Integer, ChgrHistRepo
.map(ChgrHistResponseDto::toEntity)
.toList();
int insertCount = 0;
int updateCount = 0;
for (ChgrHist h : entityList) {
Integer id = BaseService.getId(h);
if (id != null && super.repository.existsById(id)) {
super.update(h);
updateCount++;
} else {
super.insert(h);
insertCount++;
}
}
log.info("충전이력 동기화 완료 (신규 추가: {}건 / 기존 업데이트: {}건)", insertCount, updateCount);
}
@Override
@@ -3,6 +3,8 @@ package net.jwsi.jcms.vpp.ratePlan;
import lombok.extern.slf4j.Slf4j;
import net.jwsi.jcms.base.BaseController;
import net.jwsi.jcms.exception.JsonDataException;
import net.jwsi.jcms.vpp.api.ApiClient;
import net.jwsi.jcms.vpp.api.ApiResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
@@ -12,28 +14,33 @@ import java.util.Map;
@Controller
@Slf4j
@RequestMapping("/cms/ratePlan")
public class RatePlanController extends BaseController<RatePlan,Integer,RatePlanRepository,RatePlanService> {
@RequestMapping("/cms/ratePlan/")
public class RatePlanController extends BaseController<RatePlan, Integer, RatePlanRepository, RatePlanService> {
private final String path = "/system/ratePlan/";
private final RatePlanService ratePlanService;
private final ApiClient apiClient;
public RatePlanController(RatePlanService service, RatePlanService ratePlanService) {
public RatePlanController(RatePlanService service, RatePlanService ratePlanService, ApiClient apiClient) {
super(service);
this.ratePlanService = ratePlanService;
this.apiClient = apiClient;
}
@GetMapping("list")
public String list(){
public String list() {
ratePlanService.getRatePlan();
return path+"list";
return path + "list";
}
@PostMapping("list.json")
@ResponseBody
public Map<String,Object> list(RatePlan ratePlan){
if(ratePlan == null) ratePlan = new RatePlan();
public Map<String, Object> list(RatePlan ratePlan) {
if (ratePlan == null) ratePlan = new RatePlan();
return svcSelectList(ratePlan);
}
@PostMapping("view.json")
@ResponseBody
public Map<String, Object> viewData(@RequestParam("ratePlanId") Integer ratePlanId) {
@@ -49,6 +56,28 @@ public class RatePlanController extends BaseController<RatePlan,Integer,RatePlan
return result;
}
@PostMapping("insert.json")
@ResponseBody
public Map<String, Object> insertRatePlan(@RequestBody RatePlanResponseDto requestDto) {
Map<String, Object> resultMap = new HashMap<>();
try {
ApiResponse<?> apiRes = apiClient.registerRatePlan(requestDto);
if (apiRes == null || !Integer.valueOf(200).equals(apiRes.getCode())) {
String msg = (apiRes != null) ? apiRes.getMessage() : "API 서버 응답 없음";
throw new RuntimeException("API 서버 등록 실패: " + msg);
}
resultMap.put("result", "success");
resultMap.put("message", "요금제 등록 요청이 완료되었습니다. (승인 대기)");
} catch (Exception e) {
log.error("요금제 등록 중 오류 발생", e);
resultMap.put("result", "fail");
resultMap.put("message", e.getMessage());
}
return resultMap;
}
@Override
protected void chkSelectList(List<RatePlan> list) throws JsonDataException {
@@ -41,7 +41,9 @@ public class RatePlanResponseDto {
ratePlan.setApplyStartDate(this.applyStartDate);
ratePlan.setApplyEndDate(this.applyEndDate);
ratePlan.setFixedPrice(this.fixedPrice);
ratePlan.setIsActive(this.isActive);
ratePlan.setApprovalStatus(this.approvalStatus);
ratePlan.setDescription(this.description);
return ratePlan;
}
@@ -78,10 +80,6 @@ public class RatePlanResponseDto {
chargerRatePlan.setApplyStartDate(chargerDto.getApplyStartDate());
chargerRatePlan.setApplyEndDate(chargerDto.getApplyEndDate());
if (chargerDto.getIsActive() != null) {
chargerRatePlan.setIsActive(chargerDto.getIsActive() ? "Y" : "N");
}
chargerRatePlanList.add(chargerRatePlan);
}
return chargerRatePlanList;
@@ -112,6 +110,5 @@ public class RatePlanResponseDto {
private String chargerCd;
private String applyStartDate;
private String applyEndDate;
private Boolean isActive;
}
}
@@ -151,7 +151,6 @@ public class RatePlanService extends BaseService<RatePlan, Integer, RatePlanRepo
cpDto.setChargerCd(cp.getChargerCd());
cpDto.setApplyStartDate(cp.getApplyStartDate());
cpDto.setApplyEndDate(cp.getApplyEndDate());
cpDto.setIsActive("Y".equals(cp.getIsActive()));
return cpDto;
}).collect(Collectors.toList());
@@ -159,7 +158,6 @@ public class RatePlanService extends BaseService<RatePlan, Integer, RatePlanRepo
return dto;
}
@Override
protected Integer getUserId() {
return HttpUtil.getUserId();
@@ -14,7 +14,7 @@ import java.util.Map;
@Controller
@Slf4j
@RequestMapping("/cms/rechgingList")
@RequestMapping("/cms/rechgingList/")
public class RechgingListController extends BaseController<RechgingList, Long, RechgingListRepository, RechgingListService> {
private final String path = "/system/rechgingList/";
@@ -1,27 +0,0 @@
package net.jwsi.jcms.vpp.timeslots;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.jwsi.jcms.base.BaseStEntity;
@NoArgsConstructor
@Setter
@Getter
@Entity(name="vpp_time_slots")
public class TimeSlots extends BaseStEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id; //시간대 id(pk)
private String name; //시간대명 (예: 경부하, 최대부하, 09시)
private String slotType; //시간대 구분 (basic=계시별, hour=시간단위)
private Integer hourNumber; //시간 구분 (slot_type=hour일 경우 0~23)
private String startTime; //시작 시간
private String endTime; //종료 시간
private String description; //시간대 설명
private String isActive; //활성화 여부
}
@@ -1,28 +0,0 @@
package net.jwsi.jcms.vpp.timeslots;
import lombok.extern.slf4j.Slf4j;
import net.jwsi.jcms.base.BaseController;
import net.jwsi.jcms.exception.JsonDataException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@Controller
@Slf4j
@RequestMapping("/cms/timeSlots")
public class TimeSlotsController extends BaseController<TimeSlots,Integer,TimeSlotsRepository,TimeSlotsService> {
public TimeSlotsController(TimeSlotsService service) {
super(service);
}
@Override
protected void chkSelectList(List<TimeSlots> list) throws JsonDataException {
}
@Override
protected void chkSelect(TimeSlots data) throws JsonDataException {
}
}
@@ -1,6 +0,0 @@
package net.jwsi.jcms.vpp.timeslots;
import net.jwsi.jcms.base.BaseRecStRepository;
public interface TimeSlotsRepository extends BaseRecStRepository<TimeSlots,Integer> {
}
@@ -1,53 +0,0 @@
package net.jwsi.jcms.vpp.timeslots;
import lombok.extern.slf4j.Slf4j;
import net.jwsi.jcms.base.BaseService;
import net.jwsi.jcms.base.EnumSqlType;
import org.springframework.data.domain.Page;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@Slf4j
public class TimeSlotsService extends BaseService<TimeSlots,Integer,TimeSlotsRepository> {
public TimeSlotsService(TimeSlotsRepository repository) {
super(repository);
}
@Override
protected Integer getUserId() {
return null;
}
@Override
protected String getUserName(Integer id) {
return null;
}
@Override
protected TimeSlots newSearchParam(TimeSlots timeSlots) {
return null;
}
@Override
protected EnumSqlType useSqlTyp() {
return null;
}
@Override
protected Specification<TimeSlots> getSpecification(TimeSlots timeSlots) {
return null;
}
@Override
protected Page<TimeSlots> _selectPage(TimeSlots timeSlots) {
return null;
}
@Override
protected List<TimeSlots> _selectList(TimeSlots timeSlots) {
return null;
}
}
+3 -3
View File
@@ -14,9 +14,9 @@ spring:
datasource:
jcms-db:
driver-class-name: org.mariadb.jdbc.Driver
jdbc-url: jdbc:mariadb://192.168.62.37:3306/jcmsdb
username: jcms
password: jcms3593
jdbc-url: jdbc:mariadb://localhost:3306/jcmsdb
username: root
password: jcms
server:
port: 8080
@@ -99,10 +99,10 @@
return d;
},
{
order: [],
order: [[0, 'desc']],
columns: [
{title: "충전소 ", name: "ST_NM", data: "stId", className, orderable: false},
{title: "충전기 ", name: "CHGR_NM", data: "chgrId", className, orderable: false},
{title: "충전소 아이디", name: "ST_ID", data: "stId", className, orderable: false},
{title: "충전기 아이디", name: "CHGR_ID", data: "chgrId", className, orderable: false},
{title: "결제구분", name: "PAY_TP", data: "payTp", className, orderable: false},
{title: "충전기타입 코드", name: "CHGR_TP", data: "plugTp", className, orderable: false},
{title: "충전 시작일시", name: "RECHG_S_DT", data: "rechgSDt", className, orderable: false},
@@ -39,6 +39,9 @@
<div class="col-xs-12 ">
<button type="button" class="btn btn-sm btn-info" id="searchBtn">검색</button>
<button type="button" class="btn btn-sm btn-info" id="resetBtn">리셋</button>
<th:block th:if="${menuRole?.roleWrite eq true}">
<button type="button" class="btn btn-sm btn-primary pl-2" onclick="showModal(ModalMode.REGISTER, {}, '');">등록</button>
</th:block>
</div>
</div>
</div>
@@ -65,30 +68,13 @@
#ratePlanDetailScrollArea::-webkit-scrollbar-thumb { background: #d0d0d0; border-radius: 3px; }
#ratePlanDetailScrollArea::-webkit-scrollbar-thumb:hover { background: #b5b5b5; }
.custom-card {
border: 1px solid #e5e5e5;
border-radius: 0.25rem;
margin-bottom: 1rem;
background-color: #fff;
}
.custom-card-header {
color: #333;
font-weight: 600;
font-size: 0.88rem;
padding: 0.6rem 1rem;
border-bottom: 1px solid #e5e5e5;
background-color: #fafafa;
}
.custom-card-body {
padding: 0.75rem 1rem;
}
.custom-card { border: 1px solid #e5e5e5; border-radius: 0.25rem; margin-bottom: 1rem; background-color: #fff; }
.custom-card-header { color: #333; font-weight: 600; font-size: 0.88rem; padding: 0.6rem 1rem; border-bottom: 1px solid #e5e5e5; background-color: #fafafa; }
.custom-card-body { padding: 0.75rem 1rem; }
.info-table th, .info-table td {
padding: 0.15rem 0 !important;
font-size: 0.85rem;
color: #444;
}
.info-table th { font-weight: 600; width: 100px; color: #222; }
.info-table th, .info-table td { padding: 0.2rem 0.4rem !important; font-size: 0.85rem; color: #444; }
.info-table th { font-weight: 600; width: 110px; color: #222; white-space: nowrap; }
.info-table td { width: 100%; }
.price-table { font-size: 0.85rem; border-color: #e5e5e5; }
.price-table th, .price-table td { border-color: #e5e5e5; padding: 0.5rem; }
@@ -98,39 +84,50 @@
.val-price { color: #222; font-weight: 700; }
.val-unit { color: #999; font-size: 0.8rem; }
.charger-item {
border: 1px solid #e5e5e5;
border-radius: 0.35rem;
padding: 0.6rem;
background-color: #fff;
height: 100%;
}
.charger-item { border: 1px solid #e5e5e5; border-radius: 0.35rem; padding: 0.6rem; background-color: #fff; height: 100%; }
.charger-cd { font-size: 0.9rem; font-weight: bold; color: #222; margin-bottom: 0.1rem; }
.charger-dt { font-size: 0.75rem; color: #aaa; margin-bottom: 0.4rem; }
.badge-square {
background-color: #f0f0f0;
color: #333;
border: 1px solid #d8d8d8;
padding: 0.15rem 0.4rem;
font-size: 0.7rem;
border-radius: 2px;
font-weight: 500;
}
.badge-square.active {
background-color: #eef1fb;
color: #3b5bdb;
border-color: #d6dcf5;
}
.badge-square { background-color: #f0f0f0; color: #333; border: 1px solid #d8d8d8; padding: 0.15rem 0.4rem; font-size: 0.7rem; border-radius: 2px; font-weight: 500; }
.badge-square.active { background-color: #eef1fb; color: #3b5bdb; border-color: #d6dcf5; }
.approval-ok { color: #333; font-weight: 600; }
.approval-ok i { color: #3b5bdb; }
.season-tabs { display: flex; border-bottom: 1px solid #e5e5e5; margin-bottom: 0.75rem; }
.season-tab-btn { flex: 1; text-align: center; padding: 0.5rem 0; cursor: pointer; font-size: 0.85rem; color: #666; border-bottom: 2px solid transparent; background: none; border-top:none; border-left:none; border-right:none; }
.season-tab-btn.active { color: #3b5bdb; font-weight: 600; border-bottom-color: #3b5bdb; }
.time-mode-toggle { display: flex; border: 1px solid #e5e5e5; border-radius: 0.25rem; overflow: hidden; margin-bottom: 0.75rem; }
.time-mode-btn { flex: 1; text-align: center; padding: 0.4rem 0; cursor: pointer; font-size: 0.82rem; color: #666; background: #fafafa; }
.time-mode-btn.active { background: #3b5bdb; color: #fff; font-weight: 600; }
.basic-price-row { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid #f2f2f2; }
.basic-price-row:last-child { border-bottom: none; }
.basic-price-label { font-size: 0.85rem; color: #333; }
.basic-price-input { width: 140px; text-align: right; }
.detail-price-row { display: flex; align-items: center; justify-content: space-between; padding: 0.4rem 0; border-bottom: 1px solid #f5f5f5; }
.detail-price-label { font-size: 0.82rem; color: #444; }
.detail-price-input { width: 140px; text-align: right; }
.detail-scroll { max-height: 260px; overflow-y: auto; padding-right: 4px; }
.charger-search-list { max-height: 260px; overflow-y: auto; border: 1px solid #eee; border-radius: 0.25rem; padding: 0.4rem; }
.charger-check-item { display: flex; align-items: flex-start; gap: 0.5rem; padding: 0.5rem; border: 1px solid #eee; border-radius: 0.3rem; margin-bottom: 0.4rem; background:#fff; }
.charger-check-item label { margin: 0; font-size: 0.82rem; }
</style>
<script th:inline="none">
const rootPath = "/cms/ratePlan/";
let datatable;
const BASIC_SLOTS = [
{ id: 1, label: "경부하 (22:00 - 08:00)", hours: [22,23,0,1,2,3,4,5,6,7] },
{ id: 2, label: "중간부하 (08:00 - 16:00)", hours: [8,9,10,11,12,13,14,15] },
{ id: 3, label: "최대부하 (16:00 - 22:00)", hours: [16,17,18,19,20,21] }
];
const SEASON_LABEL = { spring:"봄", summer:"여름", fall:"가을", winter:"겨울", all_year:"연중" };
$(document).ready(function() {
const className = "dt-head-center dt-body-center";
datatable = newDataTable(
@@ -145,22 +142,22 @@
{
order: [[0, 'desc']],
columns: [
{title: "요금제 ID", name: "RATE_PLAN_ID", data: "ratePlanId", className, width: "80px"},
{title: "요금제명", name: "RATE_PLAN_NAME", data: "ratePlanName", className: "dt-head-center dt-body-left"},
{title: "요금제 구분", name: "RATE_PLAN_TYPE", data: "ratePlanType", className,
{title: "요금제 ID", name: "RATE_PLAN_ID", data: "ratePlanId", className, orderable:false, width: "80px"},
{title: "요금제명", name: "RATE_PLAN_NAME", data: "ratePlanName", orderable:false, className: "dt-head-center dt-body-left"},
{title: "요금제 구분", name: "RATE_PLAN_TYPE", data: "ratePlanType", orderable:false, className,
render: (data) => data === 'time' ? '계시별 요금제' : data
},
{title: "적용 시작일", name: "APPLY_START_DATE", data: "applyStartDate", className, width: "120px"},
{title: "적용 종료일", name: "APPLY_END_DATE", data: "applyEndDate", className, width: "120px"},
{title: "승인 상태", name: "APPROVAL_STATUS", data: "approvalStatus", className, width: "100px",
{title: "적용 시작일", name: "APPLY_START_DATE", data: "applyStartDate", orderable:false, className, width: "120px"},
{title: "적용 종료일", name: "APPLY_END_DATE", data: "applyEndDate", orderable:false, className, width: "120px"},
{title: "승인 상태", name: "APPROVAL_STATUS", data: "approvalStatus", orderable:false, className, width: "100px",
render: (data) => data === 'approved' ? '<span class="text-success fw-bold"><i class="bi bi-check-lg"></i> 승인완료</span>' : '<span class="text-secondary">대기중</span>'
},
{title: "활성화", name: "IS_ACTIVE", data: "isActive", className, width: "80px",
{title: "활성화", name: "IS_ACTIVE", data: "isActive", orderable:false, className, width: "80px",
render: (data) => data === "true" || data === 'Y' ? '<span class="text-primary fw-bold">활성</span>' : '<span class="text-secondary">비활성</span>'
},
{title: "기능", orderable:false, width:"80px",
{title: "기능", orderable:false, width:"120px",
render: (data, type, row) => {
return mkRowDataFunctions({ratePlanId: row['ratePlanId']}, row['ratePlanName'], true, false, false);
return mkRowDataFunctions({ratePlanId: row['ratePlanId']}, row['ratePlanName'], true, true, true);
}
}
],
@@ -172,105 +169,65 @@
const showModal = (mode, ids, title) => {
if(!checkRole(mode)) { alert("권한이 없습니다."); return; }
let modalInputArray = [
new ModalInput({
inputType: ModalInputType.customer,
inputId: "detailViewTemplate",
inputName: "detailViewTemplate",
options: () => {
return `
<div class="col-12 p-0" id="ratePlanDetailScrollArea">
<div class="custom-card">
<div class="custom-card-header">기본 정보</div>
<div class="custom-card-body">
<table class="table table-borderless mb-0 info-table">
<tr><th>요금제명:</th><td id="v_ratePlanName">-</td></tr>
<tr><th>요금제 구분:</th><td id="v_ratePlanType">-</td></tr>
<tr><th>적용 기간:</th><td id="v_applyDate">-</td></tr>
<tr><th>활성화 상태:</th><td id="v_isActive">-</td></tr>
<tr><th>설명:</th><td id="v_description">-</td></tr>
</table>
</div>
</div>
<div class="custom-card">
<div class="custom-card-header">승인 정보</div>
<div class="custom-card-body">
<table class="table table-borderless mb-0 info-table">
<tr><th>승인 상태:</th><td id="v_approvalStatus">-</td></tr>
</table>
</div>
</div>
<div class="custom-card">
<div class="custom-card-header">시간대별 단가 설정</div>
<div class="custom-card-body p-0">
<table class="table mb-0 text-center align-middle price-table">
<thead>
<tr>
<th class="border-end text-dark">시간대구분</th>
<th>봄 (Spring)</th>
<th>여름 (Summer)</th>
<th>가을 (Fall)</th>
<th>겨울 (Winter)</th>
</tr>
</thead>
<tbody>
<tr>
<td class="border-end py-2">
<div class="fw-bold text-dark">경부하</div>
<div class="text-muted" style="font-size:0.7rem; letter-spacing: -0.5px;">(22:00~08:00)</div>
</td>
<td id="price_spring_4">-</td>
<td id="price_summer_4">-</td>
<td id="price_fall_4">-</td>
<td id="price_winter_4">-</td>
</tr>
<tr>
<td class="border-end py-2">
<div class="fw-bold text-dark">중간부하</div>
<div class="text-muted" style="font-size:0.7rem; letter-spacing: -0.5px;">(08:00~16:00)</div>
</td>
<td id="price_spring_12">-</td>
<td id="price_summer_12">-</td>
<td id="price_fall_12">-</td>
<td id="price_winter_12">-</td>
</tr>
<tr>
<td class="border-end py-2">
<div class="fw-bold text-dark">최대부하</div>
<div class="text-muted" style="font-size:0.7rem; letter-spacing: -0.5px;">(16:00~22:00)</div>
</td>
<td id="price_spring_20">-</td>
<td id="price_summer_20">-</td>
<td id="price_fall_20">-</td>
<td id="price_winter_20">-</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="custom-card mb-1">
<div class="custom-card-header" style="border-bottom: 1px solid #e5e5e5;">
적용 충전기 목록 (<span id="v_chargerCount">0</span>개)
</div>
<div class="custom-card-body pt-3 pb-2" style="background-color: #fcfcfc;">
<div class="row g-2" id="v_chargerListArea"></div>
</div>
</div>
</div>
`;
}
})
];
const loadUrl = rootPath + "view.json";
const modalWidth = 780;
const labelWidth = '1px';
// ==========================================
// 💡 조회 모드 (VIEW)
// ==========================================
if(mode === ModalMode.VIEW || mode === ModalMode.VIEW_ONLY) {
newModal(new ModalInfo({modalTitle: "요금제 상세보기", inputArray: modalInputArray, modalWidth}), mode, 'main', {
let viewInputArray = [
new ModalInput({
inputType: ModalInputType.customer,
inputId: "detailViewTemplate",
inputName: "detailViewTemplate",
options: () => {
return `
<div class="col-12 p-0" id="ratePlanDetailScrollArea">
<div class="custom-card">
<div class="custom-card-header">기본 정보</div>
<div class="custom-card-body">
<table class="table table-borderless mb-0 info-table">
<tr><th>요금제명:</th><td id="v_ratePlanName">-</td></tr>
<tr><th>요금제 구분:</th><td id="v_ratePlanType">-</td></tr>
<tr><th>계절 구분:</th><td id="v_seasonType">-</td></tr>
<tr><th>적용 기간:</th><td id="v_applyDate">-</td></tr>
<tr><th>활성화 상태:</th><td id="v_isActive">-</td></tr>
<tr><th>설명:</th><td id="v_description">-</td></tr>
</table>
</div>
</div>
<div class="custom-card">
<div class="custom-card-header">승인 정보</div>
<div class="custom-card-body">
<table class="table table-borderless mb-0 info-table">
<tr><th>승인 상태:</th><td id="v_approvalStatus">-</td></tr>
</table>
</div>
</div>
<div class="custom-card">
<div class="custom-card-header">시간대별 단가 설정</div>
<div class="custom-card-body p-0" id="v_priceTableArea"></div>
</div>
<div class="custom-card mb-1">
<div class="custom-card-header" style="border-bottom: 1px solid #e5e5e5;">
적용 충전기 목록 (<span id="v_chargerCount">0</span>개)
</div>
<div class="custom-card-body pt-3 pb-2" style="background-color: #fcfcfc;">
<div class="row g-2" id="v_chargerListArea"></div>
</div>
</div>
</div>
`;
}
})
];
newModal(new ModalInfo({modalTitle: "요금제 상세보기", inputArray: viewInputArray, modalWidth: modalWidth}), mode, 'main', {
loadUrl: loadUrl,
ids: ids,
labelWidth: labelWidth,
@@ -281,14 +238,12 @@
else if (param1 && param1.data) data = param1.data;
else if (param2 && param2.data) data = param2.data;
if(!data) {
console.error("데이터 추출 실패!");
return;
}
if(!data) { return; }
setTimeout(() => {
$('#v_ratePlanName').text(data.ratePlanName || '-');
$('#v_ratePlanType').text(data.ratePlanType === 'time' ? '계시별 요금제' : data.ratePlanType);
$('#v_seasonType').text(data.seasonType === 'season' ? '계절별' : '연중');
$('#v_applyDate').text(`${data.applyStartDate || ''} ~ ${data.applyEndDate || ''}`);
$('#v_isActive').text((data.isActive === "true" || data.isActive === 'Y' || data.active === true) ? '활성화' : '비활성');
$('#v_description').text(data.description || '-');
@@ -299,16 +254,7 @@
$('#v_approvalStatus').text(data.approvalStatus || '대기중');
}
if (data.detailsTime && data.detailsTime.length > 0) {
data.detailsTime.forEach(seasonData => {
let season = seasonData.seasonType;
if(seasonData.timeRates) {
seasonData.timeRates.forEach(rate => {
$(`#price_${season}_${rate.id}`).html(`<span class="val-price">${rate.price}</span> <span class="val-unit">원/kWh</span>`);
});
}
});
}
renderViewPriceTable(data.detailsTime || []);
if (data.chargerList && data.chargerList.length > 0) {
$('#v_chargerCount').text(data.chargerList.length);
@@ -316,7 +262,6 @@
let isActive = (c.isActive === true || c.active === true || c.isActive === 'Y');
let badgeClass = isActive ? 'badge-square active' : 'badge-square';
let badgeText = isActive ? '적용중' : '중지';
return `
<div class="col-6 col-sm-4 mb-2">
<div class="charger-item">
@@ -335,8 +280,370 @@
}
});
}
// ==========================================
// 💡 등록 모드 (REGISTER)
// ==========================================
else if(mode === ModalMode.REGISTER) {
let registerInputArray = [
new ModalInput({
inputType: ModalInputType.customer,
inputId: "registerViewTemplate",
options: () => {
return `
<div class="col-12 p-0" id="ratePlanDetailScrollArea">
<div class="custom-card">
<div class="custom-card-header">기본 정보</div>
<div class="custom-card-body">
<table class="table table-borderless mb-0 info-table">
<tr><th>요금제명:</th><td><input type="text" id="i_ratePlanName" class="form-control form-control-sm" placeholder="예: 2026년 계시별 요금제"></td></tr>
<tr><th>요금제 구분:</th><td><select id="i_ratePlanType" class="form-control form-control-sm" disabled><option value="time" selected>계시별</option></select></td></tr>
<tr><th>계절 선택:</th><td>
<select id="i_seasonType" class="form-control form-control-sm">
<option value="season" selected>계절별</option>
<option value="all_year">연중</option>
</select>
</td></tr>
<tr><th>적용 시작일:</th><td><input type="date" id="i_applyStartDate" class="form-control form-control-sm"></td></tr>
<tr><th>적용 종료일:</th><td><input type="date" id="i_applyEndDate" class="form-control form-control-sm"></td></tr>
<tr><th>요금제 설명:</th><td><textarea id="i_description" class="form-control form-control-sm" rows="2"></textarea></td></tr>
</table>
</div>
</div>
<div class="custom-card">
<div class="custom-card-header">계시별 단가 설정</div>
<div class="custom-card-body">
<div id="i_seasonTabsArea"></div>
<div id="i_pricePanelsArea"></div>
</div>
</div>
<div class="custom-card mb-1">
<div class="custom-card-header">적용할 충전기 선택</div>
<div class="custom-card-body">
<div class="d-flex gap-2 mb-2">
<input type="text" id="i_chargerSearch" class="form-control form-control-sm" placeholder="충전기명 또는 위치로 검색...">
<select id="i_chargerFilter" class="form-control form-control-sm" style="max-width:120px;">
<option value="">전체</option>
<option value="완속">완속</option>
<option value="급속">급속</option>
<option value="초급속">초급속</option>
</select>
</div>
<div class="mb-2">
<input type="checkbox" id="i_chargerAll"> <label for="i_chargerAll" class="ms-1">전체 선택 (<span id="i_chargerAllCount">0</span>개)</label>
</div>
<div class="charger-search-list" id="i_chargerListArea"></div>
</div>
</div>
</div>
`;
}
})
];
newModal(new ModalInfo({modalTitle: "새 단가 정책 생성", inputArray: registerInputArray, modalWidth: modalWidth}), mode, 'main', {
// 🚨 insertUrl을 제거하여 프레임워크 폼 전송 방지
labelWidth: '1px',
callInit: function() {
setTimeout(() => {
initRegisterForm();
// 프레임워크 저장 버튼 이벤트 가로채기
$('.modal-footer button[name="approve"]').off('click').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
if(confirm("등록하시겠습니까?")) {
submitRegisterForm();
}
});
}, 200);
}
});
}
};
let priceState = {};
let activeSeason = null;
function getSeasons() {
const seasonType = $('#i_seasonType').val();
return seasonType === 'season' ? ['spring','summer','fall','winter'] : ['all_year'];
}
function initPriceStateFor(seasonKey) {
if (!priceState[seasonKey]) {
let basic = {}; BASIC_SLOTS.forEach(s => basic[s.id] = 0);
let detail = {}; for (let h = 0; h < 24; h++) detail[h] = 0;
priceState[seasonKey] = { mode: 'basic', basic, detail };
}
}
function initRegisterForm() {
priceState = {};
$('#i_seasonType').off('change').on('change', renderSeasonTabsAndPanel);
renderSeasonTabsAndPanel();
loadChargerList();
$('#i_chargerSearch, #i_chargerFilter').off('input change').on('input change', filterChargerList);
$('#i_chargerAll').off('change').on('change', function() {
$('#i_chargerListArea .charger-check-cb:visible').prop('checked', $(this).is(':checked'));
updateAllCheckState();
});
}
function renderSeasonTabsAndPanel() {
const seasons = getSeasons();
seasons.forEach(initPriceStateFor);
activeSeason = seasons[0];
let tabsHtml = '';
if (seasons.length > 1) {
tabsHtml = `<div class="season-tabs">` +
seasons.map((s, i) => `<button type="button" class="season-tab-btn ${i===0?'active':''}" data-season="${s}">${SEASON_LABEL[s]}</button>`).join('') +
`</div>`;
}
$('#i_seasonTabsArea').html(tabsHtml);
$('.season-tab-btn').off('click').on('click', function() {
saveCurrentPanelValues(activeSeason); // 탭 이동 시 기존 탭 데이터 저장
$('.season-tab-btn').removeClass('active');
$(this).addClass('active');
activeSeason = $(this).data('season');
renderPricePanel(activeSeason);
});
renderPricePanel(activeSeason);
}
function renderPricePanel(seasonKey) {
const state = priceState[seasonKey];
const modeToggleHtml = `
<div class="d-flex align-items-center gap-2 mb-2">
<div class="time-mode-toggle flex-grow-1">
<div class="time-mode-btn ${state.mode==='basic'?'active':''}" data-mode="basic">기본 시간대</div>
<div class="time-mode-btn ${state.mode==='detail'?'active':''}" data-mode="detail">세부 시간대</div>
</div>
</div>
`;
let bodyHtml;
if (state.mode === 'basic') {
bodyHtml = `<div id="i_basicPanel">` + BASIC_SLOTS.map(slot => `
<div class="basic-price-row">
<span class="basic-price-label">${slot.label}</span>
<div>
<input type="number" class="form-control form-control-sm basic-price-input i_basic_input" data-id="${slot.id}" value="${state.basic[slot.id]}"> 원/kWh
</div>
</div>
`).join('') + `</div>`;
} else {
let rows = '';
for (let h = 0; h < 24; h++) {
const start = String(h).padStart(2,'0') + ':00';
const end = h === 23 ? '23:59' : String(h+1).padStart(2,'0') + ':00';
rows += `
<div class="detail-price-row">
<span class="detail-price-label">${String(h).padStart(2,'0')}시 (${start} - ${end})</span>
<div><input type="number" class="form-control form-control-sm detail-price-input i_detail_input" data-hour="${h}" value="${state.detail[h]}"> 원/kWh</div>
</div>`;
}
bodyHtml = `<div class="detail-scroll" id="i_detailPanel">${rows}</div>`;
}
$('#i_pricePanelsArea').html(modeToggleHtml + bodyHtml);
$('.time-mode-btn').off('click').on('click', function() {
saveCurrentPanelValues(seasonKey);
priceState[seasonKey].mode = $(this).data('mode');
renderPricePanel(seasonKey);
});
}
function saveCurrentPanelValues(seasonKey) {
const state = priceState[seasonKey];
if (state.mode === 'basic') {
$('.i_basic_input').each(function() {
state.basic[$(this).data('id')] = parseFloat($(this).val()) || 0;
});
} else {
$('.i_detail_input').each(function() {
state.detail[$(this).data('hour')] = parseFloat($(this).val()) || 0;
});
}
}
function loadChargerList() {
$.ajax({
url: "/cms/chgr/list.json",
type: "POST",
data: {
start: 0,
length: 9999
},
success: function(res) {
const list = res.data || res.list || [];
renderChargerList(list);
},
error: function() {
$('#i_chargerListArea').html('<div class="text-muted small p-2">충전기 목록을 불러오지 못했습니다.</div>');
}
});
}
function renderChargerList(list) {
const mapped = list.map(c => {
const chargerCd = `${c.providerId || ''}${c.stId || ''}${c.chgrId || ''}`;
return {
chargerCd: chargerCd,
chargerName: c.chgrNm || chargerCd,
location: c.stNm || '',
chargerType: c.speedTp || ''
};
});
$('#i_chargerAllCount').text(mapped.length);
let html = mapped.map(c => `
<div class="charger-check-item" data-name="${c.chargerName.toLowerCase()}"
data-loc="${c.location.toLowerCase()}" data-type="${c.chargerType}">
<input type="checkbox" class="charger-check-cb" value="${c.chargerCd}">
<label>
<div><strong>${c.chargerName}</strong> <span class="badge-square">${c.chargerType}</span></div>
<div class="text-muted" style="font-size:0.75rem;">${c.chargerCd} · ${c.location}</div>
</label>
</div>
`).join('');
$('#i_chargerListArea').html(html || '<div class="text-muted small p-2">등록된 충전기가 없습니다.</div>');
$('.charger-check-cb').off('change').on('change', updateAllCheckState);
}
function filterChargerList() {
const kw = ($('#i_chargerSearch').val() || '').toLowerCase();
const type = $('#i_chargerFilter').val();
$('.charger-check-item').each(function() {
const nameMatch = $(this).data('name').includes(kw) || $(this).data('loc').includes(kw);
const typeMatch = !type || $(this).data('type') === type;
$(this).toggle(nameMatch && typeMatch);
});
}
function updateAllCheckState() {
const total = $('.charger-check-cb:visible').length;
const checked = $('.charger-check-cb:visible:checked').length;
$('#i_chargerAll').prop('checked', total > 0 && total === checked);
}
function renderViewPriceTable(detailsTime) {
const seasonsSet = new Set(detailsTime.map(d => d.seasonType));
const seasons = seasonsSet.size ? Array.from(seasonsSet) : ['all_year'];
let priceMap = {};
let allIds = new Set();
detailsTime.forEach(d => {
(d.timeRates || []).forEach(r => {
priceMap[r.id] = priceMap[r.id] || {};
priceMap[r.id][d.seasonType] = r.price;
allIds.add(r.id);
});
});
const isBasic = [1, 2, 3].some(id => allIds.has(id));
let headerHtml = seasons.map(s => `<th>${SEASON_LABEL[s] || s}</th>`).join('');
let rows = '';
if (isBasic) {
BASIC_SLOTS.forEach(slot => {
rows += `<tr><td class="border-end py-2 fw-bold">${slot.label}</td>` +
seasons.map(s => `<td>${priceMap[slot.id] && priceMap[slot.id][s] != null ? `<span class="val-price">${priceMap[slot.id][s]}</span> <span class="val-unit">원/kWh</span>` : '-'}</td>`).join('') +
`</tr>`;
});
} else {
for (let h = 0; h < 24; h++) {
const id = h + 4;
rows += `<tr><td class="border-end py-2">${String(h).padStart(2, '0')}시</td>` +
seasons.map(s => `<td>${priceMap[id] && priceMap[id][s] != null ? `<span class="val-price">${priceMap[id][s]}</span> <span class="val-unit">원/kWh</span>` : '-'}</td>`).join('') +
`</tr>`;
}
}
$('#v_priceTableArea').html(`
<table class="table mb-0 text-center align-middle price-table">
<thead><tr><th class="border-end text-dark">시간대구분</th>${headerHtml}</tr></thead>
<tbody>${rows}</tbody>
</table>
`);
}
function submitRegisterForm() {
const seasons = getSeasons();
saveCurrentPanelValues(activeSeason);
let detailsTime = seasons.map(seasonKey => {
const state = priceState[seasonKey];
let timeRates = [];
if (state.mode === 'basic') {
BASIC_SLOTS.forEach(slot => timeRates.push({ id: slot.id, price: state.basic[slot.id] || 0 }));
} else {
for (let h = 0; h < 24; h++) timeRates.push({ id: h + 4, price: state.detail[h] || 0 });
}
return { seasonType: seasonKey, timeRates };
});
let chargerList = [];
$('.charger-check-cb:checked').each(function() {
chargerList.push({
chargerCd: $(this).val(),
applyStartDate: $('#i_applyStartDate').val(),
applyEndDate: $('#i_applyEndDate').val(),
isActive: true
});
});
let requestDto = {
ratePlanName: $('#i_ratePlanName').val(),
ratePlanType: "time",
seasonType: $('#i_seasonType').val(),
applyStartDate: $('#i_applyStartDate').val(),
applyEndDate: $('#i_applyEndDate').val(),
description: $('#i_description').val(),
isActive: true,
approvalStatus: "pending",
detailsTime: detailsTime,
chargerList: chargerList
};
if(!requestDto.ratePlanName) { alert("요금제명을 입력해주세요."); return; }
if(!requestDto.applyStartDate) { alert("적용 시작일을 입력해주세요."); return; }
if(!requestDto.applyEndDate) { alert("적용 종료일을 입력해주세요."); return; }
if(chargerList.length === 0) { alert("적용할 충전기를 1개 이상 선택해주세요."); return; }
$.ajax({
url: rootPath + "insert.json",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(requestDto),
success: function(res) {
if(res.result === 'success') {
alert("등록되었습니다.");
} else {
alert("오류 발생: " + res.message);
}
$('.modal-footer .btn-cls-main').trigger('click');
datatable.ajax.reload();
},
error: function(err) {
alert("서버 통신 오류가 발생했습니다.");
$('.modal-footer .btn-cls-main').trigger('click');
}
});
}
$("#searchBtn").click(function() { datatable.ajax.reload(); });
$("#resetBtn").click(function() {
$("#ratePlanNameLike").val("");