Compare commits

..
16 Commits
60 changed files with 2853 additions and 239 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.
@@ -0,0 +1,25 @@
package net.jwsi.jcms.vpp.RatePlanDetailTime;
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_rate_plan_detail_time")
public class RatePlanDetailTime extends BaseStEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer detailTimeId; //상세 id (계시별 단가 기준)(pk)
private Integer ratePlanId; //요금제 id (fk)
private Integer detailCapacityId; //상세 id (충전용량 기준, fk)
private Integer timeSlotId; //시간대 id (fk: time_slots)(fk)
private Double price; //시간대별 단가
private String seasonType; //계절 타입
}
@@ -0,0 +1,13 @@
package net.jwsi.jcms.vpp.RatePlanDetailTime;
import net.jwsi.jcms.base.BaseRecStRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface RatePlanDetailTimeRepository extends BaseRecStRepository<RatePlanDetailTime, Integer> {
void deleteByRatePlanId(Integer ratePlanId);
List<RatePlanDetailTime> findByRatePlanId(Integer ratePlanId);
}
@@ -2,15 +2,15 @@ package net.jwsi.jcms.vpp.api;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import net.jwsi.jcms.vpp.chgr.ChgrResponseDto; 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.chgrHist.ChgrHistResponseDto;
import net.jwsi.jcms.vpp.provider.ProviderDto; import net.jwsi.jcms.vpp.provider.ProviderDto;
import net.jwsi.jcms.vpp.rechgingList.RechgingListDto;
import net.jwsi.jcms.vpp.ratePlan.RatePlanResponseDto;
import net.jwsi.jcms.vpp.station.StationDto; import net.jwsi.jcms.vpp.station.StationDto;
import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -19,64 +19,83 @@ import java.util.Map;
@RequiredArgsConstructor @RequiredArgsConstructor
public class ApiClient { public class ApiClient {
private final RestClient restClient; private final RestClient externalApiClient;
public ApiResponse<List<StationDto>> fetchStations(String baseUrl, String token) { public ApiResponse<List<StationDto>> fetchStations() {
URI uri = UriComponentsBuilder.fromUriString(baseUrl) return externalApiClient.post()
.path("/api/st/list") .uri("/api/st/list")
.build()
.toUri();
return restClient.post()
.uri(uri)
.header("Authorization", "Bearer " + token)
.body(Collections.emptyMap()) .body(Collections.emptyMap())
.retrieve() .retrieve()
.body(new ParameterizedTypeReference<ApiResponse<List<StationDto>>>() {}); .body(new ParameterizedTypeReference<ApiResponse<List<StationDto>>>() {});
} }
public ApiResponse<List<ProviderDto>> fetchProviders(String baseUrl, String token) { public ApiResponse<List<ProviderDto>> fetchProviders() {
URI uri = UriComponentsBuilder.fromUriString(baseUrl) return externalApiClient.get()
.path("/api/provider/list") .uri("/api/provider/list")
.build()
.toUri();
return restClient.get()
.uri(uri)
.header("Authorization", "Bearer " + token)
.retrieve() .retrieve()
.body(new ParameterizedTypeReference<ApiResponse<List<ProviderDto>>>() {}); .body(new ParameterizedTypeReference<ApiResponse<List<ProviderDto>>>() {});
} }
public ApiResponse<List<ChgrResponseDto>> fetchChgr(String baseUrl, String token) { public ApiResponse<List<ChgrResponseDto>> fetchChgr() {
URI uri = UriComponentsBuilder.fromUriString(baseUrl) return externalApiClient.post()
.path("/api/charger/list") .uri("/api/charger/list")
.build()
.toUri();
return restClient.post()
.uri(uri)
.header("Authorization", "Bearer " + token)
.body(Collections.emptyMap()) .body(Collections.emptyMap())
.retrieve() .retrieve()
.body(new ParameterizedTypeReference<ApiResponse<List<ChgrResponseDto>>>() {}); .body(new ParameterizedTypeReference<ApiResponse<List<ChgrResponseDto>>>() {});
} }
public ApiResponse<List<ChgrHistResponseDto>> fetchChgrHist(String baseUrl, String token) {
URI uri = UriComponentsBuilder.fromUriString(baseUrl)
.path("/api/charge/hist")
.build()
.toUri();
public ApiResponse<List<ChgrHistResponseDto>> fetchChgrHist(String startDt, String pageSize) {
Map<String, String> requestBody = Map.of( Map<String, String> requestBody = Map.of(
"startDt", "2026-07-29", "startDt", startDt,
"pageSize","50" "pageSize", pageSize
); );
return restClient.post() return externalApiClient.post()
.uri(uri) .uri("/api/charge/hist")
.header("Authorization", "Bearer " + token)
.body(requestBody) .body(requestBody)
.retrieve() .retrieve()
.body(new ParameterizedTypeReference<ApiResponse<List<ChgrHistResponseDto>>>() {}); .body(new ParameterizedTypeReference<ApiResponse<List<ChgrHistResponseDto>>>() {});
} }
public ApiResponse<List<ChgrCurrState>> fetchChgrCurrState(String pageSize) {
Map<String, String> requestBody = Map.of(
"pageSize", pageSize
);
return externalApiClient.post()
.uri("/api/charge/stateInfo")
.body(requestBody)
.retrieve()
.body(new ParameterizedTypeReference<ApiResponse<List<ChgrCurrState>>>() {});
}
public ApiResponse<List<RechgingListDto>> fetchRechgingList() {
return externalApiClient.post()
.uri("/api/charge/chargingInfo")
.body(Collections.emptyMap())
.retrieve()
.body(new ParameterizedTypeReference<ApiResponse<List<RechgingListDto>>>() {});
}
public ApiResponse<List<RatePlanResponseDto>> fetchRatePlan(){
Map<String, String> requestBody = Map.of(
"type","all",
"pageSize", "10000"
);
return externalApiClient.post()
.uri("/api/ratePlan/list")
.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);
}
} }
@@ -0,0 +1,13 @@
package net.jwsi.jcms.vpp.api;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.List;
@NoArgsConstructor
@Getter
public class ApiToken {
private String jwt;
private List<String> roles;
}
@@ -0,0 +1,63 @@
package net.jwsi.jcms.vpp.api;
import net.jwsi.jcms.vpp.utils.JwtUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.time.LocalDateTime;
import java.util.Map;
@Service
public class ExternalApiService {
private final RestClient authRestClient = RestClient.create();
@Value("${jcms.api.base-url}")
private String baseUrl;
@Value("${jcms.api.user-id}")
private String
apiUserId;
@Value("${jcms.api.user-pwd}")
private String apiUserPwd;
private String cachedToken;
private LocalDateTime tokenExpiryTime;
public synchronized String getAccessToken() {
if (cachedToken != null && tokenExpiryTime != null && LocalDateTime.now().plusMinutes(1).isBefore(tokenExpiryTime)) {
return cachedToken;
}
URI uri = UriComponentsBuilder.fromUriString(baseUrl)
.path("/api/auth/token")
.build()
.toUri();
Map<String, String> requestBody = Map.of(
"userId", apiUserId,
"pwd", apiUserPwd
);
ApiResponse<ApiToken> response = authRestClient.post()
.uri(uri)
.body(requestBody)
.retrieve()
.body(new ParameterizedTypeReference<ApiResponse<ApiToken>>() {});
if (response != null && response.getData() != null) {
ApiToken tokenDto = response.getData();
this.cachedToken = tokenDto.getJwt();
this.tokenExpiryTime = JwtUtil.getExpirationTime(this.cachedToken);
return this.cachedToken;
}
throw new IllegalStateException("외부 API 토큰 발급에 실패했습니다.");
}
}
@@ -1,23 +1,25 @@
package net.jwsi.jcms.vpp.api; package net.jwsi.jcms.vpp.api;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient;
import java.time.Duration;
@Configuration @Configuration
public class RestClientConfig { public class RestClientConfig {
@Bean @Value("${jcms.api.base-url}")
public RestClient restClient() { private String baseUrl;
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout((int) Duration.ofSeconds(5).toMillis());
factory.setReadTimeout((int) Duration.ofSeconds(30).toMillis());
@Bean
public RestClient externalApiClient(ExternalApiService externalApiService) {
return RestClient.builder() return RestClient.builder()
.requestFactory(factory) .baseUrl(baseUrl)
.requestInterceptor((request, body, execution) -> {
String token = externalApiService.getAccessToken();
request.getHeaders().set("Authorization", "Bearer " + token);
return execution.execute(request, body);
})
.build(); .build();
} }
} }
@@ -0,0 +1,26 @@
package net.jwsi.jcms.vpp.chargerRatePlan;
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_charger_rate_plan")
public class ChargerRatePlan extends BaseStEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 🚨 무조건 추가!
private Integer id; //적용 이력 id(pk)
private String chargerCd; //충전기 코드 (사업자코드+충전소번호+충전기번호)
private Integer ratePlanId; //적용 요금제 id (fk)
private String applyStartDate; //적용 시작일
private String applyEndDate; //적용 종료일 (null = 현재 적용중)
private String isActive; //활성화 여부
private String notes; //비고 / 변경 이유
}
@@ -0,0 +1,12 @@
package net.jwsi.jcms.vpp.chargerRatePlan;
import net.jwsi.jcms.base.BaseRecStRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ChargerRatePlanRepository extends BaseRecStRepository<ChargerRatePlan,Integer> {
void deleteByRatePlanId(Integer ratePlanId);
List<ChargerRatePlan> findByRatePlanId(Integer ratePlanId);
}
@@ -1,8 +1,6 @@
package net.jwsi.jcms.vpp.chgr; package net.jwsi.jcms.vpp.chgr;
import jakarta.persistence.Column; import jakarta.persistence.*;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
@@ -14,12 +12,20 @@ import net.jwsi.jcms.base.BaseStEntity;
@Getter @Getter
@ToString @ToString
@Entity(name = "vpp_chgr") @Entity(name = "vpp_chgr")
@IdClass(ChgrId.class)
public class Chgr extends BaseStEntity { public class Chgr extends BaseStEntity {
@Transient
private ChgrId oldId__;
@Id @Id
@Column(name = "CHGR_ID") @Column(name = "CHGR_ID")
private String chgrId; //충전기 id(pk) private String chgrId; //충전기 id(pk)
@Id
@Column(name = "PROVIDER_ID") @Column(name = "PROVIDER_ID")
private String providerId; //충전사업자 id(pk) @Id private String providerId; //충전사업자 id(pk) @Id
@Id
@Column(name = "ST_ID")
private String stId; //충전소 id(pk) @Id private String stId; //충전소 id(pk) @Id
private String stNm; //충전소 명 private String stNm; //충전소 명
private String chgrNm; //충전기 명 private String chgrNm; //충전기 명
@@ -14,7 +14,7 @@ import java.util.Map;
@Controller @Controller
@Slf4j @Slf4j
@RequestMapping("/cms/chgr") @RequestMapping("/cms/chgr/")
public class ChgrController extends BaseController<Chgr,String,ChgrRepository,ChgrService> { public class ChgrController extends BaseController<Chgr,String,ChgrRepository,ChgrService> {
private final String path = "/system/chgr/"; private final String path = "/system/chgr/";
@@ -28,6 +28,7 @@ public class ChgrController extends BaseController<Chgr,String,ChgrRepository,Ch
} }
@GetMapping("list") @GetMapping("list")
public String list(){ public String list(){
chgrService.getCharger();
return path+"list"; return path+"list";
} }
@PostMapping("list.json") @PostMapping("list.json")
@@ -0,0 +1,18 @@
package net.jwsi.jcms.vpp.chgr;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class ChgrId implements Serializable {
private String providerId;
private String stId;
private String chgrId;
}
@@ -12,7 +12,7 @@ public class ChgrResponseDto {
private String chgrId; private String chgrId;
private String chgrNm; private String chgrNm;
private String speedTp; private String speedTp;
private String chgrTp; private String chgrType;
private double gpsXpos; private double gpsXpos;
private double gpsYpos; private double gpsYpos;
private String locInfo; private String locInfo;
@@ -29,7 +29,7 @@ public class ChgrResponseDto {
chgr.setStNm(this.stNm); chgr.setStNm(this.stNm);
chgr.setChgrNm(this.chgrNm); chgr.setChgrNm(this.chgrNm);
chgr.setSpeedTp(this.speedTp); chgr.setSpeedTp(this.speedTp);
chgr.setChgrTp(this.chgrTp); chgr.setChgrTp(this.chgrType);
return chgr; return chgr;
} }
} }
@@ -8,7 +8,6 @@ import net.jwsi.jcms.utils.HttpUtil;
import net.jwsi.jcms.utils.LoginUtil; import net.jwsi.jcms.utils.LoginUtil;
import net.jwsi.jcms.vpp.api.ApiClient; import net.jwsi.jcms.vpp.api.ApiClient;
import net.jwsi.jcms.vpp.api.ApiResponse; import net.jwsi.jcms.vpp.api.ApiResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
@@ -22,11 +21,6 @@ import java.util.stream.Collectors;
public class ChgrService extends BaseService<Chgr,String,ChgrRepository> { public class ChgrService extends BaseService<Chgr,String,ChgrRepository> {
private final ApiClient apiClient; private final ApiClient apiClient;
@Value("${jcms.api.base-url}")
private String baseUrl;
@Value("${jcms.api.token}")
private String apiToken;
public ChgrService(ChgrRepository repository, ApiClient apiClient) { public ChgrService(ChgrRepository repository, ApiClient apiClient) {
super(repository); super(repository);
this.apiClient = apiClient; this.apiClient = apiClient;
@@ -34,7 +28,7 @@ public class ChgrService extends BaseService<Chgr,String,ChgrRepository> {
@Transactional @Transactional
@Scheduled(cron = "0 0 0 * * *") @Scheduled(cron = "0 0 0 * * *")
public void getCharger() { public void getCharger() {
ApiResponse<List<ChgrResponseDto>> response = apiClient.fetchChgr(baseUrl, apiToken); ApiResponse<List<ChgrResponseDto>> response = apiClient.fetchChgr();
if (response == null || !Integer.valueOf(200).equals(response.getCode())) { if (response == null || !Integer.valueOf(200).equals(response.getCode())) {
String msg = (response != null) ? response.getMessage() : "응답 없음"; String msg = (response != null) ? response.getMessage() : "응답 없음";
@@ -79,7 +73,12 @@ public class ChgrService extends BaseService<Chgr,String,ChgrRepository> {
@Override @Override
protected Specification<Chgr> getSpecification(Chgr chgr) { protected Specification<Chgr> getSpecification(Chgr chgr) {
return null; Specification<Chgr> rst = null;
if(chgr == null) return null;
rst = addWhere(rst, chgr.getChgrId(), ChgrSpecification.getChgrId(chgr.getChgrId()));
rst = addWhere(rst, chgr.getStId(), ChgrSpecification.getStId(chgr.getStId()));
return rst;
} }
@Override @Override
@@ -1,4 +1,17 @@
package net.jwsi.jcms.vpp.chgr; package net.jwsi.jcms.vpp.chgr;
public class ChgrSpecification { import net.jwsi.jcms.base.BaseSpecification;
import org.springframework.data.jpa.domain.Specification;
public class ChgrSpecification extends BaseSpecification {
public static Specification<Chgr> getStId(String stId) {
return (root, query, cb) -> {
return cb.equal(root.get("stId"), stId);
};
}
public static Specification<Chgr> getChgrId(String chgrId) {
return (root, query, cb) -> {
return cb.equal(root.get("chgrId"), chgrId);
};
}
} }
@@ -0,0 +1,74 @@
package net.jwsi.jcms.vpp.chgrCurrState;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import net.jwsi.jcms.base.BaseStEntity;
@NoArgsConstructor
@Getter
@Setter
@ToString
@Entity(name = "vpp_chgr_curr_state")
@IdClass(ChgrCurrStateId.class)
public class ChgrCurrState extends BaseStEntity {
@Transient
private ChgrCurrStateId oldId__;
@Id
@Column(name = "provider_id")
private String providerId; // provider_id (PK)
@Id
@Column(name = "st_id")
private String stId; // st_id (PK)
@Id
@Column(name = "chgr_id")
private String chgrId; // chgr_id (PK)
@Column(name = "state_dt")
private String stateDt;
@Column(name = "op_mode")
private String opMode;
@Column(name = "ch1_rechg_state_cd")
private String ch1RechgStateCd;
@Column(name = "ch1_door_state_cd")
private String ch1DoorStateCd;
@Column(name = "ch1_plug_state_cd")
private String ch1PlugStateCd;
@Column(name = "ch2_rechg_state_cd")
private String ch2RechgStateCd;
@Column(name = "ch2_door_state_cd")
private String ch2DoorStateCd;
@Column(name = "ch2_plug_state_cd")
private String ch2PlugStateCd;
@Column(name = "ch3_rechg_state_cd")
private String ch3RechgStateCd;
@Column(name = "ch3_door_state_cd")
private String ch3DoorStateCd;
@Column(name = "ch3_plug_state_cd")
private String ch3PlugStateCd;
@Column(name = "integrated_kwh")
private Double integratedKwh;
@Column(name = "dc_power_wh")
private Double dcPowerWh;
@Column(name = "ac_power_wh")
private Double acPowerWh;
}
@@ -0,0 +1,63 @@
package net.jwsi.jcms.vpp.chgrCurrState;
import net.jwsi.jcms.base.BaseController;
import net.jwsi.jcms.exception.JsonDataException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
import java.util.Map;
@Controller
@RequestMapping("/cms/chgrCurrState/")
public class ChgrCurrStateController extends BaseController<ChgrCurrState,ChgrCurrStateId,ChgrCurrStateRepository,ChgrCurrStateService> {
private final ChgrCurrStateService chgrCurrStateService;
private final String path = "/system/chgrCurrState/";
public ChgrCurrStateController(ChgrCurrStateService service, ChgrCurrStateService chgrCurrStateService) {
super(service);
this.chgrCurrStateService = chgrCurrStateService;
}
@GetMapping("list")
public String list(){
chgrCurrStateService.getChgrCurrState();
return path+"list";
}
@PostMapping("list.json")
@ResponseBody
public Map<String,Object> list(ChgrCurrState chgrCurrState){
return svcSelectList(chgrCurrState);
}
@PostMapping("view.json")
@ResponseBody
public Map<String, Object> viewData(ChgrCurrState chgrCurrState) throws JsonDataException {
return svcSelectOne(chgrCurrState);
}
@PostMapping("insert.json")
@ResponseBody
public Map<String, Object> insert(ChgrCurrState chgrCurrState) throws JsonDataException {
return svcInsert(chgrCurrState);
}
@PostMapping("update.json")
@ResponseBody
public Map<String, Object> update(ChgrCurrState chgrCurrState) throws JsonDataException {
return svcUpdate(chgrCurrState);
}
@Override
protected void chkSelectList(List<ChgrCurrState> list) throws JsonDataException {
}
@Override
protected void chkSelect(ChgrCurrState data) throws JsonDataException {
}
}
@@ -0,0 +1,19 @@
package net.jwsi.jcms.vpp.chgrCurrState;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class ChgrCurrStateId implements Serializable {
private String providerId;
private String stId;
private String chgrId;
}
@@ -0,0 +1,8 @@
package net.jwsi.jcms.vpp.chgrCurrState;
import net.jwsi.jcms.base.BaseRecStRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ChgrCurrStateRepository extends BaseRecStRepository<ChgrCurrState,ChgrCurrStateId> {
}
@@ -0,0 +1,95 @@
package net.jwsi.jcms.vpp.chgrCurrState;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import net.jwsi.jcms.base.BaseService;
import net.jwsi.jcms.base.EnumSqlType;
import net.jwsi.jcms.utils.HttpUtil;
import net.jwsi.jcms.utils.LoginUtil;
import net.jwsi.jcms.vpp.api.ApiClient;
import net.jwsi.jcms.vpp.api.ApiResponse;
import org.springframework.data.domain.Page;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@Slf4j
public class ChgrCurrStateService extends BaseService<ChgrCurrState,ChgrCurrStateId,ChgrCurrStateRepository> {
private final ApiClient apiClient;
public ChgrCurrStateService(ChgrCurrStateRepository repository, ApiClient apiClient) {
super(repository);
this.apiClient = apiClient;
}
@Transactional
@Scheduled(cron = "0 0 0 * * *")
public void getChgrCurrState() {
ApiResponse<List<ChgrCurrState>> response = apiClient.fetchChgrCurrState("12");
if (response == null || !Integer.valueOf(200).equals(response.getCode())) {
String msg = (response != null) ? response.getMessage() : "응답 없음";
throw new RuntimeException("API 호출 실패: " + msg);
}
List<ChgrCurrState> list = response.getData();
if (list == null || list.isEmpty()) {
log.info("가져온 충전소 데이터가 없습니다.");
}
List<ChgrCurrState> savedStations = super.repository.saveAll(list);
log.info("총 {}건의 충전소 데이터가 DB에 동기화되었습니다.", savedStations.size());
}
@Override
protected Integer getUserId() {
return HttpUtil.getUserId();
}
@Override
protected String getUserName(Integer id) {
return LoginUtil.getUserName(id);
}
@Override
protected ChgrCurrState newSearchParam(ChgrCurrState chgrCurrState) {
if(chgrCurrState == null){
chgrCurrState = new ChgrCurrState();
}
return chgrCurrState;
}
@Override
protected EnumSqlType useSqlTyp() {
return EnumSqlType.SPECIFICATION;
}
@Override
protected Specification<ChgrCurrState> getSpecification(ChgrCurrState chgrCurrState) {
Specification<ChgrCurrState> rst = null;
if(chgrCurrState == null) return null;
rst = addWhere(rst, chgrCurrState.getChgrId(), ChgrCurrStateSpecification.getChgrId(chgrCurrState.getChgrId()));
rst = addWhere(rst, chgrCurrState.getStId(), ChgrCurrStateSpecification.getStId(chgrCurrState.getStId()));
rst = addWhere(rst, chgrCurrState.getCh1DoorStateCd(), ChgrCurrStateSpecification.getDoorState(chgrCurrState.getCh1DoorStateCd()));
rst = addWhere(rst, chgrCurrState.getCh1PlugStateCd(), ChgrCurrStateSpecification.getPlugState(chgrCurrState.getCh1PlugStateCd()));
rst = addWhere(rst, chgrCurrState.getCh1RechgStateCd(), ChgrCurrStateSpecification.getRechgState(chgrCurrState.getCh1RechgStateCd()));
return rst;
}
@Override
protected Page<ChgrCurrState> _selectPage(ChgrCurrState chgrCurrState) {
return null;
}
@Override
protected List<ChgrCurrState> _selectList(ChgrCurrState chgrCurrState) {
return null;
}
}
@@ -0,0 +1,35 @@
package net.jwsi.jcms.vpp.chgrCurrState;
import net.jwsi.jcms.base.BaseSpecification;
import org.springframework.data.jpa.domain.Specification;
public class ChgrCurrStateSpecification extends BaseSpecification {
public static Specification<ChgrCurrState> getStId(String stId) {
return (root, query, cb) -> {
return cb.equal(root.get("stId"), stId);
};
}
public static Specification<ChgrCurrState> getChgrId(String chgrId) {
return (root, query, cb) -> {
return cb.equal(root.get("chgrId"), chgrId);
};
}
public static Specification<ChgrCurrState> getDoorState(String doorState) {
return (root, query, cb) -> {
return cb.equal(root.get("ch1DoorStateCd"), doorState);
};
}
public static Specification<ChgrCurrState> getPlugState(String plugState) {
return (root, query, cb) -> {
return cb.equal(root.get("ch1PlugStateCd"), plugState);
};
}
public static Specification<ChgrCurrState> getRechgState(String rechgState) {
return (root, query, cb) -> {
return cb.equal(root.get("ch1RechgStateCd"), rechgState);
};
}
}
@@ -22,6 +22,9 @@ public class ChgrHist extends BaseStEntity {
@Column(name = "CHGR_ID", nullable = false) @Column(name = "CHGR_ID", nullable = false)
private String chgrId; // 충전기 id private String chgrId; // 충전기 id
@Column(name = "CH_ID")
private Integer chId;
@Column(name = "ST_ID", nullable = false) @Column(name = "ST_ID", nullable = false)
private String stId; // 충전소 id private String stId; // 충전소 id
@@ -40,7 +43,7 @@ public class ChgrHist extends BaseStEntity {
@Column(name = "RECHG_TIME") @Column(name = "RECHG_TIME")
private String rechgTime; // 충전시간 private String rechgTime; // 충전시간
@Column(name = "RECH_GWH") @Column(name = "RECHG_WH")
private Integer rechGwh; // 충전량 private Integer rechGwh; // 충전량
@Column(name = "RECHG_AMT") @Column(name = "RECHG_AMT")
@@ -10,7 +10,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
@Controller @Controller
@RequestMapping("/cms/chgrHist") @RequestMapping("/cms/chgrHist/")
@Slf4j @Slf4j
public class ChgrHistController extends BaseController<ChgrHist,Integer,ChgrHistRepository,ChgrHistService> { public class ChgrHistController extends BaseController<ChgrHist,Integer,ChgrHistRepository,ChgrHistService> {
@@ -23,6 +23,7 @@ public class ChgrHistController extends BaseController<ChgrHist,Integer,ChgrHist
@GetMapping("list") @GetMapping("list")
public String list() throws IllegalAccessException { public String list() throws IllegalAccessException {
chgrHistService.getChgrHist();
return path+"list"; return path+"list";
} }
@@ -5,4 +5,5 @@ import org.springframework.stereotype.Repository;
@Repository @Repository
public interface ChgrHistRepository extends BaseRecStRepository<ChgrHist,Integer> { public interface ChgrHistRepository extends BaseRecStRepository<ChgrHist,Integer> {
boolean existsByChgrIdAndChIdAndRechgEDt(String chgrId, Integer chId,String rechgEDt);
} }
@@ -52,6 +52,7 @@ public class ChgrHistResponseDto {
chgrHist.setRechgAmt(this.rechgAmt); chgrHist.setRechgAmt(this.rechgAmt);
chgrHist.setPayFnsh(this.payFnsh); chgrHist.setPayFnsh(this.payFnsh);
chgrHist.setRechGwh(this.rechgWh); chgrHist.setRechGwh(this.rechgWh);
chgrHist.setChId(this.chId);
return chgrHist; return chgrHist;
} }
} }
@@ -8,13 +8,12 @@ import net.jwsi.jcms.utils.HttpUtil;
import net.jwsi.jcms.utils.LoginUtil; import net.jwsi.jcms.utils.LoginUtil;
import net.jwsi.jcms.vpp.api.ApiClient; import net.jwsi.jcms.vpp.api.ApiClient;
import net.jwsi.jcms.vpp.api.ApiResponse; import net.jwsi.jcms.vpp.api.ApiResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList; import java.time.LocalDate;
import java.util.List; import java.util.List;
@Slf4j @Slf4j
@@ -23,11 +22,6 @@ public class ChgrHistService extends BaseService<ChgrHist, Integer, ChgrHistRepo
private final ApiClient apiClient; private final ApiClient apiClient;
@Value("${jcms.api.base-url}")
private String baseUrl;
@Value("${jcms.api.token}")
private String apiToken;
public ChgrHistService(ChgrHistRepository repository, ApiClient apiClient) { public ChgrHistService(ChgrHistRepository repository, ApiClient apiClient) {
super(repository); super(repository);
@@ -36,8 +30,10 @@ public class ChgrHistService extends BaseService<ChgrHist, Integer, ChgrHistRepo
@Transactional @Transactional
@Scheduled(cron = "0 */10 * * * *") @Scheduled(cron = "0 */10 * * * *")
public void getChgrHist() throws IllegalAccessException { public void getChgrHist() {
ApiResponse<List<ChgrHistResponseDto>> response = apiClient.fetchChgrHist(baseUrl, apiToken);
String startDt = LocalDate.now().toString();
ApiResponse<List<ChgrHistResponseDto>> response = apiClient.fetchChgrHist(startDt, "50");
if (response == null || !Integer.valueOf(200).equals(response.getCode())) { if (response == null || !Integer.valueOf(200).equals(response.getCode())) {
String msg = (response != null) ? response.getMessage() : "응답 없음"; String msg = (response != null) ? response.getMessage() : "응답 없음";
@@ -46,21 +42,26 @@ public class ChgrHistService extends BaseService<ChgrHist, Integer, ChgrHistRepo
List<ChgrHistResponseDto> dtoList = response.getData(); List<ChgrHistResponseDto> dtoList = response.getData();
if (dtoList == null || dtoList.isEmpty()) { if (dtoList == null || dtoList.isEmpty()) {
log.info("가져온 충전 데이터가 없습니다."); log.info("가져온 충전이력 데이터가 없습니다.");
return;
} }
List<ChgrHist> entityList = dtoList.stream() List<ChgrHist> entityList = dtoList.stream()
.map(ChgrHistResponseDto::toEntity) .map(ChgrHistResponseDto::toEntity)
.toList(); .toList();
List<ChgrHist> updateList = new ArrayList<>(); int insertCount = 0;
for (ChgrHist h : entityList) { for (ChgrHist h : entityList) {
update(h);
updateList.add(h); boolean isExist = ((ChgrHistRepository) super.repository).existsByChgrIdAndChIdAndRechgEDt(h.getChgrId(), h.getChId(),h.getRechgEDt());
if (!isExist) {
super.insert(h);
insertCount++;
}
} }
log.info("총 {}건의 충전소 데이터가 DB에 동기화되었습니다.", updateList.size()); log.info("충전이력 수집 완료: API 응답 {}건 중 신규 저장 {}건", entityList.size(), insertCount);
} }
@Override @Override
@@ -88,7 +89,12 @@ public class ChgrHistService extends BaseService<ChgrHist, Integer, ChgrHistRepo
@Override @Override
protected Specification<ChgrHist> getSpecification(ChgrHist chgrHist) { protected Specification<ChgrHist> getSpecification(ChgrHist chgrHist) {
return null; Specification<ChgrHist> rst = null;
if(chgrHist == null) return null;
rst = addWhere(rst, chgrHist.getChgrId(), ChgrHistSpecification.getChgrId(chgrHist.getChgrId()));
rst = addWhere(rst, chgrHist.getStId(), ChgrHistSpecification.getStId(chgrHist.getStId()));
return rst;
} }
@Override @Override
@@ -0,0 +1,17 @@
package net.jwsi.jcms.vpp.chgrHist;
import net.jwsi.jcms.base.BaseSpecification;
import org.springframework.data.jpa.domain.Specification;
public class ChgrHistSpecification extends BaseSpecification {
public static Specification<ChgrHist> getStId(String stId) {
return (root, query, cb) -> {
return cb.equal(root.get("stId"), stId);
};
}
public static Specification<ChgrHist> getChgrId(String chgrId) {
return (root, query, cb) -> {
return cb.equal(root.get("chgrId"), chgrId);
};
}
}
@@ -26,6 +26,7 @@ public class ProviderController extends BaseController<Provider, String, Provide
@GetMapping("list") @GetMapping("list")
public String list() { public String list() {
providerService.getProviders();
return path + "list"; return path + "list";
} }
@@ -8,15 +8,11 @@ import net.jwsi.jcms.utils.HttpUtil;
import net.jwsi.jcms.utils.LoginUtil; import net.jwsi.jcms.utils.LoginUtil;
import net.jwsi.jcms.vpp.api.ApiClient; import net.jwsi.jcms.vpp.api.ApiClient;
import net.jwsi.jcms.vpp.api.ApiResponse; import net.jwsi.jcms.vpp.api.ApiResponse;
import net.jwsi.jcms.vpp.station.Station;
import net.jwsi.jcms.vpp.station.StationDto;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -26,12 +22,6 @@ public class ProviderService extends BaseService<Provider, String, ProviderRepos
private final ApiClient apiClient; private final ApiClient apiClient;
@Value("${jcms.api.base-url}")
private String baseUrl;
@Value("${jcms.api.token}")
private String apiToken;
public ProviderService(ProviderRepository repository, ApiClient apiClient) { public ProviderService(ProviderRepository repository, ApiClient apiClient) {
super(repository); super(repository);
this.apiClient = apiClient; this.apiClient = apiClient;
@@ -40,7 +30,7 @@ public class ProviderService extends BaseService<Provider, String, ProviderRepos
@Transactional @Transactional
@Scheduled(cron = "0 0 0 * * *") @Scheduled(cron = "0 0 0 * * *")
public void getProviders() { public void getProviders() {
ApiResponse<List<ProviderDto>> response = apiClient.fetchProviders(baseUrl, apiToken); ApiResponse<List<ProviderDto>> response = apiClient.fetchProviders();
if (response == null || !Integer.valueOf(200).equals(response.getCode())) { if (response == null || !Integer.valueOf(200).equals(response.getCode())) {
String msg = (response != null) ? response.getMessage() : "응답 없음"; String msg = (response != null) ? response.getMessage() : "응답 없음";
@@ -89,8 +79,13 @@ public class ProviderService extends BaseService<Provider, String, ProviderRepos
@Override @Override
protected Specification<Provider> getSpecification(Provider srch) { protected Specification<Provider> getSpecification(Provider srch) {
if (srch == null) return null; if (srch == null) return null;
Specification<Provider> rst = null; Specification<Provider> rst = null;
rst = addWhere(rst, srch.getProviderId(), ProviderSpecification.providerId(srch.getProviderId())); rst = addWhere(rst, srch.getProviderNm(), ProviderSpecification.providerNm(srch.getProviderNm()));
rst = addWhere(rst, srch.getBizTaxId(), ProviderSpecification.bizTaxId(srch.getBizTaxId()));
rst = addWhere(rst, srch.getCeo(), ProviderSpecification.ceo(srch.getCeo()));
rst = addWhere(rst, srch.getTel(), ProviderSpecification.tel(srch.getTel()));
rst = addWhere(rst, srch.getFax(), ProviderSpecification.fax(srch.getFax()));
return rst; return rst;
} }
@@ -1,10 +1,28 @@
package net.jwsi.jcms.vpp.provider; package net.jwsi.jcms.vpp.provider;
import net.jwsi.jcms.utils.StrUtil;
import net.jwsi.jcms.vpp.rechgingList.RechgingList;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
public class ProviderSpecification { public class ProviderSpecification {
public static Specification<Provider> providerId(String providerId) { public static Specification<Provider> providerNm(String providerNm) {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("providerId"), providerId); return (root, query, cb) -> cb.like(root.get("providerNm"), StrUtil.sqlLikeParam(providerNm));
}
public static Specification<Provider> bizTaxId(String bizTaxId) {
return (root, query, cb) -> cb.like(root.get("bizTaxId"), StrUtil.sqlLikeParam(bizTaxId));
}
public static Specification<Provider> ceo(String ceo) {
return (root, query, cb) -> cb.like(root.get("ceo"), StrUtil.sqlLikeParam(ceo));
}
public static Specification<Provider> tel(String tel) {
return (root, query, cb) -> cb.like(root.get("tel"), StrUtil.sqlLikeParam(tel));
}
public static Specification<Provider> fax(String fax) {
return (root, query, cb) -> cb.like(root.get("fax"), StrUtil.sqlLikeParam(fax));
} }
} }
@@ -0,0 +1,29 @@
package net.jwsi.jcms.vpp.ratePlan;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
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_rate_plan")
public class RatePlan extends BaseStEntity {
@Id
private Integer ratePlanId; //요금제 id(pk)
private String ratePlanName; //요금제명
private String ratePlanType; //요금제 타입 (고정/계시별/충전용량별/계시+충전용량별)
private String seasonType; //계절 타입 (연중/봄/여름/가을/겨울)
private String applyStartDate; //적용 시작일
private String applyEndDate; //적용 종료일
private Double fixedPrice; //고정 단가 (rate_plan_type=fixed일 경우)
private String description; //요금제 설명
private String approvalStatus; //승인 상태
private String rejectionReason; //승인 거절 사유
@Column(name = "IS_ACTIVE")
private String isActive;
}
@@ -0,0 +1,90 @@
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.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Controller
@Slf4j
@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, ApiClient apiClient) {
super(service);
this.ratePlanService = ratePlanService;
this.apiClient = apiClient;
}
@GetMapping("list")
public String list() {
ratePlanService.getRatePlan();
return path + "list";
}
@PostMapping("list.json")
@ResponseBody
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) {
Map<String, Object> result = new HashMap<>();
try {
RatePlanResponseDto detailData = ratePlanService.getRatePlanDetail(ratePlanId);
result.put("result", "success");
result.put("data", detailData);
} catch (Exception e) {
result.put("result", "fail");
result.put("message", e.getMessage());
}
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 {
}
@Override
protected void chkSelect(RatePlan data) throws JsonDataException {
}
}
@@ -0,0 +1,8 @@
package net.jwsi.jcms.vpp.ratePlan;
import net.jwsi.jcms.base.BaseRecStRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface RatePlanRepository extends BaseRecStRepository<RatePlan,Integer> {
}
@@ -0,0 +1,116 @@
package net.jwsi.jcms.vpp.ratePlan;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import net.jwsi.jcms.vpp.RatePlanDetailTime.RatePlanDetailTime;
import net.jwsi.jcms.vpp.chargerRatePlan.ChargerRatePlan;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
@ToString
public class RatePlanResponseDto {
private Integer ratePlanId;
private String ratePlanName;
private String ratePlanType;
private String seasonType;
private String applyStartDate;
private String applyEndDate;
private Double fixedPrice;
private String isActive;
private String approvalStatus;
private String description;
// 계시별 단가 리스트
private List<DetailsTimeDto> detailsTime;
// 적용 대상 충전기 리스트
private List<ChargerListDto> chargerList;
public RatePlan toEntity() {
RatePlan ratePlan = new RatePlan();
ratePlan.setRatePlanId(this.ratePlanId);
ratePlan.setRatePlanName(this.ratePlanName);
ratePlan.setRatePlanType(this.ratePlanType);
ratePlan.setSeasonType(this.seasonType);
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;
}
public List<RatePlanDetailTime> toDetailTimeEntities() {
List<RatePlanDetailTime> detailTimeList = new ArrayList<>();
if (this.detailsTime == null) return detailTimeList;
for (DetailsTimeDto seasonDto : this.detailsTime) {
if (seasonDto.getTimeRates() == null) continue;
for (TimeRateDto timeRateDto : seasonDto.getTimeRates()) {
RatePlanDetailTime detailTime = new RatePlanDetailTime();
detailTime.setRatePlanId(this.ratePlanId);
detailTime.setSeasonType(seasonDto.getSeasonType());
detailTime.setTimeSlotId(timeRateDto.getId());
detailTime.setPrice(timeRateDto.getPrice());
detailTimeList.add(detailTime);
}
}
return detailTimeList;
}
public List<ChargerRatePlan> toChargerRatePlanEntities() {
List<ChargerRatePlan> chargerRatePlanList = new ArrayList<>();
if (this.chargerList == null) return chargerRatePlanList;
for (ChargerListDto chargerDto : this.chargerList) {
ChargerRatePlan chargerRatePlan = new ChargerRatePlan();
if (this.ratePlanId != null && chargerDto.getChargerCd() != null) {
chargerRatePlan.setRatePlanId((this.ratePlanId));
}
chargerRatePlan.setChargerCd(chargerDto.getChargerCd());
chargerRatePlan.setApplyStartDate(chargerDto.getApplyStartDate());
chargerRatePlan.setApplyEndDate(chargerDto.getApplyEndDate());
chargerRatePlan.setIsActive(chargerDto.getIsActive());
chargerRatePlanList.add(chargerRatePlan);
}
return chargerRatePlanList;
}
@Getter
@Setter
@ToString
public static class DetailsTimeDto {
private String seasonType;
private List<TimeRateDto> timeRates;
}
@Getter
@Setter
@ToString
public static class TimeRateDto {
private Integer id;
private String startTime;
private String endTime;
private Double price;
}
@Getter
@Setter
@ToString
public static class ChargerListDto {
private String chargerCd;
private String applyStartDate;
private String applyEndDate;
private String isActive;
}
}
@@ -0,0 +1,231 @@
package net.jwsi.jcms.vpp.ratePlan;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import net.jwsi.jcms.base.BaseService;
import net.jwsi.jcms.base.EnumSqlType;
import net.jwsi.jcms.utils.HttpUtil;
import net.jwsi.jcms.utils.LoginUtil;
import net.jwsi.jcms.vpp.RatePlanDetailTime.RatePlanDetailTime;
import net.jwsi.jcms.vpp.RatePlanDetailTime.RatePlanDetailTimeRepository;
import net.jwsi.jcms.vpp.api.ApiClient;
import net.jwsi.jcms.vpp.api.ApiResponse;
import net.jwsi.jcms.vpp.chargerRatePlan.ChargerRatePlan;
import net.jwsi.jcms.vpp.chargerRatePlan.ChargerRatePlanRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Service
@Slf4j
public class RatePlanService extends BaseService<RatePlan, Integer, RatePlanRepository> {
private final ApiClient apiClient;
private final RatePlanRepository ratePlanRepository;
private final RatePlanDetailTimeRepository ratePlanDetailTimeRepository;
private final ChargerRatePlanRepository chargerRatePlanRepository;
public RatePlanService(RatePlanRepository repository, ApiClient apiClient, RatePlanRepository ratePlanRepository, RatePlanDetailTimeRepository ratePlanDetailTimeRepository, ChargerRatePlanRepository chargerRatePlanRepository) {
super(repository);
this.apiClient = apiClient;
this.ratePlanRepository = ratePlanRepository;
this.ratePlanDetailTimeRepository = ratePlanDetailTimeRepository;
this.chargerRatePlanRepository = chargerRatePlanRepository;
}
@Transactional
@Scheduled(cron = "0 0 0 * * *")
public void getRatePlan() {
ApiResponse<List<RatePlanResponseDto>> response = apiClient.fetchRatePlan();
if (response == null || !Integer.valueOf(200).equals(response.getCode())) {
String msg = (response != null) ? response.getMessage() : "응답 없음";
throw new RuntimeException("API 호출 실패: " + msg);
}
List<RatePlanResponseDto> dtoList = response.getData();
if (dtoList == null) {
dtoList = new ArrayList<>();
}
Set<Integer> validApiIds = dtoList.stream()
.map(RatePlanResponseDto::getRatePlanId)
.collect(Collectors.toSet());
List<RatePlan> localRatePlans = ratePlanRepository.findAll();
int deleteCount = 0;
for (RatePlan localPlan : localRatePlans) {
if (!validApiIds.contains(localPlan.getRatePlanId())) {
Integer idToDelete = localPlan.getRatePlanId();
ratePlanDetailTimeRepository.deleteByRatePlanId(idToDelete);
chargerRatePlanRepository.deleteByRatePlanId(idToDelete);
ratePlanRepository.deleteById(idToDelete);
deleteCount++;
log.info("운영 DB에서 삭제된 요금제를 로컬 DB에서 제거했습니다. - ID: {}", idToDelete);
}
}
if (dtoList.isEmpty()) {
log.info("가져온 요금제 데이터가 없습니다. (로컬 동기화 및 삭제 완료. 삭제 건수: {})", deleteCount);
return;
}
List<RatePlan> ratePlanEntities = new ArrayList<>();
List<RatePlanDetailTime> allDetailTimes = new ArrayList<>();
List<ChargerRatePlan> allChargerRatePlans = new ArrayList<>();
Set<Integer> processedIds = new HashSet<>();
for (RatePlanResponseDto dto : dtoList) {
Integer currentRatePlanId = dto.getRatePlanId();
if (processedIds.contains(currentRatePlanId)) {
continue;
}
processedIds.add(currentRatePlanId);
RatePlan entity = ratePlanRepository.findById(currentRatePlanId)
.orElseGet(() -> {
RatePlan newEntity = new RatePlan();
newEntity.setRatePlanId(currentRatePlanId);
return newEntity;
});
entity.setRatePlanName(dto.getRatePlanName());
entity.setRatePlanType(dto.getRatePlanType());
entity.setSeasonType(dto.getSeasonType());
entity.setApplyStartDate(dto.getApplyStartDate());
entity.setApplyEndDate(dto.getApplyEndDate());
entity.setFixedPrice(dto.getFixedPrice());
entity.setApprovalStatus(dto.getApprovalStatus());
entity.setIsActive(dto.getIsActive());
ratePlanEntities.add(entity);
allDetailTimes.addAll(dto.toDetailTimeEntities());
allChargerRatePlans.addAll(dto.toChargerRatePlanEntities());
ratePlanDetailTimeRepository.deleteByRatePlanId(currentRatePlanId);
chargerRatePlanRepository.deleteByRatePlanId(currentRatePlanId);
}
ratePlanRepository.saveAll(ratePlanEntities);
ratePlanDetailTimeRepository.flush();
chargerRatePlanRepository.flush();
if (!allDetailTimes.isEmpty()) {
ratePlanDetailTimeRepository.saveAll(allDetailTimes);
}
if (!allChargerRatePlans.isEmpty()) {
chargerRatePlanRepository.saveAll(allChargerRatePlans);
}
log.info("요금제 동기화 완료: 마스터 추가/수정 {}건, 삭제 {}건, 상세단가 {}건, 충전기매핑 {}건",
ratePlanEntities.size(), deleteCount, allDetailTimes.size(), allChargerRatePlans.size());
}
public RatePlanResponseDto getRatePlanDetail(Integer ratePlanId) {
RatePlan ratePlan = ratePlanRepository.findById(ratePlanId)
.orElseThrow(() -> new RuntimeException("해당 요금제를 찾을 수 없습니다. ID: " + ratePlanId));
RatePlanResponseDto dto = new RatePlanResponseDto();
dto.setRatePlanId(ratePlan.getRatePlanId());
dto.setRatePlanName(ratePlan.getRatePlanName());
dto.setRatePlanType(ratePlan.getRatePlanType());
dto.setSeasonType(ratePlan.getSeasonType());
dto.setApplyStartDate(ratePlan.getApplyStartDate());
dto.setApplyEndDate(ratePlan.getApplyEndDate());
dto.setFixedPrice(ratePlan.getFixedPrice());
dto.setIsActive(ratePlan.getIsActive());
dto.setApprovalStatus(ratePlan.getApprovalStatus());
dto.setDescription(ratePlan.getDescription());
List<RatePlanDetailTime> detailTimes = ratePlanDetailTimeRepository.findByRatePlanId(ratePlanId);
Map<String, List<RatePlanDetailTime>> groupedBySeason = detailTimes.stream()
.collect(Collectors.groupingBy(RatePlanDetailTime::getSeasonType));
List<RatePlanResponseDto.DetailsTimeDto> detailsTimeDtos = new ArrayList<>();
for (Map.Entry<String, List<RatePlanDetailTime>> entry : groupedBySeason.entrySet()) {
RatePlanResponseDto.DetailsTimeDto seasonDto = new RatePlanResponseDto.DetailsTimeDto();
seasonDto.setSeasonType(entry.getKey());
seasonDto.setTimeRates(entry.getValue().stream().map(dt -> {
RatePlanResponseDto.TimeRateDto tr = new RatePlanResponseDto.TimeRateDto();
tr.setId(dt.getTimeSlotId());
tr.setPrice(dt.getPrice());
return tr;
}).collect(Collectors.toList()));
detailsTimeDtos.add(seasonDto);
}
dto.setDetailsTime(detailsTimeDtos);
List<ChargerRatePlan> chargerRatePlans = chargerRatePlanRepository.findByRatePlanId(ratePlanId);
log.info("🚨 [디버깅] 조회된 충전기 매핑 개수: {}", chargerRatePlans.size());
List<RatePlanResponseDto.ChargerListDto> chargerListDtos = chargerRatePlans.stream().map(cp -> {
RatePlanResponseDto.ChargerListDto cpDto = new RatePlanResponseDto.ChargerListDto();
cpDto.setChargerCd(cp.getChargerCd());
cpDto.setApplyStartDate(cp.getApplyStartDate());
cpDto.setApplyEndDate(cp.getApplyEndDate());
cpDto.setIsActive(cp.getIsActive());
return cpDto;
}).collect(Collectors.toList());
dto.setChargerList(chargerListDtos);
return dto;
}
@Override
protected Integer getUserId() {
return HttpUtil.getUserId();
}
@Override
protected String getUserName(Integer id) {
return LoginUtil.getUserName(id);
}
@Override
protected RatePlan newSearchParam(RatePlan ratePlan) {
if (ratePlan == null) {
ratePlan = new RatePlan();
}
return ratePlan;
}
@Override
protected EnumSqlType useSqlTyp() {
return EnumSqlType.SPECIFICATION;
}
@Override
protected Specification<RatePlan> getSpecification(RatePlan ratePlan) {
Specification<RatePlan> rst = null;
if(ratePlan == null) return null;
rst = addWhere(rst, ratePlan.getRatePlanName(), RatePlanSpecification.getRatePlanNm(ratePlan.getRatePlanName()));
rst = addWhere(rst, ratePlan.getApprovalStatus(), RatePlanSpecification.getApprovalStatus(ratePlan.getApprovalStatus()));
rst = addWhere(rst, ratePlan.getIsActive(), RatePlanSpecification.getIsActive(ratePlan.getIsActive()));
return rst;
}
@Override
protected Page<RatePlan> _selectPage(RatePlan ratePlan) {
return null;
}
@Override
protected List<RatePlan> _selectList(RatePlan ratePlan) {
return null;
}
}
@@ -0,0 +1,23 @@
package net.jwsi.jcms.vpp.ratePlan;
import net.jwsi.jcms.base.BaseSpecification;
import org.springframework.data.jpa.domain.Specification;
public class RatePlanSpecification extends BaseSpecification {
public static Specification<RatePlan> getRatePlanNm(String ratePlanName) {
return (root, query, cb) -> {
return cb.like(root.get("ratePlanName"), "%"+ratePlanName+"%");
};
}
public static Specification<RatePlan> getApprovalStatus(String approvalStatus) {
return (root, query, cb) -> {
return cb.equal(root.get("approvalStatus"), approvalStatus);
};
}
public static Specification<RatePlan> getIsActive(String isActive) {
return (root, query, cb) -> {
return cb.equal(root.get("isActive"), isActive);
};
}
}
@@ -0,0 +1,82 @@
package net.jwsi.jcms.vpp.rechgingList;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import net.jwsi.jcms.base.BaseStEntity;
@NoArgsConstructor
@Setter
@Getter
@ToString
@Entity(name = "vpp_rechging_list")
public class RechgingList extends BaseStEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "RECHGING_LIST_ID")
private Long rechgingListId;
@Column(name = "SEARCH_DT")
private String searchDt;
@Column(name = "PROVIDER_ID")
private String providerId;
@Column(name = "ST_ID")
private String stId;
@Column(name = "CHGR_ID")
private String chgrId;
@Column(name = "CH_ID")
private Integer chId;
@Column(name = "PLUG_TYPE")
private String plugType;
@Column(name = "MEM_AUTH_INPUT_NO")
private String memAuthInputNo;
@Column(name = "CREDIT_P_PAY_TRX_NO")
private String creditPPayTrxNo;
@Column(name = "CREDIT_P_PAY_TRX_DT")
private String creditPPayTrxDt;
@Column(name = "RECHG_SDT")
private String rechgSdt;
@Column(name = "RECHG_REMAIN_TIME")
private String rechgRemainTime;
@Column(name = "RECHGING_WH")
private Integer rechgingWh;
@Column(name = "RECHG_DEMAND_AMT")
private Integer rechgDemandAmt;
@Column(name = "RECHGING_AMT")
private Integer rechgingAmt;
@Column(name = "PAY_TYPE")
private String payType;
@Column(name = "INTEGRATED_WH")
private Double integratedWh;
@Column(name = "DC_POWER_WH")
private Double dcPowerWh;
@Column(name = "AC_POWER_WH")
private Double AcPowerWh;
@Column(name = "CURR_VOLT")
private Double currVolt;
@Column(name = "CURR_C")
private Double currC;
}
@@ -0,0 +1,57 @@
package net.jwsi.jcms.vpp.rechgingList;
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.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
import java.util.Map;
@Controller
@Slf4j
@RequestMapping("/cms/rechgingList/")
public class RechgingListController extends BaseController<RechgingList, Long, RechgingListRepository, RechgingListService> {
private final String path = "/system/rechgingList/";
private final RechgingListService rechgingListService;
public RechgingListController(RechgingListService service, RechgingListService rechgingListService) {
super(service);
this.rechgingListService = rechgingListService;
}
@GetMapping("list")
public String list() throws IllegalAccessException {
return path + "list";
}
@PostMapping("list.json")
@ResponseBody
public Map<String, Object> list(RechgingList rechgingList) {
return svcSelectList(rechgingList);
}
@PostMapping("view.json")
@ResponseBody
public Map<String, Object> viewData(RechgingList rechgingList) throws JsonDataException {
return svcSelectList(rechgingList);
}
@Override
protected void chkSelectList(List<RechgingList> list) throws JsonDataException {
}
@Override
protected void chkSelect(RechgingList data) throws JsonDataException {
}
}
@@ -0,0 +1,59 @@
package net.jwsi.jcms.vpp.rechgingList;
import lombok.*;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RechgingListDto {
private Long rechgingListId;
private String searchDt;
private String providerId;
private String stId;
private String chgrId;
private Integer chId;
private String plugType;
private String memAuthInputNo;
private String creditPPayTrxNo;
private String creditPPayTrxDt;
private String rechgSdt;
private String rechgRemainTime;
private Integer rechgingWh;
private Integer rechgDemandAmt;
private Integer rechgingAmt;
private String payType;
private Double integratedWh;
private Double dcPowerWh;
private Double acPowerWh;
private Double currVolt;
private Double currC;
public RechgingList toEntity() {
RechgingList rechgingList = new RechgingList();
rechgingList.setRechgingListId(this.rechgingListId);
rechgingList.setSearchDt(this.searchDt);
rechgingList.setProviderId(this.providerId);
rechgingList.setStId(this.stId);
rechgingList.setChgrId(this.chgrId);
rechgingList.setChId(this.chId);
rechgingList.setPlugType(this.plugType);
rechgingList.setMemAuthInputNo(this.memAuthInputNo);
rechgingList.setCreditPPayTrxNo(this.creditPPayTrxNo);
rechgingList.setCreditPPayTrxDt(this.creditPPayTrxDt);
rechgingList.setRechgSdt(this.rechgSdt);
rechgingList.setRechgRemainTime(this.rechgRemainTime);
rechgingList.setRechgingWh(this.rechgingWh);
rechgingList.setRechgDemandAmt(this.rechgDemandAmt);
rechgingList.setRechgingAmt(this.rechgingAmt);
rechgingList.setPayType(this.payType);
rechgingList.setIntegratedWh(this.integratedWh);
rechgingList.setDcPowerWh(this.dcPowerWh);
rechgingList.setAcPowerWh(this.acPowerWh);
rechgingList.setCurrVolt(this.currVolt);
rechgingList.setCurrC(this.currC);
return rechgingList;
}
}
@@ -0,0 +1,8 @@
package net.jwsi.jcms.vpp.rechgingList;
import net.jwsi.jcms.base.BaseRecStRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface RechgingListRepository extends BaseRecStRepository<RechgingList, Long> {
}
@@ -0,0 +1,107 @@
package net.jwsi.jcms.vpp.rechgingList;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import net.jwsi.jcms.base.BaseService;
import net.jwsi.jcms.base.EnumSqlType;
import net.jwsi.jcms.utils.HttpUtil;
import net.jwsi.jcms.utils.LoginUtil;
import net.jwsi.jcms.vpp.api.ApiClient;
import net.jwsi.jcms.vpp.api.ApiResponse;
import org.springframework.data.domain.Page;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
public class RechgingListService extends BaseService<RechgingList, Long, RechgingListRepository> {
private final ApiClient apiClient;
public RechgingListService(RechgingListRepository repository, ApiClient apiClient) {
super(repository);
this.apiClient = apiClient;
}
@Transactional
@Scheduled(cron = "0 0/15 * * * *")
public void getRechgingList() {
ApiResponse<List<RechgingListDto>> response = apiClient.fetchRechgingList();
if (response == null || !Integer.valueOf(200).equals(response.getCode())) {
String msg =(response != null) ? response.getMessage() : "응답 없음";
throw new RuntimeException("API 호출 실패: " + msg);
}
List<RechgingListDto> dtoList = response.getData();
if (dtoList == null || dtoList.isEmpty()) {
log.info("가져온 충전중 데이터가 없습니다.");
}
List<RechgingList> entityList = dtoList.stream()
.map(RechgingListDto::toEntity)
.collect(Collectors.toList());
List<RechgingList> savedRechgingList = super.repository.saveAll(entityList);
log.info("총 {}건의 충전중 데이터가 DB에 동기화되었습니다.",savedRechgingList.size());
}
@Override
protected Integer getUserId() {
return HttpUtil.getUserId();
}
@Override
protected String getUserName(Integer id) {
return LoginUtil.getUserName(id);
}
@Override
protected RechgingList newSearchParam(RechgingList rechgingList) {
if (rechgingList == null) {
rechgingList = new RechgingList();
}
return rechgingList;
}
@Override
protected EnumSqlType useSqlTyp() {
return EnumSqlType.SPECIFICATION;
}
@Override
protected Specification<RechgingList> getSpecification(RechgingList srch) {
if (srch == null) return null;
Specification<RechgingList> rst = null;
rst = addWhere(rst, srch.getProviderId(), RechgingListSpecification.providerId(srch.getProviderId()));
rst = addWhere(rst, srch.getStId(), RechgingListSpecification.stId(srch.getStId()));
rst = addWhere(rst, srch.getChgrId(), RechgingListSpecification.chgrId(srch.getChgrId()));
rst = addWhere(rst, srch.getPlugType(), RechgingListSpecification.plugType(srch.getPlugType()));
rst = addWhere(rst, srch.getMemAuthInputNo(), RechgingListSpecification.memAuthInputNo(srch.getMemAuthInputNo()));
rst = addWhere(rst, srch.getCreditPPayTrxNo(), RechgingListSpecification.creditPPayTrxNo(srch.getCreditPPayTrxNo()));
rst = addWhere(rst, srch.getCreditPPayTrxDt(), RechgingListSpecification.creditPPayTrxDt(srch.getCreditPPayTrxDt()));
rst = addWhere(rst, srch.getRechgSdt(), RechgingListSpecification.rechgSdtBetween(srch.getRechgSdt()));
rst = addWhere(rst, srch.getPayType(), RechgingListSpecification.payType(srch.getPayType()));
return rst;
}
@Override
protected Page<RechgingList> _selectPage(RechgingList rechgingList) {
return null;
}
@Override
protected List<RechgingList> _selectList(RechgingList rechgingList) {
return null;
}
}
@@ -0,0 +1,57 @@
package net.jwsi.jcms.vpp.rechgingList;
import net.jwsi.jcms.base.BaseSpecification;
import net.jwsi.jcms.utils.StrUtil;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.StringUtils;
public class RechgingListSpecification extends BaseSpecification {
public static Specification<RechgingList> providerId(String providerId) {
return (root, query, cb) -> cb.like(root.get("providerId"), StrUtil.sqlLikeParam(providerId));
}
public static Specification<RechgingList> stId(String stId) {
return (root, query, cb) -> cb.like(root.get("stId"), StrUtil.sqlLikeParam(stId));
}
public static Specification<RechgingList> chgrId(String chgrId) {
return (root, query, cb) -> cb.like(root.get("chgrId"), StrUtil.sqlLikeParam(chgrId));
}
public static Specification<RechgingList> plugType(String plugType) {
return (root, query, cb) -> cb.like(root.get("plugType"), StrUtil.sqlLikeParam(plugType));
}
public static Specification<RechgingList> memAuthInputNo(String memAuthInputNo) {
return (root, query, cb) -> cb.like(root.get("memAuthInputNo"), StrUtil.sqlLikeParam(memAuthInputNo));
}
public static Specification<RechgingList> creditPPayTrxNo(String creditPPayTrxNo) {
return (root, query, cb) -> cb.like(root.get("creditPPayTrxNo"), StrUtil.sqlLikeParam(creditPPayTrxNo));
}
public static Specification<RechgingList> creditPPayTrxDt(String creditPPayTrxDt) {
return (root, query, cb) -> cb.like(root.get("creditPPayTrxDt"), StrUtil.sqlLikeParam(creditPPayTrxDt));
}
public static Specification<RechgingList> rechgSdtBetween(String rechgSdt) {
return (root, query, cb) -> {
// 날짜 파라미터가 없으면 검색 조건에서 제외 (null 반환)
if (!StringUtils.hasText(rechgSdt)) {
return null;
}
// 예: "2026-07-31" -> "2026-07-31 00:00:00" ~ "2026-07-31 23:59:59"
String startDateTime = rechgSdt.trim() + " 00:00:00";
String endDateTime = rechgSdt.trim() + " 23:59:59";
return cb.between(root.get("rechgSdt"), startDateTime, endDateTime);
};
}
public static Specification<RechgingList> payType(String payType) {
return (root, query, cb) -> cb.like(root.get("payType"), StrUtil.sqlLikeParam(payType));
}
}
@@ -6,15 +6,13 @@ import net.jwsi.jcms.base.BaseService;
import net.jwsi.jcms.base.EnumSqlType; import net.jwsi.jcms.base.EnumSqlType;
import net.jwsi.jcms.utils.HttpUtil; import net.jwsi.jcms.utils.HttpUtil;
import net.jwsi.jcms.utils.LoginUtil; import net.jwsi.jcms.utils.LoginUtil;
import net.jwsi.jcms.vpp.api.ApiResponse;
import net.jwsi.jcms.vpp.api.ApiClient; import net.jwsi.jcms.vpp.api.ApiClient;
import org.springframework.beans.factory.annotation.Value; import net.jwsi.jcms.vpp.api.ApiResponse;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -23,11 +21,7 @@ import java.util.stream.Collectors;
public class StationService extends BaseService<Station,String,StationRepository> { public class StationService extends BaseService<Station,String,StationRepository> {
private final ApiClient apiClient; private final ApiClient apiClient;
@Value("${jcms.api.base-url}")
private String baseUrl;
@Value("${jcms.api.token}")
private String apiToken;
public StationService(StationRepository repository, ApiClient apiClient) { public StationService(StationRepository repository, ApiClient apiClient) {
super(repository); super(repository);
this.apiClient = apiClient; this.apiClient = apiClient;
@@ -36,7 +30,7 @@ public class StationService extends BaseService<Station,String,StationRepository
@Transactional @Transactional
@Scheduled(cron = "0 0 0 * * *") @Scheduled(cron = "0 0 0 * * *")
public void getStations() { public void getStations() {
ApiResponse<List<StationDto>> response = apiClient.fetchStations(baseUrl, apiToken); ApiResponse<List<StationDto>> response = apiClient.fetchStations();
if (response == null || !Integer.valueOf(200).equals(response.getCode())) { if (response == null || !Integer.valueOf(200).equals(response.getCode())) {
String msg = (response != null) ? response.getMessage() : "응답 없음"; String msg = (response != null) ? response.getMessage() : "응답 없음";
@@ -83,7 +77,8 @@ public class StationService extends BaseService<Station,String,StationRepository
protected Specification<Station> getSpecification(Station srch) { protected Specification<Station> getSpecification(Station srch) {
if(srch == null) return null; if(srch == null) return null;
Specification<Station> rst = null; Specification<Station> rst = null;
rst = addWhere(rst, srch.getStationId(), StationSpecification.stId(srch.getStationId())); rst = addWhere(rst, srch.getStNm(), StationSpecification.stNm(srch.getStNm()));
rst = addWhere(rst, srch.getAddrLdM(), StationSpecification.addrLdM(srch.getAddrLdM()));
return rst; return rst;
} }
@@ -5,7 +5,10 @@ import org.springframework.data.jpa.domain.Specification;
public class StationSpecification { public class StationSpecification {
public static Specification<Station> stId(String stId) { public static Specification<Station> stNm(String stNm) {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("stId"), stId); return (root, query, criteriaBuilder) -> criteriaBuilder.like(root.get("stNm"),"%"+stNm+"%");
}
public static Specification<Station> addrLdM(String addrLdM) {
return (root, query, criteriaBuilder) -> criteriaBuilder.like(root.get("addrLdM"),"%"+addrLdM+"%");
} }
} }
@@ -0,0 +1,38 @@
package net.jwsi.jcms.vpp.utils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Base64;
public class JwtUtil {
private static final ObjectMapper objectMapper = new ObjectMapper();
public static LocalDateTime getExpirationTime(String jwtToken) {
try {
String[] parts = jwtToken.split("\\.");
if (parts.length < 2) {
throw new IllegalArgumentException("올바르지 않은 JWT 토큰 형식입니다.");
}
String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
JsonNode payloadNode = objectMapper.readTree(payloadJson);
if (!payloadNode.has("exp")) {
throw new IllegalStateException("토큰에 exp(만료시간) 필드가 존재하지 않습니다.");
}
long expSeconds = payloadNode.get("exp").asLong();
return LocalDateTime.ofInstant(Instant.ofEpochSecond(expSeconds), ZoneId.systemDefault());
} catch (Exception e) {
throw new RuntimeException("토큰 만료 시간 추출 실패", e);
}
}
}
+2 -2
View File
@@ -15,8 +15,8 @@ spring:
jcms-db: jcms-db:
driver-class-name: org.mariadb.jdbc.Driver driver-class-name: org.mariadb.jdbc.Driver
jdbc-url: jdbc:mariadb://192.168.62.37:3306/jcmsdb jdbc-url: jdbc:mariadb://192.168.62.37:3306/jcmsdb
username: jcms username: root
password: jcms3593 password: jcms
server: server:
port: 8080 port: 8080
+2 -2
View File
@@ -9,9 +9,9 @@ spring:
datasource: datasource:
jcms-db: jcms-db:
driver-class-name: org.mariadb.jdbc.Driver driver-class-name: org.mariadb.jdbc.Driver
jdbc-url: jdbc:mariadb://192.168.62.37:33068/jcms jdbc-url: jdbc:mariadb://dev.jinwoosi.co.kr:33091/jcms
username: jcms username: jcms
password: ENC(nqt7VLhtxqwiJcQQw9ncNg==) password: jw3593
jpa: jpa:
entity-packages: net.jwsi.jcms entity-packages: net.jwsi.jcms
show-sql: false show-sql: false
+4 -3
View File
@@ -19,12 +19,12 @@ jcms:
manager-path: cms manager-path: cms
attr: attr:
main-url: /cms/main main-url: /cms/main
new-user-st: 9 # 1: ??, 9:???? new-user-st: 9
cms-name: VPP cms-name: VPP
cms-title: VPP cms-title: VPP
cms-bg-color: '#499' cms-bg-color: '#499'
cms-image-url: /public/images/logo_jcms3.png cms-image-url: /public/images/logo_jcms3.png
copyright: Copyright 2026 ??????????? Inc. copyright: Copyright 2026 주식회사 진우시스템 Inc.
authenticated-patten: /cms/**, /system/**, /api/** authenticated-patten: /cms/**, /system/**, /api/**
pass-url: /cms/main pass-url: /cms/main
public-url: '' public-url: ''
@@ -33,4 +33,5 @@ jcms:
days: 30 days: 30
api: api:
base-url: ${API_BASE_URL:http://dev.jinwoosi.co.kr:8072} base-url: ${API_BASE_URL:http://dev.jinwoosi.co.kr:8072}
token: ${API_TOKEN} user-id: ${API_USER_ID:apiuser}
user-pwd: ${API_USER_PWD:apiuser}
@@ -6,12 +6,6 @@
<div th:replace="~{common/fragments/searchBox :: SearchBoxFragment(~{ :: #searchBoxContent}, ~{ :: #searchBoxFooter})}"> <div th:replace="~{common/fragments/searchBox :: SearchBoxFragment(~{ :: #searchBoxContent}, ~{ :: #searchBoxFooter})}">
<div id="searchBoxContent"> <div id="searchBoxContent">
<div class="row px-2"> <div class="row px-2">
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:140px">충전사업자 ID</span>
<input type="text" id="providerIdLike" name="providerIdLike" class="form-control" placeholder="충전사업자 ID 입력" maxlength="50">
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1"> <div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group"> <div class="input-group">
<span class="input-group-text" style="width:140px">충전소 ID</span> <span class="input-group-text" style="width:140px">충전소 ID</span>
@@ -24,30 +18,6 @@
<input type="text" id="chgrIdLike" name="chgrIdLike" class="form-control" placeholder="충전기 ID 입력" maxlength="50"> <input type="text" id="chgrIdLike" name="chgrIdLike" class="form-control" placeholder="충전기 ID 입력" maxlength="50">
</div> </div>
</div> </div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:140px">충전소 명</span>
<input type="text" id="stNmLike" name="stNmLike" class="form-control" placeholder="충전소 명 입력" maxlength="50">
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:140px">충전기 명</span>
<input type="text" id="chgrNmLike" name="chgrNmLike" class="form-control" placeholder="충전기 명 입력" maxlength="50">
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:140px">충전기속도</span>
<input type="text" id="speedTpLike" name="speedTpLike" class="form-control" placeholder="충전기속도 입력" maxlength="50">
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:140px">충전기타입</span>
<input type="text" id="chgrTpLike" name="chgrTpLike" class="form-control" placeholder="충전기타입 입력" maxlength="50">
</div>
</div>
</div> </div>
</div> </div>
<div id="searchBoxFooter"> <div id="searchBoxFooter">
@@ -0,0 +1,282 @@
<!DOCTYPE html>
<html xmlns:th="http://thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/cmsLayout}">
<body>
<section layout:fragment="Content">
<div class="w-100" style="margin-bottom: .3rem;">
<div th:replace="~{common/fragments/searchBox :: SearchBoxFragment(~{ :: #searchBoxContent}, ~{ :: #searchBoxFooter})}">
<div id="searchBoxContent">
<div class="row px-2">
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:180px">충전소 아이디</span>
<input type="text" id="stIdLike" name="stIdLike" class="form-control" placeholder="충전소 아이디 입력" maxlength="50">
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:180px">충전기 아이디</span>
<input type="text" id="chgrIdLike" name="chgrIdLike" class="form-control" placeholder="충전기 아이디 입력" maxlength="50">
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:180px">충전상태</span>
<select id="ch1RechgStateCdLike" name="ch1RechgStateCdLike" class="form-control"></select>
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:180px">도어상태</span>
<select id="ch1DoorStateCdLike" name="ch1DoorStateCdLike" class="form-control"></select>
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:180px">플러그상태</span>
<select id="ch1PlugStateCdLike" name="ch1PlugStateCdLike" class="form-control"></select>
</div>
</div>
</div>
</div>
<div id="searchBoxFooter">
<div class="row float-end">
<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>
</div>
</div>
<div class="w-100">
<div class="box box-primary">
<div class="form">
<table id="datatable" class="table table-striped table-hover dataTable" style="width:100%;"></table>
</div>
</div>
</div>
<script th:inline="none">
const rootPath = "/cms/chgrCurrState/";
let datatable;
const ch1RechgStateCdCdList = getCdDtlList("rechgState");
const ch1DoorStateCdCdList = getCdDtlList("doorState");
const ch1PlugStateCdCdList = getCdDtlList("plugState");
$(document).ready(function() {
setSearchCdDtlOptions("ch1RechgStateCdLike", ch1RechgStateCdCdList);
setSearchCdDtlOptions("ch1DoorStateCdLike", ch1DoorStateCdCdList);
setSearchCdDtlOptions("ch1PlugStateCdLike", ch1PlugStateCdCdList);
const className = "dt-head-center dt-body-center";
datatable = newDataTable(
"#datatable",
rootPath + "list.json",
function(d) {
if (strUtil.isNotEmpty($('#providerIdLike').val())) {d.providerId = $('#providerIdLike').val();}
if (strUtil.isNotEmpty($('#stIdLike').val())) {d.stId = $('#stIdLike').val();}
if (strUtil.isNotEmpty($('#chgrIdLike').val())) {d.chgrId = $('#chgrIdLike').val();}
if (strUtil.isNotEmpty($('#stateDtLike').val())) {d.stateDt = $('#stateDtLike').val();}
if (strUtil.isNotEmpty($('#opModeLike').val())) {d.opMode = $('#opModeLike').val();}
if (strUtil.isNotEmpty($('#ch1RechgStateCdLike').val())) {d.ch1RechgStateCd = $('#ch1RechgStateCdLike').val();}
if (strUtil.isNotEmpty($('#ch1DoorStateCdLike').val())) {d.ch1DoorStateCd = $('#ch1DoorStateCdLike').val();}
if (strUtil.isNotEmpty($('#ch1PlugStateCdLike').val())) {d.ch1PlugStateCd = $('#ch1PlugStateCdLike').val();}
return d;
},
{
order: [],
columns: [
{title: "사업자 ID", name: "provider_id", data: "providerId", className},
{title: "충전소 ID", name: "st_id", data: "stId", className},
{title: "충전기 ID", name: "chgr_id", data: "chgrId", className},
{title: "운영모드", name: "op_mode", data: "opMode", className, orderable: false},
{title: "CH1 상태", orderable: false, className,
render: (data, type, row) => {
let s1 = getCdDtlDtColNm(ch1RechgStateCdCdList, row["ch1RechgStateCd"]) || "";
let s2 = getCdDtlDtColNm(ch1DoorStateCdCdList, row["ch1DoorStateCd"]) || "";
let s3 = getCdDtlDtColNm(ch1PlugStateCdCdList, row["ch1PlugStateCd"]) || "";
return `<div class="d-flex justify-content-center gap-1">${s1} ${s2} ${s3}</div>`;
}
},
{title: "상태 일시", name: "state_dt", data: "stateDt", className, orderable: false},
{title: "integrated_kwh", name: "integrated_kwh", data: "integratedKwh", className, visible: false},
{title: "dc_power_wh", name: "dc_power_wh", data: "dcPowerWh", className, visible: false},
{title: "ac_power_wh", name: "ac_power_wh", data: "acPowerWh", className, visible: false},
{title: "기능", orderable:false, width:"110px",
render: (data, type, row) => {
return mkRowDataFunctions({providerId: row['providerId'], stId: row['stId'], chgrId: row['chgrId']}, row['stId'], true, menuRoleModify, menuRoleDel);
}
}
],
allCheck: false,
});
});
const showModal = (mode, ids, title) => {
if(!checkRole(mode)) { alert("권한이 없습니다."); return; }
let modalInputArray = [
new ModalInput({
inputType: ModalInputType.text,
inputId: "providerId",
inputName: "providerId",
hasOldPk: true,
inputLabel: "provider_id",
inputPlaceholder: "provider_id",
isReq: false,
isEnable: true,
minLen: 0,
maxLen: 20,
}),
new ModalInput({
inputType: ModalInputType.text,
inputId: "stId",
inputName: "stId",
hasOldPk: true,
inputLabel: "st_id",
inputPlaceholder: "st_id",
isReq: false,
isEnable: true,
minLen: 0,
maxLen: 20,
}),
new ModalInput({
inputType: ModalInputType.text,
inputId: "chgrId",
inputName: "chgrId",
hasOldPk: true,
inputLabel: "chgr_id",
inputPlaceholder: "chgr_id",
isReq: false,
isEnable: true,
minLen: 0,
maxLen: 20,
}),
new ModalInput({
inputType: ModalInputType.textarea,
inputId: "stateDt",
inputName: "stateDt",
inputLabel: "상태 일시",
inputPlaceholder: "상태 일시",
isReq: false,
isEnable: true,
minLen: 0,
maxLen: 250,
}),
new ModalInput({
inputType: ModalInputType.textarea,
inputId: "opMode",
inputName: "opMode",
inputLabel: "op_mode_cd",
inputPlaceholder: "op_mode_cd",
isReq: false,
isEnable: true,
minLen: 0,
maxLen: 250,
}),
new ModalInput({
inputType: ModalInputType.select,
inputId: "ch1RechgStateCd",
inputName: "ch1RechgStateCd",
inputLabel: "ch1_rechg_state_cd",
inputPlaceholder: "ch1_rechg_state_cd",
isReq: false,
isEnable: true,
itemArray: toCdDtlItemArray(ch1RechgStateCdCdList),
}),
new ModalInput({
inputType: ModalInputType.select,
inputId: "ch1DoorStateCd",
inputName: "ch1DoorStateCd",
inputLabel: "ch1_door_state_cd",
inputPlaceholder: "ch1_door_state_cd",
isReq: false,
isEnable: true,
itemArray: toCdDtlItemArray(ch1DoorStateCdCdList),
}),
new ModalInput({
inputType: ModalInputType.select,
inputId: "ch1PlugStateCd",
inputName: "ch1PlugStateCd",
inputLabel: "ch1_plug_state_cd",
inputPlaceholder: "ch1_plug_state_cd",
isReq: false,
isEnable: true,
itemArray: toCdDtlItemArray(ch1PlugStateCdCdList),
}),
new ModalInput({
inputType: ModalInputType.text,
inputId: "integratedKwh",
inputName: "integratedKwh",
inputLabel: "integrated_kwh",
inputPlaceholder: "integrated_kwh",
isReq: false,
isEnable: true,
}),
new ModalInput({
inputType: ModalInputType.text,
inputId: "dcPowerWh",
inputName: "dcPowerWh",
inputLabel: "dc_power_wh",
inputPlaceholder: "dc_power_wh",
isReq: false,
isEnable: true,
}),
new ModalInput({
inputType: ModalInputType.text,
inputId: "acPowerWh",
inputName: "acPowerWh",
inputLabel: "ac_power_wh",
inputPlaceholder: "ac_power_wh",
isReq: false,
isEnable: true,
})
];
if(!rootPath){ alert("rootPath를 지정하십시요."); return; }
const insertUrl = rootPath+"insert.json";
const updateUrl = rootPath+"update.json";
const loadUrl = rootPath+"view.json";
const deleteUrl = rootPath+"delete.json";
const modalWidth = 500;
const labelWidth = '140px';
if(mode === ModalMode.REGISTER) {
newModal(new ModalInfo({modalTitle: "등록", inputArray: modalInputArray, modalWidth}), mode, 'main', {insertUrl, labelWidth, callBack: () => { defaultCallback(); }})
} else if(mode===ModalMode.VIEW || mode===ModalMode.VIEW_ONLY || mode===ModalMode.MODIFY) {
newModal(new ModalInfo({modalTitle: "상세정보", inputArray: modalInputArray, modalWidth}), mode, 'main', {loadUrl, updateUrl, ids, labelWidth, callBack: () => { defaultCallback(); }})
} else if(mode === ModalMode.DELETE) {
newDelete(deleteUrl, ids, title, () => { defaultCallback(); });
}
const defaultCallback = () => {
datatable.ajax.reload();
}
};
$("#searchBtn").click(function() {
datatable.ajax.reload();
});
$("#resetBtn").click(function() {
$("#providerIdLike").val("");
$("#stIdLike").val("");
$("#chgrIdLike").val("");
$("#stateDtLike").val("");
$("#opModeLike").val("");
$("#ch1RechgStateCdLike").val("");
$("#ch1DoorStateCdLike").val("");
$("#ch1PlugStateCdLike").val("");
datatable.ajax.reload();
});
</script>
</section>
</body>
</html>
@@ -8,52 +8,16 @@
<div class="row px-2"> <div class="row px-2">
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1"> <div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group"> <div class="input-group">
<span class="input-group-text" style="width:160px">충전소 </span> <span class="input-group-text" style="width:160px">충전소 아이디</span>
<input type="text" id="stIdLike" name="stIdLike" class="form-control" placeholder="충전소 명 입력" maxlength="50"> <input type="text" id="stIdLike" name="stIdLike" class="form-control" placeholder="충전소 명 입력" maxlength="50">
</div> </div>
</div> </div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1"> <div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group"> <div class="input-group">
<span class="input-group-text" style="width:160px">충전기 </span> <span class="input-group-text" style="width:160px">충전기 아이디</span>
<input type="text" id="chgrIdLike" name="chgrIdLike" class="form-control" placeholder="충전기 명 입력" maxlength="50"> <input type="text" id="chgrIdLike" name="chgrIdLike" class="form-control" placeholder="충전기 명 입력" maxlength="50">
</div> </div>
</div> </div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:160px">결제구분</span>
<input type="text" id="payTpLike" name="payTpLike" class="form-control" placeholder="결제구분 입력" maxlength="50">
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:160px">충전기타입 코드</span>
<input type="text" id="plugTpLike" name="plugTpLike" class="form-control" placeholder="충전기타입 코드 입력" maxlength="50">
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:160px">충전 시작일시</span>
<input type="text" id="rechgSDtLike" name="rechgSDtLike" class="form-control" placeholder="충전 시작일시 입력" maxlength="50">
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:160px">충전 종료일시</span>
<input type="text" id="rechgEDtLike" name="rechgEDtLike" class="form-control" placeholder="충전 종료일시 입력" maxlength="50">
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:160px">충전시간</span>
<input type="text" id="rechgTimeLike" name="rechgTimeLike" class="form-control" placeholder="충전시간 입력" maxlength="50">
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:160px">신용결제완료구분</span>
<input type="text" id="payFnshLike" name="payFnshLike" class="form-control" placeholder="신용결제완료구분 입력" maxlength="50">
</div>
</div>
</div> </div>
</div> </div>
<div id="searchBoxFooter"> <div id="searchBoxFooter">
@@ -96,15 +60,18 @@
if (strUtil.isNotEmpty($('#rechgEDtLike').val())) {d.rechgEDt = $('#rechgEDtLike').val();} if (strUtil.isNotEmpty($('#rechgEDtLike').val())) {d.rechgEDt = $('#rechgEDtLike').val();}
if (strUtil.isNotEmpty($('#rechgTimeLike').val())) {d.rechgTime = $('#rechgTimeLike').val();} if (strUtil.isNotEmpty($('#rechgTimeLike').val())) {d.rechgTime = $('#rechgTimeLike').val();}
if (strUtil.isNotEmpty($('#payFnshLike').val())) {d.payFnsh = $('#payFnshLike').val();} if (strUtil.isNotEmpty($('#payFnshLike').val())) {d.payFnsh = $('#payFnshLike').val();}
if (strUtil.isNotEmpty($('#chIdLike').val())) {d.chId = $('#chIdLike').val();}
return d; return d;
}, },
{ {
order: [], order: [[5, 'desc']],
columns: [ columns: [
{title: "충전소 ", name: "ST_NM", data: "stId", className, orderable: false}, {title: "충전소 아이디", name: "ST_ID", data: "stId", className, orderable: false},
{title: "충전기 ", name: "CHGR_NM", data: "chgrId", className, orderable: false}, {title: "충전기 아이디", name: "CHGR_ID", data: "chgrId", className, orderable: false},
{title: "결제구분", name: "PAY_TP", data: "payTp", className, orderable: false}, {title: "결제구분", name: "PAY_TP", data: "payTp", className, orderable: false},
{title: "충전기타입 코드", name: "CHGR_TP", data: "plugTp", className, orderable: false}, {title: "채널", name: "CH_ID", data: "chId", className, orderable: false},
{title: "충전기타입 코드", name: "PLUG_TP", data: "plugTp", className, orderable: false},
{title: "충전 시작일시", name: "RECHG_S_DT", data: "rechgSDt", className, orderable: false}, {title: "충전 시작일시", name: "RECHG_S_DT", data: "rechgSDt", className, orderable: false},
{title: "충전 종료일시", name: "RECHG_E_DT", data: "rechgEDt", className, orderable: false}, {title: "충전 종료일시", name: "RECHG_E_DT", data: "rechgEDt", className, orderable: false},
{title: "충전시간", name: "RECHG_TIME", data: "rechgTime", className, orderable: false}, {title: "충전시간", name: "RECHG_TIME", data: "rechgTime", className, orderable: false},
@@ -170,6 +137,17 @@
minLen: 0, minLen: 0,
maxLen: 50, maxLen: 50,
}), }),
new ModalInput({
inputType: ModalInputType.text,
inputId: "chId",
inputName: "chId",
inputLabel: "채널",
inputPlaceholder: "채널",
isReq: false,
isEnable: true,
minLen: 0,
maxLen: 50,
}),
new ModalInput({ new ModalInput({
inputType: ModalInputType.textarea, inputType: ModalInputType.textarea,
inputId: "payTp", inputId: "payTp",
@@ -283,12 +261,6 @@
$("#resetBtn").click(function() { $("#resetBtn").click(function() {
$("#stIdLike").val(""); $("#stIdLike").val("");
$("#chgrIdLike").val(""); $("#chgrIdLike").val("");
$("#payTpLike").val("");
$("#plugTpLike").val("");
$("#rechgSDtLike").val("");
$("#rechgEDtLike").val("");
$("#rechgTimeLike").val("");
$("#payFnshLike").val("");
datatable.ajax.reload(); datatable.ajax.reload();
}); });
</script> </script>
@@ -12,30 +12,6 @@
<input type="text" id="providerNmLike" name="providerNmLike" class="form-control" placeholder="충전사업자 명 입력" maxlength="50"> <input type="text" id="providerNmLike" name="providerNmLike" class="form-control" placeholder="충전사업자 명 입력" maxlength="50">
</div> </div>
</div> </div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:140px">사업자번호</span>
<input type="text" id="bizTaxIdLike" name="bizTaxIdLike" class="form-control" placeholder="사업자번호 입력" maxlength="50">
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:140px">대표자</span>
<input type="text" id="ceoLike" name="ceoLike" class="form-control" placeholder="대표자 입력" maxlength="50">
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:140px">전화번호</span>
<input type="text" id="telLike" name="telLike" class="form-control" placeholder="전화번호 입력" maxlength="50">
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:140px">팩스번호</span>
<input type="text" id="faxLike" name="faxLike" class="form-control" placeholder="팩스번호 입력" maxlength="50">
</div>
</div>
</div> </div>
</div> </div>
<div id="searchBoxFooter"> <div id="searchBoxFooter">
@@ -281,10 +257,6 @@
$("#resetBtn").click(function() { $("#resetBtn").click(function() {
$("#providerNmLike").val(""); $("#providerNmLike").val("");
$("#bizTaxIdLike").val("");
$("#ceoLike").val("");
$("#telLike").val("");
$("#faxLike").val("");
datatable.ajax.reload(); datatable.ajax.reload();
}); });
</script> </script>
@@ -0,0 +1,573 @@
<!DOCTYPE html>
<html xmlns:th="http://thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/cmsLayout}">
<body>
<section layout:fragment="Content">
<div class="w-100" style="margin-bottom: .3rem;">
<div th:replace="~{common/fragments/searchBox :: SearchBoxFragment(~{ :: #searchBoxContent}, ~{ :: #searchBoxFooter})}">
<div id="searchBoxContent">
<div class="row px-2">
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:120px">요금제명</span>
<input type="text" id="ratePlanNameLike" name="ratePlanNameLike" class="form-control" placeholder="요금제명 입력" maxlength="50">
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:120px">승인 상태</span>
<select id="approvalStatusLike" name="approvalStatusLike" class="form-control">
<option value="">전체</option>
<option value="approved">승인완료</option>
<option value="pending">대기중</option>
</select>
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:120px">활성화 여부</span>
<select id="isActiveLike" name="isActiveLike" class="form-control">
<option value="">전체</option>
<option value="true">활성</option>
<option value="false">비활성</option>
</select>
</div>
</div>
</div>
</div>
<div id="searchBoxFooter">
<div class="row float-end">
<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>
</div>
</div>
<div class="w-100">
<div class="box box-primary">
<div class="form">
<table id="datatable" class="table table-striped table-hover dataTable" style="width:100%;"></table>
</div>
</div>
</div>
<style>
#ratePlanDetailScrollArea { max-height: 65vh; overflow-y: auto; overflow-x: hidden; padding-right: 5px; }
#ratePlanDetailScrollArea::-webkit-scrollbar { width: 6px; }
#ratePlanDetailScrollArea::-webkit-scrollbar-track { background: #f5f5f5; border-radius: 3px; }
#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; }
.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; }
.price-table thead th { border-bottom-width: 1px; font-weight: 600; color: #555; }
.price-table thead th:first-child { color: #222; }
.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-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; }
.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(
"#datatable",
rootPath + "list.json",
function(d) {
if (strUtil.isNotEmpty($('#ratePlanNameLike').val())) {d.ratePlanName = $('#ratePlanNameLike').val();}
if (strUtil.isNotEmpty($('#approvalStatusLike').val())) {d.approvalStatus = $('#approvalStatusLike').val();}
if (strUtil.isNotEmpty($('#isActiveLike').val())) {d.isActive = $('#isActiveLike').val();}
return d;
},
{
order: [[0, 'desc']],
columns: [
{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", 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", orderable:false, className, width: "80px", render: (data) => {
let val = String(data || '').toLowerCase();
return (val === 'true' || val === 'y') ? '<span class="text-primary fw-bold">활성</span>' : '<span class="text-secondary">비활성</span>';
}},
{title: "기능", orderable:false, width:"120px", render: (data, type, row) => mkRowDataFunctions({ratePlanId: row['ratePlanId']}, row['ratePlanName'], true, true, true)}
],
allCheck: false,
}
);
});
const showModal = (mode, ids, title) => {
if(!checkRole(mode)) { alert("권한이 없습니다."); return; }
const loadUrl = rootPath + "view.json";
const modalWidth = 780;
const labelWidth = '1px';
if(mode === ModalMode.VIEW || mode === ModalMode.VIEW_ONLY) {
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,
callInit: function(param1, param2, param3) {
let data;
if (param1 && param1.ratePlanId) data = param1;
else if (param2 && param2.ratePlanId) data = param2;
else if (param1 && param1.data) data = param1.data;
else if (param2 && param2.data) data = param2.data;
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 || ''}`);
let mainActiveVal = String(data.isActive || data.active || '').toLowerCase();
$('#v_isActive').text((mainActiveVal === 'true' || mainActiveVal === 'y') ? '활성화' : '비활성');
$('#v_description').text(data.description || '-');
if(data.approvalStatus === 'approved') {
$('#v_approvalStatus').html('<span class="approval-ok"><i class="bi bi-check-lg"></i> 승인완료</span>');
} else {
$('#v_approvalStatus').text(data.approvalStatus || '대기중');
}
renderViewPriceTable(data.detailsTime || []);
if (data.chargerList && data.chargerList.length > 0) {
$('#v_chargerCount').text(data.chargerList.length);
let chargerHtml = data.chargerList.map(c => {
let isActiveVal = String(c.isActive || c.active || '').toLowerCase();
let isActive = (isActiveVal === 'true' || isActiveVal === '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">
<div class="charger-cd"><i class="bi bi-ev-front"></i> ${c.chargerCd}</div>
<div class="charger-dt">${c.applyStartDate || ''} ~ ${c.applyEndDate || ''}</div>
<div><span class="${badgeClass}">${badgeText}</span></div>
</div>
</div>
`}).join('');
$('#v_chargerListArea').html(chargerHtml);
} else {
$('#v_chargerCount').text('0');
$('#v_chargerListArea').html('<div class="col-12 text-center text-muted py-4">적용된 충전기가 없습니다.</div>');
}
}, 200);
}
});
}
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', {
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() {
return $('#i_seasonType').val() === '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) { renderChargerList(res.data || res.list || []); },
error: function() { $('#i_chargerListArea').html('<div class="text-muted small p-2">충전기 목록을 불러오지 못했습니다.</div>'); }
});
}
function renderChargerList(list) {
const mapped = list.map(c => ({
chargerCd: `${c.providerId || ''}${c.stId || ''}${c.chgrId || ''}`,
chargerName: c.chgrNm || `${c.providerId || ''}${c.stId || ''}${c.chgrId || ''}`,
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("");
$("#approvalStatusLike").val("");
$("#isActiveLike").val("");
datatable.ajax.reload();
});
</script>
</section>
</body>
</html>
@@ -0,0 +1,412 @@
<!DOCTYPE html>
<html xmlns:th="http://thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/cmsLayout}">
<body>
<section layout:fragment="Content">
<div class="w-100" style="margin-bottom: .3rem;">
<div th:replace="~{common/fragments/searchBox :: SearchBoxFragment(~{ :: #searchBoxContent}, ~{ :: #searchBoxFooter})}">
<div id="searchBoxContent">
<div class="row px-2">
<!-- <div class="col-lg-4 col-sm-6 col-xs-12 pb-1">-->
<!-- <div class="input-group">-->
<!-- <span class="input-group-text" style="width:300px">요청 일시</span>-->
<!-- <input type="text" id="searchDtLike" name="searchDtLike" class="form-control" placeholder="요청 일시 입력" maxlength="50">-->
<!-- </div>-->
<!-- </div>-->
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:300px">충전사업자 ID</span>
<input type="text" id="providerIdLike" name="providerIdLike" class="form-control" placeholder="충전사업자 ID 입력" maxlength="50">
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:300px">충전소 ID</span>
<input type="text" id="stIdLike" name="stIdLike" class="form-control" placeholder="충전소 ID 입력" maxlength="50">
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:300px">충전기 ID</span>
<input type="text" id="chgrIdLike" name="chgrIdLike" class="form-control" placeholder="충전기 ID 입력" maxlength="50">
</div>
</div>
<!-- <div class="col-lg-4 col-sm-6 col-xs-12 pb-1">-->
<!-- <div class="input-group">-->
<!-- <span class="input-group-text" style="width:300px">플러그 타입</span>-->
<!-- <input type="text" id="plugTypeLike" name="plugTypeLike" class="form-control" placeholder="플러그 타입 입력" maxlength="50">-->
<!-- </div>-->
<!-- </div>-->
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:300px">회원카드번호</span>
<input type="text" id="memAuthInputNoLike" name="memAuthInputNoLike" class="form-control" placeholder="회원카드번호 입력" maxlength="50">
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:300px">신용카드 승인번호</span>
<input type="text" id="creditPPayTrxNoLike" name="creditPPayTrxNoLike" class="form-control" placeholder="신용카드 승인번호 입력" maxlength="50">
</div>
</div>
<!-- <div class="col-lg-4 col-sm-6 col-xs-12 pb-1">-->
<!-- <div class="input-group">-->
<!-- <span class="input-group-text" style="width:300px">신용카드 승인일시</span>-->
<!-- <input type="text" id="creditPPayTrxDtLike" name="creditPPayTrxDtLike" class="form-control" placeholder="신용카드 승인일시 입력" maxlength="50">-->
<!-- </div>-->
<!-- </div>-->
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:300px">충전 시작일시</span>
<input type="date" id="rechgSdtLike" name="rechgSdtLike" class="form-control" placeholder="충전 시작일시 입력" maxlength="50">
</div>
</div>
<!-- <div class="col-lg-4 col-sm-6 col-xs-12 pb-1">-->
<!-- <div class="input-group">-->
<!-- <span class="input-group-text" style="width:300px">충전 잔여 시간</span>-->
<!-- <input type="text" id="rechgRemainTimeLike" name="rechgRemainTimeLike" class="form-control" placeholder="충전 잔여 시간 입력" maxlength="50">-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="col-lg-4 col-sm-6 col-xs-12 pb-1">-->
<!-- <div class="input-group">-->
<!-- <span class="input-group-text" style="width:300px">결제타입</span>-->
<!-- <select id="payType" name="payType" class="form-select">-->
<!-- <option value="">전체</option>-->
<!-- <option value="회원카드">회원카드</option>-->
<!-- <option value="신용카드">신용카드</option>-->
<!-- <option value="무과금">무과금</option>-->
<!-- </select>-->
<!-- </div>-->
<!-- </div>-->
</div>
</div>
<div id="searchBoxFooter">
<div class="row float-end">
<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>
</div>
</div>
<div class="w-100">
<div class="box box-primary">
<div class="form">
<table id="datatable" class="table table-striped table-hover dataTable" style="width:100%;"></table>
</div>
</div>
</div>
<script th:inline="none">
const rootPath = "/cms/rechgingList/";
let datatable;
$(document).ready(function() {
const className = "dt-head-center dt-body-center";
datatable = newDataTable(
"#datatable",
rootPath + "list.json",
function(d) {
if (strUtil.isNotEmpty($('#searchDtLike').val())) {d.searchDt = $('#searchDtLike').val();}
if (strUtil.isNotEmpty($('#providerIdLike').val())) {d.providerId = $('#providerIdLike').val();}
if (strUtil.isNotEmpty($('#stIdLike').val())) {d.stId = $('#stIdLike').val();}
if (strUtil.isNotEmpty($('#chgrIdLike').val())) {d.chgrId = $('#chgrIdLike').val();}
if (strUtil.isNotEmpty($('#plugTypeLike').val())) {d.plugType = $('#plugTypeLike').val();}
if (strUtil.isNotEmpty($('#memAuthInputNoLike').val())) {d.memAuthInputNo = $('#memAuthInputNoLike').val();}
if (strUtil.isNotEmpty($('#creditPPayTrxNoLike').val())) {d.creditPPayTrxNo = $('#creditPPayTrxNoLike').val();}
if (strUtil.isNotEmpty($('#creditPPayTrxDtLike').val())) {d.creditPPayTrxDt = $('#creditPPayTrxDtLike').val();}
if (strUtil.isNotEmpty($('#rechgSdtLike').val())) {d.rechgSdt = $('#rechgSdtLike').val();}
if (strUtil.isNotEmpty($('#rechgRemainTimeLike').val())) {d.rechgRemainTime = $('#rechgRemainTimeLike').val();}
if (strUtil.isNotEmpty($('#payTypeLike').val())) {d.payType = $('#payTypeLike').val();}
return d;
},
{
order: [[0, 'desc'], [2, 'desc'], [3, 'desc'], [4, 'desc']],
columns: [
{title: "충전중 ID", name: "RECHGING_LIST_ID", data: "rechgingListId", className, visible: false},
// {title: "요청 일시", name: "SEARCH_DT", data: "searchDt", className, orderable: false},
{title: "충전사업자 ID", name: "PROVIDER_ID", data: "providerId", className},
{title: "충전소 ID", name: "ST_ID", data: "stId", className},
{title: "충전기 ID", name: "CHGR_ID", data: "chgrId", className},
// {title: "채널 ID", name: "CH_ID", data: "chId", className, orderable: false},
{title: "플러그 타입", name: "PLUG_TYPE", data: "plugType", className, orderable: false},
{title: "회원카드번호", name: "MEM_AUTH_INPUT_NO", data: "memAuthInputNo", className, orderable: false},
{title: "신용카드 승인번호", name: "CREDIT_P_PAY_TRX_NO", data: "creditPPayTrxNo", className, orderable: false},
{title: "신용카드 승인일시", name: "CREDIT_P_PAY_TRX_DT", data: "creditPPayTrxDt", className, orderable: false},
{title: "충전 시작일시", name: "RECHG_SDT", data: "rechgSdt", className, orderable: false},
{title: "충전 잔여 시간", name: "RECHG_REMAIN_TIME", data: "rechgRemainTime", className, orderable: false},
{title: "진행 충전량", name: "RECHGING_WH", data: "rechgingWh", className, orderable: false},
{title: "선결제 금액", name: "RECHG_DEMAND_AMT", data: "rechgDemandAmt", className, orderable: false},
{title: "진행 충전 금액", name: "RECHGING_AMT", data: "rechgingAmt", className, orderable: false},
{title: "결제타입", name: "PAY_TYPE", data: "payType", className, orderable: false},
// {title: "기능", orderable:false, width:"110px",
// render: (data, type, row) => {
// return mkRowDataFunctions({rechgingListId: row['rechgingListId'], providerId: row['providerId'], stId: row['stId'], chgrId: row['chgrId']}, row['#####Title'], true, menuRoleModify, menuRoleDel);}}
],
allCheck: false,
});
});
const showModal = (mode, ids, title) => {
if(!checkRole(mode)) { alert("권한이 없습니다."); return; }
let modalInputArray = [
new ModalInput({
inputType: ModalInputType.hidden,
inputId: "rechgingListId",
inputName: "rechgingListId",
}),
new ModalInput({
inputType: ModalInputType.textarea,
inputId: "searchDt",
inputName: "searchDt",
inputLabel: "요청 일시",
inputPlaceholder: "요청 일시",
isReq: false,
isEnable: true,
minLen: 0,
maxLen: 250,
}),
new ModalInput({
inputType: ModalInputType.text,
inputId: "providerId",
inputName: "providerId",
hasOldPk: true,
inputLabel: "충전사업자 ID",
inputPlaceholder: "충전사업자 ID",
isReq: false,
isEnable: true,
minLen: 0,
maxLen: 20,
}),
new ModalInput({
inputType: ModalInputType.text,
inputId: "stId",
inputName: "stId",
hasOldPk: true,
inputLabel: "충전소 ID",
inputPlaceholder: "충전소 ID",
isReq: false,
isEnable: true,
minLen: 0,
maxLen: 20,
}),
new ModalInput({
inputType: ModalInputType.text,
inputId: "chgrId",
inputName: "chgrId",
hasOldPk: true,
inputLabel: "충전기 ID",
inputPlaceholder: "충전기 ID",
isReq: false,
isEnable: true,
minLen: 0,
maxLen: 20,
}),
new ModalInput({
inputType: ModalInputType.text,
inputId: "chId",
inputName: "chId",
inputLabel: "채널 ID",
inputPlaceholder: "채널 ID",
isReq: false,
isEnable: true,
}),
new ModalInput({
inputType: ModalInputType.textarea,
inputId: "plugType",
inputName: "plugType",
inputLabel: "플러그 타입",
inputPlaceholder: "플러그 타입",
isReq: false,
isEnable: true,
minLen: 0,
maxLen: 250,
}),
new ModalInput({
inputType: ModalInputType.text,
inputId: "memAuthInputNo",
inputName: "memAuthInputNo",
inputLabel: "회원카드번호",
inputPlaceholder: "회원카드번호",
isReq: false,
isEnable: true,
minLen: 0,
maxLen: 20,
}),
new ModalInput({
inputType: ModalInputType.textarea,
inputId: "creditPPayTrxNo",
inputName: "creditPPayTrxNo",
inputLabel: "신용카드 승인번호",
inputPlaceholder: "신용카드 승인번호",
isReq: false,
isEnable: true,
minLen: 0,
maxLen: 250,
}),
new ModalInput({
inputType: ModalInputType.textarea,
inputId: "creditPPayTrxDt",
inputName: "creditPPayTrxDt",
inputLabel: "신용카드 승인일시",
inputPlaceholder: "신용카드 승인일시",
isReq: false,
isEnable: true,
minLen: 0,
maxLen: 250,
}),
new ModalInput({
inputType: ModalInputType.textarea,
inputId: "rechgSdt",
inputName: "rechgSdt",
inputLabel: "충전 시작일시",
inputPlaceholder: "충전 시작일시",
isReq: false,
isEnable: true,
minLen: 0,
maxLen: 250,
}),
new ModalInput({
inputType: ModalInputType.textarea,
inputId: "rechgRemainTime",
inputName: "rechgRemainTime",
inputLabel: "충전 잔여 시간",
inputPlaceholder: "충전 잔여 시간",
isReq: false,
isEnable: true,
minLen: 0,
maxLen: 250,
}),
new ModalInput({
inputType: ModalInputType.text,
inputId: "rechgingWh",
inputName: "rechgingWh",
inputLabel: "진행 충전량",
inputPlaceholder: "진행 충전량",
isReq: false,
isEnable: true,
}),
new ModalInput({
inputType: ModalInputType.text,
inputId: "rechgDemandAmt",
inputName: "rechgDemandAmt",
inputLabel: "선결제 금액",
inputPlaceholder: "선결제 금액",
isReq: false,
isEnable: true,
}),
new ModalInput({
inputType: ModalInputType.text,
inputId: "rechgingAmt",
inputName: "rechgingAmt",
inputLabel: "진행 충전 금액",
inputPlaceholder: "진행 충전 금액",
isReq: false,
isEnable: true,
}),
new ModalInput({
inputType: ModalInputType.textarea,
inputId: "payType",
inputName: "payType",
inputLabel: "결제타입",
inputPlaceholder: "결제타입",
isReq: false,
isEnable: true,
minLen: 0,
maxLen: 250,
}),
new ModalInput({
inputType: ModalInputType.text,
inputId: "integratedWh",
inputName: "integratedWh",
inputLabel: "적산량",
inputPlaceholder: "적산량",
isReq: false,
isEnable: true,
}),
new ModalInput({
inputType: ModalInputType.text,
inputId: "dcPowerWh",
inputName: "dcPowerWh",
inputLabel: "DC 적산량",
inputPlaceholder: "DC 적산량",
isReq: false,
isEnable: true,
}),
new ModalInput({
inputType: ModalInputType.text,
inputId: "acPowerWh",
inputName: "acPowerWh",
inputLabel: "AC 적산량",
inputPlaceholder: "AC 적산량",
isReq: false,
isEnable: true,
}),
new ModalInput({
inputType: ModalInputType.text,
inputId: "currVolt",
inputName: "currVolt",
inputLabel: "현재 전압",
inputPlaceholder: "현재 전압",
isReq: false,
isEnable: true,
}),
new ModalInput({
inputType: ModalInputType.text,
inputId: "currC",
inputName: "currC",
inputLabel: "현재 전류",
inputPlaceholder: "현재 전류",
isReq: false,
isEnable: true,
})
];
if(!rootPath){ alert("rootPath를 지정하십시요."); return; }
const insertUrl = rootPath+"insert.json";
const updateUrl = rootPath+"update.json";
const loadUrl = rootPath+"view.json";
const deleteUrl = rootPath+"delete.json";
const modalWidth = 500;
const labelWidth = '100px';
if(mode === ModalMode.REGISTER) {
newModal(new ModalInfo({modalTitle: "등록", inputArray: modalInputArray, modalWidth}), mode, 'main', {insertUrl, labelWidth, callBack: () => { defaultCallback(); }})
} else if(mode===ModalMode.VIEW || mode===ModalMode.VIEW_ONLY || mode===ModalMode.MODIFY) {
newModal(new ModalInfo({modalTitle: "상세정보", inputArray: modalInputArray, modalWidth}), mode, 'main', {loadUrl, updateUrl, ids, labelWidth, callBack: () => { defaultCallback(); }})
} else if(mode === ModalMode.DELETE) {
newDelete(deleteUrl, ids, title, () => { defaultCallback(); });
}
const defaultCallback = () => {
datatable.ajax.reload();
}
};
$("#searchBtn").click(function() {
datatable.ajax.reload();
});
$("#resetBtn").click(function() {
$("#searchDtLike").val("");
$("#providerIdLike").val("");
$("#stIdLike").val("");
$("#chgrIdLike").val("");
$("#plugTypeLike").val("");
$("#memAuthInputNoLike").val("");
$("#creditPPayTrxNoLike").val("");
$("#creditPPayTrxDtLike").val("");
$("#rechgSdtLike").val("");
$("#rechgRemainTimeLike").val("");
$("#payTypeLike").val("");
datatable.ajax.reload();
});
</script>
</section>
</body>
</html>
@@ -18,24 +18,6 @@
<input type="text" id="addrLdMLike" name="addrLdMLike" class="form-control" placeholder="주소 지번 메인 입력" maxlength="50"> <input type="text" id="addrLdMLike" name="addrLdMLike" class="form-control" placeholder="주소 지번 메인 입력" maxlength="50">
</div> </div>
</div> </div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:200px">법정동 도 명</span>
<input type="text" id="pnuDoNmLike" name="pnuDoNmLike" class="form-control" placeholder="법정동 도 명 입력" maxlength="50">
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:200px">법정동 시 명</span>
<input type="text" id="pnuSiNmLike" name="pnuSiNmLike" class="form-control" placeholder="법정동 시 명 입력" maxlength="50">
</div>
</div>
<div class="col-lg-4 col-sm-6 col-xs-12 pb-1">
<div class="input-group">
<span class="input-group-text" style="width:200px">법정동 읍면동 명</span>
<input type="text" id="pnuDongNmLike" name="pnuDongNmLike" class="form-control" placeholder="법정동 읍면동 명 입력" maxlength="50">
</div>
</div>
</div> </div>
</div> </div>
<div id="searchBoxFooter"> <div id="searchBoxFooter">