diff --git a/WebContent/jsp/onl/apim/portalinquiry/portalInquiryMan.jsp b/WebContent/jsp/onl/apim/portalinquiry/portalInquiryMan.jsp index 37de99d..d2c25ca 100644 --- a/WebContent/jsp/onl/apim/portalinquiry/portalInquiryMan.jsp +++ b/WebContent/jsp/onl/apim/portalinquiry/portalInquiryMan.jsp @@ -20,15 +20,22 @@ var select_orgId = ""; function formatInquiryStatus(cellvalue, options, rowObject) { - return cellvalue === 'PENDING' - ? '문의중' - : '답변완료'; + if (cellvalue == 'PENDING') { + return '문의중'; + } else if (cellvalue == 'RESPONDED') { + return '답변완료' + } else if (cellvalue == 'CLOSED') { + return '답변종료' + } else { + return cellvalue; + } } function formatFile(cellvalue, options, rowObject) { var iconPath = '${pageContext.request.contextPath}/images/icon_file.png'; var icon = rowObject.hasAttachment ? 'File Attached' : ''; - return cellvalue + icon; + var commentCount = rowObject.commentCount > 0 ? ' [' + rowObject.commentCount + ']' : ''; + return cellvalue + icon + commentCount; } function deleteSelectedRows() { diff --git a/WebContent/jsp/onl/apim/portalinquiry/portalInquiryManDetail.jsp b/WebContent/jsp/onl/apim/portalinquiry/portalInquiryManDetail.jsp index a302d4e..a6cd313 100644 --- a/WebContent/jsp/onl/apim/portalinquiry/portalInquiryManDetail.jsp +++ b/WebContent/jsp/onl/apim/portalinquiry/portalInquiryManDetail.jsp @@ -84,6 +84,7 @@ var url = ''; var url_view = ''; var file_download = ''; + var inquiryStatus = 'PENDING'; function decodeHTMLEntities(text) { var textArea = document.createElement('textarea'); @@ -98,10 +99,21 @@ dataType: "json", data: {cmd: 'DETAIL', id: key}, success: function (data) { + + $("#id").val(key); + $("#inquirySubject").text(data.inquirySubject); $("#inquiryDetail").html(data.inquiryDetail); - $("#inquiryStatus").text(data.inquiryStatus === 'PENDING' ? '문의중' : '답변완료'); + + inquiryStatus = data.inquiryStatus; + if (data.inquiryStatus == 'PENDING') { + $("#inquiryStatus").text('문의중'); + } else if (data.inquiryStatus == 'RESPONDED') { + $("#inquiryStatus").text('답변완료'); + } else if (data.inquiryStatus == 'CLOSED') { + $("#inquiryStatus").text('답변종료'); + } $("#inquirer").text(data.inquirer.userName); $("#createdDate").text(timeStampFormat(data.createdDate)); $("#responder").text(data.responderName || ''); @@ -158,6 +170,10 @@ } function deleteComment(commentId, inquiryId) { + if (inquiryStatus == 'CLOSED') { + alert('답변이 종료되어 삭제할 수 없습니다'); + return; + } if (!confirm("댓글을 삭제하시겠습니까?")) return; $.ajax({ type: "POST", @@ -188,6 +204,10 @@ } $("#btn_modify").click(function () { + if (inquiryStatus == 'CLOSED') { + alert('답변이 종료되어 수정할 수 없습니다'); + return; + } if (!checkRequired("ajaxForm")) return; if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return; @@ -209,7 +229,10 @@ }); $("#btn_comment").click(function () { - + if (inquiryStatus == 'CLOSED') { + alert('답변이 종료되어 댓글을 등록할 수 없습니다'); + return; + } if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return; $.ajax({ @@ -297,6 +320,10 @@
+
+
상태
+
+
답변자
diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/InquiryCommentRepository.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/InquiryCommentRepository.java deleted file mode 100644 index caf0618..0000000 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/InquiryCommentRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.eactive.eai.rms.data.entity.onl.apim.portalinquiry; - -import com.eactive.apim.portal.qna.entity.InquiryComment; -import com.eactive.eai.data.jpa.BaseRepository; -import com.eactive.eai.rms.data.EMSDataSource; - -import java.util.List; - -@EMSDataSource -public interface InquiryCommentRepository extends BaseRepository { - - long countByInquiry_Id(String inquiryId); - - List findByInquiry_IdAndDelYnOrderBySeqAsc(String inquiryId, String delYn); -} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/PortalInquiryCommentRepository.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/PortalInquiryCommentRepository.java new file mode 100644 index 0000000..6fc58f4 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/PortalInquiryCommentRepository.java @@ -0,0 +1,28 @@ +package com.eactive.eai.rms.data.entity.onl.apim.portalinquiry; + +import com.eactive.apim.portal.qna.entity.InquiryComment; +import com.eactive.eai.data.jpa.BaseRepository; +import com.eactive.eai.rms.data.EMSDataSource; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@EMSDataSource +public interface PortalInquiryCommentRepository extends BaseRepository { + + long countByInquiry_Id(String inquiryId); + + long countByInquiry_IdAndDelYn(String inquiryId, String delYn); + + List findByInquiry_IdAndDelYnOrderByCreatedDateAsc(String inquiryId, String delYn); + + Optional findTopByInquiry_IdAndDelYnOrderByCreatedDateDesc( + String inquiryId, String delYn); + + Optional findTopByInquiry_IdAndAdminYnAndDelYnOrderByCreatedDateDesc( + String inquiryId, String adminYn, String delYn); + + boolean existsByInquiry_IdAndAdminYnAndDelYnAndCreatedDateAfter( + String inquiryId, String adminYn, String delYn, LocalDateTime after); +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/PortalInquiryCommentService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/PortalInquiryCommentService.java index b13b422..0ccbb30 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/PortalInquiryCommentService.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/PortalInquiryCommentService.java @@ -10,18 +10,22 @@ import java.util.List; @Service @Transactional(transactionManager = "transactionManagerForEMS") -public class PortalInquiryCommentService extends AbstractEMSDataSerivce { +public class PortalInquiryCommentService extends AbstractEMSDataSerivce { // AbstractEMSDataSerivce의 repository 필드가 BaseRepository로 고정되어 // 커스텀 메서드를 호출할 수 없으므로 별도 필드로 직접 주입 @Autowired - private InquiryCommentRepository inquiryCommentRepository; + private PortalInquiryCommentRepository inquiryCommentRepository; public long countByInquiryId(String inquiryId) { return inquiryCommentRepository.countByInquiry_Id(inquiryId); } + public long countActiveByInquiryId(String inquiryId) { + return inquiryCommentRepository.countByInquiry_IdAndDelYn(inquiryId, "N"); + } + public List findByInquiryId(String inquiryId) { - return inquiryCommentRepository.findByInquiry_IdAndDelYnOrderBySeqAsc(inquiryId, "N"); + return inquiryCommentRepository.findByInquiry_IdAndDelYnOrderByCreatedDateAsc(inquiryId, "N"); } } diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/PortalInquiryRepository.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/PortalInquiryRepository.java index 3dc9f19..9dd631a 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/PortalInquiryRepository.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/PortalInquiryRepository.java @@ -4,7 +4,10 @@ import com.eactive.apim.portal.qna.entity.Inquiry; import com.eactive.eai.data.jpa.BaseRepository; import com.eactive.eai.rms.data.EMSDataSource; +import java.util.List; + @EMSDataSource public interface PortalInquiryRepository extends BaseRepository { + List findByInquiryStatus(String inquiryStatus); } diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/PortalInquiryService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/PortalInquiryService.java index bb20d6c..a9456d9 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/PortalInquiryService.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/PortalInquiryService.java @@ -1,20 +1,28 @@ package com.eactive.eai.rms.data.entity.onl.apim.portalinquiry; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + import com.eactive.apim.portal.qna.entity.Inquiry; import com.eactive.apim.portal.qna.entity.QInquiry; import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce; import com.querydsl.core.BooleanBuilder; -import org.apache.commons.lang3.StringUtils; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import java.util.List; @Service @Transactional(transactionManager = "transactionManagerForEMS") public class PortalInquiryService extends AbstractEMSDataSerivce { + // AbstractEMSDataSerivce의 repository 필드가 BaseRepository로 고정되어 + // 커스텀 메서드를 호출할 수 없으므로 별도 필드로 직접 주입 + @Autowired + private PortalInquiryRepository portalInquiryRepository; + public Page findAll(Pageable pageable, PortalInquirySearch portalInquirySearch) { QInquiry qInquiry = QInquiry.inquiry; @@ -58,4 +66,8 @@ public class PortalInquiryService extends AbstractEMSDataSerivce findByInquiryStatus(String inquiryStatus) { + return portalInquiryRepository.findByInquiryStatus(inquiryStatus); + } } \ No newline at end of file diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/job/PortalInquiryClosingJob.java b/src/main/java/com/eactive/eai/rms/ext/djb/job/PortalInquiryClosingJob.java new file mode 100644 index 0000000..2c976cd --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/ext/djb/job/PortalInquiryClosingJob.java @@ -0,0 +1,73 @@ +package com.eactive.eai.rms.ext.djb.job; + +import com.eactive.eai.rms.common.datasource.DataSourceContextHolder; +import com.eactive.eai.rms.common.datasource.DataSourceTypeManager; +import com.eactive.eai.rms.common.util.CommonUtil; +import com.eactive.eai.rms.onl.apim.portalInquiry.PortalInquiryClosingService; +import com.eactive.eai.rms.onl.common.util.DateUtil; +import org.quartz.DisallowConcurrentExecution; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * 매일 00:05 실행 - RESPONDED 상태 문의글 자동 종료 배치 + * + *

종료 조건 (기본 7일):

+ *
    + *
  • 답변완료 후 댓글이 7일간 없는 경우
  • + *
  • 관리자 댓글 등록 후 7일간 포탈사용자 댓글이 없는 경우
  • + *
+ * + *

Cron: 0 5 0 * * ?

+ * + *

Job 파라미터 (JobDataMap):

+ *
    + *
  • {@value #KEY_INQUIRY_COMMENT_CLOSING_DAY}: 종료 기준 일수 (기본값: 7)
  • + *
+ */ +@DisallowConcurrentExecution +public class PortalInquiryClosingJob implements Job { + + private static final Logger log = LoggerFactory.getLogger(PortalInquiryClosingJob.class); + + /** Job 파라미터 키: 댓글종료 기간(일) */ + public static final String KEY_INQUIRY_COMMENT_CLOSING_DAY = "inquiry.comment.closing_day"; + + private static final int DEFAULT_CLOSING_DAYS = 7; + + @Autowired + private PortalInquiryClosingService inquiryCommentClosingService; + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + log.info("*** START PortalInquiryCommentClosingService run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm")); + + int closingDays = DEFAULT_CLOSING_DAYS; + String closingDayParam = context.getMergedJobDataMap().getString(KEY_INQUIRY_COMMENT_CLOSING_DAY); + if (closingDayParam != null) { + try { + closingDays = Integer.parseInt(closingDayParam); + } catch (NumberFormatException e) { + log.warn("잘못된 {} 파라미터 값: '{}', 기본값 {}일 사용", KEY_INQUIRY_COMMENT_CLOSING_DAY, closingDayParam, DEFAULT_CLOSING_DAYS); + } + } + + DataSourceContextHolder.setDataSourceType( + DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW)); + try { + int closedCount = inquiryCommentClosingService.closeExpiredInquiries(closingDays); + log.info("PortalInquiryCommentClosingService 완료: {}건 CLOSED 처리", closedCount); + } catch (Exception e) { + log.error("PortalInquiryCommentClosingService execution failed", e); + throw new JobExecutionException(e); + } finally { + DataSourceContextHolder.clearDataSourceType(); + } + + log.info("*** END PortalInquiryCommentClosingService run({})", DateUtil.getDateTime("yyyy-MM-dd HH:mm")); + } +} diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryClosingService.java b/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryClosingService.java new file mode 100644 index 0000000..48b6dae --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryClosingService.java @@ -0,0 +1,78 @@ +package com.eactive.eai.rms.onl.apim.portalInquiry; + +import com.eactive.apim.portal.qna.entity.Inquiry; +import com.eactive.apim.portal.qna.entity.InquiryComment; +import com.eactive.eai.rms.data.entity.onl.apim.portalinquiry.PortalInquiryCommentRepository; +import com.eactive.eai.rms.data.entity.onl.apim.portalinquiry.PortalInquiryCommentService; +import com.eactive.eai.rms.data.entity.onl.apim.portalinquiry.PortalInquiryService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +/** + * RESPONDED 상태 문의글 중 아래 조건에 해당하는 경우 CLOSED로 변경한다. + * - 조건 A: 활성 댓글이 없고 답변완료일(responseDate)로부터 N일 경과 + * - 조건 B: 마지막 관리자 댓글(adminYn=Y) 이후 N일간 포탈사용자(adminYn=N) 댓글 없음 + */ +@Service +@Transactional(transactionManager = "transactionManagerForEMS") +@Slf4j +@RequiredArgsConstructor +public class PortalInquiryClosingService { + + private static final String RESPONDED = "RESPONDED"; + private static final String CLOSED = "CLOSED"; + + private final PortalInquiryService portalInquiryService; + private final PortalInquiryCommentService portalInquiryCommentService; + private final PortalInquiryCommentRepository inquiryCommentRepository; + + public int closeExpiredInquiries(int closingDays) { + LocalDateTime threshold = LocalDateTime.now().minusDays(closingDays); + List respondedInquiries = portalInquiryService.findByInquiryStatus(RESPONDED); + + int closedCount = 0; + for (Inquiry inquiry : respondedInquiries) { + if (shouldClose(inquiry, threshold)) { + inquiry.setInquiryStatus(CLOSED); + portalInquiryService.save(inquiry); + closedCount++; + log.info("문의글 CLOSED 처리: id={}", inquiry.getId()); + } + } + log.info("InquiryCommentClosingService: 총 {}건 CLOSED 처리 (기준={}일)", closedCount, closingDays); + return closedCount; + } + + private boolean shouldClose(Inquiry inquiry, LocalDateTime threshold) { + long activeCommentCount = portalInquiryCommentService.countActiveByInquiryId(inquiry.getId()); + + // 댓글 없음 → 답변완료일 기준 N일 경과 여부만 확인 + if (activeCommentCount == 0) { + LocalDateTime responseDate = inquiry.getResponseDate(); + return responseDate != null && !responseDate.isAfter(threshold); + } + + // 가장 최근 활성 댓글 확인 + Optional lastComment = inquiryCommentRepository + .findTopByInquiry_IdAndDelYnOrderByCreatedDateDesc(inquiry.getId(), "N"); + + if (!lastComment.isPresent()) { + return false; + } + + // 포탈사용자(adminYn != 'Y')가 마지막 댓글 작성자 → CLOSED 불가 + if (!"Y".equals(lastComment.get().getAdminYn())) { + return false; + } + + // 관리자가 마지막 댓글 작성자 → N일 경과 여부 확인 + LocalDateTime lastAdminDate = lastComment.get().getCreatedDate(); + return lastAdminDate != null && !lastAdminDate.isAfter(threshold); + } +} diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryCommentUI.java b/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryCommentUI.java index b5eddea..d609ca4 100644 --- a/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryCommentUI.java +++ b/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryCommentUI.java @@ -20,9 +20,7 @@ public class PortalInquiryCommentUI { private String inquiryId; private String commentDetail; - - private int seq; - + private String delYn; private String createdBy; diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryManService.java b/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryManService.java index e9895ff..fff483c 100644 --- a/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryManService.java +++ b/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryManService.java @@ -9,6 +9,7 @@ import com.eactive.apim.portal.qna.entity.Inquiry; import com.eactive.apim.portal.qna.entity.InquiryComment; import com.eactive.apim.portal.user.entity.UserInfo; import com.eactive.eai.rms.common.base.BaseService; +import com.eactive.eai.rms.common.exception.IntegrationException; import com.eactive.eai.rms.data.entity.man.user.UserInfoService; import com.eactive.eai.rms.data.entity.onl.apim.portalinquiry.PortalInquiryCommentService; import com.eactive.eai.rms.data.entity.onl.apim.portalinquiry.PortalInquirySearch; @@ -41,6 +42,7 @@ public class PortalInquiryManService extends BaseService { public static final String PENDING = "PENDING"; public static final String RESPONDED = "RESPONDED"; + public static final String CLOSED = "CLOSED"; public PortalInquiryManService(PortalInquiryService portalInquiryService, @@ -68,6 +70,7 @@ public class PortalInquiryManService extends BaseService { if (ui.getInquirer() != null) { maskPortalInquiryUI(ui); } + ui.setCommentCount((int) portalInquiryCommentService.countActiveByInquiryId(inquiry.getId())); return setInquiryAttachmentStatus(ui); }); } @@ -154,6 +157,7 @@ public class PortalInquiryManService extends BaseService { UserInfo userinfo = userInfoService.getById(userId); Inquiry inquiry = portalInquiryService.getById(portalInquiryUI.getId()); + validateNotClosed(inquiry); // 답변 대기중 상태일 경우만 답변완료로 상태 변경 if(inquiry.getInquiryStatus().equals(PENDING)) { inquiry.setInquiryStatus(RESPONDED); @@ -199,21 +203,28 @@ public class PortalInquiryManService extends BaseService { public void insertComment(PortalInquiryCommentUI commentUI, String userId) { Inquiry inquiry = portalInquiryService.getById(commentUI.getInquiryId()); + validateNotClosed(inquiry); InquiryComment comment = new InquiryComment(); comment.setInquiry(inquiry); comment.setCommentDetail(commentUI.getCommentDetail()); comment.setDelYn("N"); - int nextSeq = (int) portalInquiryCommentService.countByInquiryId(commentUI.getInquiryId()) + 1; - comment.setSeq(nextSeq); + comment.setAdminYn("Y"); portalInquiryCommentService.save(comment); } public void deleteComment(String commentId) { InquiryComment comment = portalInquiryCommentService.getById(commentId); + validateNotClosed(comment.getInquiry()); comment.setDelYn("Y"); portalInquiryCommentService.save(comment); } + private void validateNotClosed(Inquiry inquiry) { + if (CLOSED.equals(inquiry.getInquiryStatus())) { + throw new IntegrationException("답변종료되어 수정할 수 없습니다."); + } + } + } diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryUI.java b/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryUI.java index fd48292..df3669c 100644 --- a/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryUI.java +++ b/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryUI.java @@ -55,4 +55,6 @@ public class PortalInquiryUI { private LocalDateTime lastModifiedDate; private String orgName; + + private int commentCount; }