state-check 스케줄 추가, pnu-update 수정

This commit is contained in:
2026-02-03 11:16:41 +09:00
parent 58d8df7a38
commit eca21f8f41
305 changed files with 8934 additions and 1011 deletions

View File

@@ -1,4 +1,4 @@
# IMAGERY MAKE DATASET # GUKYUIN PNU UPDATE
> 국유인에 연동된 객체 조회하여 pnu update schedule > 국유인에 연동된 객체 조회하여 pnu update schedule

View File

@@ -650,7 +650,7 @@ code + .copy-button {
<script type="text/javascript"> <script type="text/javascript">
function configurationCacheProblems() { return ( function configurationCacheProblems() { return (
// begin-report-data // begin-report-data
{"diagnostics":[{"locations":[{}],"problem":[{"text":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated."}],"severity":"WARNING","problemDetails":[{"text":"This is scheduled to be removed in Gradle 10.0."}],"contextualLabel":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated.","documentationLink":"https://docs.gradle.org/8.14/userguide/upgrading_version_8.html#groovy_space_assignment_syntax","problemId":[{"name":"deprecation","displayName":"Deprecation"},{"name":"properties-should-be-assigned-using-the-propname-value-syntax-setting-a-property-via-the-gradle-generated-propname-value-or-propname-value-syntax-in-groovy-dsl","displayName":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated."}],"solutions":[[{"text":"Use assignment ('url = <value>') instead."}]]},{"locations":[{"path":"C:\\workspace\\kamco-cd-cron\\label\\review-to-down\\src\\main\\java\\com\\kamco\\cd\\kamcoback\\dto\\LabelAllocateDto.java"},{"taskPath":":compileJava"}],"problem":[{"text":"C:\\workspace\\kamco-cd-cron\\label\\review-to-down\\src\\main\\java\\com\\kamco\\cd\\kamcoback\\dto\\LabelAllocateDto.java uses or overrides a deprecated API."}],"severity":"ADVICE","problemDetails":[{"text":"Note: C:\\workspace\\kamco-cd-cron\\label\\review-to-down\\src\\main\\java\\com\\kamco\\cd\\kamcoback\\dto\\LabelAllocateDto.java uses or overrides a deprecated API."}],"contextualLabel":"C:\\workspace\\kamco-cd-cron\\label\\review-to-down\\src\\main\\java\\com\\kamco\\cd\\kamcoback\\dto\\LabelAllocateDto.java uses or overrides a deprecated API.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.deprecated.filename","displayName":"C:\\workspace\\kamco-cd-cron\\label\\review-to-down\\src\\main\\java\\com\\kamco\\cd\\kamcoback\\dto\\LabelAllocateDto.java uses or overrides a deprecated API."}]},{"locations":[{"path":"C:\\workspace\\kamco-cd-cron\\label\\review-to-down\\src\\main\\java\\com\\kamco\\cd\\kamcoback\\dto\\LabelAllocateDto.java"},{"taskPath":":compileJava"}],"problem":[{"text":"Recompile with -Xlint:deprecation for details."}],"severity":"ADVICE","problemDetails":[{"text":"Note: Recompile with -Xlint:deprecation for details."}],"contextualLabel":"Recompile with -Xlint:deprecation for details.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.deprecated.recompile","displayName":"Recompile with -Xlint:deprecation for details."}]},{"locations":[{"path":"C:\\workspace\\kamco-cd-cron\\label\\review-to-down\\src\\main\\java\\com\\kamco\\cd\\kamcoback\\common\\utils\\enums\\Enums.java"},{"taskPath":":compileJava"}],"problem":[{"text":"Some input files use unchecked or unsafe operations."}],"severity":"ADVICE","problemDetails":[{"text":"Note: Some input files use unchecked or unsafe operations."}],"contextualLabel":"Some input files use unchecked or unsafe operations.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.unchecked.plural","displayName":"Some input files use unchecked or unsafe operations."}]},{"locations":[{"path":"C:\\workspace\\kamco-cd-cron\\label\\review-to-down\\src\\main\\java\\com\\kamco\\cd\\kamcoback\\common\\utils\\enums\\Enums.java"},{"taskPath":":compileJava"}],"problem":[{"text":"Recompile with -Xlint:unchecked for details."}],"severity":"ADVICE","problemDetails":[{"text":"Note: Recompile with -Xlint:unchecked for details."}],"contextualLabel":"Recompile with -Xlint:unchecked for details.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.unchecked.recompile","displayName":"Recompile with -Xlint:unchecked for details."}]}],"problemsReport":{"totalProblemCount":5,"buildName":"kamco-review-to-down-job","requestedTasks":"clean build","documentationLink":"https://docs.gradle.org/8.14/userguide/reporting_problems.html","documentationLinkCaption":"Problem report","summaries":[]}} {"diagnostics":[{"locations":[{}],"problem":[{"text":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated."}],"severity":"WARNING","problemDetails":[{"text":"This is scheduled to be removed in Gradle 10.0."}],"contextualLabel":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated.","documentationLink":"https://docs.gradle.org/8.14/userguide/upgrading_version_8.html#groovy_space_assignment_syntax","problemId":[{"name":"deprecation","displayName":"Deprecation"},{"name":"properties-should-be-assigned-using-the-propname-value-syntax-setting-a-property-via-the-gradle-generated-propname-value-or-propname-value-syntax-in-groovy-dsl","displayName":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated."}],"solutions":[[{"text":"Use assignment ('url = <value>') instead."}]]},{"locations":[{"path":"C:\\workspace\\kamco-cd-cron\\gukyuin\\pnu-update\\src\\main\\java\\com\\kamco\\cd\\kamcoback\\config\\resttemplate\\ExternalHttpClient.java"},{"taskPath":":compileJava"}],"problem":[{"text":"Some input files use or override a deprecated API."}],"severity":"ADVICE","problemDetails":[{"text":"Note: Some input files use or override a deprecated API."}],"contextualLabel":"Some input files use or override a deprecated API.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.deprecated.plural","displayName":"Some input files use or override a deprecated API."}]},{"locations":[{"path":"C:\\workspace\\kamco-cd-cron\\gukyuin\\pnu-update\\src\\main\\java\\com\\kamco\\cd\\kamcoback\\config\\resttemplate\\ExternalHttpClient.java"},{"taskPath":":compileJava"}],"problem":[{"text":"Recompile with -Xlint:deprecation for details."}],"severity":"ADVICE","problemDetails":[{"text":"Note: Recompile with -Xlint:deprecation for details."}],"contextualLabel":"Recompile with -Xlint:deprecation for details.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.deprecated.recompile","displayName":"Recompile with -Xlint:deprecation for details."}]},{"locations":[{"path":"C:\\workspace\\kamco-cd-cron\\gukyuin\\pnu-update\\src\\main\\java\\com\\kamco\\cd\\kamcoback\\common\\utils\\enums\\Enums.java"},{"taskPath":":compileJava"}],"problem":[{"text":"Some input files use unchecked or unsafe operations."}],"severity":"ADVICE","problemDetails":[{"text":"Note: Some input files use unchecked or unsafe operations."}],"contextualLabel":"Some input files use unchecked or unsafe operations.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.unchecked.plural","displayName":"Some input files use unchecked or unsafe operations."}]},{"locations":[{"path":"C:\\workspace\\kamco-cd-cron\\gukyuin\\pnu-update\\src\\main\\java\\com\\kamco\\cd\\kamcoback\\common\\utils\\enums\\Enums.java"},{"taskPath":":compileJava"}],"problem":[{"text":"Recompile with -Xlint:unchecked for details."}],"severity":"ADVICE","problemDetails":[{"text":"Note: Recompile with -Xlint:unchecked for details."}],"contextualLabel":"Recompile with -Xlint:unchecked for details.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.unchecked.recompile","displayName":"Recompile with -Xlint:unchecked for details."}]}],"problemsReport":{"totalProblemCount":5,"buildName":"kamco-pnu-update-job","requestedTasks":"clean build","documentationLink":"https://docs.gradle.org/8.14/userguide/reporting_problems.html","documentationLinkCaption":"Problem report","summaries":[]}}
// end-report-data // end-report-data
);} );}
</script> </script>

View File

@@ -65,3 +65,7 @@ file:
sync-auto-exception-start-year: 2025 sync-auto-exception-start-year: 2025
sync-auto-exception-before-year-cnt: 3 sync-auto-exception-before-year-cnt: 3
gukyuin:
#url: http://localhost:8080
url: http://192.168.2.129:5301
cdi: ${gukyuin.url}/api/kcd/cdi

View File

@@ -65,3 +65,7 @@ file:
sync-auto-exception-start-year: 2025 sync-auto-exception-start-year: 2025
sync-auto-exception-before-year-cnt: 3 sync-auto-exception-before-year-cnt: 3
gukyuin:
#url: http://localhost:8080
url: http://192.168.2.129:5301
cdi: ${gukyuin.url}/api/kcd/cdi

View File

@@ -65,3 +65,7 @@ file:
sync-auto-exception-start-year: 2025 sync-auto-exception-start-year: 2025
sync-auto-exception-before-year-cnt: 3 sync-auto-exception-before-year-cnt: 3
gukyuin:
#url: http://localhost:8080
url: http://192.168.2.129:5301
cdi: ${gukyuin.url}/api/kcd/cdi

View File

@@ -7,6 +7,6 @@ Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Build-Jdk-Spec: 21 Build-Jdk-Spec: 21
Implementation-Title: kamco-review-to-down-job Implementation-Title: kamco-pnu-update-job
Implementation-Version: 0.0.1-SNAPSHOT Implementation-Version: 0.0.1-SNAPSHOT

View File

@@ -1,54 +0,0 @@
package com.kamco.cd.kamcoback.common.utils;
import com.kamco.cd.kamcoback.auth.CustomUserDetails;
import com.kamco.cd.kamcoback.members.dto.MembersDto;
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class UserUtil {
public MembersDto.Member getCurrentUser() {
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.filter(auth -> auth.getPrincipal() instanceof CustomUserDetails)
.map(
auth -> {
CustomUserDetails user = (CustomUserDetails) auth.getPrincipal();
MemberEntity m = user.getMember();
return new MembersDto.Member(
m.getId(), m.getName(), m.getEmployeeNo(), m.getUserRole());
})
.orElse(null);
}
public Long getId() {
MembersDto.Member user = getCurrentUser();
return user != null ? user.getId() : null;
}
public String getName() {
MembersDto.Member user = getCurrentUser();
return user != null ? user.getName() : null;
}
public String getEmployeeNo() {
MembersDto.Member user = getCurrentUser();
return user != null ? user.getEmployeeNo() : null;
}
public String getRole() {
MembersDto.Member user = getCurrentUser();
return user != null ? user.getRole() : null;
}
public String getIp() {
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.map(auth -> auth.getDetails())
.map(Object::toString)
.orElse(null);
}
}

View File

@@ -1,153 +0,0 @@
package com.kamco.cd.kamcoback.config.api;
import com.kamco.cd.kamcoback.log.dto.EventStatus;
import com.kamco.cd.kamcoback.log.dto.EventType;
import com.kamco.cd.kamcoback.menu.dto.MenuDto;
import jakarta.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.util.ContentCachingRequestWrapper;
@Slf4j
public class ApiLogFunction {
// 클라이언트 IP 추출
public static String getClientIp(HttpServletRequest request) {
String[] headers = {
"X-Forwarded-For",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_CLIENT_IP",
"HTTP_X_FORWARDED_FOR"
};
for (String header : headers) {
String ip = request.getHeader(header);
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
return ip.split(",")[0];
}
}
String ip = request.getRemoteAddr();
if ("0:0:0:0:0:0:0:1".equals(ip)) { // local 일 때
ip = "127.0.0.1";
}
return ip;
}
public static String getXFowardedForIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip != null) {
ip = ip.split(",")[0].trim();
}
return ip;
}
// 사용자 ID 추출 예시 (Spring Security 기준)
public static String getUserId(HttpServletRequest request) {
try {
return request.getUserPrincipal() != null ? request.getUserPrincipal().getName() : null;
} catch (Exception e) {
return null;
}
}
public static EventType getEventType(HttpServletRequest request) {
String method = request.getMethod().toUpperCase();
String uri = request.getRequestURI().toLowerCase();
// URL 기반 DOWNLOAD/PRINT 분류 -> /download는 FileDownloadInterceptor로 옮김
if (uri.contains("/download") || uri.contains("/export")) {
return EventType.DOWNLOAD;
}
if (uri.contains("/print")) {
return EventType.OTHER;
}
// 일반 CRUD
return switch (method) {
case "POST" -> EventType.ADDED;
case "GET" -> EventType.LIST;
case "DELETE" -> EventType.REMOVE;
case "PUT", "PATCH" -> EventType.MODIFIED;
default -> EventType.OTHER;
};
}
public static String getRequestBody(
HttpServletRequest servletRequest, ContentCachingRequestWrapper contentWrapper) {
StringBuilder resultBody = new StringBuilder();
// GET, form-urlencoded POST 파라미터
Map<String, String[]> paramMap = servletRequest.getParameterMap();
String queryParams =
paramMap.entrySet().stream()
.map(e -> e.getKey() + "=" + String.join(",", e.getValue()))
.collect(Collectors.joining("&"));
resultBody.append(queryParams.isEmpty() ? "" : queryParams);
// JSON Body
if ("POST".equalsIgnoreCase(servletRequest.getMethod())
&& servletRequest.getContentType() != null
&& servletRequest.getContentType().contains("application/json")) {
try {
// json인 경우는 Wrapper를 통해 가져오기
resultBody.append(getBodyData(contentWrapper));
} catch (Exception e) {
resultBody.append("cannot read JSON body ").append(e.toString());
}
}
// Multipart form-data
if ("POST".equalsIgnoreCase(servletRequest.getMethod())
&& servletRequest.getContentType() != null
&& servletRequest.getContentType().startsWith("multipart/form-data")) {
resultBody.append("multipart/form-data request");
}
return resultBody.toString();
}
// JSON Body 읽기
public static String getBodyData(ContentCachingRequestWrapper request) {
byte[] buf = request.getContentAsByteArray();
if (buf.length == 0) {
return null;
}
try {
return new String(buf, request.getCharacterEncoding());
} catch (UnsupportedEncodingException e) {
return new String(buf);
}
}
// ApiResponse 의 Status가 2xx 범위이면 SUCCESS, 아니면 FAILED
public static EventStatus isSuccessFail(ApiResponseDto<?> apiResponse) {
return apiResponse.getHttpStatus().is2xxSuccessful() ? EventStatus.SUCCESS : EventStatus.FAILED;
}
public static String getUriMenuInfo(List<MenuDto.Basic> menuList, String uri) {
String normalizedUri = uri.replace("/api", "");
MenuDto.Basic basic =
menuList.stream()
.filter(
menu -> menu.getMenuUrl() != null && normalizedUri.startsWith(menu.getMenuUrl()))
.max(Comparator.comparingInt(m -> m.getMenuUrl().length()))
.orElse(null);
return basic != null ? basic.getMenuUid() : "SYSTEM";
}
public static String cutRequestBody(String value) {
int MAX_LEN = 255;
if (value == null) {
return null;
}
return value.length() <= MAX_LEN ? value : value.substring(0, MAX_LEN);
}
}

View File

@@ -1,143 +0,0 @@
package com.kamco.cd.kamcoback.postgres.entity;
import com.kamco.cd.kamcoback.log.dto.AuditLogDto;
import com.kamco.cd.kamcoback.log.dto.EventStatus;
import com.kamco.cd.kamcoback.log.dto.EventType;
import com.kamco.cd.kamcoback.postgres.CommonCreateEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.UUID;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "tb_audit_log")
public class AuditLogEntity extends CommonCreateEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "audit_log_uid", nullable = false)
private Long id;
@Column(name = "user_uid")
private Long userUid;
@Enumerated(EnumType.STRING)
private EventType eventType;
@Enumerated(EnumType.STRING)
private EventStatus eventStatus;
@Column(name = "menu_uid")
private String menuUid;
@Column(name = "ip_address")
private String ipAddress;
@Column(name = "request_uri")
private String requestUri;
@Column(name = "request_body")
private String requestBody;
@Column(name = "error_log_uid")
private Long errorLogUid;
@Column(name = "download_uuid")
private UUID downloadUuid;
@Column(name = "login_attempt_id")
private String loginAttemptId;
public AuditLogEntity(
Long userUid,
EventType eventType,
EventStatus eventStatus,
String menuUid,
String ipAddress,
String requestUri,
String requestBody,
Long errorLogUid,
UUID downloadUuid,
String loginAttemptId) {
this.userUid = userUid;
this.eventType = eventType;
this.eventStatus = eventStatus;
this.menuUid = menuUid;
this.ipAddress = ipAddress;
this.requestUri = requestUri;
this.requestBody = requestBody;
this.errorLogUid = errorLogUid;
this.downloadUuid = downloadUuid;
this.loginAttemptId = loginAttemptId;
}
/** 파일 다운로드 이력 생성 */
public static AuditLogEntity forFileDownload(
Long userId,
String requestUri,
String menuUid,
String ip,
int httpStatus,
UUID downloadUuid) {
return new AuditLogEntity(
userId,
EventType.DOWNLOAD, // 이벤트 타입 고정
httpStatus < 400 ? EventStatus.SUCCESS : EventStatus.FAILED, // 성공 여부
menuUid,
ip,
requestUri,
null, // requestBody 없음
null, // errorLogUid 없음
downloadUuid,
null // loginAttemptId 없음
);
}
public AuditLogDto.Basic toDto() {
return new AuditLogDto.Basic(
this.id,
this.userUid,
this.eventType,
this.eventStatus,
this.menuUid,
this.ipAddress,
this.requestUri,
this.requestBody,
this.errorLogUid,
super.getCreatedDate());
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(this.id)
.append("\n")
.append(this.userUid)
.append("\n")
.append(this.eventType)
.append("\n")
.append(this.eventStatus)
.append("\n")
.append(this.menuUid)
.append("\n")
.append(this.ipAddress)
.append("\n")
.append(this.requestUri)
.append("\n")
.append(this.requestBody)
.append("\n")
.append(this.errorLogUid);
return sb.toString();
}
}

View File

@@ -1,7 +0,0 @@
package com.kamco.cd.kamcoback.postgres.repository.log;
import com.kamco.cd.kamcoback.postgres.entity.AuditLogEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AuditLogRepository
extends JpaRepository<AuditLogEntity, Long>, AuditLogRepositoryCustom {}

View File

@@ -1,29 +0,0 @@
package com.kamco.cd.kamcoback.postgres.repository.log;
import com.kamco.cd.kamcoback.log.dto.AuditLogDto;
import com.kamco.cd.kamcoback.log.dto.AuditLogDto.DownloadReq;
import java.time.LocalDate;
import org.springframework.data.domain.Page;
public interface AuditLogRepositoryCustom {
Page<AuditLogDto.DailyAuditList> findLogByDaily(
AuditLogDto.searchReq searchReq, LocalDate startDate, LocalDate endDate);
Page<AuditLogDto.MenuAuditList> findLogByMenu(
AuditLogDto.searchReq searchReq, String searchValue);
Page<AuditLogDto.UserAuditList> findLogByAccount(
AuditLogDto.searchReq searchReq, String searchValue);
Page<AuditLogDto.DownloadRes> findDownloadLog(
AuditLogDto.searchReq searchReq, DownloadReq downloadReq);
Page<AuditLogDto.DailyDetail> findLogByDailyResult(
AuditLogDto.searchReq searchReq, LocalDate logDate);
Page<AuditLogDto.MenuDetail> findLogByMenuResult(AuditLogDto.searchReq searchReq, String menuId);
Page<AuditLogDto.UserDetail> findLogByAccountResult(
AuditLogDto.searchReq searchReq, Long accountId);
}

View File

@@ -1,523 +0,0 @@
package com.kamco.cd.kamcoback.postgres.repository.log;
import static com.kamco.cd.kamcoback.postgres.entity.QAuditLogEntity.auditLogEntity;
import static com.kamco.cd.kamcoback.postgres.entity.QErrorLogEntity.errorLogEntity;
import static com.kamco.cd.kamcoback.postgres.entity.QMemberEntity.memberEntity;
import static com.kamco.cd.kamcoback.postgres.entity.QMenuEntity.menuEntity;
import com.kamco.cd.kamcoback.log.dto.AuditLogDto;
import com.kamco.cd.kamcoback.log.dto.AuditLogDto.DownloadReq;
import com.kamco.cd.kamcoback.log.dto.ErrorLogDto;
import com.kamco.cd.kamcoback.log.dto.EventStatus;
import com.kamco.cd.kamcoback.log.dto.EventType;
import com.kamco.cd.kamcoback.postgres.entity.AuditLogEntity;
import com.kamco.cd.kamcoback.postgres.entity.QMenuEntity;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.CaseBuilder;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.NumberExpression;
import com.querydsl.core.types.dsl.StringExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import io.micrometer.common.util.StringUtils;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Objects;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
public class AuditLogRepositoryImpl extends QuerydslRepositorySupport
implements AuditLogRepositoryCustom {
private static final ZoneId ZONE = ZoneId.of("Asia/Seoul");
private final JPAQueryFactory queryFactory;
private final StringExpression NULL_STRING = Expressions.stringTemplate("cast(null as text)");
public AuditLogRepositoryImpl(JPAQueryFactory queryFactory) {
super(AuditLogEntity.class);
this.queryFactory = queryFactory;
}
@Override
public Page<AuditLogDto.DailyAuditList> findLogByDaily(
AuditLogDto.searchReq searchReq, LocalDate startDate, LocalDate endDate) {
StringExpression groupDateTime =
Expressions.stringTemplate("to_char({0}, 'YYYY-MM-DD')", auditLogEntity.createdDate);
Pageable pageable = searchReq.toPageable();
List<AuditLogDto.DailyAuditList> foundContent =
queryFactory
.select(
Projections.constructor(
AuditLogDto.DailyAuditList.class,
readCount().as("readCount"),
cudCount().as("cudCount"),
printCount().as("printCount"),
downloadCount().as("downloadCount"),
auditLogEntity.count().as("totalCount"),
groupDateTime.as("baseDate")))
.from(auditLogEntity)
.where(eventEndedAtBetween(startDate, endDate))
.groupBy(groupDateTime)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(groupDateTime.desc())
.fetch();
Long countQuery =
queryFactory
.select(groupDateTime.countDistinct())
.from(auditLogEntity)
.where(eventEndedAtBetween(startDate, endDate))
.fetchOne();
return new PageImpl<>(foundContent, pageable, countQuery);
}
@Override
public Page<AuditLogDto.MenuAuditList> findLogByMenu(
AuditLogDto.searchReq searchReq, String searchValue) {
Pageable pageable = searchReq.toPageable();
List<AuditLogDto.MenuAuditList> foundContent =
queryFactory
.select(
Projections.constructor(
AuditLogDto.MenuAuditList.class,
auditLogEntity.menuUid.as("menuId"),
menuEntity.menuNm.max().as("menuName"),
readCount().as("readCount"),
cudCount().as("cudCount"),
printCount().as("printCount"),
downloadCount().as("downloadCount"),
auditLogEntity.count().as("totalCount")))
.from(auditLogEntity)
.leftJoin(menuEntity)
.on(auditLogEntity.menuUid.eq(menuEntity.menuUid))
.where(auditLogEntity.menuUid.ne("SYSTEM"), menuNameEquals(searchValue))
.groupBy(auditLogEntity.menuUid)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(auditLogEntity.createdDate.max().desc())
.fetch();
// count query group by 를 지정하면 하나의 row 가 아니라 그룹핑된 여러 row 가 나올 수 있다.
// select query 의 group by 대상의 컬럼을 count query 에선 select distinct 로 처리 한다.
Long countQuery =
queryFactory
.select(auditLogEntity.menuUid.countDistinct())
.from(auditLogEntity)
.leftJoin(menuEntity)
.on(auditLogEntity.menuUid.eq(menuEntity.menuUid))
.where(menuNameEquals(searchValue))
.fetchOne();
return new PageImpl<>(foundContent, pageable, countQuery);
}
@Override
public Page<AuditLogDto.UserAuditList> findLogByAccount(
AuditLogDto.searchReq searchReq, String searchValue) {
Pageable pageable = searchReq.toPageable();
List<AuditLogDto.UserAuditList> foundContent =
queryFactory
.select(
Projections.constructor(
AuditLogDto.UserAuditList.class,
auditLogEntity.userUid.as("accountId"),
memberEntity.employeeNo.as("loginId"),
memberEntity.name.as("username"),
readCount().as("readCount"),
cudCount().as("cudCount"),
printCount().as("printCount"),
downloadCount().as("downloadCount"),
auditLogEntity.count().as("totalCount")))
.from(auditLogEntity)
.leftJoin(memberEntity)
.on(auditLogEntity.userUid.eq(memberEntity.id))
.where(auditLogEntity.userUid.isNotNull(), loginIdOrUsernameContains(searchValue))
.groupBy(auditLogEntity.userUid, memberEntity.employeeNo, memberEntity.name)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
// .orderBy(auditLogEntity.eventEndedAt.max().desc())
.fetch();
Long countQuery =
queryFactory
.select(auditLogEntity.userUid.countDistinct())
.from(auditLogEntity)
.leftJoin(memberEntity)
.on(auditLogEntity.userUid.eq(memberEntity.id))
.where(loginIdOrUsernameContains(searchValue))
.fetchOne();
return new PageImpl<>(foundContent, pageable, countQuery);
}
@Override
public Page<AuditLogDto.DownloadRes> findDownloadLog(
AuditLogDto.searchReq searchReq, DownloadReq req) {
Pageable pageable = searchReq.toPageable();
BooleanBuilder whereBuilder = new BooleanBuilder();
whereBuilder.and(auditLogEntity.eventStatus.ne(EventStatus.valueOf("FAILED")));
whereBuilder.and(auditLogEntity.eventType.eq(EventType.valueOf("DOWNLOAD")));
if (req.getMenuId() != null && !req.getMenuId().isEmpty()) {
whereBuilder.and(auditLogEntity.menuUid.eq(req.getMenuId()));
}
if (req.getUuid() != null) {
whereBuilder.and(auditLogEntity.requestUri.contains("/api/inference/download/"));
whereBuilder.and(auditLogEntity.requestUri.endsWith(String.valueOf(req.getUuid())));
}
if (req.getSearchValue() != null && !req.getSearchValue().isEmpty()) {
whereBuilder.and(
memberEntity
.name
.contains(req.getSearchValue())
.or(memberEntity.employeeNo.contains(req.getSearchValue())));
}
List<AuditLogDto.DownloadRes> foundContent =
queryFactory
.select(
Projections.constructor(
AuditLogDto.DownloadRes.class,
memberEntity.name,
memberEntity.employeeNo,
auditLogEntity.createdDate.as("downloadDttm")))
.from(auditLogEntity)
.leftJoin(memberEntity)
.on(auditLogEntity.userUid.eq(memberEntity.id))
.where(whereBuilder, createdDateBetween(req.getStartDate(), req.getEndDate()))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(auditLogEntity.createdDate.desc())
.fetch();
Long countQuery =
queryFactory
.select(auditLogEntity.userUid.countDistinct())
.from(auditLogEntity)
.leftJoin(memberEntity)
.on(auditLogEntity.userUid.eq(memberEntity.id))
.where(whereBuilder, createdDateBetween(req.getStartDate(), req.getEndDate()))
.fetchOne();
return new PageImpl<>(foundContent, pageable, countQuery);
}
@Override
public Page<AuditLogDto.DailyDetail> findLogByDailyResult(
AuditLogDto.searchReq searchReq, LocalDate logDate) {
Pageable pageable = searchReq.toPageable();
QMenuEntity parent = new QMenuEntity("parent");
// 1depth menu name
StringExpression parentMenuName =
new CaseBuilder()
.when(parent.menuUid.isNull())
.then(menuEntity.menuNm)
.otherwise(parent.menuNm);
// 2depth menu name
StringExpression menuName =
new CaseBuilder()
.when(parent.menuUid.isNull())
.then(NULL_STRING)
.otherwise(menuEntity.menuNm);
List<AuditLogDto.DailyDetail> foundContent =
queryFactory
.select(
Projections.constructor(
AuditLogDto.DailyDetail.class,
auditLogEntity.id.as("logId"),
memberEntity.name.as("userName"),
memberEntity.employeeNo.as("loginId"),
menuEntity.menuNm.as("menuName"),
auditLogEntity.eventType.as("eventType"),
Expressions.stringTemplate(
"to_char({0}, 'YYYY-MM-DD HH24:MI')", auditLogEntity.createdDate)
.as("logDateTime"),
Projections.constructor(
AuditLogDto.LogDetail.class,
Expressions.constant("한국자산관리공사"), // serviceName
parentMenuName.as("parentMenuName"),
menuName,
menuEntity.menuUrl.as("menuUrl"),
menuEntity.description.as("menuDescription"),
menuEntity.menuOrder.as("sortOrder"),
menuEntity.isUse.as("used")))) // TODO
.from(auditLogEntity)
.leftJoin(menuEntity)
.on(auditLogEntity.menuUid.eq(menuEntity.menuUid))
.leftJoin(menuEntity.parent, parent)
.leftJoin(memberEntity)
.on(auditLogEntity.userUid.eq(memberEntity.id))
.where(eventEndedAtEqDate(logDate))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(auditLogEntity.createdDate.desc())
.fetch();
Long countQuery =
queryFactory
.select(auditLogEntity.id.countDistinct())
.from(auditLogEntity)
.leftJoin(menuEntity)
.on(auditLogEntity.menuUid.eq(menuEntity.menuUid))
.leftJoin(menuEntity.parent, parent)
.leftJoin(memberEntity)
.on(auditLogEntity.userUid.eq(memberEntity.id))
.where(eventEndedAtEqDate(logDate))
.fetchOne();
return new PageImpl<>(foundContent, pageable, countQuery);
}
@Override
public Page<AuditLogDto.MenuDetail> findLogByMenuResult(
AuditLogDto.searchReq searchReq, String menuUid) {
Pageable pageable = searchReq.toPageable();
QMenuEntity parent = new QMenuEntity("parent");
// 1depth menu name
StringExpression parentMenuName =
new CaseBuilder()
.when(parent.menuUid.isNull())
.then(menuEntity.menuNm)
.otherwise(parent.menuNm);
// 2depth menu name
StringExpression menuName =
new CaseBuilder()
.when(parent.menuUid.isNull())
.then(NULL_STRING)
.otherwise(menuEntity.menuNm);
List<AuditLogDto.MenuDetail> foundContent =
queryFactory
.select(
Projections.constructor(
AuditLogDto.MenuDetail.class,
auditLogEntity.id.as("logId"),
Expressions.stringTemplate(
"to_char({0}, 'YYYY-MM-DD HH24:MI')", auditLogEntity.createdDate)
.as("logDateTime"),
memberEntity.name.as("userName"),
memberEntity.employeeNo.as("loginId"),
auditLogEntity.eventType.as("eventType"),
Projections.constructor(
AuditLogDto.LogDetail.class,
Expressions.constant("한국자산관리공사"), // serviceName
parentMenuName.as("parentMenuName"),
menuName,
menuEntity.menuUrl.as("menuUrl"),
menuEntity.description.as("menuDescription"),
menuEntity.menuOrder.as("sortOrder"),
menuEntity.isUse.as("used"))))
.from(auditLogEntity)
.leftJoin(menuEntity)
.on(auditLogEntity.menuUid.eq(menuEntity.menuUid))
.leftJoin(menuEntity.parent, parent)
.leftJoin(memberEntity)
.on(auditLogEntity.userUid.eq(memberEntity.id))
.where(menuUidEq(menuUid))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(auditLogEntity.createdDate.desc())
.fetch();
Long countQuery =
queryFactory
.select(auditLogEntity.id.countDistinct())
.from(auditLogEntity)
.leftJoin(menuEntity)
.on(auditLogEntity.menuUid.eq(menuEntity.menuUid))
.leftJoin(menuEntity.parent, parent)
.leftJoin(memberEntity)
.on(auditLogEntity.userUid.eq(memberEntity.id))
.where(menuUidEq(menuUid))
.fetchOne();
return new PageImpl<>(foundContent, pageable, countQuery);
}
@Override
public Page<AuditLogDto.UserDetail> findLogByAccountResult(
AuditLogDto.searchReq searchReq, Long userUid) {
Pageable pageable = searchReq.toPageable();
QMenuEntity parent = new QMenuEntity("parent");
// 1depth menu name
StringExpression parentMenuName =
new CaseBuilder()
.when(parent.menuUid.isNull())
.then(menuEntity.menuNm)
.otherwise(parent.menuNm);
// 2depth menu name
StringExpression menuName =
new CaseBuilder()
.when(parent.menuUid.isNull())
.then(NULL_STRING)
.otherwise(menuEntity.menuNm);
List<AuditLogDto.UserDetail> foundContent =
queryFactory
.select(
Projections.constructor(
AuditLogDto.UserDetail.class,
auditLogEntity.id.as("logId"),
Expressions.stringTemplate(
"to_char({0}, 'YYYY-MM-DD HH24:MI')", auditLogEntity.createdDate)
.as("logDateTime"),
menuEntity.menuNm.as("menuName"),
auditLogEntity.eventType.as("eventType"),
Projections.constructor(
AuditLogDto.LogDetail.class,
Expressions.constant("한국자산관리공사"), // serviceName
parentMenuName.as("parentMenuName"),
menuName,
menuEntity.menuUrl.as("menuUrl"),
menuEntity.description.as("menuDescription"),
menuEntity.menuOrder.as("sortOrder"),
menuEntity.isUse.as("used"))))
.from(auditLogEntity)
.leftJoin(menuEntity)
.on(auditLogEntity.menuUid.eq(menuEntity.menuUid))
.leftJoin(menuEntity.parent, parent)
.leftJoin(memberEntity)
.on(auditLogEntity.userUid.eq(memberEntity.id))
.where(userUidEq(userUid))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(auditLogEntity.createdDate.desc())
.fetch();
Long countQuery =
queryFactory
.select(auditLogEntity.id.countDistinct())
.from(auditLogEntity)
.leftJoin(menuEntity)
.on(auditLogEntity.menuUid.eq(menuEntity.menuUid))
.leftJoin(menuEntity.parent, parent)
.leftJoin(memberEntity)
.on(auditLogEntity.userUid.eq(memberEntity.id))
.where(userUidEq(userUid))
.fetchOne();
return new PageImpl<>(foundContent, pageable, countQuery);
}
private BooleanExpression eventEndedAtBetween(LocalDate startDate, LocalDate endDate) {
if (Objects.isNull(startDate) || Objects.isNull(endDate)) {
return null;
}
ZoneId zoneId = ZoneId.of("Asia/Seoul");
ZonedDateTime startDateTime = startDate.atStartOfDay(zoneId);
ZonedDateTime endDateTime = endDate.plusDays(1).atStartOfDay(zoneId);
return auditLogEntity
.createdDate
.goe(startDateTime)
.and(auditLogEntity.createdDate.lt(endDateTime));
}
private BooleanExpression createdDateBetween(LocalDate startDate, LocalDate endDate) {
if (startDate == null || endDate == null) {
return null;
}
ZonedDateTime start = startDate.atStartOfDay(ZONE);
ZonedDateTime endExclusive = endDate.plusDays(1).atStartOfDay(ZONE);
return auditLogEntity.createdDate.goe(start).and(auditLogEntity.createdDate.lt(endExclusive));
}
private BooleanExpression menuNameEquals(String searchValue) {
if (StringUtils.isBlank(searchValue)) {
return null;
}
return menuEntity.menuNm.contains(searchValue);
}
private BooleanExpression loginIdOrUsernameContains(String searchValue) {
if (StringUtils.isBlank(searchValue)) {
return null;
}
return memberEntity
.employeeNo
.contains(searchValue)
.or(memberEntity.name.contains(searchValue));
}
private BooleanExpression eventStatusEqFailed() {
return auditLogEntity.eventStatus.eq(EventStatus.FAILED);
}
private BooleanExpression eventTypeEq(EventType eventType) {
if (Objects.isNull(eventType)) {
return null;
}
return auditLogEntity.eventType.eq(eventType);
}
private BooleanExpression errorLevelEq(ErrorLogDto.LogErrorLevel level) {
if (Objects.isNull(level)) {
return null;
}
return errorLogEntity.errorLevel.eq(ErrorLogDto.LogErrorLevel.valueOf(level.name()));
}
private BooleanExpression eventEndedAtEqDate(LocalDate logDate) {
ZoneId zoneId = ZoneId.of("Asia/Seoul");
ZonedDateTime start = logDate.atStartOfDay(zoneId);
ZonedDateTime end = logDate.plusDays(1).atStartOfDay(zoneId);
return auditLogEntity.createdDate.goe(start).and(auditLogEntity.createdDate.lt(end));
}
private BooleanExpression menuUidEq(String menuUid) {
return auditLogEntity.menuUid.eq(menuUid);
}
private BooleanExpression userUidEq(Long userUid) {
return auditLogEntity.userUid.eq(userUid);
}
private NumberExpression<Integer> readCount() {
return new CaseBuilder()
.when(auditLogEntity.eventType.in(EventType.LIST, EventType.DETAIL))
.then(1)
.otherwise(0)
.sum();
}
private NumberExpression<Integer> cudCount() {
return new CaseBuilder()
.when(auditLogEntity.eventType.in(EventType.ADDED, EventType.MODIFIED, EventType.REMOVE))
.then(1)
.otherwise(0)
.sum();
}
private NumberExpression<Integer> printCount() {
return new CaseBuilder()
.when(auditLogEntity.eventType.eq(EventType.OTHER))
.then(1)
.otherwise(0)
.sum();
}
private NumberExpression<Integer> downloadCount() {
return new CaseBuilder()
.when(auditLogEntity.eventType.eq(EventType.DOWNLOAD))
.then(1)
.otherwise(0)
.sum();
}
}

View File

@@ -1,8 +1,6 @@
package com.kamco.cd.kamcoback.scheduler.service; package com.kamco.cd.kamcoback.scheduler.service;
import com.kamco.cd.kamcoback.common.utils.NetUtils; import com.kamco.cd.kamcoback.common.utils.NetUtils;
import com.kamco.cd.kamcoback.common.utils.UserUtil;
import com.kamco.cd.kamcoback.config.api.ApiLogFunction;
import com.kamco.cd.kamcoback.config.resttemplate.ExternalHttpClient; import com.kamco.cd.kamcoback.config.resttemplate.ExternalHttpClient;
import com.kamco.cd.kamcoback.config.resttemplate.ExternalHttpClient.ExternalCallResult; import com.kamco.cd.kamcoback.config.resttemplate.ExternalHttpClient.ExternalCallResult;
import com.kamco.cd.kamcoback.gukyuin.dto.ChngDetectContDto; import com.kamco.cd.kamcoback.gukyuin.dto.ChngDetectContDto;
@@ -12,11 +10,7 @@ import com.kamco.cd.kamcoback.gukyuin.dto.ChngDetectMastDto;
import com.kamco.cd.kamcoback.gukyuin.dto.ChngDetectMastDto.LearnKeyDto; import com.kamco.cd.kamcoback.gukyuin.dto.ChngDetectMastDto.LearnKeyDto;
import com.kamco.cd.kamcoback.gukyuin.dto.ChngDetectMastDto.ResultDto; import com.kamco.cd.kamcoback.gukyuin.dto.ChngDetectMastDto.ResultDto;
import com.kamco.cd.kamcoback.gukyuin.dto.GukYuinStatus; import com.kamco.cd.kamcoback.gukyuin.dto.GukYuinStatus;
import com.kamco.cd.kamcoback.log.dto.EventStatus;
import com.kamco.cd.kamcoback.log.dto.EventType;
import com.kamco.cd.kamcoback.postgres.core.GukYuinPnuJobCoreService; import com.kamco.cd.kamcoback.postgres.core.GukYuinPnuJobCoreService;
import com.kamco.cd.kamcoback.postgres.entity.AuditLogEntity;
import com.kamco.cd.kamcoback.postgres.repository.log.AuditLogRepository;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
@@ -24,8 +18,6 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Log4j2 @Log4j2
@Service @Service
@@ -35,9 +27,6 @@ public class GukYuinApiPnuJobService {
private final GukYuinPnuJobCoreService gukYuinPnuJobCoreService; private final GukYuinPnuJobCoreService gukYuinPnuJobCoreService;
private final ExternalHttpClient externalHttpClient; private final ExternalHttpClient externalHttpClient;
private final NetUtils netUtils = new NetUtils(); private final NetUtils netUtils = new NetUtils();
private final AuditLogRepository auditLogRepository;
private final UserUtil userUtil;
@Value("${spring.profiles.active}") @Value("${spring.profiles.active}")
private String profile; private String profile;
@@ -97,14 +86,6 @@ public class GukYuinApiPnuJobService {
externalHttpClient.call( externalHttpClient.call(
url, HttpMethod.GET, null, netUtils.jsonHeaders(), ChngDetectMastDto.ResultDto.class); url, HttpMethod.GET, null, netUtils.jsonHeaders(), ChngDetectMastDto.ResultDto.class);
this.insertGukyuinAuditLog(
EventType.DETAIL.getId(),
netUtils.getLocalIP(),
userUtil.getId(),
url.replace(gukyuinUrl, ""),
null,
response.body().getSuccess());
ResultDto result = response.body(); ResultDto result = response.body();
if (result == null || result.getResult() == null || result.getResult().isEmpty()) { if (result == null || result.getResult() == null || result.getResult().isEmpty()) {
return succCnt; return succCnt;
@@ -161,14 +142,6 @@ public class GukYuinApiPnuJobService {
} }
} }
this.insertGukyuinAuditLog(
EventType.LIST.getId(),
netUtils.getLocalIP(),
userUtil.getId(),
url.replace(gukyuinUrl, ""),
null,
response.body().getSuccess());
ResultContDto cont = response.body(); ResultContDto cont = response.body();
if (cont == null || cont.getResult().isEmpty()) { if (cont == null || cont.getResult().isEmpty()) {
return result; // 외부 API 이상 방어 return result; // 외부 API 이상 방어
@@ -188,33 +161,4 @@ public class GukYuinApiPnuJobService {
return result; return result;
} }
@Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = false)
public void insertGukyuinAuditLog(
String actionType,
String myIp,
Long userUid,
String requestUri,
Object requestBody,
boolean successFail) {
try {
AuditLogEntity log =
new AuditLogEntity(
userUid,
EventType.fromName(actionType),
successFail ? EventStatus.SUCCESS : EventStatus.FAILED,
"GUKYUIN", // 메뉴도 국유인으로 하나 따기
myIp,
requestUri,
requestBody == null ? null : ApiLogFunction.cutRequestBody(requestBody.toString()),
null,
null,
null);
auditLogRepository.save(log);
} catch (Exception e) {
log.error(e.getMessage());
throw e;
}
}
} }

View File

@@ -0,0 +1,2 @@
#Mon Feb 02 19:50:45 KST 2026
gradle.version=8.14

Binary file not shown.

View File

@@ -0,0 +1,112 @@
# Code Style 설정 가이드
이 문서는 프로젝트에서 Google Java Style을 자동으로 적용하기 위한 설정 가이드입니다.
## 자동 포맷팅 구성
### 1. 커밋 시점 자동 포맷팅 (Git Pre-commit Hook)
커밋 전에 자동으로 코드를 포맷팅하고 스테이징합니다.
**설정 완료:** `.git/hooks/pre-commit` 파일이 자동으로 실행됩니다.
**동작 방식:**
- 커밋 시도 시 `./gradlew spotlessApply` 자동 실행
- 스테이징된 Java 파일을 자동으로 포맷팅
- 포맷팅된 파일을 자동으로 다시 스테이징
- 포맷팅이 완료되면 커밋 진행
**장점:**
- 수동으로 `spotlessApply`를 실행할 필요 없음
- 항상 일관된 코드 스타일 유지
- 포맷팅 누락 방지
### 2. IntelliJ IDEA 저장 시점 자동 포맷팅
#### 방법 1: Code Style 설정 임포트 (권장)
1. **IntelliJ IDEA 열기**
2. **Settings/Preferences** (Mac: `⌘,` / Windows: `Ctrl+Alt+S`)
3. **Editor > Code Style > Java**
4. **⚙️ (톱니바퀴)** 클릭 > **Import Scheme > IntelliJ IDEA code style XML**
5. 프로젝트 루트의 `intellij-java-google-style.xml` 파일 선택
6. **OK** 클릭
#### 방법 2: 저장 시 자동 포맷팅 활성화
**Option A: Actions on Save 설정**
1. **Settings/Preferences** > **Tools > Actions on Save**
2. 다음 옵션들을 활성화:
-**Reformat code**
-**Optimize imports**
-**Rearrange code** (선택사항)
3. **Changed lines** 또는 **Whole file** 선택
4. **OK** 클릭
**Option B: Save Actions Plugin 사용 (더 많은 옵션)**
1. **Settings/Preferences** > **Plugins**
2. **Marketplace**에서 "Save Actions" 검색 및 설치
3. **Settings/Preferences** > **Other Settings > Save Actions**
4. 다음 옵션 활성화:
-**Activate save actions on save**
-**Reformat file**
-**Optimize imports**
-**Rearrange fields and methods** (선택사항)
### 3. Gradle Spotless Plugin 수동 실행
#### 코드 포맷팅 체크
```bash
# 포맷팅 문제 확인만 (수정하지 않음)
./gradlew spotlessCheck
```
#### 코드 자동 포맷팅
```bash
# 모든 Java 파일 자동 포맷팅 적용
./gradlew spotlessApply
```
#### 빌드 시 자동 체크
```bash
# 빌드 전에 자동으로 spotlessCheck 실행됨
./gradlew build
```
## 코드 스타일 규칙
프로젝트는 **Google Java Style Guide** 기반으로 다음 규칙을 따릅니다:
- **Indentation**: 2 spaces (탭 아님)
- **Line Length**: 180 characters
- **Line Endings**: LF (Unix-style)
- **Charset**: UTF-8
- **Import Order**: Static imports → 빈 줄 → Regular imports
- **Braces**: 모든 if, for, while, do 문에 중괄호 필수
## 문제 해결
### Pre-commit hook이 실행되지 않는 경우
```bash
# 실행 권한 확인 및 부여
chmod +x .git/hooks/pre-commit
```
### Spotless 플러그인이 동작하지 않는 경우
```bash
# Gradle 의존성 다시 다운로드
./gradlew clean build --refresh-dependencies
```
### IntelliJ 포맷팅이 다르게 적용되는 경우
1. `intellij-java-google-style.xml` 다시 임포트
2. **File > Invalidate Caches** > **Invalidate and Restart**
## 추가 정보
- **Google Java Style Guide**: https://google.github.io/styleguide/javaguide.html
- **Spotless Plugin**: https://github.com/diffplug/spotless
- **IntelliJ Code Style**: https://www.jetbrains.com/help/idea/code-style.html

View File

@@ -0,0 +1,282 @@
# 공통코드 Redis 캐시 시스템 - DanielLee
## 요구사항 검토
### 1. **API를 통해 공통코드 제공**
- **구현 완료**: `CommonCodeApiController`에서 전체 공통코드 조회 API 제공
```
GET /api/code
→ 모든 공통코드 조회
```
- **추가 구현**: 캐시 갱신 및 상태 확인 API
```
POST /api/code/cache/refresh → 캐시 갱신
GET /api/code/cache/status → 캐시 상태 확인
```
---
### 2. **애플리케이션 로딩시 Redis 캐시에 올리기**
- **구현 완료**: `CommonCodeCacheManager` 클래스 생성
#### 초기화 메커니즘
```java
@Component
@RequiredArgsConstructor
public class CommonCodeCacheManager {
@EventListener(ApplicationReadyEvent.class)
public void initializeCommonCodeCache() {
// 애플리케이션 완전히 시작된 후 공통코드를 Redis에 미리 로드
List<Basic> allCommonCodes = commonCodeService.getFindAll();
// @Cacheable이 자동으로 Redis에 캐시함
}
}
```
#### 동작 흐름
1. 애플리케이션 시작
2. Spring이 모든 Bean 생성 완료 (`ApplicationReadyEvent` 발생)
3. `CommonCodeCacheManager.initializeCommonCodeCache()` 실행
4. `commonCodeService.getFindAll()` 호출 (DB에서 조회)
5. `@Cacheable(value = "commonCodes")` 에노테이션이 결과를 Redis에 저장
---
### 3. **공통코드 변경시 데이터 갱신**
#### 자동 갱신
- **등록 (CREATE)**: `@CacheEvict` → 캐시 전체 삭제
- **수정 (UPDATE)**: `@CacheEvict` → 캐시 전체 삭제
- **삭제 (DELETE)**: `@CacheEvict` → 캐시 전체 삭제
- **순서 변경**: `@CacheEvict` → 캐시 전체 삭제
```java
@CacheEvict(value = "commonCodes", allEntries = true)
public ResponseObj save(CommonCodeDto.AddReq req) {
// 공통코드 저장
// ↓
// 캐시 전체 삭제 (다음 조회 시 DB에서 새로 로드)
}
```
#### 수동 갱신 (관리자)
```java
POST /api/code/cache/refresh
```
- 공통코드 설정이 변경된 후 API를 호출하여 캐시를 강제 갱신
#### 캐시 상태 모니터링
```java
GET /api/code/cache/status
→ 응답: { "data": 150 } // 캐시된 공통코드 150개
```
---
## 전체 아키텍처
```
┌─────────────────────────────────────────────────────────┐
│ 클라이언트 요청 │
└──────────────────┬──────────────────────────────────────┘
┌──────────▼──────────┐
│ CommonCodeApiController
└──────────┬──────────┘
┌─────────┴──────────┐
│ │
┌────▼─────┐ ┌──────▼────────────┐
│ 조회 API │ │ 캐시 관리 API │
│ (GET) │ │(POST, GET) │
└────┬─────┘ └──────┬────────────┘
│ │
│ ┌────────▼──────────┐
│ │CommonCodeCacheManager
│ │(캐시 초기화/갱신) │
│ └────────┬──────────┘
│ │
┌────▼─────────────────┬─▼────┐
│ CommonCodeService │ │
│ (@Cacheable) │ │
│ (@CacheEvict) │ │
└────┬──────────────────┴──────┘
┌────▼──────────┐
│ Redis 캐시 │
│ (공통코드) │
└────┬──────────┘
┌────▼──────────┐
│ PostgreSQL DB │
│ (공통코드) │
└───────────────┘
```
---
## API 명세
### 1. 공통코드 조회 (캐시됨)
```
GET /api/code
응답:
{
"data": [
{
"id": 1,
"code": "STATUS",
"name": "상태",
"description": "상태 공통코드",
"used": true,
...
},
...
]
}
```
### 2. 공통코드 캐시 갱신
```
POST /api/code/cache/refresh
응답:
{
"data": "공통코드 캐시가 갱신되었습니다."
}
```
### 3. 캐시 상태 확인
```
GET /api/code/cache/status
응답:
{
"data": 150 // Redis에 캐시된 공통코드 개수
}
```
---
## 캐시 갱신 흐름
### 자동 갱신 (CRUD 작업)
```
관리자가 공통코드 등록/수정/삭제
CommonCodeService.save() / update() / removeCode()
(@CacheEvict 실행)
Redis 캐시 전체 삭제
다음 조회 시 DB에서 새로 로드
```
### 수동 갱신 (API 호출)
```
관리자: POST /api/code/cache/refresh
CommonCodeCacheManager.refreshCommonCodeCache()
캐시 정리 + 새로운 데이터 로드
Redis 캐시 업데이트 완료
```
---
## 성능 최적화 효과
| 항목 | 개선 전 | 개선 후 |
|------|--------|--------|
| **조회 속도** | DB 직접 조회 (10-100ms) | Redis 캐시 (1-5ms) |
| **DB 부하** | 매번 조회 | 캐시 미스시만 조회 |
| **네트워크 대역폭** | 높음 (DB 왕복) | 낮음 (로컬 캐시) |
| **응답 시간** | 변동적 | 일정 (캐시) |
---
## 추가 기능
### CommonCodeUtil - 전역 공통코드 조회
```java
@Component
public class CommonCodeUtil {
// 모든 공통코드 조회 (캐시 활용)
public List<Basic> getAllCommonCodes()
// 특정 코드로 조회
public List<Basic> getCommonCodesByCode(String code)
// ID로 단건 조회
public Optional<Basic> getCommonCodeById(Long id)
// 코드명 조회
public Optional<String> getCodeName(String parentCode, String childCode)
// 하위 코드 조회
public List<Basic> getChildCodesByParentCode(String parentCode)
// 코드 사용 가능 여부 확인
public boolean isCodeAvailable(Long parentId, String code)
}
```
### 사용 예시
```java
@RequiredArgsConstructor
@RestController
public class SomeController {
private final CommonCodeUtil commonCodeUtil;
@GetMapping("/example")
public void example() {
// 1. 모든 공통코드 조회 (캐시됨)
List<Basic> allCodes = commonCodeUtil.getAllCommonCodes();
// 2. 특정 코드 조회
Optional<String> name = commonCodeUtil.getCodeName("PARENT", "CHILD");
// 3. 코드 사용 가능 여부 확인
boolean available = commonCodeUtil.isCodeAvailable(1L, "NEW_CODE");
}
}
```
---
## 완료 체크리스트
- Redis 캐싱 어노테이션 적용 (@Cacheable, @CacheEvict)
- 애플리케이션 로딩시 캐시 초기화
- CRUD 작업시 자동 캐시 갱신
- 수동 캐시 갱신 API 제공
- 캐시 상태 모니터링 API
- 전역 공통코드 조회 유틸리티
- 포괄적인 유닛 테스트 (12개)
---
## 모니터링
캐시 상태를 주기적으로 모니터링:
```bash
# 캐시 상태 확인
curl http://localhost:8080/api/code/cache/status
# 캐시 갱신
curl -X POST http://localhost:8080/api/code/cache/refresh
```
로그 확인:
```
=== 공통코드 캐시 초기화 시작 ===
✓ 공통코드 150개가 Redis 캐시에 로드되었습니다.
- [STATUS] 상태 (ID: 1)
- [TYPE] 타입 (ID: 2)
...
=== 공통코드 캐시 초기화 완료 ===
```

View File

@@ -0,0 +1,29 @@
# Stage 1: Build stage (gradle build는 Jenkins에서 이미 수행)
FROM eclipse-temurin:21-jre-jammy
# GDAL 설치
RUN apt-get update && apt-get install -y \
gdal-bin \
libgdal-dev \
&& rm -rf /var/lib/apt/lists/*
ARG UID=1000
ARG GID=1000
RUN groupadd -g ${GID} manager01 \
&& useradd -u ${UID} -g ${GID} -m manager01
USER manager01
# 작업 디렉토리 설정
WORKDIR /app
# JAR 파일 복사 (Jenkins에서 빌드된 ROOT.jar)
COPY build/libs/ROOT.jar app.jar
# 포트 노출
EXPOSE 8080
# 애플리케이션 실행
# dev 프로파일로 실행
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=dev", "app.jar"]

View File

@@ -0,0 +1,94 @@
pipeline {
agent any
tools {
jdk 'jdk21'
}
environment {
BRANCH = 'develop'
GIT_REPO = 'https://10.100.0.10:3210/dabeeo/kamco-dabeeo-backoffice.git'
}
stages {
stage('Checkout') {
steps {
checkout([
$class: 'GitSCM',
branches: [[name: "${env.BRANCH}"]],
userRemoteConfigs: [[
url: "${env.GIT_REPO}",
credentialsId: 'jenkins-dev-token'
]]
])
}
}
stage('Get Commit Hash') {
steps {
script {
env.COMMIT_HASH = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim()
echo "Current commit hash: ${env.COMMIT_HASH}"
}
}
}
stage('Build') {
steps {
sh "./gradlew clean build -x test"
}
}
stage('Docker Build & Deploy') {
steps {
script {
echo "Building Docker image with tag: ${env.COMMIT_HASH}"
// IMAGE_TAG 환경변수 설정 후 docker-compose로 빌드 및 배포
sh """
export IMAGE_TAG=${env.COMMIT_HASH}
# 기존 컨테이너 중지 및 제거
docker-compose -f docker-compose-dev.yml down || true
# 새 이미지 빌드
docker-compose -f docker-compose-dev.yml build
# latest 태그도 추가
docker tag kamco-changedetection-api:${env.COMMIT_HASH} kamco-changedetection-api:latest
# 컨테이너 시작
docker-compose -f docker-compose-dev.yml up -d
"""
// 헬스체크 대기
echo "Waiting for application to be ready..."
sh """
for i in {1..30}; do
if docker exec kamco-changedetection-api curl -f http://localhost:8080/monitor/health > /dev/null 2>&1; then
echo "✅ Application is healthy!"
docker-compose -f docker-compose-dev.yml ps
exit 0
fi
echo "⏳ Waiting for application... (\$i/30)"
sleep 2
done
echo "⚠️ Warning: Health check timeout, checking container status..."
docker-compose -f docker-compose-dev.yml ps
"""
}
}
}
stage('Cleanup Old Images') {
steps {
script {
echo "Cleaning up old Docker images..."
sh """
# Keep latest 5 images, remove older ones
docker images kamco-changedetection-api --format "{{.ID}} {{.Tag}}" | \
grep -v latest | tail -n +6 | awk '{print \$1}' | xargs -r docker rmi || true
"""
}
}
}
}
}

View File

@@ -0,0 +1,26 @@
# GUKYUIN STATE CHECK
> 국유인에 연동된 마스터 정보에서 이노팸에 다운로드 100%가 되었는지 상태를 체크 schedule
## 📋 프로젝트 소개
**state-check**는 국유인에 연동된 마스터 정보에서 이노팸에 다운로드 100%가 되었는지 상태를 체크하는 schedule 입니다.
## 🚀 시작하기
GukYuinApiStatusJobService 의 findGukYuinMastCompleteYn 메소드가 매 10분마다 schedule 실행됨
```bash
./gradlew spotlessApply
```
```bash
./gradlew clean build
```
```bash
Java -jar state-check.jar \
```
### 필수 요구사항
- Java 21 (JDK 21)
- PostgreSQL 12+ (PostGIS 확장 필요)
- Gradle 8.x (또는 Gradle Wrapper 사용)
- Docker & Docker Compose (선택사항)

View File

@@ -0,0 +1,110 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.7'
id 'io.spring.dependency-management' version '1.1.7'
id 'com.diffplug.spotless' version '6.25.0'
}
group = 'com.kamco.cd'
version = '0.0.1-SNAPSHOT'
description = 'state-check'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
bootJar {
archiveFileName = "state-check.jar"
}
jar {
enabled = false // plain.jar 안 만들기(혼동 방지)
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
maven { url "https://repo.osgeo.org/repository/release/" }
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.boot:spring-boot-starter-validation'
//geometry
implementation 'com.fasterxml.jackson.core:jackson-databind'
implementation "org.geotools:gt-shapefile:30.0"
implementation "org.geotools:gt-referencing:30.0"
implementation "org.geotools:gt-geojson:30.0"
implementation 'org.locationtech.jts.io:jts-io-common:1.20.0'
implementation 'org.locationtech.jts:jts-core:1.19.0'
implementation 'org.hibernate:hibernate-spatial:6.2.7.Final'
implementation 'org.geotools:gt-main:30.0'
implementation("org.geotools:gt-geotiff:30.0") {
exclude group: "javax.media", module: "jai_core"
}
implementation 'org.geotools:gt-epsg-hsql:30.0'
// QueryDSL JPA
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
// Q클래스 생성용 annotationProcessor
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
// actuator
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// SpringDoc OpenAPI (Swagger)
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
// Apache Commons Compress for archive handling
implementation 'org.apache.commons:commons-compress:1.26.0'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
implementation 'org.reflections:reflections:0.10.2'
implementation 'org.locationtech.jts:jts-core:1.19.0'
implementation 'org.locationtech.jts.io:jts-io-common:1.19.0'
}
configurations.configureEach {
exclude group: 'javax.media', module: 'jai_core'
}
tasks.named('test') {
useJUnitPlatform()
}
// Spotless configuration for code formatting (2-space indent)
spotless {
java {
target 'src/**/*.java'
googleJavaFormat('1.19.2') // Default Google Style = 2 spaces (NO .aosp()!)
trimTrailingWhitespace()
endWithNewline()
}
}
// Run spotlessCheck before build
tasks.named('build') {
dependsOn 'spotlessCheck'
}

Some files were not shown because too many files have changed in this diff Show More