feat: 발주기관 통계 기능 안정화 건

main
thkim 2025-11-14 18:24:38 +09:00
parent 4e19566bf0
commit 812a10b004
7 changed files with 764 additions and 180 deletions

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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 &lt;= 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>

View File

@ -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;

View File

@ -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;
}