feat: 발주기관 통계 기능 안정화 건
parent
4e19566bf0
commit
812a10b004
|
|
@ -4,14 +4,17 @@ import java.io.IOException;
|
|||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.json.simple.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.ModelMap;
|
||||
|
|
@ -21,6 +24,7 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import geoinfo.drilling.input.service.DrillingInputService;
|
||||
import geoinfo.drilling.inquiry.service.DrillingInquiryService;
|
||||
import geoinfo.drilling.statistics.service.DrillingStatisticsService;
|
||||
import geoinfo.util.MyUtil;
|
||||
|
|
@ -33,6 +37,9 @@ public class DrillingStatisticsController {
|
|||
@Autowired
|
||||
DrillingInquiryService drillingInquiryService;
|
||||
|
||||
@Autowired
|
||||
DrillingInputService drillingInputService;
|
||||
|
||||
@Autowired
|
||||
DrillingStatisticsService drillingStatisticsService;
|
||||
|
||||
|
|
@ -87,4 +94,157 @@ public class DrillingStatisticsController {
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* [신규] 통계 페이지 - 프로젝트 상태 파이 차트 데이터 조회
|
||||
* @param session
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/drilling/statistics/project-status-chart.do", method = RequestMethod.GET, produces = "application/json; charset=utf-8")
|
||||
@ResponseBody
|
||||
public ResponseEntity<JSONObject> getProjectStatusChartData( HttpServletRequest request, HttpSession session ) {
|
||||
|
||||
JSONObject response = new JSONObject();
|
||||
|
||||
try {
|
||||
// 세션에서 사용자 정보 (Box 또는 VO)를 가져옵니다.
|
||||
// "sessionInfo"는 예시이며, 실제 세션에 저장된 Key를 사용해야 합니다.
|
||||
// DrillingInquiryController의 로직을 참고하여 사용자 조직 코드를 가져옵니다.
|
||||
String userId = MyUtil.getStringFromObject(request.getSession().getAttribute("USERID"));
|
||||
|
||||
if (userId == null) {
|
||||
response.put("resultCode", 401);
|
||||
response.put("resultMessage", "로그인이 필요합니다.");
|
||||
return new ResponseEntity<>(response, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
HashMap<String, Object> spGetMasterCompanyDistrictParams = drillingInputService.getOrganizationUserGlGmGsGfCodes(userId);
|
||||
|
||||
String masterCompanyOCode = MyUtil.getStringFromObject( spGetMasterCompanyDistrictParams.get("v_gl") );
|
||||
String masterCompanyTwCode = MyUtil.getStringFromObject( spGetMasterCompanyDistrictParams.get("v_gm") );
|
||||
String masterCompanyThCode = MyUtil.getStringFromObject( spGetMasterCompanyDistrictParams.get("v_gs") );
|
||||
String masterCompanyName = MyUtil.getStringFromObject( spGetMasterCompanyDistrictParams.get("v_gf") );
|
||||
|
||||
|
||||
HashMap<String, Object> params = new HashMap<>();
|
||||
|
||||
// DrillingInquiryController에서 사용하는 파라미터와 동일하게 설정
|
||||
// (예: O_CODE, TW_CODE, TH_CODE 등)
|
||||
params.put("masterCompanyOCode", masterCompanyOCode);
|
||||
params.put("masterCompanyTwCode", masterCompanyTwCode);
|
||||
params.put("masterCompanyThCode", masterCompanyThCode);
|
||||
// params.put("USERID", sessionInfo.getString("USERID"));
|
||||
|
||||
List<HashMap<String, Object>> chartData = drillingStatisticsService.getProjectStatusCounts(params);
|
||||
|
||||
response.put("resultCode", 200);
|
||||
response.put("datas", chartData);
|
||||
|
||||
} catch (Exception e) {
|
||||
response.put("resultCode", 500);
|
||||
response.put("resultMessage", "차트 데이터 조회 중 오류가 발생했습니다: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return new ResponseEntity<>(response, HttpStatus.OK);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* [신규] 통계 페이지 - 용역사별 성과 현황
|
||||
* @param session
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/drilling/statistics/company-performance.do", method = RequestMethod.GET, produces = "application/json; charset=utf-8")
|
||||
@ResponseBody
|
||||
public ResponseEntity<JSONObject> getCompanyPerformanceStats( HttpServletRequest request, HttpSession session) {
|
||||
|
||||
JSONObject response = new JSONObject();
|
||||
|
||||
try {
|
||||
|
||||
String userId = MyUtil.getStringFromObject(request.getSession().getAttribute("USERID"));
|
||||
|
||||
if (userId == null) {
|
||||
response.put("resultCode", 401);
|
||||
response.put("resultMessage", "로그인이 필요합니다.");
|
||||
return new ResponseEntity<>(response, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
HashMap<String, Object> spGetMasterCompanyDistrictParams = drillingInputService.getOrganizationUserGlGmGsGfCodes(userId);
|
||||
|
||||
String masterCompanyOCode = MyUtil.getStringFromObject( spGetMasterCompanyDistrictParams.get("v_gl") );
|
||||
String masterCompanyTwCode = MyUtil.getStringFromObject( spGetMasterCompanyDistrictParams.get("v_gm") );
|
||||
String masterCompanyThCode = MyUtil.getStringFromObject( spGetMasterCompanyDistrictParams.get("v_gs") );
|
||||
String masterCompanyName = MyUtil.getStringFromObject( spGetMasterCompanyDistrictParams.get("v_gf") );
|
||||
|
||||
HashMap<String, Object> params = new HashMap<>();
|
||||
|
||||
// DrillingInquiryController의 발주기관 필터링 로직과 동일하게 파라미터 설정
|
||||
params.put("masterCompanyOCode", masterCompanyOCode);
|
||||
params.put("masterCompanyTwCode", masterCompanyTwCode);
|
||||
params.put("masterCompanyThCode", masterCompanyThCode);
|
||||
|
||||
List<HashMap<String, Object>> statsData = drillingStatisticsService.getCompanyPerformanceStats(params);
|
||||
|
||||
response.put("resultCode", 200);
|
||||
response.put("datas", statsData);
|
||||
|
||||
} catch (Exception e) {
|
||||
response.put("resultCode", 500);
|
||||
response.put("resultMessage", "성과 현황 조회 중 오류가 발생했습니다: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return new ResponseEntity<>(response, HttpStatus.OK);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 통계 페이지 - 데이터 품질 현황 (수정 요청 횟수)
|
||||
* @param request
|
||||
* @param session
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/drilling/statistics/data-quality-stats.do", method = RequestMethod.GET, produces = "application/json; charset=utf-8")
|
||||
@ResponseBody
|
||||
public ResponseEntity<JSONObject> getDataQualityStats(HttpServletRequest request, HttpSession session) {
|
||||
|
||||
JSONObject response = new JSONObject();
|
||||
|
||||
try {
|
||||
// 세션에서 사용자 정보 (USERID)
|
||||
String userId = MyUtil.getStringFromObject(request.getSession().getAttribute("USERID"));
|
||||
if (userId == null) {
|
||||
response.put("resultCode", 401);
|
||||
response.put("resultMessage", "로그인이 필요합니다.");
|
||||
return new ResponseEntity<>(response, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
// 세션 사용자의 조직 코드 조회
|
||||
HashMap<String, Object> spGetMasterCompanyDistrictParams = drillingInputService.getOrganizationUserGlGmGsGfCodes(userId);
|
||||
String masterCompanyOCode = MyUtil.getStringFromObject( spGetMasterCompanyDistrictParams.get("v_gl") );
|
||||
String masterCompanyTwCode = MyUtil.getStringFromObject( spGetMasterCompanyDistrictParams.get("v_gm") );
|
||||
String masterCompanyThCode = MyUtil.getStringFromObject( spGetMasterCompanyDistrictParams.get("v_gs") );
|
||||
|
||||
HashMap<String, Object> params = new HashMap<>();
|
||||
params.put("masterCompanyOCode", masterCompanyOCode);
|
||||
params.put("masterCompanyTwCode", masterCompanyTwCode);
|
||||
params.put("masterCompanyThCode", masterCompanyThCode);
|
||||
|
||||
// Service 호출
|
||||
HashMap<String, Object> statsData = drillingStatisticsService.getDataQualityStats(params);
|
||||
|
||||
response.put("resultCode", 200);
|
||||
response.put("datas", statsData); // { "projects": [...], "companies": [...] }
|
||||
|
||||
} catch (Exception e) {
|
||||
response.put("resultCode", 500);
|
||||
response.put("resultMessage", "데이터 품질 현황 조회 중 오류가 발생했습니다: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return new ResponseEntity<>(response, HttpStatus.OK);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,4 +11,37 @@ import egovframework.rte.psl.dataaccess.util.EgovMap;
|
|||
public interface DrillingStatisticsMapper {
|
||||
public List<EgovMap> selectConstructSiteHistList(HashMap<String, Object> params) throws SQLException;
|
||||
public Long selectConstructSiteHistListCnt(HashMap<String, Object> params) throws SQLException;
|
||||
|
||||
/**
|
||||
* 소속 기관의 프로젝트 상태별 건수 조회 (XML 매핑)
|
||||
* @param params
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
List<HashMap<String, Object>> selectProjectStatusCounts(HashMap<String, Object> params) throws Exception;
|
||||
|
||||
/**
|
||||
* 소속 기관의 용역사(조사기관)별 성과 통계 조회 (XML 매핑)
|
||||
* @param params
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
List<HashMap<String, Object>> selectCompanyPerformanceStats(HashMap<String, Object> params) throws Exception;
|
||||
|
||||
/**
|
||||
* 수정 요청이 많은 프로젝트 Top 5 조회 (XML 매핑)
|
||||
* @param params
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
List<HashMap<String, Object>> selectRevisionCountByProject(HashMap<String, Object> params) throws Exception;
|
||||
|
||||
/**
|
||||
* 용역사(시공사)별 누적 수정 요청 횟수 조회 (XML 매핑)
|
||||
* @param params
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
List<HashMap<String, Object>> selectRevisionCountByCompany(HashMap<String, Object> params) throws Exception;
|
||||
|
||||
}
|
||||
|
|
@ -13,4 +13,31 @@ import egovframework.rte.psl.dataaccess.util.EgovMap;
|
|||
|
||||
public interface DrillingStatisticsService {
|
||||
public JSONObject getConstructSiteHistList(HttpServletRequest request, HashMap<String, Object> params) throws Exception;
|
||||
|
||||
/**
|
||||
* 소속 기관의 프로젝트 상태별 건수 조회
|
||||
* @param params
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
List<HashMap<String, Object>> getProjectStatusCounts(HashMap<String, Object> params) throws Exception;
|
||||
|
||||
|
||||
/**
|
||||
* 소속 기관의 용역사(조사기관)별 성과 통계 조회
|
||||
* @param params
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
List<HashMap<String, Object>> getCompanyPerformanceStats(HashMap<String, Object> params) throws Exception;
|
||||
|
||||
|
||||
/**
|
||||
* 데이터 품질(수정 요청 횟수) 통계 조회
|
||||
* @param params
|
||||
* @return projects (Top 5) 와 companies (전체) 리스트가 담긴 Map
|
||||
* @throws Exception
|
||||
*/
|
||||
HashMap<String, Object> getDataQualityStats(HashMap<String, Object> params) throws Exception;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,6 +94,41 @@ public class DrillingStatisticsServiceImpl implements DrillingStatisticsService
|
|||
throw new Exception("이력 조회 중 오류가 발생하였습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 소속 기관의 프로젝트 상태별 건수 조회
|
||||
*/
|
||||
@Override
|
||||
public List<HashMap<String, Object>> getProjectStatusCounts(HashMap<String, Object> params) throws Exception {
|
||||
return drillingStatisticsMapper.selectProjectStatusCounts(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 소속 기관의 용역사(조사기관)별 성과 통계 조회
|
||||
*/
|
||||
@Override
|
||||
public List<HashMap<String, Object>> getCompanyPerformanceStats(HashMap<String, Object> params) throws Exception {
|
||||
return drillingStatisticsMapper.selectCompanyPerformanceStats(params);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 데이터 품질(수정 요청 횟수) 통계 조회
|
||||
*/
|
||||
@Override
|
||||
public HashMap<String, Object> getDataQualityStats(HashMap<String, Object> params) throws Exception {
|
||||
|
||||
// 1. 수정 요청 많은 프로젝트 Top 5
|
||||
List<HashMap<String, Object>> projectStats = drillingStatisticsMapper.selectRevisionCountByProject(params);
|
||||
|
||||
// 2. 용역사별 누적 수정 요청 횟수
|
||||
List<HashMap<String, Object>> companyStats = drillingStatisticsMapper.selectRevisionCountByCompany(params);
|
||||
|
||||
HashMap<String, Object> result = new HashMap<>();
|
||||
result.put("projects", projectStats);
|
||||
result.put("companies", companyStats);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,5 +56,112 @@
|
|||
AND csi.MASTER_COMPANY_TH_CODE = #{masterCompanyThCode}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<select id="selectProjectStatusCounts" parameterType="java.util.HashMap" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
PROJECT_STATE_CODE, COUNT(*) AS "count"
|
||||
FROM
|
||||
TEMP_CONSTRUCT_SITE_INFO
|
||||
WHERE 1=1
|
||||
<if test="masterCompanyOCode != null and masterCompanyOCode != ''">
|
||||
AND MASTER_COMPANY_O_CODE = #{masterCompanyOCode}
|
||||
</if>
|
||||
<if test="masterCompanyTwCode != null and masterCompanyTwCode != ''">
|
||||
AND MASTER_COMPANY_TW_CODE = #{masterCompanyTwCode}
|
||||
</if>
|
||||
<if test="masterCompanyThCode != null and masterCompanyThCode != ''">
|
||||
AND MASTER_COMPANY_TH_CODE = #{masterCompanyThCode}
|
||||
</if>
|
||||
GROUP BY
|
||||
PROJECT_STATE_CODE ORDER BY
|
||||
PROJECT_STATE_CODE
|
||||
</select>
|
||||
|
||||
|
||||
<select id="selectCompanyPerformanceStats" parameterType="java.util.HashMap" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
NVL(T_PI.PROJECT_CONST_COMPANY, '미지정') AS "COMPANY_NAME",
|
||||
COUNT(T_CSI.CID) AS "TOTAL_PROJECTS",
|
||||
SUM(CASE WHEN T_CSI.PROJECT_STATE_CODE = '3' THEN 1 ELSE 0 END) AS "IN_INSPECTION",
|
||||
SUM(CASE WHEN T_CSI.PROJECT_STATE_CODE = '4' THEN 1 ELSE 0 END) AS "FIX_REQUEST",
|
||||
SUM(CASE WHEN T_CSI.PROJECT_STATE_CODE = '5' THEN 1 ELSE 0 END) AS "COMPLETED_INSPECTION",
|
||||
SUM(CASE WHEN T_CSI.PROJECT_STATE_CODE = '6' THEN 1 ELSE 0 END) AS "COMPLETED_REGISTRATION"
|
||||
FROM
|
||||
TEMP_CONSTRUCT_SITE_INFO T_CSI
|
||||
LEFT JOIN
|
||||
TEMP_PROJECT_INFO T_PI ON T_CSI.PROJECT_CODE = T_PI.PROJECT_CODE
|
||||
WHERE
|
||||
1=1
|
||||
<if test="masterCompanyOCode != null and masterCompanyOCode != ''">
|
||||
AND T_CSI.MASTER_COMPANY_O_CODE = #{masterCompanyOCode}
|
||||
</if>
|
||||
<if test="masterCompanyTwCode != null and masterCompanyTwCode != ''">
|
||||
AND T_CSI.MASTER_COMPANY_TW_CODE = #{masterCompanyTwCode}
|
||||
</if>
|
||||
<if test="masterCompanyThCode != null and masterCompanyThCode != ''">
|
||||
AND T_CSI.MASTER_COMPANY_TH_CODE = #{masterCompanyThCode}
|
||||
</if>
|
||||
GROUP BY
|
||||
T_PI.PROJECT_CONST_COMPANY ORDER BY
|
||||
"TOTAL_PROJECTS" DESC, "COMPANY_NAME" ASC
|
||||
</select>
|
||||
|
||||
<select id="selectRevisionCountByProject" parameterType="java.util.HashMap" resultType="java.util.HashMap">
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
csi.CONST_NAME,
|
||||
COUNT(h.HIST_ID) AS "REVISION_COUNT"
|
||||
FROM
|
||||
TEMP_CONSTRUCT_SITE_HIST h
|
||||
JOIN
|
||||
TEMP_CONSTRUCT_SITE_INFO csi ON h.CID = csi.CID
|
||||
WHERE
|
||||
h.PROJECT_STATE_CODE = '4' -- '수정 요청' 상태 코드
|
||||
|
||||
<if test="masterCompanyOCode != null and masterCompanyOCode != ''">
|
||||
AND csi.MASTER_COMPANY_O_CODE = #{masterCompanyOCode}
|
||||
</if>
|
||||
<if test="masterCompanyTwCode != null and masterCompanyTwCode != ''">
|
||||
AND csi.MASTER_COMPANY_TW_CODE = #{masterCompanyTwCode}
|
||||
</if>
|
||||
<if test="masterCompanyThCode != null and masterCompanyThCode != ''">
|
||||
AND csi.MASTER_COMPANY_TH_CODE = #{masterCompanyThCode}
|
||||
</if>
|
||||
GROUP BY
|
||||
csi.PROJECT_CODE, csi.CONST_NAME
|
||||
ORDER BY
|
||||
"REVISION_COUNT" DESC
|
||||
)
|
||||
WHERE ROWNUM <= 5
|
||||
</select>
|
||||
|
||||
|
||||
<select id="selectRevisionCountByCompany" parameterType="java.util.HashMap" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
NVL(tpi.PROJECT_CONST_COMPANY, '미지정') AS "COMPANY_NAME",
|
||||
COUNT(h.HIST_ID) AS "REVISION_COUNT"
|
||||
FROM
|
||||
TEMP_CONSTRUCT_SITE_HIST h
|
||||
JOIN
|
||||
TEMP_CONSTRUCT_SITE_INFO csi ON h.CID = csi.CID
|
||||
LEFT JOIN
|
||||
TEMP_PROJECT_INFO tpi ON csi.PROJECT_CODE = tpi.PROJECT_CODE
|
||||
WHERE
|
||||
h.PROJECT_STATE_CODE = '4' -- '수정 요청' 상태 코드
|
||||
|
||||
<if test="masterCompanyOCode != null and masterCompanyOCode != ''">
|
||||
AND csi.MASTER_COMPANY_O_CODE = #{masterCompanyOCode}
|
||||
</if>
|
||||
<if test="masterCompanyTwCode != null and masterCompanyTwCode != ''">
|
||||
AND csi.MASTER_COMPANY_TW_CODE = #{masterCompanyTwCode}
|
||||
</if>
|
||||
<if test="masterCompanyThCode != null and masterCompanyThCode != ''">
|
||||
AND csi.MASTER_COMPANY_TH_CODE = #{masterCompanyThCode}
|
||||
</if>
|
||||
GROUP BY
|
||||
tpi.PROJECT_CONST_COMPANY
|
||||
ORDER BY
|
||||
"REVISION_COUNT" DESC, "COMPANY_NAME" ASC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
@ -63,54 +63,47 @@ function getStatusInfo(statusCode) {
|
|||
};
|
||||
|
||||
switch (String(statusCode)) {
|
||||
case '0':
|
||||
case '0': // Gray
|
||||
status.name = '미입력';
|
||||
// 아이콘: 마이너스 (MinusCircle) - 비어있거나 시작 안 함
|
||||
status.icon = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>';
|
||||
status.bgColor = 'bg-gray-500'; // 회색 (중립)
|
||||
status.bgColor = 'bg-gray-500';
|
||||
status.textColor = 'text-gray-600';
|
||||
break; //
|
||||
case '1':
|
||||
break;
|
||||
case '1': // Blue
|
||||
status.name = '입력 중';
|
||||
// 아이콘: 연필 (Pencil) - 수정/작성 중 (기존과 동일)
|
||||
status.icon = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.5L14.732 3.732z"></path></svg>';
|
||||
status.bgColor = 'bg-blue-500'; // 파란색 (진행중)
|
||||
status.bgColor = 'bg-blue-500';
|
||||
status.textColor = 'text-blue-600';
|
||||
break;
|
||||
case '2':
|
||||
case '2': // Orange
|
||||
status.name = '검수 준비 대기중';
|
||||
// 아이콘: 시계 (Clock) - 대기 중
|
||||
status.icon = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>';
|
||||
status.bgColor = 'bg-yellow-500'; // 노란색 (대기)
|
||||
status.textColor = 'text-yellow-600';
|
||||
break; //
|
||||
case '3':
|
||||
status.bgColor = 'bg-orange-500'; // [수정] (Orange)
|
||||
status.textColor = 'text-orange-600'; // [수정]
|
||||
break;
|
||||
case '3': // Yellow
|
||||
status.name = '검수중';
|
||||
// 아이콘: 돋보기 (Search) - 검토 중
|
||||
status.icon = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>';
|
||||
status.bgColor = 'bg-yellow-500'; // 노란색 (대기/검토)
|
||||
status.bgColor = 'bg-yellow-500'; // (Yellow)
|
||||
status.textColor = 'text-yellow-600';
|
||||
break;
|
||||
case '4':
|
||||
case '4': // Red
|
||||
status.name = '수정 요청';
|
||||
// 아이콘: 느낌표 (ExclamationCircle) - 주의/수정 필요
|
||||
status.icon = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>';
|
||||
status.bgColor = 'bg-red-500'; // 빨간색 (거절/오류)
|
||||
status.bgColor = 'bg-red-500';
|
||||
status.textColor = 'text-red-600';
|
||||
break;
|
||||
case '5':
|
||||
case '5': // Green
|
||||
status.name = '검수 완료';
|
||||
// 아이콘: 체크 (CheckCircle) - 성공 (기존과 동일)
|
||||
status.icon = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>';
|
||||
status.bgColor = 'bg-green-500'; // 초록색 (완료/성공)
|
||||
status.bgColor = 'bg-green-500';
|
||||
status.textColor = 'text-green-600';
|
||||
break;
|
||||
case '6':
|
||||
case '6': // Teal
|
||||
status.name = '등록 완료';
|
||||
// 아이콘: 체크 (CheckCircle) - 성공 (기존과 동일)
|
||||
status.icon = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>';
|
||||
status.bgColor = 'bg-green-500'; // 초록색 (완료/성공)
|
||||
status.textColor = 'text-green-600';
|
||||
status.bgColor = 'bg-teal-500'; // [수정] (Teal)
|
||||
status.textColor = 'text-teal-600'; // [수정]
|
||||
break;
|
||||
}
|
||||
return status;
|
||||
|
|
|
|||
|
|
@ -25,10 +25,12 @@ if (request.getSession().getAttribute("CLS") == null || "2".equals(request.getSe
|
|||
<c:import url="/drilling/common/includeTopMenu.do" charEncoding="UTF-8" />
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Noto+Sans+KR:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
|
||||
|
||||
<style>
|
||||
@keyframes shake {
|
||||
0% { transform: translateX(0); }
|
||||
|
|
@ -105,6 +107,32 @@ if (request.getSession().getAttribute("CLS") == null || "2".equals(request.getSe
|
|||
#const-state-code {
|
||||
width: 160px;
|
||||
}
|
||||
/* ▼▼▼ PDF 생성 시 로딩 오버레이 스타일 추가 ▼▼▼ */
|
||||
#pdf-loading-overlay {
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.7);
|
||||
z-index: 9998;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
#pdf-loading-overlay.show {
|
||||
visibility: visible;
|
||||
}
|
||||
#pdf-loading-overlay-content {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
/* ▲▲▲ PDF 생성 시 로딩 오버레이 스타일 추가 ▲▲▲ */
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
|
@ -134,7 +162,7 @@ if (request.getSession().getAttribute("CLS") == null || "2".equals(request.getSe
|
|||
<main class="flex flex-col lg:flex-row gap-4">
|
||||
<aside class="w-full flex flex-col gap-4">
|
||||
<div class="flex flex-col lg:flex-row gap-4">
|
||||
<div class="w-full lg:w-1/2 bg-white p-4 rounded-lg shadow-md">
|
||||
<div id="project-status-section" class="w-full lg:w-1/2 bg-white p-4 rounded-lg shadow-md">
|
||||
<h3 class="font-semibold text-gray-800 mb-2 text-4xl">건설현장 프로젝트 입력상태 별 그래프</h3>
|
||||
<div class="w-full h-[28rem] flex justify-center items-center">
|
||||
<canvas id="projectStatusChart"></canvas>
|
||||
|
|
@ -147,37 +175,47 @@ if (request.getSession().getAttribute("CLS") == null || "2".equals(request.getSe
|
|||
<a href="/drilling/notice.do" class="text-3xl text-blue-600 hover:underline">모두 보기</a>
|
||||
</div>
|
||||
|
||||
<%-- ▼▼▼ [수정] 기존 하드코딩된 알림 내역을 삭제하고, id를 부여합니다. ▼▼▼ --%>
|
||||
<div id="notification-list" class="space-y-3 flex-grow">
|
||||
</div>
|
||||
<%-- ▲▲▲ [수정] 여기까지 ▲▲▲ --%>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="company-performance-section" class="bg-white p-4 rounded-lg shadow-md">
|
||||
<h3 class="font-semibold text-gray-800 mb-4 text-4xl">수주기관별 성과 현황</h3>
|
||||
<div id="company-performance-list" class="overflow-x-auto">
|
||||
<div class="p-4 text-center text-gray-500">성과 현황을 불러오는 중입니다...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="data-quality-section" class="bg-white p-4 rounded-lg shadow-md">
|
||||
<h3 class="font-semibold text-gray-800 mb-4 text-4xl">데이터 품질 현황 (수정 요청 횟수)</h3>
|
||||
<div class="flex flex-col lg:flex-row gap-4">
|
||||
<div class="w-full lg:w-2/3">
|
||||
<h4 class="text-3xl font-medium text-gray-700 mb-2">수주기관별 누적 수정 요청 횟수</h4>
|
||||
<div id="revision-by-company-list" class="overflow-x-auto">
|
||||
<div class="p-4 text-center text-gray-500">데이터를 불러오는 중입니다...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full lg:w-1/3">
|
||||
<h4 class="text-3xl font-medium text-gray-700 mb-2">수정 요청 많은 프로젝트 Top 5</h4>
|
||||
<div id="revision-by-project-list" class="overflow-x-auto">
|
||||
<div class="p-4 text-center text-gray-500">데이터를 불러오는 중입니다...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-4 rounded-lg shadow-md">
|
||||
<h3 class="font-semibold text-gray-800 mb-2 text-4xl">통계 보고서 다운로드</h3>
|
||||
<p class="text-2xl text-gray-600 mb-4">발주기관 통계 기능을 보고서 형태로 다운로드 받을 수 있습니다.</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
<div class="md:col-span-1 flex flex-col items-center">
|
||||
<h4 class="text-2xl font-medium text-center text-gray-700 mb-2">위험도 현황</h4>
|
||||
<div class="relative h-96 w-96">
|
||||
<canvas id="riskChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:col-span-2 flex flex-col items-center">
|
||||
<h4 class="text-2xl font-medium text-center text-gray-700 mb-2">프로젝트 재정 현황</h4>
|
||||
<div class="relative h-96 w-full">
|
||||
<canvas id="financialChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<button class="w-full md:w-[15%] bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-lg flex items-center justify-center transition duration-300 text-3xl">
|
||||
<button id="export-pdf-btn" class="w-full md:w-[15%] bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-lg flex items-center justify-center transition duration-300 text-3xl">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
|
||||
PDF로 내보내기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
|
|
@ -185,91 +223,291 @@ if (request.getSession().getAttribute("CLS") == null || "2".equals(request.getSe
|
|||
<script>
|
||||
// Chart.js initialization
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 1. Project Status Pie Chart
|
||||
var ctxProjectStatus = document.getElementById('projectStatusChart').getContext('2d');
|
||||
new Chart(ctxProjectStatus, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: ['등록완료', '미입력', '검수중', '수정요청'],
|
||||
datasets: [{
|
||||
label: '프로젝트 상태',
|
||||
data: [34, 32, 18, 16],
|
||||
backgroundColor: [ 'rgba(239, 68, 68, 0.8)', 'rgba(245, 158, 11, 0.8)', 'rgba(16, 185, 129, 0.8)', 'rgba(59, 130, 246, 0.8)' ],
|
||||
borderColor: [ 'rgba(239, 68, 68, 1)', 'rgba(245, 158, 11, 1)', 'rgba(16, 185, 129, 1)', 'rgba(59, 130, 246, 1)' ],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { position: 'top' },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
var label = context.label || '';
|
||||
if (label) { label += ': '; }
|
||||
if (context.parsed !== null) { label += context.parsed + '%'; }
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// Project Status Pie Chart
|
||||
function loadProjectStatusChart() {
|
||||
// 백엔드 API 호출
|
||||
const statusMap = {
|
||||
'0': { name: '미입력', color: 'rgba(107, 114, 128, 0.8)' },
|
||||
'1': { name: '입력 중', color: 'rgba(59, 130, 246, 0.8)' },
|
||||
'2': { name: '검수 준비 대기중', color: 'rgba(245, 158, 11, 0.8)' },
|
||||
'3': { name: '검수중', color: 'rgba(234, 179, 8, 0.8)' },
|
||||
'4': { name: '수정 요청', color: 'rgba(239, 68, 68, 0.8)' },
|
||||
'5': { name: '검수 완료', color: 'rgba(16, 185, 129, 0.8)' },
|
||||
'6': { name: '등록 완료', color: 'rgba(13, 148, 136, 0.8)' },
|
||||
'default': { name: '알 수 없음', color: 'rgba(107, 114, 128, 0.8)' }
|
||||
};
|
||||
fetch('/drilling/statistics/project-status-chart.do')
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (response && response.resultCode === 200 && response.datas) {
|
||||
var chartLabels = [];
|
||||
var chartData = [];
|
||||
var chartColors = [];
|
||||
response.datas.forEach(item => {
|
||||
let statusInfo = statusMap[String(item.PROJECT_STATE_CODE)] || statusMap['default'];
|
||||
chartLabels.push(statusInfo.name);
|
||||
chartData.push(item.count);
|
||||
chartColors.push(statusInfo.color);
|
||||
});
|
||||
if (response.datas.length === 0) {
|
||||
chartLabels = ['소속된 현장 데이터가 없습니다'];
|
||||
chartData = [1];
|
||||
chartColors = ['rgba(209, 213, 219, 0.8)'];
|
||||
}
|
||||
var ctxProjectStatus = document.getElementById('projectStatusChart').getContext('2d');
|
||||
new Chart(ctxProjectStatus, {
|
||||
type: 'pie',
|
||||
data: { labels: chartLabels, datasets: [{ label: '프로젝트 상태', data: chartData, backgroundColor: chartColors, borderWidth: 1 }] },
|
||||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top' }, tooltip: { callbacks: { label: function(context) {
|
||||
var label = context.label || '';
|
||||
if (label) { label += ': '; }
|
||||
var value = context.parsed;
|
||||
var total = context.chart.data.datasets[0].data.reduce((a, b) => a + b, 0);
|
||||
var percentage = ((value / total) * 100).toFixed(1);
|
||||
label += value + '건 (' + percentage + '%)';
|
||||
return label;
|
||||
}}
|
||||
}}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error("Error fetching project status chart data:", response.resultMessage);
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error("Fetch error for project status chart:", error);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ▼▼▼ 발주기관별 성과 로드 함수 ▼▼▼
|
||||
function loadCompanyPerformance() {
|
||||
var listContainer = document.getElementById('company-performance-list');
|
||||
listContainer.innerHTML = '<div class="p-4 text-center text-gray-500">성과 현황을 불러오는 중입니다...</div>';
|
||||
fetch('/drilling/statistics/company-performance.do')
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (response && response.resultCode === 200 && response.datas) {
|
||||
var datas = response.datas;
|
||||
if (datas.length === 0) {
|
||||
listContainer.innerHTML = '<div class="p-4 text-center text-gray-500">집계된 용역사 성과 현황이 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
var contentHtml = '<table class="w-full text-left border-collapse text-2xl">';
|
||||
contentHtml += '<thead>' +
|
||||
'<tr class="border-b border-gray-300">' +
|
||||
'<th class="p-2 font-semibold text-gray-700">용역사명</th>' +
|
||||
'<th class="p-2 font-semibold text-gray-700 text-center">총 프로젝트</th>' +
|
||||
'<th class="p-2 font-semibold text-gray-700 text-center">검수중</th>' +
|
||||
'<th class="p-2 font-semibold text-gray-700 text-center">수정 요청</th>' +
|
||||
'<th class="p-2 font-semibold text-gray-700 text-center">검수 완료</th>' +
|
||||
'<th class="p-2 font-semibold text-gray-700 text-center">등록 완료</th>' +
|
||||
'</tr>' +
|
||||
'</thead><tbody>';
|
||||
datas.forEach(item => {
|
||||
contentHtml += '<tr class="border-b border-gray-200">' +
|
||||
'<td class="p-2 font-medium text-gray-800">' + (item.COMPANY_NAME || '미지정') + '</td>' +
|
||||
'<td class="p-2 text-center text-gray-600">' + item.TOTAL_PROJECTS + '</td>' +
|
||||
'<td class="p-2 text-center text-yellow-600">' + item.IN_INSPECTION + '</td>' +
|
||||
'<td class="p-2 text-center text-red-600 font-bold">' + item.FIX_REQUEST + '</td>' +
|
||||
'<td class="p-2 text-center text-green-600">' + item.COMPLETED_INSPECTION + '</td>' +
|
||||
'<td class="p-2 text-center text-teal-600">' + item.COMPLETED_REGISTRATION + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
contentHtml += '</tbody></table>';
|
||||
listContainer.innerHTML = contentHtml;
|
||||
} else {
|
||||
listContainer.innerHTML = '<div class="p-4 text-center text-gray-500">성과 현황 로드에 실패했습니다.</div>';
|
||||
console.error("Error fetching company performance:", response.resultMessage);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
listContainer.innerHTML = '<div class="p-4 text-center text-gray-500">성과 현황 로드 중 오류 발생.</div>';
|
||||
console.error("Fetch error for company performance:", error);
|
||||
});
|
||||
}
|
||||
|
||||
// ▼▼▼ 데이터 품질 (수정 요청) 통계 로드 함수 ▼▼▼
|
||||
function loadDataQualityStats() {
|
||||
var projectListContainer = document.getElementById('revision-by-project-list');
|
||||
var companyListContainer = document.getElementById('revision-by-company-list');
|
||||
projectListContainer.innerHTML = '<div class="p-4 text-center text-gray-500">데이터를 불러오는 중입니다...</div>';
|
||||
companyListContainer.innerHTML = '<div class="p-4 text-center text-gray-500">데이터를 불러오는 중입니다...</div>';
|
||||
fetch('/drilling/statistics/data-quality-stats.do')
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (response && response.resultCode === 200) {
|
||||
displayRevisionByProject(response.datas.projects, projectListContainer);
|
||||
displayRevisionByCompany(response.datas.companies, companyListContainer);
|
||||
} else {
|
||||
projectListContainer.innerHTML = '<div class="p-4 text-center text-gray-500">데이터 품질 현황 로드에 실패했습니다.</div>';
|
||||
companyListContainer.innerHTML = '<div class="p-4 text-center text-gray-500">데이터 품질 현황 로드에 실패했습니다.</div>';
|
||||
console.error("Error fetching data quality stats:", response.resultMessage);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
projectListContainer.innerHTML = '<div class="p-4 text-center text-gray-500">오류 발생.</div>';
|
||||
companyListContainer.innerHTML = '<div class="p-4 text-center text-gray-500">오류 발생.</div>';
|
||||
console.error("Fetch error for data quality stats:", error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정 요청 Top 5 프로젝트 목록 렌더링
|
||||
*/
|
||||
function displayRevisionByProject(datas, container) {
|
||||
if (!datas || datas.length === 0) {
|
||||
container.innerHTML = '<div class="p-4 text-center text-gray-500">수정 요청 이력이 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
var contentHtml = '<ul class="divide-y divide-gray-200 text-2xl">';
|
||||
datas.forEach(item => {
|
||||
contentHtml += '<li class="p-2 flex justify-between items-center">' +
|
||||
'<span class="font-medium text-gray-800 truncate" title="' + (item.CONST_NAME || '알 수 없음') + '">' + (item.CONST_NAME || '알 수 없음') + '</span>' +
|
||||
'<span class="font-bold text-red-600">' + item.REVISION_COUNT + ' 건</span>' +
|
||||
'</li>';
|
||||
});
|
||||
contentHtml += '</ul>';
|
||||
container.innerHTML = contentHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* 발주기관별 수정 요청 횟수 테이블 렌더링
|
||||
*/
|
||||
function displayRevisionByCompany(datas, container) {
|
||||
if (!datas || datas.length === 0) {
|
||||
container.innerHTML = '<div class="p-4 text-center text-gray-500">수정 요청 이력이 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
var contentHtml = '<table class="w-full text-left border-collapse text-2xl">';
|
||||
contentHtml += '<thead>' +
|
||||
'<tr class="border-b border-gray-300">' +
|
||||
'<th class="p-2 font-semibold text-gray-700">용역사명</th>' +
|
||||
'<th class="p-2 font-semibold text-gray-700 text-center">누적 수정 요청 횟수</th>' +
|
||||
'</tr>' +
|
||||
'</thead><tbody>';
|
||||
datas.forEach(item => {
|
||||
contentHtml += '<tr class="border-b border-gray-200">' +
|
||||
'<td class="p-2 font-medium text-gray-800">' + (item.COMPANY_NAME || '미지정') + '</td>' +
|
||||
'<td class="p-2 text-center text-red-600 font-bold">' + item.REVISION_COUNT + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
contentHtml += '</tbody></table>';
|
||||
container.innerHTML = contentHtml;
|
||||
}
|
||||
// ▲▲▲ 데이터 품질 (수정 요청) 통계 로드 함수 ▲▲▲
|
||||
|
||||
// 페이지 로드 시 차트 데이터 로드 함수 호출
|
||||
loadProjectStatusChart();
|
||||
|
||||
// 발주기관별 성과 함수 호출
|
||||
loadCompanyPerformance();
|
||||
|
||||
// 데이터 품질 통계 호출
|
||||
loadDataQualityStats();
|
||||
|
||||
// ▼▼▼ PDF 내보내기 기능 구현 ▼▼▼
|
||||
document.getElementById('export-pdf-btn').addEventListener('click', exportToPDF);
|
||||
|
||||
/**
|
||||
* PDF 로딩 오버레이 표시
|
||||
*/
|
||||
function showPdfLoading(message) {
|
||||
const overlay = document.getElementById('pdf-loading-overlay');
|
||||
if(overlay) {
|
||||
overlay.querySelector('div').textContent = message || 'PDF 생성 중...';
|
||||
overlay.classList.add('show');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PDF 로딩 오버레이 숨김
|
||||
*/
|
||||
function hidePdfLoading() {
|
||||
const overlay = document.getElementById('pdf-loading-overlay');
|
||||
if(overlay) {
|
||||
overlay.classList.remove('show');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML 요소를 캔버스로 변환하여 PDF에 이미지로 추가
|
||||
* (doc.text()를 사용하지 않아 한글 깨짐 방지)
|
||||
*/
|
||||
async function addHtmlElementAsImage(doc, elementId, yPos) {
|
||||
const A4_WIDTH = 210;
|
||||
const MARGIN = 15;
|
||||
const CONTENT_WIDTH = A4_WIDTH - (MARGIN * 2);
|
||||
|
||||
const element = document.getElementById(elementId);
|
||||
if (!element) {
|
||||
console.error(elementId + " 요소를 찾을 수 없습니다.");
|
||||
return yPos;
|
||||
}
|
||||
|
||||
// 1. html2canvas로 캡처 (이제 제목(h3)까지 포함하여 캡처)
|
||||
const canvas = await html2canvas(element, {
|
||||
scale: 2, // 해상도
|
||||
useCORS: true,
|
||||
backgroundColor: '#ffffff' // 배경색 강제 지정
|
||||
});
|
||||
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
const imgWidth = canvas.width;
|
||||
const imgHeight = canvas.height;
|
||||
|
||||
// 2. A4 폭에 맞게 이미지 비율 계산
|
||||
const ratio = imgHeight / imgWidth;
|
||||
const pdfImgHeight = CONTENT_WIDTH * ratio;
|
||||
|
||||
// 3. 페이지 넘김 처리 (A4높이 297mm - 상하마진 30mm = 267mm)
|
||||
if (yPos + pdfImgHeight > 282) { // 297-15(상단) = 282. 하단 마진 고려
|
||||
doc.addPage();
|
||||
yPos = 20; // 새 페이지 상단 마진
|
||||
}
|
||||
|
||||
// 4. PDF에 이미지 추가
|
||||
doc.addImage(imgData, 'PNG', MARGIN, yPos, CONTENT_WIDTH, pdfImgHeight);
|
||||
|
||||
return yPos + pdfImgHeight + 10; // 다음 요소와의 간격
|
||||
}
|
||||
|
||||
/**
|
||||
* 메인 PDF 내보내기 함수
|
||||
*/
|
||||
async function exportToPDF() {
|
||||
try {
|
||||
showPdfLoading('보고서 생성 중... (1/3)');
|
||||
const { jsPDF } = window.jspdf;
|
||||
const doc = new jsPDF('p', 'mm', 'a4');
|
||||
|
||||
const A4_WIDTH = 210;
|
||||
let yPos = 20; // PDF 문서 내의 현재 Y축 위치
|
||||
|
||||
// --- 1. 프로젝트 입력상태 별 그래프 ---
|
||||
// (1단계에서 <div ...>에 id="project-status-section"을 추가해야 합니다)
|
||||
yPos = await addHtmlElementAsImage(doc, 'project-status-section', yPos);
|
||||
|
||||
// --- 2. 용역사별 성과 현황 ---
|
||||
showPdfLoading('성과 현황 캡처 중... (2/3)');
|
||||
yPos = await addHtmlElementAsImage(doc, 'company-performance-section', yPos);
|
||||
|
||||
// --- 3. 데이터 품질 현황 ---
|
||||
showPdfLoading('데이터 품질 현황 캡처 중... (3/3)');
|
||||
yPos = await addHtmlElementAsImage(doc, 'data-quality-section', yPos);
|
||||
|
||||
// --- 4. 저장 ---
|
||||
showPdfLoading('파일 저장 중...');
|
||||
doc.save('국토지반정보_통계보고서.pdf');
|
||||
|
||||
} catch (error) {
|
||||
console.error("PDF 생성 중 오류 발생:", error);
|
||||
alert("PDF 생성 중 오류가 발생했습니다. 콘솔을 확인해주세요.");
|
||||
} finally {
|
||||
hidePdfLoading();
|
||||
}
|
||||
}
|
||||
// ▲▲▲ PDF 내보내기 기능 구현 ▲▲▲
|
||||
|
||||
// 2. Risk Status Donut Chart
|
||||
var ctxRisk = document.getElementById('riskChart').getContext('2d');
|
||||
new Chart(ctxRisk, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['낮은 위험', '중간 위험', '높은 위험'],
|
||||
datasets: [{
|
||||
data: [41, 22, 37],
|
||||
backgroundColor: [ 'rgba(34, 197, 94, 0.8)', 'rgba(245, 158, 11, 0.8)', 'rgba(239, 68, 68, 0.8)' ],
|
||||
borderColor: [ 'rgba(34, 197, 94, 1)', 'rgba(245, 158, 11, 1)', 'rgba(239, 68, 68, 1)' ],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
plugins: { legend: { position: 'bottom', labels: { boxWidth: 12 } } }
|
||||
}
|
||||
});
|
||||
// 3. Financial Status Bar Chart
|
||||
var ctxFinancial = document.getElementById('financialChart').getContext('2d');
|
||||
new Chart(ctxFinancial, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['프로젝트 A', '프로젝트 B', '프로젝트 C', '프로젝트 D'],
|
||||
datasets: [{
|
||||
label: '예산',
|
||||
data: [65, 59, 80, 81],
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.7)',
|
||||
borderColor: 'rgba(59, 130, 246, 1)',
|
||||
borderWidth: 1
|
||||
}, {
|
||||
label: '실제 비용',
|
||||
data: [45, 49, 60, 70],
|
||||
backgroundColor: 'rgba(239, 68, 68, 0.7)',
|
||||
borderColor: 'rgba(239, 68, 68, 1)',
|
||||
borderWidth: 1
|
||||
},{
|
||||
label: '예상 비용',
|
||||
data: [75, 69, 90, 91],
|
||||
backgroundColor: 'rgba(16, 185, 129, 0.7)',
|
||||
borderColor: 'rgba(16, 185, 129, 1)',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: { y: { beginAtZero: true, ticks: { callback: function(value) { return value + '억'; } } } },
|
||||
plugins: { legend: { display: false } }
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
|
@ -307,56 +545,47 @@ function getStatusInfo(statusCode) {
|
|||
};
|
||||
|
||||
switch (String(statusCode)) {
|
||||
case '0':
|
||||
status.name = '미입력';
|
||||
// 아이콘: 마이너스 (MinusCircle) - 비어있거나 시작 안 함
|
||||
status.icon = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>';
|
||||
status.bgColor = 'bg-gray-500'; // 회색 (중립)
|
||||
status.textColor = 'text-gray-600';
|
||||
break; //
|
||||
case '1':
|
||||
status.name = '입력 중';
|
||||
// 아이콘: 연필 (Pencil) - 수정/작성 중 (기존과 동일)
|
||||
status.icon = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.5L14.732 3.732z"></path></svg>';
|
||||
status.bgColor = 'bg-blue-500'; // 파란색 (진행중)
|
||||
status.textColor = 'text-blue-600';
|
||||
break;
|
||||
case '2':
|
||||
status.name = '검수 준비 대기중';
|
||||
// 아이콘: 시계 (Clock) - 대기 중
|
||||
status.icon = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>';
|
||||
status.bgColor = 'bg-yellow-500'; // 노란색 (대기)
|
||||
status.textColor = 'text-yellow-600';
|
||||
break; //
|
||||
case '3':
|
||||
status.name = '검수중';
|
||||
// 아이콘: 돋보기 (Search) - 검토 중
|
||||
status.icon = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>';
|
||||
status.bgColor = 'bg-yellow-500'; // 노란색 (대기/검토)
|
||||
status.textColor = 'text-yellow-600';
|
||||
break;
|
||||
case '4':
|
||||
status.name = '수정 요청';
|
||||
// 아이콘: 느낌표 (ExclamationCircle) - 주의/수정 필요
|
||||
status.icon = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>';
|
||||
status.bgColor = 'bg-red-500'; // 빨간색 (거절/오류)
|
||||
status.textColor = 'text-red-600';
|
||||
break;
|
||||
case '5':
|
||||
status.name = '검수 완료';
|
||||
// 아이콘: 체크 (CheckCircle) - 성공 (기존과 동일)
|
||||
status.icon = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>';
|
||||
status.bgColor = 'bg-green-500'; // 초록색 (완료/성공)
|
||||
status.textColor = 'text-green-600';
|
||||
break;
|
||||
case '6':
|
||||
status.name = '등록 완료';
|
||||
// 아이콘: 체크 (CheckCircle) - 성공 (기존과 동일)
|
||||
status.icon = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>';
|
||||
status.bgColor = 'bg-green-500'; // 초록색 (완료/성공)
|
||||
status.textColor = 'text-green-600';
|
||||
break;
|
||||
}
|
||||
case '0': // Gray
|
||||
status.name = '미입력';
|
||||
status.icon = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>';
|
||||
status.bgColor = 'bg-gray-500';
|
||||
status.textColor = 'text-gray-600';
|
||||
break;
|
||||
case '1': // Blue
|
||||
status.name = '입력 중';
|
||||
status.icon = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.5L14.732 3.732z"></path></svg>';
|
||||
status.bgColor = 'bg-blue-500';
|
||||
status.textColor = 'text-blue-600';
|
||||
break;
|
||||
case '2': // Orange
|
||||
status.name = '검수 준비 대기중';
|
||||
status.icon = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>';
|
||||
status.bgColor = 'bg-orange-500'; // (Orange)
|
||||
status.textColor = 'text-orange-600';
|
||||
break;
|
||||
case '3': // Yellow
|
||||
status.name = '검수중';
|
||||
status.icon = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>';
|
||||
status.bgColor = 'bg-yellow-500'; // (Yellow)
|
||||
status.textColor = 'text-yellow-600';
|
||||
break;
|
||||
case '4': // Red
|
||||
status.name = '수정 요청';
|
||||
status.icon = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>';
|
||||
break;
|
||||
case '5': // Green
|
||||
status.name = '검수 완료';
|
||||
status.icon = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>';
|
||||
status.bgColor = 'bg-green-500';
|
||||
status.textColor = 'text-green-600';
|
||||
break;
|
||||
case '6': // Teal
|
||||
status.name = '등록 완료';
|
||||
status.icon = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>';
|
||||
status.bgColor = 'bg-teal-500'; // (Teal)
|
||||
status.textColor = 'text-teal-600';
|
||||
break;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue