사용자 최근 작성 글 조회 기능 추가:
eapim-portal CI / build (push) Waiting to run
eapim-portal Test / test (push) Waiting to run

- 최근 3건 조회 서비스/페이사드/리포지토리 구현
- HTML 아코디언 UI 및 스타일 추가
This commit is contained in:
Rinjae
2026-07-13 17:27:06 +09:00
parent 778d579ada
commit 4395757d98
7 changed files with 88 additions and 0 deletions
@@ -40,6 +40,7 @@ public class PartnershipApplicationController {
model.addAttribute("partnership", new PartnershipApplicationDTO()); model.addAttribute("partnership", new PartnershipApplicationDTO());
model.addAttribute("isInternalUser", UserTypeUtil.isCurrentUserInternal()); model.addAttribute("isInternalUser", UserTypeUtil.isCurrentUserInternal());
model.addAttribute("recentApplications", partnershipApplicationFacade.getMyRecentApplications());
return "apps/community/mainPartnershipForm"; return "apps/community/mainPartnershipForm";
} }
@@ -2,8 +2,10 @@ package com.eactive.apim.portal.apps.community.partnership.mapper;
import com.eactive.apim.portal.apps.community.partnership.dto.PartnershipApplicationDTO; import com.eactive.apim.portal.apps.community.partnership.dto.PartnershipApplicationDTO;
import com.eactive.apim.portal.apps.community.partnership.dto.PartnershipApplicationSummaryDTO;
import com.eactive.apim.portal.common.mapper.CommonMapper; import com.eactive.apim.portal.common.mapper.CommonMapper;
import com.eactive.apim.portal.partnershipapplication.entity.PartnershipApplication; import com.eactive.apim.portal.partnershipapplication.entity.PartnershipApplication;
import java.util.List;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.ReportingPolicy; import org.mapstruct.ReportingPolicy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -16,4 +18,8 @@ import org.springframework.stereotype.Component;
public interface PartnershipApplicationMapper { public interface PartnershipApplicationMapper {
PartnershipApplication toEntity(PartnershipApplicationDTO dto); PartnershipApplication toEntity(PartnershipApplicationDTO dto);
PartnershipApplicationSummaryDTO toSummaryDto(PartnershipApplication entity);
List<PartnershipApplicationSummaryDTO> toSummaryDtoList(List<PartnershipApplication> entities);
} }
@@ -2,6 +2,7 @@ package com.eactive.apim.portal.apps.community.partnership.repository;
import com.eactive.apim.portal.partnershipapplication.entity.PartnershipApplication; import com.eactive.apim.portal.partnershipapplication.entity.PartnershipApplication;
import com.eactive.eai.rms.data.EMSDataSource; import com.eactive.eai.rms.data.EMSDataSource;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@@ -9,4 +10,10 @@ import org.springframework.stereotype.Repository;
@Repository @Repository
@EMSDataSource @EMSDataSource
public interface PartnershipApplicationRepository extends JpaRepository<PartnershipApplication, String>, JpaSpecificationExecutor<PartnershipApplication> { public interface PartnershipApplicationRepository extends JpaRepository<PartnershipApplication, String>, JpaSpecificationExecutor<PartnershipApplication> {
/**
* 특정 작성자(createdBy)가 등록한 최근 3건을 최신순으로 조회한다.
* createdBy 는 PersonalDataEncryptConverter 로 결정적 암호화되므로 평문 사용자 id 로 등가 조회가 가능하다.
*/
List<PartnershipApplication> findTop3ByCreatedByOrderByCreatedDateDesc(String createdBy);
} }
@@ -1,9 +1,16 @@
package com.eactive.apim.portal.apps.community.partnership.service; package com.eactive.apim.portal.apps.community.partnership.service;
import com.eactive.apim.portal.apps.community.partnership.dto.PartnershipApplicationDTO; import com.eactive.apim.portal.apps.community.partnership.dto.PartnershipApplicationDTO;
import com.eactive.apim.portal.apps.community.partnership.dto.PartnershipApplicationSummaryDTO;
import java.io.IOException; import java.io.IOException;
import java.util.List;
public interface PartnershipApplicationFacade { public interface PartnershipApplicationFacade {
void createPartnershipApplication(PartnershipApplicationDTO partnershipApplicationDTO) throws IOException; void createPartnershipApplication(PartnershipApplicationDTO partnershipApplicationDTO) throws IOException;
/**
* 현재 로그인 사용자가 작성한 최근 3건을 조회한다. 미인증이면 빈 목록.
*/
List<PartnershipApplicationSummaryDTO> getMyRecentApplications();
} }
@@ -1,6 +1,7 @@
package com.eactive.apim.portal.apps.community.partnership.service; package com.eactive.apim.portal.apps.community.partnership.service;
import com.eactive.apim.portal.apps.community.partnership.dto.PartnershipApplicationDTO; import com.eactive.apim.portal.apps.community.partnership.dto.PartnershipApplicationDTO;
import com.eactive.apim.portal.apps.community.partnership.dto.PartnershipApplicationSummaryDTO;
import com.eactive.apim.portal.apps.community.partnership.mapper.PartnershipApplicationMapper; import com.eactive.apim.portal.apps.community.partnership.mapper.PartnershipApplicationMapper;
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser; import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
import com.eactive.apim.portal.common.util.SecurityUtil; import com.eactive.apim.portal.common.util.SecurityUtil;
@@ -15,7 +16,9 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
@Service @Service
@@ -60,4 +63,14 @@ public class PartnershipApplicationFacadeImpl implements PartnershipApplicationF
} }
portalAdminNotifier.notifyPortalAdmins(MessageCode.PARTNERSHIP_CREATED, params); portalAdminNotifier.notifyPortalAdmins(MessageCode.PARTNERSHIP_CREATED, params);
} }
@Override
public List<PartnershipApplicationSummaryDTO> getMyRecentApplications() {
PortalAuthenticatedUser user = SecurityUtil.getPortalAuthenticatedUser();
if (user == null) {
return Collections.emptyList();
}
List<PartnershipApplication> recent = partnershipApplicationService.findRecentByCreatedBy(user.getId());
return partnershipApplicationMapper.toSummaryDtoList(recent);
}
} }
@@ -2,6 +2,7 @@ package com.eactive.apim.portal.apps.community.partnership.service;
import com.eactive.apim.portal.apps.community.partnership.repository.PartnershipApplicationRepository; import com.eactive.apim.portal.apps.community.partnership.repository.PartnershipApplicationRepository;
import com.eactive.apim.portal.partnershipapplication.entity.PartnershipApplication; import com.eactive.apim.portal.partnershipapplication.entity.PartnershipApplication;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -21,4 +22,12 @@ public class PartnershipApplicationService {
public void createPartnershipApplication(PartnershipApplication partnershipApplication) { public void createPartnershipApplication(PartnershipApplication partnershipApplication) {
partnershipApplicationRepository.save(partnershipApplication); partnershipApplicationRepository.save(partnershipApplication);
} }
/**
* 작성자 id 로 최근 등록 3건 조회(최신순).
*/
@Transactional(readOnly = true)
public List<PartnershipApplication> findRecentByCreatedBy(String createdBy) {
return partnershipApplicationRepository.findTop3ByCreatedByOrderByCreatedDateDesc(createdBy);
}
} }
@@ -11,6 +11,23 @@
</section> </section>
<th:block layout:fragment="contentFragment"> <th:block layout:fragment="contentFragment">
<style>
.recent-apps { margin: 0 0 24px; }
.recent-apps-title { font-size: 15px; font-weight: 600; color: #334155; margin: 0 0 10px; }
.recent-apps-list { list-style: none; margin: 0; padding: 0; border: 1px solid #E2E8F0; border-radius: 8px; overflow: hidden; }
.recent-apps-item + .recent-apps-item { border-top: 1px solid #E2E8F0; }
.recent-apps-head { display: flex; align-items: center; gap: 12px; width: 100%; padding: 14px 16px;
background: #F8FAFC; border: 0; cursor: pointer; text-align: left; font: inherit; }
.recent-apps-head:hover { background: #F1F5F9; }
.recent-apps-subject { flex: 1; min-width: 0; font-size: 14px; font-weight: 500; color: #1E293B;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.recent-apps-date { flex-shrink: 0; font-size: 12px; color: #94A3B8; }
.recent-apps-arrow { flex-shrink: 0; color: #94A3B8; transition: transform .2s ease; }
.recent-apps-item.is-open .recent-apps-arrow { transform: rotate(180deg); }
.recent-apps-body { display: none; padding: 14px 16px; background: #fff; }
.recent-apps-item.is-open .recent-apps-body { display: block; }
.recent-apps-detail { margin: 0; font-size: 13px; line-height: 1.6; color: #475569; white-space: pre-wrap; word-break: break-word; }
</style>
<section class="inquiry-form-container"> <section class="inquiry-form-container">
<!-- Title Bar --> <!-- Title Bar -->
@@ -19,6 +36,27 @@
<span class="register-subtitle">DJBank은 온라인 비즈니스 혁신을 위한 피드백/개선요청을 환영합니다.</span> <span class="register-subtitle">DJBank은 온라인 비즈니스 혁신을 위한 피드백/개선요청을 환영합니다.</span>
</div> </div>
<!-- 내가 작성한 최근 글(최대 3건) 아코디언 -->
<div class="recent-apps" th:if="${recentApplications != null and !recentApplications.isEmpty()}">
<p class="recent-apps-title">내가 작성한 최근 글</p>
<ul class="recent-apps-list">
<li class="recent-apps-item" th:each="item : ${recentApplications}">
<button type="button" class="recent-apps-head">
<span class="recent-apps-subject" th:text="${item.bizSubject}">제목</span>
<span class="recent-apps-date"
th:text="${item.createdDate != null ? #temporals.format(item.createdDate, 'yyyy.MM.dd') : ''}">2026.07.13</span>
<svg class="recent-apps-arrow" width="18" height="18" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" aria-hidden="true">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<div class="recent-apps-body">
<p class="recent-apps-detail" th:text="${item.bizDetail}">내용</p>
</div>
</li>
</ul>
</div>
<!-- Form Content --> <!-- Form Content -->
<div class="register-form-container"> <div class="register-form-container">
<form name="partnershipForm" id="partnershipForm" th:action="@{/partnership}" th:object="${partnership}" <form name="partnershipForm" id="partnershipForm" th:action="@{/partnership}" th:object="${partnership}"
@@ -131,6 +169,13 @@
</script> </script>
<script th:inline="javascript"> <script th:inline="javascript">
$(function () { $(function () {
// 최근 글 아코디언 토글
document.querySelectorAll('.recent-apps-head').forEach(function (head) {
head.addEventListener('click', function () {
head.closest('.recent-apps-item').classList.toggle('is-open');
});
});
const btnSubmit = document.querySelector('.btn-submit-form'); const btnSubmit = document.querySelector('.btn-submit-form');
const fileInput = document.getElementById('file'); const fileInput = document.getElementById('file');
const fileDisplayText = document.getElementById('fileDisplayText'); const fileDisplayText = document.getElementById('fileDisplayText');