기관회원 건설현장 입력시 알림 내역 표기되도록 구현

main
thkim 2025-10-02 14:34:47 +09:00
parent 396073a070
commit 3eff840e71
16 changed files with 512 additions and 329 deletions

1
Copying Normal file
View File

@ -0,0 +1 @@
- to "C:\Users\dbnt\eclipse-workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps\geoinfo_eGov_work\WEB-INF\views\drilling\statistics\drilling_statistics.jsp"

1
SUCCESS Normal file
View File

@ -0,0 +1 @@
-

View File

@ -455,13 +455,6 @@
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.oracle.database.jdbc/ojdbc8 -->
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>23.2.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-dbcp/commons-dbcp -->
<dependency>

View File

@ -23,6 +23,14 @@ public interface DrillingInputMapper {
public int updateProjectCodeAndProjectStateCodeByCid(HashMap<String, Object> params) throws SQLException;
public int updateProjectCodeAndProjectStateCodeByProjectCode(HashMap<String, Object> params) throws SQLException;
/**
* .
* @param params
* @return
* @throws SQLException
*/
public int insertConstructSiteHist(HashMap<String, Object> params) throws SQLException;
}

View File

@ -137,14 +137,6 @@ public class DrillingInputServiceImpl implements DrillingInputService {
params.put("userId", userId);
try {
/*
List<EgovMap> sPGetTblCsiByCidParams = drillingInputMapper.sPGetTblCsiByCid( params );
if( sPGetTblCsiByCidParams.size() == 0 ) {
return params;
}
EgovMap tbl = sPGetTblCsiByCidParams.get(0);
*/
EgovMap tbl = drillingInputMapper.getItemByCid( params );
if( tbl != null ) {
@ -161,7 +153,22 @@ public class DrillingInputServiceImpl implements DrillingInputService {
throw new Exception( "해당 프로젝트는 이미 다른 프로젝트와 연결되어 있습니다." );
}
}
if (nResult > 0) { // 업데이트가 성공했을 경우에만 이력 기록
HashMap<String, Object> histParams = new HashMap<String, Object>();
// 이전 상태값 (EgovMap은 보통 camelCase로 키를 반환합니다)
Object preStateCode = tbl.get("projectStateCode");
histParams.put("CID", params.get("CID"));
histParams.put("PROJECT_CODE", params.get("PROJECT_CODE"));
histParams.put("PRE_PROJECT_STATE_CODE", preStateCode != null ? preStateCode.toString() : null); // 이전 상태
histParams.put("PROJECT_STATE_CODE", params.get("PROJECT_STATE_CODE")); // 현재 변경된 상태
histParams.put("MOD_REASON", "지반정보 등록 프로젝트 연결"); // 변경 사유 (필요에 따라 파라미터로 받아서 설정 가능)
histParams.put("userId", userId);
drillingInputMapper.insertConstructSiteHist(histParams);
}
}
return params;
} catch (SQLException e) {

View File

@ -22,6 +22,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import geoinfo.drilling.inquiry.service.DrillingInquiryService;
import geoinfo.drilling.statistics.service.DrillingStatisticsService;
import geoinfo.util.MyUtil;
@Controller
@ -31,6 +32,9 @@ public class DrillingStatisticsController {
@Autowired
DrillingInquiryService drillingInquiryService;
@Autowired
DrillingStatisticsService drillingStatisticsService;
@RequestMapping(value = "/drilling/statistics.do")
public String drillingStatistics(@RequestParam HashMap<String, Object> params, ModelMap model, HttpServletRequest request, HttpServletResponse response) throws Exception {
@ -50,4 +54,37 @@ public class DrillingStatisticsController {
return "/drilling/statistics/drilling_notice";
}
@RequestMapping(value = "/drilling/statistics/hist-list.do", method = RequestMethod.GET, produces = { "application/json; charset=utf-8" })
@ResponseBody
public ResponseEntity<JSONObject> drillingStatisticsHistList (
HttpServletRequest request,
@RequestParam HashMap<String, Object> params,
HttpServletResponse response
) {
JSONObject jSONOResponse = null;
try {
jSONOResponse = drillingStatisticsService.getConstructSiteHistList(request, params);
jSONOResponse.put("resultCode", 200);
jSONOResponse.put("resultMessage", "OK");
} catch (Exception e) {
jSONOResponse = new JSONObject();
jSONOResponse.put("resultCode", -1);
jSONOResponse.put("resultMessage", e.getMessage());
LOGGER.error("drillingStatisticsHistList Error: ", e);
}
response.setStatus(HttpServletResponse.SC_OK);
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Type", "application/json; charset=utf-8");
try {
response.getWriter().print(jSONOResponse);
} catch (IOException e) {
LOGGER.error("Response Write Error: ", e);
}
return null;
}
}

View File

@ -8,8 +8,7 @@ import egovframework.rte.psl.dataaccess.mapper.Mapper;
import egovframework.rte.psl.dataaccess.util.EgovMap;
@Mapper("drillingStatisticsMapper")
public interface DrillingStatisticsMapper {
}
public interface DrillingStatisticsMapper {
public List<EgovMap> selectConstructSiteHistList(HashMap<String, Object> params) throws SQLException;
public Long selectConstructSiteHistListCnt(HashMap<String, Object> params) throws SQLException;
}

View File

@ -12,5 +12,5 @@ import egovframework.rte.psl.dataaccess.util.EgovMap;
public interface DrillingStatisticsService {
public JSONObject getConstructSiteHistList(HttpServletRequest request, HashMap<String, Object> params) throws Exception;
}

View File

@ -4,6 +4,7 @@ import geoinfo.drilling.input.service.DrillingInputMapper;
import geoinfo.drilling.input.service.DrillingInputService;
import geoinfo.drilling.inquiry.service.DrillingInquiryMapper;
import geoinfo.drilling.inquiry.service.DrillingInquiryService;
import geoinfo.drilling.statistics.service.DrillingStatisticsMapper;
import geoinfo.drilling.statistics.service.DrillingStatisticsService;
import geoinfo.main.login.service.LoginMapper;
import geoinfo.main.login.service.LoginService;
@ -32,12 +33,16 @@ import egovframework.rte.psl.dataaccess.util.EgovMap;
public class DrillingStatisticsServiceImpl implements DrillingStatisticsService {
@Resource(name="drillingInquiryMapper")
private DrillingInquiryMapper drillingInquiryMapper;
@Resource(name="drillingInputMapper")
private DrillingInputMapper drillingInputMapper;
@Resource(name="drillingStatisticsMapper")
private DrillingStatisticsMapper drillingStatisticsMapper;
@Autowired
DrillingInputService drillingInputService;
@ -50,4 +55,45 @@ public class DrillingStatisticsServiceImpl implements DrillingStatisticsService
private LoginMapper loginMapper;
@Override
public JSONObject getConstructSiteHistList(HttpServletRequest request, HashMap<String, Object> params)
throws Exception {
// TODO Auto-generated method stub
JSONObject jsonResponse = new JSONObject();
String userId = MyUtil.getStringFromObject(request.getSession().getAttribute("USERID"));
if (userId == null) {
throw new Exception("로그인이 필요한 서비스입니다.");
}
// 1. 현재 로그인한 사용자의 지역(영역) 코드 조회
HashMap<String, Object> userAreaCodes = drillingInputService.getOrganizationUserGlGmGsGfCodes(userId);
params.put("masterCompanyOCode", MyUtil.getStringFromObject(userAreaCodes.get("v_gl")));
params.put("masterCompanyTwCode", MyUtil.getStringFromObject(userAreaCodes.get("v_gm")));
params.put("masterCompanyThCode", MyUtil.getStringFromObject(userAreaCodes.get("v_gs")));
// 2. 페이징 처리를 위한 파라미터 설정
int page = params.get("page") == null ? 1 : Integer.parseInt(params.get("page").toString());
int rows = params.get("rows") == null ? 10 : Integer.parseInt(params.get("rows").toString());
params.put("firstIndex", (page - 1) * rows + 1);
params.put("lastIndex", page * rows);
try {
// 3. 총 카운트 및 목록 조회
Long count = drillingStatisticsMapper.selectConstructSiteHistListCnt(params);
List<EgovMap> datas = drillingStatisticsMapper.selectConstructSiteHistList(params);
jsonResponse.put("count", count);
jsonResponse.put("datas", datas);
return jsonResponse;
} catch (SQLException e) {
System.out.println("Error at getConstructSiteHistList: " + e.getMessage());
throw new Exception("이력 조회 중 오류가 발생하였습니다.");
}
}
}

View File

@ -5,6 +5,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URLEncoder;
@ -2213,7 +2214,8 @@ public class MainController
response.setContentType("application/octet-stream");
response.setContentLength(fileByte.length);
response.setHeader("Content-Disposition", "attachment; fileName=\"" + URLEncoder.encode(filenameDn, "utf-8") + "\";");
String headerFilename = getContentDispositionHeader( request, filenameDn);
response.setHeader("Content-Disposition", headerFilename);
response.setHeader("Content-Transfer-Encoding", "binary");
response.getOutputStream().write(fileByte);
@ -2223,6 +2225,7 @@ public class MainController
else
{
mv.addObject("msg", "<script>alert('파일을 다운받을 수 없습니다');</script>");
response.setContentType("text/html; charset=utf-8");
response.setCharacterEncoding("utf-8");
PrintWriter writer = response.getWriter();
writer.println("<script type='text/javascript'>");
@ -2237,6 +2240,34 @@ public class MainController
return null;
}
private String getContentDispositionHeader (HttpServletRequest request, String filenameDn) throws UnsupportedEncodingException {
// User-Agent를 통해 브라우저 정보 획득
String userAgent = request.getHeader("User-Agent");
// Content-Disposition 헤더에 사용할 파일명 변수
String headerFilename = "";
// IE 계열 브라우저 체크 (MSIE 또는 Trident 포함)
if (userAgent.contains("MSIE") || userAgent.contains("Trident") || userAgent.contains("Edge")) {
// IE, Edge 등: UTF-8로 인코딩 후, ISO-8859-1로 변환 (역호환성을 위함)
// URLEncoder.encode 결과는 공백이 '+'로 바뀌므로, 이를 '%20'으로 치환
headerFilename = "attachment; filename=\"" +
new String(URLEncoder.encode(filenameDn, "UTF-8").replaceAll("\\+", " ").getBytes("UTF-8"), "ISO-8859-1") +
"\"";
// **또는 더 간단한 방법 (많이 사용됨):**
// headerFilename = "attachment; filename=\"" + URLEncoder.encode(filenameDn, "UTF-8").replaceAll("\\+", "%20") + "\"";
} else {
// Chrome, Firefox 등: UTF-8 인코딩 (URL 인코딩 결과를 그대로 사용)
headerFilename = "attachment; filename=\"" + URLEncoder.encode(filenameDn, "UTF-8").replaceAll("\\+", "%20") + "\"";
// RFC 5987 표준을 따르는 인코딩 방식 (권장):
// headerFilename = "attachment; filename*=UTF-8''" + URLEncoder.encode(filenameDn, "UTF-8").replaceAll("\\+", "%20");
}
return headerFilename;
}
@RequestMapping(value = "/upload-file-and-up-load-su.do")
public ModelAndView cmuboard_save(MultipartRequest multi, HttpServletRequest request, HttpServletResponse response, Map<String, Object> map) throws Exception {

View File

@ -132,4 +132,26 @@
]]>
</update>
<insert id="insertConstructSiteHist" parameterType="map">
INSERT INTO TEMP_CONSTRUCT_SITE_HIST (
HIST_ID,
CID,
PROJECT_CODE,
PRE_PROJECT_STATE_CODE,
PROJECT_STATE_CODE,
MOD_REASON,
MOD_USERID,
MOD_DT
) VALUES (
CONSTRUCT_SITE_HIST_SEQ.NEXTVAL,
#{CID},
#{PROJECT_CODE},
#{PRE_PROJECT_STATE_CODE},
#{PROJECT_STATE_CODE},
#{MOD_REASON},
#{userId},
SYSTIMESTAMP
)
</insert>
</mapper>

View File

@ -2,5 +2,59 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="geoinfo.drilling.statistics.service.DrillingStatisticsMapper">
<select id="selectConstructSiteHistList" parameterType="map" resultType="egovMap">
SELECT
*
FROM (
SELECT
ROW_NUMBER() OVER(ORDER BY h.MOD_DT DESC) AS RNUM,
h.HIST_ID,
h.CID,
csi.CONST_NAME,
h.PROJECT_CODE,
h.PRE_PROJECT_STATE_CODE,
h.PROJECT_STATE_CODE,
h.MOD_REASON,
h.MOD_USERID,
TO_CHAR(h.MOD_DT, 'YYYY-MM-DD HH24:MI:SS') AS MOD_DT
FROM
TEMP_CONSTRUCT_SITE_HIST h
JOIN
TEMP_CONSTRUCT_SITE_INFO csi ON h.CID = csi.CID
WHERE
1=1
<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>
)
WHERE RNUM BETWEEN #{firstIndex} AND #{lastIndex}
</select>
<select id="selectConstructSiteHistListCnt" parameterType="map" resultType="long">
SELECT
COUNT(*)
FROM
TEMP_CONSTRUCT_SITE_HIST h
JOIN
TEMP_CONSTRUCT_SITE_INFO csi ON h.CID = csi.CID
WHERE
1=1
<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>
</select>
</mapper>

View File

@ -4,10 +4,7 @@
<%
if (request.getSession().getAttribute("USERID") == null) {
%>
<script>alert('로그인후 이용하실 수 있습니다.');window.location.href='/index.do';</script>
<%
@ -15,9 +12,7 @@ if (request.getSession().getAttribute("USERID") == null) {
}
%>
<%
if (request.getSession().getAttribute("CLS") == null || "2".equals(request.getSession().getAttribute("CLS") ) == false ) {
%>
<script>alert('발주 기관 회원만 이용가능합니다.');window.location.href='/index.do';</script>
<%
@ -25,24 +20,15 @@ if (request.getSession().getAttribute("CLS") == null || "2".equals(request.getSe
}
%>
<%@ include file="/include/inc_head_2021_new.jsp" %>
<!-- header start-->
<c:import url="/drilling/common/includeTopMenu.do" charEncoding="UTF-8" />
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Chart.js CDN for creating charts -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- Google Fonts: Inter -->
<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">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.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">
<!-- header end-->
<style>
@keyframes shake {
0% { transform: translateX(0); }
@ -57,98 +43,74 @@ if (request.getSession().getAttribute("CLS") == null || "2".equals(request.getSe
90% { transform: translateX(-5px); }
100% { transform: translateX(0); }
}
.shake-animation {
animation: shake 0.6s;
}
/* The snackbar - position it at the bottom and in the middle of the screen */
#snackbar {
visibility: hidden; /* Hidden by default. Visible on click */
min-width: 250px; /* Set a default minimum width */
margin-left: -125px; /* Divide value of min-width by 2 */
background-color: #000000; /* Black background color */
color: #ff0000; /* White text color */
text-align: center; /* Centered text */
border-radius: 2px; /* Rounded borders */
padding: 16px; /* Padding */
position: fixed; /* Sit on top of the screen */
z-index: 1; /* Add a z-index if needed */
left: 50%; /* Center the snackbar */
bottom: 80px; /* 30px from the bottom */
visibility: hidden;
min-width: 250px;
margin-left: -125px;
background-color: #000000;
color: #ff0000;
text-align: center;
border-radius: 2px;
padding: 16px;
position: fixed;
z-index: 1;
left: 50%;
bottom: 80px;
font-weight: 500;
}
/* Show the snackbar when clicking on a button (class added with JavaScript) */
#snackbar.show {
visibility: visible; /* Show the snackbar */
/* Add animation: Take 0.5 seconds to fade in and out the snackbar.
However, delay the fade out process for 2.5 seconds */
visibility: visible;
-webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s;
animation: fadein 0.5s, fadeout 0.5s 2.5s;
}
/* Animations to fade the snackbar in and out */
@-webkit-keyframes fadein {
from {bottom: 0; opacity: 0;}
to {bottom: 80px; opacity: 1;}
}
@keyframes fadein {
from {bottom: 0; opacity: 0;}
to {bottom: 80px; opacity: 1;}
}
@-webkit-keyframes fadeout {
from {bottom: 80px; opacity: 1;}
to {bottom: 0; opacity: 0;}
}
@keyframes fadeout {
from {bottom: 80px; opacity: 1;}
to {bottom: 0; opacity: 0;}
}
#suggestionList {
border: 1px solid #ccc;
width: 300px; /* 입력창 너비에 맞춰 조절 */
position_: absolute;
background-color: white;
display: none;
left: 91px;
top: 54px;
z-index: 3;
}
#suggestionList div {
padding: 5px;
cursor: pointer;
}
#suggestionList div:hover {
background-color: #f0f0f0;
}
#suggestionList div .organizational-structure {
color: red;
}
#const-state-code {
width: 160px;
}
#suggestionList {
border: 1px solid #ccc;
width: 300px;
position_: absolute;
background-color: white;
display: none;
left: 91px;
top: 54px;
z-index: 3;
}
#suggestionList div {
padding: 5px;
cursor: pointer;
}
#suggestionList div:hover {
background-color: #f0f0f0;
}
#suggestionList div .organizational-structure {
color: red;
}
#const-state-code {
width: 160px;
}
</style>
<!-- javascript start-->
<script type="text/javascript">
</script>
<!-- javascript end-->
<!-- 페이지 컨테이너 시작 -->
<section class="drilling-page-container">
<div class="page-content-wrapper drilling inquiry">
<!-- 서브메뉴 시작 -->
<div class="page-sidebar-wrapper">
<div class="page-sidebar">
<div class="treeview-project-name">
@ -158,12 +120,8 @@ if (request.getSession().getAttribute("CLS") == null || "2".equals(request.getSe
</div>
</div>
</div>
<!-- 서브메뉴 끝 -->
<!-- 콘텐츠 시작 -->
<div class="page-content">
<div class="page-content-inner">
<!-- 카테고리 시작 -->
<div class="category-wrapper">
<ul class="page-category">
<li class="category-item"></li>
@ -171,231 +129,261 @@ if (request.getSession().getAttribute("CLS") == null || "2".equals(request.getSe
</ul>
<a href="#" class="btn btn-help">도움말</a>
</div>
<!-- 카테고리 끝 -->
<h1 class="page-title-1depth">통계</h1>
<!-- 내용 시작 -->
<div class="content-wrapper">
<!-- Main Content -->
<main class="flex flex-col lg:flex-row gap-4">
<!-- Right Sidebar -->
<aside class="w-full flex flex-col gap-4">
<!-- Wrapper div for side-by-side layout on large screens -->
<div class="flex flex-col lg:flex-row gap-4">
<!-- Project Status Chart -->
<div 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>
</div>
</div>
<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">
<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>
</div>
</div>
<!-- Notifications -->
<div class="w-full lg:w-1/2 bg-white p-4 rounded-lg shadow-md flex flex-col">
<div class="flex justify-between items-center mb-2">
<h3 class="font-semibold text-gray-800 text-4xl">알림 내역</h3>
<a href="../drilling/notice.do" class="text-3xl text-blue-600 hover:underline">모두 보기</a>
</div>
<div class="space-y-3 flex-grow">
<div class="flex items-start p-2 bg-blue-50 rounded-lg">
<div class="bg-blue-500 text-white rounded-full h-8 w-8 flex-shrink-0 flex items-center justify-center mr-3">
<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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</div>
<div>
<p class="text-3xl font-medium text-gray-800">수정 요청</p>
<p class="text-2xl text-gray-600">'제3연륙교 건설 공사' 프로젝트의 시추정보 수정이 필요합니다.</p>
</div>
</div>
<div class="flex items-start p-2 rounded-lg">
<div class="bg-green-500 text-white rounded-full h-8 w-8 flex-shrink-0 flex items-center justify-center mr-3">
<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>
</div>
<div>
<p class="text-3xl font-medium text-gray-800">검수 완료</p>
<p class="text-2xl text-gray-600">'충북선 달천 충주간' 프로젝트가 검수 완료되었습니다.</p>
</div>
</div>
</div>
</div>
</div>
<div class="w-full lg:w-1/2 bg-white p-4 rounded-lg shadow-md flex flex-col">
<div class="flex justify-between items-center mb-2">
<h3 class="font-semibold text-gray-800 text-4xl">알림 내역</h3>
<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>
<!-- Report Download Section -->
<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>
<!-- MODIFIED: Changed grid to 3 columns to allow for different chart widths -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<!-- MODIFIED: Set column span to 1 -->
<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>
<!-- MODIFIED: Set column span to 2 to make it twice as wide -->
<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>
<!-- MODIFIED: Changed width to w-full to fill the new column span -->
<div class="relative h-96 w-full">
<canvas id="financialChart"></canvas>
</div>
</div>
</div>
<!-- MODIFIED: Wrapped button in a flex container to center it and reduced its width -->
<div class="flex justify-center">
<!-- MODIFIED: Reduced width from md:w-1/4 to md:w-[15%] (40% reduction) -->
<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">
<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>
</aside>
</main>
<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">
<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>
</aside>
</main>
<script>
// Chart.js initialization
document.addEventListener('DOMContentLoaded', () => {
// 1. Project Status Pie Chart
const 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)', // Red
'rgba(245, 158, 11, 0.8)', // Amber
'rgba(16, 185, 129, 0.8)', // Emerald
'rgba(59, 130, 246, 0.8)' // Blue
],
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) {
let label = context.label || '';
if (label) {
label += ': ';
<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;
}
}
}
}
if (context.parsed !== null) {
label += context.parsed + '%';
}
return label;
}
}
}
}
}
});
});
// 2. Risk Status Donut Chart
const 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)', // Green
'rgba(245, 158, 11, 0.8)', // Amber
'rgba(239, 68, 68, 0.8)' // Red
],
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
const 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 + '억';
// 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 } } }
}
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
});
</script>
});
// 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>
</div>
<!-- 내용 끝 -->
</div>
</div>
<!-- 콘텐츠 끝 -->
</div>
</div>
</section>
<!-- 페이지 컨테이너 끝 -->
<div id="calenderDiv" class="trViewOff" style="position:absolute;"></div>
<%@ include file="/include/inc_footer_2021_new.jsp" %>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function() {
// 페이지 로드 시 이력 목록을 불러옵니다.
loadHistoryList();
});
/**
* 서버에 건설현장 이력 목록을 요청하는 함수
*/
function loadHistoryList() {
// 알림 내역은 최신 3개만 가져오도록 설정
var url = '/drilling/statistics/hist-list.do?page=1&rows=3';
requesetGet(url, displayHistoryList, null);
}
/**
* 상태 코드에 따라 아이콘과 색상, 텍스트를 반환하는 함수
*/
function getStatusInfo(statusCode) {
var status = {
name: '알 수 없음',
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="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>',
bgColor: 'bg-gray-500',
textColor: 'text-gray-600'
};
switch (String(statusCode)) {
case '0':
status.name = '미입력';
break;
case '1':
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':
case '3':
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="M8 16l4-4m0 0l4-4m-4 4v6m0-6H8a2 2 0 00-2 2v6a2 2 0 002 2h8a2 2 0 002-2v-6a2 2 0 00-2-2h-2"></path></svg>';
status.bgColor = 'bg-yellow-500';
status.textColor = 'text-yellow-600';
break;
case '5':
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="M13 16h-1v-4h-1m1-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 '6':
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;
}
return status;
}
/**
* API 호출 결과를 받아 화면에 이력 목록을 표시하는 콜백 함수
*/
function displayHistoryList(response) {
var notificationList = document.getElementById('notification-list');
notificationList.innerHTML = ''; // 기존 목록 초기화
if (response && response.resultCode === 200 && response.datas) {
var datas = response.datas;
var contentHtml = '';
for (var i = 0; i < datas.length; i++) {
var item = datas[i];
var preStateInfo = getStatusInfo(item.preProjectStateCode);
var currentStateInfo = getStatusInfo(item.projectStateCode);
var dateParts = item.modDt ? item.modDt.split(' ') : ['', ''];
var ymd = dateParts[0];
var hms = dateParts[1];
contentHtml +=
'<div class="flex items-start p-2 bg-gray-50 rounded-lg">' +
'<div class="' + currentStateInfo.bgColor + ' text-white rounded-full h-8 w-8 flex-shrink-0 flex items-center justify-center mr-3">' +
currentStateInfo.icon +
'</div>' +
'<div>' +
'<p class="text-3xl font-medium text-gray-800">\'' + item.constName + '\'</p>' +
'<p class="text-2xl text-gray-600">' +
'상태가 <span class="font-semibold ' + preStateInfo.textColor + '">' + preStateInfo.name + '</span>에서 ' +
'<span class="font-semibold ' + currentStateInfo.textColor + '">' + currentStateInfo.name + '</span>으로 변경되었습니다.' +
'</p>' +
'</div>' +
'</div>';
}
if (datas.length === 0) {
contentHtml = '<div class="p-4 text-center text-gray-500">최근 알림 내역이 없습니다.</div>';
}
notificationList.innerHTML = contentHtml;
} else {
notificationList.innerHTML = '<div class="p-4 text-center text-gray-500">알림 목록을 불러오는 데 실패했습니다.</div>';
console.error("Error fetching history list:", response.resultMessage);
}
}
</script>
<%@ include file="/include/inc_footer_2021_new.jsp" %>

View File

@ -1,11 +1,3 @@
let xhr;
if(window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else {
// IE5, IE6 일때
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}
function onClickBtnViewOnMap() {
const projectMasterCompanyName = '${mbr.projectMasterCompanyName}';
let projectCode = '${mbr.ProjectCode}';

View File

@ -43,6 +43,7 @@
============================================================== -->
<!-- <link rel="stylesheet" href="/web/css/common.css"/> -->
<script type="text/javaScript" src="/web/js/common.js"></script>
<script language=JavaScript src="${pageContext.request.contextPath}/js/common/myXhr.js"></script>
<!-- ==============================================================
공통 커스텀

View File

@ -12,7 +12,10 @@ function requesetGet(URL, callback, callbackParamAsJson) {
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
const obj = JSON.parse(xhr.responseText);
var obj = JSON.parse(xhr.responseText);
if (typeof callback === 'function') {
callback(obj, callbackParamAsJson);
}
} else if (xhr.readyState === 4) {
// 요청 실패 시 처리
console.error('요청 실패:', xhr.status);