블로그 게시판 관련 java 수정
This commit is contained in:
parent
65905e4e5d
commit
38cb5608d8
@ -0,0 +1,16 @@
|
||||
package com.itn.admin.itn.blog.mapper;
|
||||
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogAccountVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface BlogAccountMapper {
|
||||
List<BlogAccountVO> getBlogAccountList();
|
||||
List<BlogAccountVO> getBlogAccountListWithStats();
|
||||
BlogAccountVO getBlogAccountDetail(Long blogId);
|
||||
int insertBlogAccount(BlogAccountVO blogAccountVO);
|
||||
int updateBlogAccount(BlogAccountVO blogAccountVO);
|
||||
int deleteBlogAccount(Long blogId);
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package com.itn.admin.itn.blog.mapper;
|
||||
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogCookieMappingVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface BlogCookieMappingMapper {
|
||||
|
||||
/**
|
||||
* 블로그 계정별 쿠키 매핑 정보 조회
|
||||
*/
|
||||
BlogCookieMappingVO selectCookieMappingByBlogId(@Param("blogId") Long blogId);
|
||||
|
||||
/**
|
||||
* 활성화된 쿠키 매핑 정보 조회
|
||||
*/
|
||||
List<BlogCookieMappingVO> selectActiveCookieMappings();
|
||||
|
||||
/**
|
||||
* 쿠키 매핑 정보 저장
|
||||
*/
|
||||
int insertCookieMapping(BlogCookieMappingVO cookieMappingVO);
|
||||
|
||||
/**
|
||||
* 쿠키 매핑 정보 업데이트
|
||||
*/
|
||||
int updateCookieMapping(BlogCookieMappingVO cookieMappingVO);
|
||||
|
||||
/**
|
||||
* 쿠키 유효성 검증 시간 업데이트
|
||||
*/
|
||||
int updateLastValidated(@Param("mappingId") Long mappingId);
|
||||
|
||||
/**
|
||||
* 쿠키 만료 시간 업데이트
|
||||
*/
|
||||
int updateCookieExpiration(@Param("mappingId") Long mappingId,
|
||||
@Param("cookieExpiresAt") java.time.LocalDateTime cookieExpiresAt);
|
||||
|
||||
/**
|
||||
* 쿠키 매핑 비활성화
|
||||
*/
|
||||
int deactivateCookieMapping(@Param("blogId") Long blogId);
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package com.itn.admin.itn.blog.mapper;
|
||||
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogPostHistoryVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface BlogPostingMapper {
|
||||
// 발행 히스토리 관련 메서드
|
||||
int insertBlogPostHistory(BlogPostHistoryVO historyVO);
|
||||
List<BlogPostHistoryVO> selectBlogPostHistories(@Param("blogId") String blogId,
|
||||
@Param("urlId") String urlId,
|
||||
@Param("limit") int limit,
|
||||
@Param("offset") int offset);
|
||||
|
||||
// 워크플로우 단계별 업데이트 메서드
|
||||
int updateHtmlGenerated(BlogPostHistoryVO historyVO);
|
||||
int updatePublishStarted(BlogPostHistoryVO historyVO);
|
||||
int updatePublishCompleted(BlogPostHistoryVO historyVO);
|
||||
int updatePublishFailed(BlogPostHistoryVO historyVO);
|
||||
|
||||
// 발행 히스토리 조회 메서드
|
||||
BlogPostHistoryVO selectBlogPostHistoryById(@Param("postId") Long postId);
|
||||
|
||||
// 소스별 발행 통계 조회
|
||||
List<java.util.Map<String, Object>> selectPublishStatsBySource(@Param("blogId") Long blogId);
|
||||
|
||||
// 워크플로우 히스토리 조회 (필터링 및 페이징 지원)
|
||||
List<BlogPostHistoryVO> selectWorkflowHistory(@Param("blogId") String blogId,
|
||||
@Param("urlId") String urlId,
|
||||
@Param("status") String status,
|
||||
@Param("limit") int limit,
|
||||
@Param("offset") int offset);
|
||||
|
||||
// 워크플로우 통계 조회
|
||||
java.util.Map<String, Object> selectWorkflowStatistics(@Param("blogId") Long blogId);
|
||||
|
||||
// 실패 원인별 분석
|
||||
List<java.util.Map<String, Object>> selectFailureAnalysis(@Param("blogId") Long blogId, @Param("days") int days);
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
package com.itn.admin.itn.blog.mapper;
|
||||
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogScheduleExecutionVO;
|
||||
import com.itn.admin.itn.blog.mapper.domain.ScheduleSearchDTO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface BlogScheduleExecutionMapper {
|
||||
|
||||
// 기본 CRUD 작업
|
||||
int insertScheduleExecution(BlogScheduleExecutionVO executionVO);
|
||||
int updateScheduleExecution(BlogScheduleExecutionVO executionVO);
|
||||
BlogScheduleExecutionVO getScheduleExecutionDetail(Long executionId);
|
||||
|
||||
// 특정 스케줄의 실행 이력 조회
|
||||
List<BlogScheduleExecutionVO> getExecutionHistory(@Param("scheduleId") Long scheduleId,
|
||||
@Param("searchDTO") ScheduleSearchDTO searchDTO);
|
||||
|
||||
int getExecutionHistoryCount(@Param("scheduleId") Long scheduleId,
|
||||
@Param("searchDTO") ScheduleSearchDTO searchDTO);
|
||||
|
||||
// 전체 실행 이력 조회 (관리자용)
|
||||
List<BlogScheduleExecutionVO> getAllExecutionHistory(ScheduleSearchDTO searchDTO);
|
||||
int getAllExecutionHistoryCount(ScheduleSearchDTO searchDTO);
|
||||
|
||||
// 실행 상태별 조회
|
||||
List<BlogScheduleExecutionVO> getExecutionsByStatus(@Param("status") String status,
|
||||
@Param("limit") int limit);
|
||||
|
||||
List<BlogScheduleExecutionVO> getRunningExecutions();
|
||||
|
||||
List<BlogScheduleExecutionVO> getPendingExecutions(@Param("limit") int limit);
|
||||
|
||||
List<BlogScheduleExecutionVO> getFailedExecutions(@Param("fromTime") LocalDateTime fromTime,
|
||||
@Param("toTime") LocalDateTime toTime);
|
||||
|
||||
// 성능 통계 조회
|
||||
List<BlogScheduleExecutionVO> getSlowExecutions(@Param("thresholdMs") int thresholdMs,
|
||||
@Param("limit") int limit);
|
||||
|
||||
Double getAverageExecutionTime(@Param("scheduleId") Long scheduleId,
|
||||
@Param("days") int days);
|
||||
|
||||
// 실행 상태 업데이트
|
||||
int updateExecutionStatus(@Param("executionId") Long executionId,
|
||||
@Param("status") String status,
|
||||
@Param("resultMessage") String resultMessage);
|
||||
|
||||
int updateExecutionStartTime(@Param("executionId") Long executionId,
|
||||
@Param("startedAt") LocalDateTime startedAt);
|
||||
|
||||
int updateExecutionEndTime(@Param("executionId") Long executionId,
|
||||
@Param("completedAt") LocalDateTime completedAt,
|
||||
@Param("executionTimeMs") Integer executionTimeMs);
|
||||
|
||||
int updateExecutionError(@Param("executionId") Long executionId,
|
||||
@Param("errorDetails") String errorDetails,
|
||||
@Param("resultMessage") String resultMessage);
|
||||
|
||||
// 스케줄별 통계
|
||||
BlogScheduleExecutionVO getLastExecutionBySchedule(@Param("scheduleId") Long scheduleId);
|
||||
|
||||
int getSuccessCountBySchedule(@Param("scheduleId") Long scheduleId,
|
||||
@Param("days") int days);
|
||||
|
||||
int getFailureCountBySchedule(@Param("scheduleId") Long scheduleId,
|
||||
@Param("days") int days);
|
||||
|
||||
int getTotalExecutionCount(@Param("scheduleId") Long scheduleId);
|
||||
|
||||
// 재시도 관련
|
||||
List<BlogScheduleExecutionVO> getRetryableExecutions(@Param("currentTime") LocalDateTime currentTime,
|
||||
@Param("maxRetryInterval") int maxRetryInterval);
|
||||
|
||||
int incrementAttemptCount(@Param("executionId") Long executionId);
|
||||
|
||||
// 정리 작업
|
||||
int deleteOldExecutions(@Param("beforeDate") LocalDateTime beforeDate);
|
||||
|
||||
int deleteExecutionsBySchedule(@Param("scheduleId") Long scheduleId);
|
||||
|
||||
// 통계용 집계 쿼리
|
||||
List<Object[]> getExecutionStatsGroupByHour(@Param("fromTime") LocalDateTime fromTime,
|
||||
@Param("toTime") LocalDateTime toTime);
|
||||
|
||||
List<Object[]> getExecutionStatsGroupByDay(@Param("fromTime") LocalDateTime fromTime,
|
||||
@Param("toTime") LocalDateTime toTime);
|
||||
|
||||
List<Object[]> getExecutionStatsGroupByBlog(@Param("fromTime") LocalDateTime fromTime,
|
||||
@Param("toTime") LocalDateTime toTime,
|
||||
@Param("limit") int limit);
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
package com.itn.admin.itn.blog.mapper;
|
||||
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogScheduleVO;
|
||||
import com.itn.admin.itn.blog.mapper.domain.ScheduleSearchDTO;
|
||||
import com.itn.admin.itn.blog.mapper.domain.ScheduleStatisticsDTO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface BlogScheduleMapper {
|
||||
|
||||
// 기본 CRUD 작업
|
||||
int insertBlogSchedule(BlogScheduleVO blogScheduleVO);
|
||||
int updateBlogSchedule(BlogScheduleVO blogScheduleVO);
|
||||
int deleteBlogSchedule(Long scheduleId);
|
||||
BlogScheduleVO getBlogScheduleDetail(Long scheduleId);
|
||||
|
||||
// 목록 조회 (검색 조건 + 페이징)
|
||||
List<BlogScheduleVO> getBlogScheduleList(ScheduleSearchDTO searchDTO);
|
||||
int getBlogScheduleCount(ScheduleSearchDTO searchDTO);
|
||||
|
||||
// 통계 포함 상세 목록 조회
|
||||
List<BlogScheduleVO> getBlogScheduleListWithStats(ScheduleSearchDTO searchDTO);
|
||||
|
||||
// 스케줄링 전용 조회 메서드들
|
||||
List<BlogScheduleVO> getActiveSchedulesByTime(@Param("currentTime") LocalDateTime currentTime,
|
||||
@Param("bufferMinutes") int bufferMinutes);
|
||||
|
||||
List<BlogScheduleVO> getPendingSchedulesByPriority();
|
||||
|
||||
List<BlogScheduleVO> getFailedSchedulesForRetry(@Param("currentTime") LocalDateTime currentTime);
|
||||
|
||||
// 상태 업데이트
|
||||
int updateScheduleStatus(@Param("scheduleId") Long scheduleId,
|
||||
@Param("status") String status);
|
||||
|
||||
int updateScheduleNextExecution(@Param("scheduleId") Long scheduleId,
|
||||
@Param("nextExecuteAt") LocalDateTime nextExecuteAt);
|
||||
|
||||
int incrementScheduleExecutionCount(@Param("scheduleId") Long scheduleId);
|
||||
|
||||
// 중복 실행 방지를 위한 락킹
|
||||
int lockScheduleForExecution(@Param("scheduleId") Long scheduleId,
|
||||
@Param("serverInfo") String serverInfo);
|
||||
|
||||
int unlockSchedule(@Param("scheduleId") Long scheduleId);
|
||||
|
||||
// 통계 조회
|
||||
ScheduleStatisticsDTO getOverallStatistics();
|
||||
|
||||
List<ScheduleStatisticsDTO.BlogScheduleStatsDTO> getBlogScheduleStats(@Param("limit") int limit);
|
||||
|
||||
List<ScheduleStatisticsDTO.HourlyExecutionStatsDTO> getHourlyExecutionStats();
|
||||
|
||||
List<ScheduleStatisticsDTO.DailyExecutionStatsDTO> getDailyExecutionStats(@Param("days") int days);
|
||||
|
||||
// 유지보수를 위한 쿼리들
|
||||
int deleteCompletedSchedules(@Param("beforeDate") LocalDateTime beforeDate);
|
||||
|
||||
int cleanupFailedSchedules(@Param("beforeDate") LocalDateTime beforeDate,
|
||||
@Param("maxFailures") int maxFailures);
|
||||
|
||||
List<BlogScheduleVO> getSchedulesRequiringCleanup(@Param("beforeDate") LocalDateTime beforeDate);
|
||||
|
||||
// 블로그별 스케줄 조회
|
||||
List<BlogScheduleVO> getSchedulesByBlogId(@Param("blogId") Long blogId,
|
||||
@Param("status") String status);
|
||||
|
||||
// 다음 실행 예정 스케줄 조회
|
||||
List<BlogScheduleVO> getUpcomingSchedules(@Param("fromTime") LocalDateTime fromTime,
|
||||
@Param("toTime") LocalDateTime toTime,
|
||||
@Param("limit") int limit);
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.itn.admin.itn.blog.mapper;
|
||||
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogSourceVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface BlogSourceMapper {
|
||||
List<BlogSourceVO> selectBlogSourceList();
|
||||
List<BlogSourceVO> selectBlogSourceListWithStats(Long blogId);
|
||||
BlogSourceVO getBlogSourceDetail(Long sourceId);
|
||||
int insertBlogSource(BlogSourceVO blogSourceVO);
|
||||
int updateBlogSource(BlogSourceVO blogSourceVO);
|
||||
int deleteBlogSource(Long sourceId);
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package com.itn.admin.itn.blog.mapper.domain;
|
||||
|
||||
import com.itn.admin.cmn.vo.CmnVO;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime; // For datetime fields
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
public class BlogAccountVO extends CmnVO {
|
||||
private Long blogId; // 블로그 고유 ID (bigint, AUTO_INCREMENT)
|
||||
private String platform; // 플랫폼 (예: NAVER, TISTORY)
|
||||
private String blogName; // 사용자가 식별할 블로그 이름
|
||||
private String blogUrl; // 블로그 주소
|
||||
private String apiKey; // API 키
|
||||
private String apiSecret; // API 시크릿 키
|
||||
private String authInfo1; // 추가 인증 정보 1
|
||||
private String authInfo2; // 추가 인증 정보 2
|
||||
private String status; // 계정 활성 상태 (enum 'Y','N')
|
||||
|
||||
private String platformNm;
|
||||
|
||||
// 발행 통계 필드 추가
|
||||
private Integer totalPostCount; // 총 포스팅 발행 횟수
|
||||
private LocalDateTime lastPublishedAt; // 마지막 발행 시간
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package com.itn.admin.itn.blog.mapper.domain;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
public class BlogAccountWithSourcesDTO {
|
||||
private BlogAccountVO account;
|
||||
private List<BlogSourceVO> sources;
|
||||
}
|
||||
@ -0,0 +1,114 @@
|
||||
package com.itn.admin.itn.blog.mapper.domain;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 블로그 계정별 쿠키 파일 매핑 정보 VO
|
||||
*/
|
||||
public class BlogCookieMappingVO {
|
||||
|
||||
private Long mappingId;
|
||||
private Long blogId;
|
||||
private String cookieFilePath;
|
||||
private String cookieFileName;
|
||||
private String isActive;
|
||||
private LocalDateTime lastValidatedAt;
|
||||
private LocalDateTime cookieExpiresAt;
|
||||
private String frstRegisterId;
|
||||
private LocalDateTime frstRegistPnttm;
|
||||
private String lastUpdusrId;
|
||||
private LocalDateTime lastUpdtPnttm;
|
||||
|
||||
// 조인 필드
|
||||
private String blogName;
|
||||
private String blogUrl;
|
||||
private String platform;
|
||||
|
||||
// 기본 생성자
|
||||
public BlogCookieMappingVO() {}
|
||||
|
||||
// 편의 생성자
|
||||
public BlogCookieMappingVO(Long blogId, String cookieFilePath, String cookieFileName) {
|
||||
this.blogId = blogId;
|
||||
this.cookieFilePath = cookieFilePath;
|
||||
this.cookieFileName = cookieFileName;
|
||||
this.isActive = "Y";
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿠키 파일 전체 경로 반환
|
||||
*/
|
||||
public String getFullCookieFilePath() {
|
||||
if (cookieFilePath == null || cookieFileName == null) {
|
||||
return null;
|
||||
}
|
||||
return cookieFilePath.endsWith("/") ?
|
||||
cookieFilePath + cookieFileName :
|
||||
cookieFilePath + "/" + cookieFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿠키가 활성 상태인지 확인
|
||||
*/
|
||||
public boolean isActiveCookie() {
|
||||
return "Y".equals(isActive);
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿠키가 만료되었는지 확인
|
||||
*/
|
||||
public boolean isCookieExpired() {
|
||||
return cookieExpiresAt != null && LocalDateTime.now().isAfter(cookieExpiresAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿠키 유효성 검증이 필요한지 확인 (마지막 검증 후 1시간 경과)
|
||||
*/
|
||||
public boolean needsValidation() {
|
||||
return lastValidatedAt == null ||
|
||||
LocalDateTime.now().isAfter(lastValidatedAt.plusHours(1));
|
||||
}
|
||||
|
||||
// Getter & Setter
|
||||
public Long getMappingId() { return mappingId; }
|
||||
public void setMappingId(Long mappingId) { this.mappingId = mappingId; }
|
||||
|
||||
public Long getBlogId() { return blogId; }
|
||||
public void setBlogId(Long blogId) { this.blogId = blogId; }
|
||||
|
||||
public String getCookieFilePath() { return cookieFilePath; }
|
||||
public void setCookieFilePath(String cookieFilePath) { this.cookieFilePath = cookieFilePath; }
|
||||
|
||||
public String getCookieFileName() { return cookieFileName; }
|
||||
public void setCookieFileName(String cookieFileName) { this.cookieFileName = cookieFileName; }
|
||||
|
||||
public String getIsActive() { return isActive; }
|
||||
public void setIsActive(String isActive) { this.isActive = isActive; }
|
||||
|
||||
public LocalDateTime getLastValidatedAt() { return lastValidatedAt; }
|
||||
public void setLastValidatedAt(LocalDateTime lastValidatedAt) { this.lastValidatedAt = lastValidatedAt; }
|
||||
|
||||
public LocalDateTime getCookieExpiresAt() { return cookieExpiresAt; }
|
||||
public void setCookieExpiresAt(LocalDateTime cookieExpiresAt) { this.cookieExpiresAt = cookieExpiresAt; }
|
||||
|
||||
public String getFrstRegisterId() { return frstRegisterId; }
|
||||
public void setFrstRegisterId(String frstRegisterId) { this.frstRegisterId = frstRegisterId; }
|
||||
|
||||
public LocalDateTime getFrstRegistPnttm() { return frstRegistPnttm; }
|
||||
public void setFrstRegistPnttm(LocalDateTime frstRegistPnttm) { this.frstRegistPnttm = frstRegistPnttm; }
|
||||
|
||||
public String getLastUpdusrId() { return lastUpdusrId; }
|
||||
public void setLastUpdusrId(String lastUpdusrId) { this.lastUpdusrId = lastUpdusrId; }
|
||||
|
||||
public LocalDateTime getLastUpdtPnttm() { return lastUpdtPnttm; }
|
||||
public void setLastUpdtPnttm(LocalDateTime lastUpdtPnttm) { this.lastUpdtPnttm = lastUpdtPnttm; }
|
||||
|
||||
public String getBlogName() { return blogName; }
|
||||
public void setBlogName(String blogName) { this.blogName = blogName; }
|
||||
|
||||
public String getBlogUrl() { return blogUrl; }
|
||||
public void setBlogUrl(String blogUrl) { this.blogUrl = blogUrl; }
|
||||
|
||||
public String getPlatform() { return platform; }
|
||||
public void setPlatform(String platform) { this.platform = platform; }
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package com.itn.admin.itn.blog.mapper.domain;
|
||||
|
||||
import com.itn.admin.cmn.vo.CmnVO;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
public class BlogPostHistoryVO extends CmnVO {
|
||||
// 상태 상수 정의
|
||||
public static final String STATUS_IN_PROGRESS = "I";
|
||||
public static final String STATUS_SUCCESS = "S";
|
||||
public static final String STATUS_FAILED = "F";
|
||||
|
||||
private Long postId; // DB의 post_id (AUTO_INCREMENT)
|
||||
private Long blogId; // blog_accounts.blog_id
|
||||
private Long urlId; // blog_source_urls.url_id (소스-포스트 매칭)
|
||||
private String postTitle; // 포스트 제목
|
||||
private String postContent; // 포스트 내용 (longtext)
|
||||
private String publishedUrl; // 발행된 URL
|
||||
private String status; // enum('I','S','F') - I(In Progress), S(Success), F(Failed)
|
||||
private String errorMessage; // 에러 메시지
|
||||
private LocalDateTime publishedAt; // 발행 일시
|
||||
private LocalDateTime htmlGeneratedAt; // HTML 생성 완료 시간
|
||||
private LocalDateTime publishStartedAt; // 발행 시작 시간
|
||||
|
||||
// JOIN용 필드들
|
||||
private String blogName; // from blog_accounts
|
||||
private String sourceTitle; // from blog_source_urls
|
||||
|
||||
// 상태 확인 편의 메서드
|
||||
public boolean isInProgress() { return STATUS_IN_PROGRESS.equals(this.status); }
|
||||
public boolean isSuccess() { return STATUS_SUCCESS.equals(this.status); }
|
||||
public boolean isFailed() { return STATUS_FAILED.equals(this.status); }
|
||||
|
||||
// 워크플로우 단계 확인 메서드
|
||||
public boolean isHtmlGenerated() { return this.htmlGeneratedAt != null; }
|
||||
public boolean isPublishStarted() { return this.publishStartedAt != null; }
|
||||
public boolean isPublishCompleted() { return this.publishedAt != null; }
|
||||
|
||||
// MyBatis 결과 매핑용 별칭 (하위 호환성)
|
||||
public Long getHistoryId() { return this.postId; }
|
||||
public void setHistoryId(Long historyId) { this.postId = historyId; }
|
||||
|
||||
public String getPublishStatus() { return this.status; }
|
||||
public void setPublishStatus(String status) { this.status = status; }
|
||||
}
|
||||
@ -0,0 +1,84 @@
|
||||
package com.itn.admin.itn.blog.mapper.domain;
|
||||
|
||||
import com.itn.admin.cmn.vo.CmnVO;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
public class BlogScheduleExecutionVO extends CmnVO {
|
||||
|
||||
// 기본 정보
|
||||
private Long executionId; // 실행 이력 ID (PK)
|
||||
private Long scheduleId; // 스케줄 ID (FK)
|
||||
|
||||
// 실행 시간 정보
|
||||
private LocalDateTime executedAt; // 실행 시도 시간
|
||||
private LocalDateTime startedAt; // 실제 실행 시작 시간
|
||||
private LocalDateTime completedAt; // 실행 완료 시간
|
||||
|
||||
// 실행 상태
|
||||
private String status; // 실행 상태 (PENDING/RUNNING/SUCCESS/FAILED/CANCELLED)
|
||||
private String resultMessage; // 실행 결과 메시지
|
||||
private String errorDetails; // 에러 상세 정보
|
||||
|
||||
// 실행 메타데이터
|
||||
private Integer attemptCount; // 시도 횟수 (기본값: 1)
|
||||
private String publishedUrl; // 발행된 URL
|
||||
private Integer executionTimeMs; // 실행 시간 (밀리초)
|
||||
private String serverInfo; // 실행 서버 정보
|
||||
|
||||
// JOIN용 필드들 (조회 시 사용)
|
||||
private String scheduleTitle; // from blog_schedules.title
|
||||
private String blogName; // from blog_accounts.blog_name
|
||||
private String schedulePriority; // from blog_schedules.priority
|
||||
|
||||
// 편의 메서드들
|
||||
public boolean isSuccess() {
|
||||
return "SUCCESS".equals(this.status);
|
||||
}
|
||||
|
||||
public boolean isFailed() {
|
||||
return "FAILED".equals(this.status);
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return "RUNNING".equals(this.status);
|
||||
}
|
||||
|
||||
public boolean isPending() {
|
||||
return "PENDING".equals(this.status);
|
||||
}
|
||||
|
||||
public boolean isCancelled() {
|
||||
return "CANCELLED".equals(this.status);
|
||||
}
|
||||
|
||||
// 실행 시간 계산 (밀리초)
|
||||
public Long getActualExecutionTimeMs() {
|
||||
if (startedAt != null && completedAt != null) {
|
||||
return java.time.Duration.between(startedAt, completedAt).toMillis();
|
||||
}
|
||||
return this.executionTimeMs != null ? this.executionTimeMs.longValue() : null;
|
||||
}
|
||||
|
||||
// 실행 결과 요약
|
||||
public String getExecutionSummary() {
|
||||
if (isSuccess()) {
|
||||
return "성공 (" + (executionTimeMs != null ? executionTimeMs + "ms" : "시간 미측정") + ")";
|
||||
} else if (isFailed()) {
|
||||
return "실패 (" + attemptCount + "번째 시도)";
|
||||
} else if (isRunning()) {
|
||||
return "실행 중...";
|
||||
} else if (isPending()) {
|
||||
return "대기 중";
|
||||
} else if (isCancelled()) {
|
||||
return "취소됨";
|
||||
}
|
||||
return "알 수 없는 상태";
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
package com.itn.admin.itn.blog.mapper.domain;
|
||||
|
||||
import com.itn.admin.cmn.vo.CmnVO;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
public class BlogScheduleVO extends CmnVO {
|
||||
|
||||
// 기본 정보
|
||||
private Long scheduleId; // 예약 ID (PK)
|
||||
private Long blogId; // 블로그 계정 ID (FK)
|
||||
private Long urlId; // 소스 URL ID (FK)
|
||||
private String title; // 예약 제목
|
||||
private String content; // 예약 내용
|
||||
|
||||
// 스케줄링 설정
|
||||
private String scheduleType; // 스케줄 유형 (ONE_TIME/RECURRING)
|
||||
private LocalDateTime scheduledAt; // 예약 실행 시간
|
||||
private String repeatInterval; // 반복 간격 (DAILY/WEEKLY/MONTHLY)
|
||||
private Integer repeatValue; // 반복 주기 값 (기본값: 1)
|
||||
private LocalDateTime endAt; // 반복 종료 시간
|
||||
|
||||
// 상태 및 우선순위
|
||||
private String status; // 스케줄 상태 (ACTIVE/INACTIVE/COMPLETED/FAILED)
|
||||
private String priority; // 실행 우선순위 (HIGH/NORMAL/LOW)
|
||||
|
||||
// 재시도 설정
|
||||
private Integer maxRetries; // 최대 재시도 횟수 (기본값: 3)
|
||||
private Integer retryInterval; // 재시도 간격 (분, 기본값: 5)
|
||||
|
||||
// 알림 설정
|
||||
private String notificationEmail; // 알림 이메일
|
||||
private String slackChannel; // 슬랙 채널명
|
||||
private Boolean enableNotification; // 알림 활성화 여부 (기본값: false)
|
||||
|
||||
// JOIN용 필드들 (조회 시 사용)
|
||||
private String blogName; // from blog_accounts.blog_name
|
||||
private String blogUrl; // from blog_accounts.blog_url
|
||||
private String sourceUrl; // from blog_source_urls.url
|
||||
private String sourceTitle; // from blog_source_urls.title
|
||||
|
||||
// 통계 정보 (조회 시 사용)
|
||||
private Integer totalExecutions; // 총 실행 횟수
|
||||
private Integer successExecutions; // 성공 실행 횟수
|
||||
private Integer failedExecutions; // 실패 실행 횟수
|
||||
private LocalDateTime lastExecutedAt; // 마지막 실행 시간
|
||||
private LocalDateTime nextExecuteAt; // 다음 실행 예정 시간
|
||||
|
||||
// 편의 메서드들
|
||||
public boolean isActive() {
|
||||
return "ACTIVE".equals(this.status);
|
||||
}
|
||||
|
||||
public boolean isRecurring() {
|
||||
return "RECURRING".equals(this.scheduleType);
|
||||
}
|
||||
|
||||
public boolean isHighPriority() {
|
||||
return "HIGH".equals(this.priority);
|
||||
}
|
||||
|
||||
public boolean isNotificationEnabled() {
|
||||
return Boolean.TRUE.equals(this.enableNotification);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package com.itn.admin.itn.blog.mapper.domain;
|
||||
|
||||
import com.itn.admin.cmn.vo.CmnVO;
|
||||
import lombok.*;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
public class BlogSourceVO extends CmnVO {
|
||||
private Long urlId;
|
||||
private String url;
|
||||
private String title;
|
||||
private String category;
|
||||
private String isActive;
|
||||
|
||||
// 발행 통계 필드 추가
|
||||
private Integer totalPublishCount; // 총 발행 횟수
|
||||
private Date lastPublishedAt; // 마지막 발행 시간
|
||||
private String lastPublishedUrl; // 마지막 발행된 URL
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.itn.admin.itn.blog.service;
|
||||
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogAccountVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface BlogAccountService {
|
||||
List<BlogAccountVO> getBlogAccountList();
|
||||
List<BlogAccountVO> getBlogAccountListWithStats();
|
||||
BlogAccountVO getBlogAccountDetail(Long blogId);
|
||||
RestResponse insertBlogAccount(BlogAccountVO blogAccountVO);
|
||||
RestResponse updateBlogAccount(BlogAccountVO blogAccountVO);
|
||||
RestResponse deleteBlogAccount(Long blogId);
|
||||
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package com.itn.admin.itn.blog.service;
|
||||
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogCookieMappingVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface BlogCookieService {
|
||||
|
||||
/**
|
||||
* 블로그 계정별 쿠키 매핑 정보 조회
|
||||
*/
|
||||
BlogCookieMappingVO getCookieMappingByBlogId(Long blogId);
|
||||
|
||||
/**
|
||||
* 활성화된 쿠키 매핑 정보 목록 조회
|
||||
*/
|
||||
List<BlogCookieMappingVO> getActiveCookieMappings();
|
||||
|
||||
/**
|
||||
* 쿠키 매핑 정보 저장
|
||||
*/
|
||||
void saveCookieMapping(BlogCookieMappingVO cookieMappingVO);
|
||||
|
||||
/**
|
||||
* 쿠키 유효성 검증 및 검증 시간 업데이트
|
||||
*/
|
||||
boolean validateAndUpdateCookie(Long blogId);
|
||||
|
||||
/**
|
||||
* 쿠키 만료 시간 업데이트
|
||||
*/
|
||||
void updateCookieExpiration(Long mappingId, java.time.LocalDateTime expiresAt);
|
||||
|
||||
/**
|
||||
* 쿠키 매핑 비활성화
|
||||
*/
|
||||
void deactivateCookieMapping(Long blogId);
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package com.itn.admin.itn.blog.service;
|
||||
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogAccountWithSourcesDTO;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogPostHistoryVO;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface BlogPostingService {
|
||||
BlogAccountWithSourcesDTO getAccountWithSources(Long blogId);
|
||||
|
||||
/**
|
||||
* 워크플로우 히스토리 조회 (페이징 지원)
|
||||
*/
|
||||
List<BlogPostHistoryVO> getWorkflowHistory(String blogId, String urlId, String status, int limit, int offset);
|
||||
|
||||
/**
|
||||
* 워크플로우 통계 조회
|
||||
*/
|
||||
Map<String, Object> getWorkflowStatistics(Long blogId);
|
||||
|
||||
/**
|
||||
* 실시간 진행상황 조회
|
||||
*/
|
||||
BlogPostHistoryVO getPublishProgress(Long postId);
|
||||
|
||||
/**
|
||||
* 실패 원인별 분석
|
||||
*/
|
||||
List<Map<String, Object>> getFailureAnalysis(Long blogId, int days);
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.itn.admin.itn.blog.service;
|
||||
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogPostHistoryVO;
|
||||
|
||||
public interface BlogPublishService {
|
||||
|
||||
/**
|
||||
* 블로그 포스팅 발행
|
||||
*
|
||||
* @param blogId 블로그 계정 ID
|
||||
* @param urlId 소스 URL ID
|
||||
* @param sourceUrl 소스 URL
|
||||
* @return 발행 히스토리 VO
|
||||
*/
|
||||
BlogPostHistoryVO publishPost(String blogId, String urlId, String sourceUrl);
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.itn.admin.itn.blog.service;
|
||||
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogSourceVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface BlogSourceService {
|
||||
List<BlogSourceVO> getBlogSourceList();
|
||||
BlogSourceVO getBlogSourceDetail(Long sourceId);
|
||||
RestResponse insertBlogSource(BlogSourceVO blogSourceVO);
|
||||
RestResponse updateBlogSource(BlogSourceVO blogSourceVO);
|
||||
RestResponse deleteBlogSource(Long sourceId);
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
package com.itn.admin.itn.blog.service.impl;
|
||||
|
||||
import com.itn.admin.cmn.util.thymeleafUtils.TCodeUtils;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogAccountVO;
|
||||
import com.itn.admin.itn.blog.mapper.BlogAccountMapper;
|
||||
import com.itn.admin.itn.blog.service.BlogAccountService;
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class BlogAccountServiceImpl implements BlogAccountService {
|
||||
|
||||
@Autowired
|
||||
TCodeUtils tCodeUtils;
|
||||
|
||||
private final BlogAccountMapper blogAccountMapper;
|
||||
|
||||
public BlogAccountServiceImpl(BlogAccountMapper blogAccountMapper) {
|
||||
this.blogAccountMapper = blogAccountMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BlogAccountVO> getBlogAccountList() {
|
||||
List<BlogAccountVO> list = blogAccountMapper.getBlogAccountList();
|
||||
list.forEach(blogAccountVO -> {
|
||||
blogAccountVO.setPlatformNm(tCodeUtils.getCodeName("BLOG_PLATFORM", blogAccountVO.getPlatform()));
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BlogAccountVO> getBlogAccountListWithStats() {
|
||||
List<BlogAccountVO> list = blogAccountMapper.getBlogAccountListWithStats();
|
||||
list.forEach(blogAccountVO -> {
|
||||
blogAccountVO.setPlatformNm(tCodeUtils.getCodeName("BLOG_PLATFORM", blogAccountVO.getPlatform()));
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlogAccountVO getBlogAccountDetail(Long blogId) {
|
||||
return blogAccountMapper.getBlogAccountDetail(blogId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestResponse insertBlogAccount(BlogAccountVO blogAccountVO) {
|
||||
int result = blogAccountMapper.insertBlogAccount(blogAccountVO);
|
||||
if (result > 0) {
|
||||
return new RestResponse(HttpStatus.OK, "등록되었습니다.", blogAccountVO.getBlogId());
|
||||
} else {
|
||||
return new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR, "등록에 실패했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestResponse updateBlogAccount(BlogAccountVO blogAccountVO) {
|
||||
int result = blogAccountMapper.updateBlogAccount(blogAccountVO);
|
||||
if (result > 0) {
|
||||
return new RestResponse(HttpStatus.OK, "수정되었습니다.", blogAccountVO.getBlogId());
|
||||
} else {
|
||||
return new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR, "수정에 실패했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestResponse deleteBlogAccount(Long blogId) {
|
||||
int result = blogAccountMapper.deleteBlogAccount(blogId);
|
||||
if (result > 0) {
|
||||
return new RestResponse(HttpStatus.OK, "삭제되었습니다.", blogId);
|
||||
} else {
|
||||
return new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR, "삭제에 실패했습니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,118 @@
|
||||
package com.itn.admin.itn.blog.service.impl;
|
||||
|
||||
import com.itn.admin.cmn.util.tistory.TistoryCookieUtil;
|
||||
import com.itn.admin.itn.blog.mapper.BlogCookieMappingMapper;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogCookieMappingVO;
|
||||
import com.itn.admin.itn.blog.service.BlogCookieService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@Transactional
|
||||
public class BlogCookieServiceImpl implements BlogCookieService {
|
||||
|
||||
@Autowired
|
||||
private BlogCookieMappingMapper blogCookieMappingMapper;
|
||||
|
||||
@Autowired
|
||||
private TistoryCookieUtil tistoryCookieUtil;
|
||||
|
||||
@Override
|
||||
public BlogCookieMappingVO getCookieMappingByBlogId(Long blogId) {
|
||||
try {
|
||||
return blogCookieMappingMapper.selectCookieMappingByBlogId(blogId);
|
||||
} catch (Exception e) {
|
||||
log.error("블로그 쿠키 매핑 정보 조회 실패: blogId={}", blogId, e);
|
||||
throw new RuntimeException("쿠키 매핑 정보 조회에 실패했습니다.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BlogCookieMappingVO> getActiveCookieMappings() {
|
||||
try {
|
||||
return blogCookieMappingMapper.selectActiveCookieMappings();
|
||||
} catch (Exception e) {
|
||||
log.error("활성 쿠키 매핑 목록 조회 실패", e);
|
||||
throw new RuntimeException("활성 쿠키 매핑 목록 조회에 실패했습니다.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveCookieMapping(BlogCookieMappingVO cookieMappingVO) {
|
||||
try {
|
||||
if (cookieMappingVO.getMappingId() == null) {
|
||||
// 새로운 매핑 저장
|
||||
cookieMappingVO.setFrstRegisterId("SYSTEM");
|
||||
cookieMappingVO.setLastUpdusrId("SYSTEM");
|
||||
blogCookieMappingMapper.insertCookieMapping(cookieMappingVO);
|
||||
log.info("새로운 쿠키 매핑 저장 완료: blogId={}, file={}",
|
||||
cookieMappingVO.getBlogId(), cookieMappingVO.getCookieFileName());
|
||||
} else {
|
||||
// 기존 매핑 업데이트
|
||||
cookieMappingVO.setLastUpdusrId("SYSTEM");
|
||||
blogCookieMappingMapper.updateCookieMapping(cookieMappingVO);
|
||||
log.info("쿠키 매핑 업데이트 완료: mappingId={}", cookieMappingVO.getMappingId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("쿠키 매핑 저장 실패: {}", cookieMappingVO, e);
|
||||
throw new RuntimeException("쿠키 매핑 저장에 실패했습니다.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateAndUpdateCookie(Long blogId) {
|
||||
try {
|
||||
BlogCookieMappingVO cookieMapping = getCookieMappingByBlogId(blogId);
|
||||
|
||||
if (cookieMapping == null) {
|
||||
log.warn("쿠키 매핑 정보를 찾을 수 없습니다: blogId={}", blogId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 쿠키 유효성 검증
|
||||
boolean isValid = tistoryCookieUtil.validateCookieFile(cookieMapping);
|
||||
|
||||
if (isValid) {
|
||||
// 검증 성공 시 검증 시간 업데이트
|
||||
blogCookieMappingMapper.updateLastValidated(cookieMapping.getMappingId());
|
||||
log.info("쿠키 유효성 검증 성공: blogId={}", blogId);
|
||||
} else {
|
||||
log.warn("쿠키 유효성 검증 실패: blogId={}", blogId);
|
||||
}
|
||||
|
||||
return isValid;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("쿠키 유효성 검증 중 오류 발생: blogId={}", blogId, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateCookieExpiration(Long mappingId, LocalDateTime expiresAt) {
|
||||
try {
|
||||
blogCookieMappingMapper.updateCookieExpiration(mappingId, expiresAt);
|
||||
log.info("쿠키 만료 시간 업데이트 완료: mappingId={}, expiresAt={}", mappingId, expiresAt);
|
||||
} catch (Exception e) {
|
||||
log.error("쿠키 만료 시간 업데이트 실패: mappingId={}", mappingId, e);
|
||||
throw new RuntimeException("쿠키 만료 시간 업데이트에 실패했습니다.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivateCookieMapping(Long blogId) {
|
||||
try {
|
||||
blogCookieMappingMapper.deactivateCookieMapping(blogId);
|
||||
log.info("쿠키 매핑 비활성화 완료: blogId={}", blogId);
|
||||
} catch (Exception e) {
|
||||
log.error("쿠키 매핑 비활성화 실패: blogId={}", blogId, e);
|
||||
throw new RuntimeException("쿠키 매핑 비활성화에 실패했습니다.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
package com.itn.admin.itn.blog.service.impl;
|
||||
|
||||
import com.itn.admin.itn.blog.mapper.BlogPostingMapper;
|
||||
import com.itn.admin.itn.blog.mapper.BlogAccountMapper;
|
||||
import com.itn.admin.itn.blog.mapper.BlogSourceMapper;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogAccountVO;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogSourceVO;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogAccountWithSourcesDTO;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogPostHistoryVO;
|
||||
import com.itn.admin.itn.blog.service.BlogPostingService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class BlogPostingServiceImpl implements BlogPostingService {
|
||||
|
||||
@Autowired
|
||||
private BlogPostingMapper blogPostingMapper;
|
||||
|
||||
@Autowired
|
||||
private BlogAccountMapper blogAccountMapper;
|
||||
|
||||
@Autowired
|
||||
private BlogSourceMapper blogSourceMapper;
|
||||
|
||||
|
||||
@Override
|
||||
public BlogAccountWithSourcesDTO getAccountWithSources(Long blogId) {
|
||||
BlogAccountVO account = blogAccountMapper.getBlogAccountDetail(blogId);
|
||||
List<BlogSourceVO> sources = blogSourceMapper.selectBlogSourceListWithStats(blogId);
|
||||
|
||||
BlogAccountWithSourcesDTO result = new BlogAccountWithSourcesDTO();
|
||||
result.setAccount(account);
|
||||
result.setSources(sources);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BlogPostHistoryVO> getWorkflowHistory(String blogId, String urlId, String status, int limit, int offset) {
|
||||
return blogPostingMapper.selectWorkflowHistory(blogId, urlId, status, limit, offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getWorkflowStatistics(Long blogId) {
|
||||
return blogPostingMapper.selectWorkflowStatistics(blogId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlogPostHistoryVO getPublishProgress(Long postId) {
|
||||
return blogPostingMapper.selectBlogPostHistoryById(postId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getFailureAnalysis(Long blogId, int days) {
|
||||
return blogPostingMapper.selectFailureAnalysis(blogId, days);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,220 @@
|
||||
package com.itn.admin.itn.blog.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.itn.admin.itn.blog.mapper.BlogPostingMapper;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogPostHistoryVO;
|
||||
import com.itn.admin.itn.blog.service.BlogPublishService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class BlogPublishServiceImpl implements BlogPublishService {
|
||||
|
||||
private final BlogPostingMapper blogPostingMapper;
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Value("${blog.generate.url:http://192.168.0.78:5000/blog/generate}")
|
||||
private String blogGenerateUrl;
|
||||
|
||||
@Override
|
||||
public BlogPostHistoryVO publishPost(String blogId, String urlId, String sourceUrl) {
|
||||
log.info("블로그 포스팅 발행 시작: blogId={}, urlId={}, sourceUrl={}", blogId, urlId, sourceUrl);
|
||||
|
||||
BlogPostHistoryVO historyVO = new BlogPostHistoryVO();
|
||||
historyVO.setBlogId(Long.parseLong(blogId));
|
||||
// sourceUrl을 publishedUrl로 임시 설정 (추후 실제 발행 URL로 업데이트)
|
||||
historyVO.setPublishedUrl(sourceUrl);
|
||||
historyVO.setPublishedAt(LocalDateTime.now());
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
// Python 서비스에 요청
|
||||
Map<String, String> requestBody = new HashMap<>();
|
||||
requestBody.put("url", sourceUrl);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
HttpEntity<Map<String, String>> requestEntity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
log.info("Python 서비스 요청: URL={}, Body={}", blogGenerateUrl, requestBody);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
blogGenerateUrl,
|
||||
HttpMethod.POST,
|
||||
requestEntity,
|
||||
String.class
|
||||
);
|
||||
|
||||
long endTime = System.currentTimeMillis();
|
||||
int responseTime = (int)(endTime - startTime);
|
||||
|
||||
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
|
||||
String htmlContent = response.getBody();
|
||||
|
||||
// HTML에서 타이틀 추출
|
||||
String extractedTitle = extractTitleFromHtml(htmlContent);
|
||||
|
||||
// 성공 히스토리 저장
|
||||
historyVO.setPostContent(htmlContent);
|
||||
historyVO.setPostTitle(extractedTitle); // 추출된 타이틀 저장
|
||||
historyVO.setStatus("Y");
|
||||
|
||||
// 발행된 URL 추출 (HTML에서 canonical URL이나 meta 정보에서 추출 가능)
|
||||
String publishedUrl = extractPublishedUrl(htmlContent, sourceUrl);
|
||||
historyVO.setPublishedUrl(publishedUrl);
|
||||
|
||||
// 히스토리 저장
|
||||
blogPostingMapper.insertBlogPostHistory(historyVO);
|
||||
|
||||
// 통계 업데이트는 현재 시스템에서 불필요 (매핑 테이블 미사용)
|
||||
// TODO: 향후 매핑 테이블 활용 시 주석 해제
|
||||
// updatePublishStatistics(blogId, urlId, historyVO.getPublishedAt(), publishedUrl);
|
||||
|
||||
log.info("블로그 포스팅 발행 성공: postId={}, responseTime={}ms",
|
||||
historyVO.getPostId(), responseTime);
|
||||
|
||||
} else {
|
||||
throw new RuntimeException("Python 서비스에서 빈 응답을 받았습니다.");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("블로그 포스팅 발행 실패: blogId={}, urlId={}, error={}", blogId, urlId, e.getMessage(), e);
|
||||
|
||||
long endTime = System.currentTimeMillis();
|
||||
int responseTime = (int)(endTime - startTime);
|
||||
|
||||
// 실패 히스토리 저장
|
||||
historyVO.setStatus("N");
|
||||
historyVO.setErrorMessage(e.getMessage());
|
||||
|
||||
blogPostingMapper.insertBlogPostHistory(historyVO);
|
||||
|
||||
// 예외를 다시 throw하여 컨트롤러에서 처리할 수 있도록 함
|
||||
throw new RuntimeException("포스팅 발행에 실패했습니다: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
return historyVO;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML 컨텐츠에서 타이틀 추출
|
||||
*
|
||||
* @param htmlContent HTML 컨텐츠
|
||||
* @return 추출된 타이틀 (최대 255자)
|
||||
*/
|
||||
private String extractTitleFromHtml(String htmlContent) {
|
||||
if (!StringUtils.hasText(htmlContent)) {
|
||||
return "제목 없음";
|
||||
}
|
||||
|
||||
try {
|
||||
Document doc = Jsoup.parse(htmlContent);
|
||||
|
||||
// 1순위: <title> 태그에서 추출
|
||||
Element titleElement = doc.selectFirst("title");
|
||||
if (titleElement != null && StringUtils.hasText(titleElement.text())) {
|
||||
String title = titleElement.text().trim();
|
||||
return truncateTitle(title);
|
||||
}
|
||||
|
||||
// 2순위: og:title 메타 태그에서 추출
|
||||
Element ogTitleElement = doc.selectFirst("meta[property=og:title]");
|
||||
if (ogTitleElement != null && StringUtils.hasText(ogTitleElement.attr("content"))) {
|
||||
String title = ogTitleElement.attr("content").trim();
|
||||
return truncateTitle(title);
|
||||
}
|
||||
|
||||
// 3순위: h1 태그에서 추출
|
||||
Element h1Element = doc.selectFirst("h1");
|
||||
if (h1Element != null && StringUtils.hasText(h1Element.text())) {
|
||||
String title = h1Element.text().trim();
|
||||
return truncateTitle(title);
|
||||
}
|
||||
|
||||
// 4순위: twitter:title 메타 태그에서 추출
|
||||
Element twitterTitleElement = doc.selectFirst("meta[name=twitter:title]");
|
||||
if (twitterTitleElement != null && StringUtils.hasText(twitterTitleElement.attr("content"))) {
|
||||
String title = twitterTitleElement.attr("content").trim();
|
||||
return truncateTitle(title);
|
||||
}
|
||||
|
||||
log.warn("HTML에서 타이틀을 찾을 수 없습니다. 기본값을 사용합니다.");
|
||||
return "제목 없음";
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("HTML 타이틀 추출 중 오류 발생: {}", e.getMessage(), e);
|
||||
return "제목 추출 실패";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 타이틀을 데이터베이스 제한 길이에 맞게 자르기
|
||||
*
|
||||
* @param title 원본 타이틀
|
||||
* @return 잘린 타이틀 (최대 255자)
|
||||
*/
|
||||
private String truncateTitle(String title) {
|
||||
if (title == null) {
|
||||
return "제목 없음";
|
||||
}
|
||||
|
||||
// 데이터베이스 VARCHAR(255) 제한에 맞춰 자르기
|
||||
if (title.length() > 255) {
|
||||
return title.substring(0, 252) + "...";
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML 컨텐츠에서 발행된 URL 추출
|
||||
* 실제 구현에서는 HTML 파싱을 통해 canonical URL이나
|
||||
* 블로그 플랫폼별 URL 패턴을 찾아야 함
|
||||
*/
|
||||
private String extractPublishedUrl(String htmlContent, String sourceUrl) {
|
||||
if (!StringUtils.hasText(htmlContent)) {
|
||||
return sourceUrl + "#published";
|
||||
}
|
||||
|
||||
try {
|
||||
Document doc = Jsoup.parse(htmlContent);
|
||||
|
||||
// 1순위: canonical URL 추출
|
||||
Element canonicalElement = doc.selectFirst("link[rel=canonical]");
|
||||
if (canonicalElement != null && StringUtils.hasText(canonicalElement.attr("href"))) {
|
||||
return canonicalElement.attr("href");
|
||||
}
|
||||
|
||||
// 2순위: og:url 메타 태그에서 추출
|
||||
Element ogUrlElement = doc.selectFirst("meta[property=og:url]");
|
||||
if (ogUrlElement != null && StringUtils.hasText(ogUrlElement.attr("content"))) {
|
||||
return ogUrlElement.attr("content");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("HTML에서 발행된 URL 추출 중 오류 발생: {}", e.getMessage(), e);
|
||||
}
|
||||
|
||||
// 기본값: 소스 URL 기반으로 블로그 URL 생성
|
||||
return sourceUrl + "#published";
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
package com.itn.admin.itn.blog.service.impl;
|
||||
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogSourceVO;
|
||||
import com.itn.admin.itn.blog.mapper.BlogSourceMapper;
|
||||
import com.itn.admin.itn.blog.service.BlogSourceService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class BlogSourceServiceImpl implements BlogSourceService {
|
||||
|
||||
private final BlogSourceMapper blogSourceMapper;
|
||||
|
||||
@Override
|
||||
public List<BlogSourceVO> getBlogSourceList() {
|
||||
return blogSourceMapper.selectBlogSourceList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlogSourceVO getBlogSourceDetail(Long sourceId) {
|
||||
return blogSourceMapper.getBlogSourceDetail(sourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestResponse insertBlogSource(BlogSourceVO blogSourceVO) {
|
||||
int result = blogSourceMapper.insertBlogSource(blogSourceVO);
|
||||
if (result > 0) {
|
||||
return new RestResponse(HttpStatus.OK, "등록되었습니다.");
|
||||
} else {
|
||||
return new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR, "등록에 실패했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestResponse updateBlogSource(BlogSourceVO blogSourceVO) {
|
||||
int result = blogSourceMapper.updateBlogSource(blogSourceVO);
|
||||
if (result > 0) {
|
||||
return new RestResponse(HttpStatus.OK, "수정되었습니다.");
|
||||
} else {
|
||||
return new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR, "수정에 실패했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestResponse deleteBlogSource(Long sourceId) {
|
||||
int result = blogSourceMapper.deleteBlogSource(sourceId);
|
||||
if (result > 0) {
|
||||
return new RestResponse(HttpStatus.OK, "삭제되었습니다.");
|
||||
} else {
|
||||
return new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR, "삭제에 실패했습니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
package com.itn.admin.itn.blog.web;
|
||||
|
||||
import com.itn.admin.cmn.config.CustomUserDetails;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogAccountVO;
|
||||
import com.itn.admin.itn.blog.service.BlogAccountService;
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/blog/accounts")
|
||||
public class BlogAccountController {
|
||||
|
||||
private final BlogAccountService blogAccountService;
|
||||
|
||||
public BlogAccountController(BlogAccountService blogAccountService) {
|
||||
this.blogAccountService = blogAccountService;
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
public String getBlogAccountListPage() {
|
||||
return "itn/blog/account/list";
|
||||
}
|
||||
|
||||
@GetMapping("/detail/{blogId}")
|
||||
public String getBlogAccountDetailPage(@PathVariable Long blogId, Model model) {
|
||||
BlogAccountVO detail = blogAccountService.getBlogAccountDetail(blogId);
|
||||
model.addAttribute("blogAccount", detail);
|
||||
return "itn/blog/account/edit";
|
||||
}
|
||||
|
||||
@GetMapping("/edit")
|
||||
public String editBlogAccountPage(Model model) {
|
||||
model.addAttribute("blogAccount", new BlogAccountVO());
|
||||
return "itn/blog/account/edit";
|
||||
}
|
||||
|
||||
@GetMapping("/api/list")
|
||||
@ResponseBody
|
||||
public ResponseEntity<List<BlogAccountVO>> getBlogAccountList() {
|
||||
List<BlogAccountVO> list = blogAccountService.getBlogAccountList();
|
||||
return ResponseEntity.ok(list);
|
||||
}
|
||||
|
||||
@GetMapping("/api/detail/{blogId}")
|
||||
@ResponseBody
|
||||
public ResponseEntity<BlogAccountVO> getBlogAccountDetail(@PathVariable Long blogId) {
|
||||
BlogAccountVO detail = blogAccountService.getBlogAccountDetail(blogId);
|
||||
return ResponseEntity.ok(detail);
|
||||
}
|
||||
|
||||
@PostMapping("/api/insert")
|
||||
@ResponseBody
|
||||
public ResponseEntity<RestResponse> insertBlogAccount(@RequestBody BlogAccountVO blogAccountVO
|
||||
, @AuthenticationPrincipal CustomUserDetails loginUser) {
|
||||
|
||||
blogAccountVO.setFrstRegisterId(loginUser.getUser().getUniqId());
|
||||
blogAccountVO.setLastUpdusrId(loginUser.getUser().getUniqId());
|
||||
|
||||
return ResponseEntity.ok().body(blogAccountService.insertBlogAccount(blogAccountVO));
|
||||
}
|
||||
|
||||
@PutMapping("/api/update")
|
||||
@ResponseBody
|
||||
public ResponseEntity<RestResponse> updateBlogAccount(@RequestBody BlogAccountVO blogAccountVO
|
||||
, @AuthenticationPrincipal CustomUserDetails loginUser) {
|
||||
|
||||
blogAccountVO.setLastUpdusrId(loginUser.getUser().getUniqId());
|
||||
|
||||
return ResponseEntity.ok().body(blogAccountService.updateBlogAccount(blogAccountVO));
|
||||
}
|
||||
|
||||
@DeleteMapping("/api/delete/{blogId}")
|
||||
@ResponseBody
|
||||
public ResponseEntity<RestResponse> deleteBlogAccount(@PathVariable Long blogId) {
|
||||
return ResponseEntity.ok().body(blogAccountService.deleteBlogAccount(blogId));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,179 @@
|
||||
package com.itn.admin.itn.blog.web;
|
||||
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
import com.itn.admin.cmn.util.tistory.TistoryCookieUtil;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogCookieMappingVO;
|
||||
import com.itn.admin.itn.blog.service.BlogCookieService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 블로그 쿠키 매핑 테스트 컨트롤러
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/blog/cookie/test")
|
||||
public class BlogCookieTestController {
|
||||
|
||||
@Autowired
|
||||
private BlogCookieService blogCookieService;
|
||||
|
||||
@Autowired
|
||||
private TistoryCookieUtil tistoryCookieUtil;
|
||||
|
||||
/**
|
||||
* 블로그별 쿠키 매핑 정보 조회 테스트
|
||||
*/
|
||||
@GetMapping("/mapping/{blogId}")
|
||||
public ResponseEntity<RestResponse> testCookieMapping(@PathVariable Long blogId) {
|
||||
try {
|
||||
BlogCookieMappingVO cookieMapping = blogCookieService.getCookieMappingByBlogId(blogId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
if (cookieMapping != null) {
|
||||
result.put("found", true);
|
||||
result.put("mappingId", cookieMapping.getMappingId());
|
||||
result.put("blogId", cookieMapping.getBlogId());
|
||||
result.put("cookieFilePath", cookieMapping.getCookieFilePath());
|
||||
result.put("cookieFileName", cookieMapping.getCookieFileName());
|
||||
result.put("fullPath", cookieMapping.getFullCookieFilePath());
|
||||
result.put("isActive", cookieMapping.isActiveCookie());
|
||||
result.put("isExpired", cookieMapping.isCookieExpired());
|
||||
result.put("needsValidation", cookieMapping.needsValidation());
|
||||
result.put("blogName", cookieMapping.getBlogName());
|
||||
result.put("blogUrl", cookieMapping.getBlogUrl());
|
||||
|
||||
log.info("쿠키 매핑 정보 조회 성공: {}", result);
|
||||
} else {
|
||||
result.put("found", false);
|
||||
result.put("message", "해당 블로그의 쿠키 매핑 정보를 찾을 수 없습니다.");
|
||||
|
||||
log.warn("쿠키 매핑 정보 없음: blogId={}", blogId);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.OK,
|
||||
"쿠키 매핑 정보 조회 완료", result));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("쿠키 매핑 정보 조회 실패: blogId={}", blogId, e);
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"쿠키 매핑 정보 조회 실패: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 블로그별 쿠키 문자열 생성 테스트
|
||||
*/
|
||||
@GetMapping("/cookie-string/{blogId}")
|
||||
public ResponseEntity<RestResponse> testCookieString(@PathVariable Long blogId) {
|
||||
try {
|
||||
BlogCookieMappingVO cookieMapping = blogCookieService.getCookieMappingByBlogId(blogId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
if (cookieMapping != null) {
|
||||
// 블로그별 쿠키 문자열 생성
|
||||
String cookieString = tistoryCookieUtil.getCookieStringForBlog(cookieMapping);
|
||||
|
||||
result.put("success", true);
|
||||
result.put("cookieLength", cookieString != null ? cookieString.length() : 0);
|
||||
result.put("cookiePreview", cookieString != null && cookieString.length() > 100 ?
|
||||
cookieString.substring(0, 100) + "..." : cookieString);
|
||||
result.put("containsTSSession", cookieString != null && cookieString.contains("TSSESSION"));
|
||||
result.put("containsTistory", cookieString != null && cookieString.contains("tistory"));
|
||||
|
||||
log.info("블로그별 쿠키 문자열 생성 성공: blogId={}, length={}",
|
||||
blogId, cookieString != null ? cookieString.length() : 0);
|
||||
} else {
|
||||
result.put("success", false);
|
||||
result.put("message", "해당 블로그의 쿠키 매핑 정보를 찾을 수 없습니다.");
|
||||
|
||||
log.warn("쿠키 매핑 정보 없음: blogId={}", blogId);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.OK,
|
||||
"쿠키 문자열 생성 테스트 완료", result));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("쿠키 문자열 생성 테스트 실패: blogId={}", blogId, e);
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"쿠키 문자열 생성 테스트 실패: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿠키 유효성 검증 테스트
|
||||
*/
|
||||
@PostMapping("/validate/{blogId}")
|
||||
public ResponseEntity<RestResponse> testCookieValidation(@PathVariable Long blogId) {
|
||||
try {
|
||||
boolean isValid = blogCookieService.validateAndUpdateCookie(blogId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("blogId", blogId);
|
||||
result.put("isValid", isValid);
|
||||
result.put("validationTime", java.time.LocalDateTime.now());
|
||||
|
||||
log.info("쿠키 유효성 검증 완료: blogId={}, isValid={}", blogId, isValid);
|
||||
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.OK,
|
||||
"쿠키 유효성 검증 완료", result));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("쿠키 유효성 검증 실패: blogId={}", blogId, e);
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"쿠키 유효성 검증 실패: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 쿠키와 블로그별 쿠키 비교 테스트
|
||||
*/
|
||||
@GetMapping("/compare/{blogId}")
|
||||
public ResponseEntity<RestResponse> compareCookies(@PathVariable Long blogId) {
|
||||
try {
|
||||
BlogCookieMappingVO cookieMapping = blogCookieService.getCookieMappingByBlogId(blogId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
// 기본 쿠키 문자열
|
||||
String defaultCookie = tistoryCookieUtil.getTistoryCookieString();
|
||||
result.put("defaultCookieLength", defaultCookie != null ? defaultCookie.length() : 0);
|
||||
result.put("defaultCookiePreview", defaultCookie != null && defaultCookie.length() > 50 ?
|
||||
defaultCookie.substring(0, 50) + "..." : defaultCookie);
|
||||
|
||||
if (cookieMapping != null) {
|
||||
// 블로그별 쿠키 문자열
|
||||
String blogCookie = tistoryCookieUtil.getCookieStringForBlog(cookieMapping);
|
||||
result.put("blogCookieLength", blogCookie != null ? blogCookie.length() : 0);
|
||||
result.put("blogCookiePreview", blogCookie != null && blogCookie.length() > 50 ?
|
||||
blogCookie.substring(0, 50) + "..." : blogCookie);
|
||||
|
||||
// 비교 결과
|
||||
result.put("isSame", defaultCookie != null && defaultCookie.equals(blogCookie));
|
||||
result.put("cookieFilePath", cookieMapping.getFullCookieFilePath());
|
||||
} else {
|
||||
result.put("blogCookieLength", 0);
|
||||
result.put("blogCookiePreview", "매핑 정보 없음");
|
||||
result.put("isSame", false);
|
||||
}
|
||||
|
||||
log.info("쿠키 비교 테스트 완료: blogId={}", blogId);
|
||||
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.OK,
|
||||
"쿠키 비교 테스트 완료", result));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("쿠키 비교 테스트 실패: blogId={}", blogId, e);
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"쿠키 비교 테스트 실패: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,337 @@
|
||||
package com.itn.admin.itn.blog.web;
|
||||
|
||||
import com.itn.admin.itn.blog.mapper.BlogPostingMapper;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogAccountWithSourcesDTO;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogAccountVO;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogPostHistoryVO;
|
||||
import com.itn.admin.itn.blog.mapper.domain.PostingRequestDTO;
|
||||
import com.itn.admin.itn.blog.mapper.domain.TistoryPublishRequestDTO;
|
||||
import com.itn.admin.itn.blog.service.BlogPostingService;
|
||||
import com.itn.admin.itn.blog.service.BlogAccountService;
|
||||
import com.itn.admin.itn.blog.service.BlogPublishService;
|
||||
import com.itn.admin.itn.blog.service.TistoryPublishService;
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/blog/posting")
|
||||
public class BlogPostingApiController {
|
||||
|
||||
@Autowired
|
||||
private BlogPostingService blogPostingService;
|
||||
|
||||
@Autowired
|
||||
private BlogAccountService blogAccountService;
|
||||
|
||||
@Autowired
|
||||
private BlogPublishService blogPublishService;
|
||||
|
||||
@Autowired
|
||||
private TistoryPublishService tistoryPublishService;
|
||||
|
||||
@Autowired
|
||||
private BlogPostingMapper blogPostingMapper;
|
||||
|
||||
@GetMapping("/list")
|
||||
public ResponseEntity<RestResponse> getBlogAccountList() {
|
||||
try {
|
||||
List<BlogAccountVO> list = blogAccountService.getBlogAccountListWithStats();
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.OK, "조회되었습니다.", list));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR, "조회에 실패했습니다."));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/manage/{blogId}")
|
||||
@ResponseBody
|
||||
public ResponseEntity<RestResponse> getAccountWithSources(@PathVariable Long blogId) {
|
||||
try {
|
||||
BlogAccountWithSourcesDTO data = blogPostingService.getAccountWithSources(blogId);
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.OK, "조회되었습니다.", data));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR, "조회에 실패했습니다."));
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/setting")
|
||||
@ResponseBody
|
||||
public ResponseEntity<RestResponse> saveSetting(@RequestBody PostingRequestDTO request) {
|
||||
try {
|
||||
// 포스팅 설정 저장 로직
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.OK, "포스팅 설정이 저장되었습니다."));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR, "설정 저장에 실패했습니다."));
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/execute")
|
||||
@ResponseBody
|
||||
public ResponseEntity<RestResponse> executePosting(@RequestBody PostingRequestDTO request) {
|
||||
try {
|
||||
String blogId = request.getBlogId();
|
||||
String sourceId = request.getSourceIds().get(0); // 첫 번째 소스 ID
|
||||
|
||||
// TODO: sourceId로부터 실제 URL을 조회하는 로직 필요
|
||||
String sourceUrl = "https://example.com/post"; // 임시
|
||||
|
||||
BlogPostHistoryVO history = blogPublishService.publishPost(blogId, sourceId, sourceUrl);
|
||||
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.OK, "포스팅이 성공적으로 실행되었습니다.", history));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR, "포스팅 실행에 실패했습니다: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 개별 포스팅 발행 (기존 단순 발행 - 하위 호환성 유지)
|
||||
*/
|
||||
@PostMapping("/publish")
|
||||
@ResponseBody
|
||||
public ResponseEntity<RestResponse> publishSinglePost(
|
||||
@RequestParam String blogId,
|
||||
@RequestParam String urlId,
|
||||
@RequestParam String sourceUrl) {
|
||||
try {
|
||||
BlogPostHistoryVO history = blogPublishService.publishPost(blogId, urlId, sourceUrl);
|
||||
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.OK,
|
||||
"포스팅이 성공적으로 발행되었습니다.", history));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"포스팅 발행에 실패했습니다: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 통합 발행 워크플로우 (HTML 생성 + 티스토리 발행)
|
||||
*/
|
||||
@PostMapping("/publish/integrated")
|
||||
@ResponseBody
|
||||
public ResponseEntity<RestResponse> publishIntegrated(
|
||||
@RequestParam Long blogId,
|
||||
@RequestParam Long urlId,
|
||||
@RequestParam String sourceUrl) {
|
||||
try {
|
||||
// 입력 유효성 검증
|
||||
if (blogId == null || urlId == null || sourceUrl == null || sourceUrl.trim().isEmpty()) {
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.BAD_REQUEST,
|
||||
"필수 파라미터가 누락되었습니다. (blogId, urlId, sourceUrl)"));
|
||||
}
|
||||
|
||||
// 통합 워크플로우 실행
|
||||
BlogPostHistoryVO history = tistoryPublishService.publishWithHtmlGeneration(blogId, urlId, sourceUrl);
|
||||
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.OK,
|
||||
"통합 발행 워크플로우가 성공적으로 완료되었습니다.", history));
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"통합 발행 워크플로우 실패: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 포스팅 히스토리 조회
|
||||
*/
|
||||
@GetMapping("/history")
|
||||
@ResponseBody
|
||||
public ResponseEntity<RestResponse> getPostingHistory(
|
||||
@RequestParam(required = false) String blogId,
|
||||
@RequestParam(required = false) String urlId,
|
||||
@RequestParam(defaultValue = "10") int limit,
|
||||
@RequestParam(defaultValue = "0") int offset) {
|
||||
try {
|
||||
// TODO: BlogPostingService에 히스토리 조회 메서드 추가 필요
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.OK, "조회되었습니다."));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR, "조회에 실패했습니다."));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 발행 진행상황 실시간 조회 (특정 postId)
|
||||
*/
|
||||
@GetMapping("/publish/status/{postId}")
|
||||
@ResponseBody
|
||||
public ResponseEntity<RestResponse> getPublishStatus(@PathVariable Long postId) {
|
||||
try {
|
||||
BlogPostHistoryVO history = blogPostingMapper.selectBlogPostHistoryById(postId);
|
||||
|
||||
if (history == null) {
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.NOT_FOUND,
|
||||
"해당 발행 기록을 찾을 수 없습니다."));
|
||||
}
|
||||
|
||||
// 진행상황 요약 정보 생성
|
||||
java.util.Map<String, Object> statusInfo = new java.util.HashMap<>();
|
||||
statusInfo.put("postId", history.getPostId());
|
||||
statusInfo.put("status", history.getStatus());
|
||||
statusInfo.put("postTitle", history.getPostTitle());
|
||||
statusInfo.put("isInProgress", history.isInProgress());
|
||||
statusInfo.put("isSuccess", history.isSuccess());
|
||||
statusInfo.put("isFailed", history.isFailed());
|
||||
statusInfo.put("isHtmlGenerated", history.isHtmlGenerated());
|
||||
statusInfo.put("isPublishStarted", history.isPublishStarted());
|
||||
statusInfo.put("isPublishCompleted", history.isPublishCompleted());
|
||||
statusInfo.put("htmlGeneratedAt", history.getHtmlGeneratedAt());
|
||||
statusInfo.put("publishStartedAt", history.getPublishStartedAt());
|
||||
statusInfo.put("publishedAt", history.getPublishedAt());
|
||||
statusInfo.put("publishedUrl", history.getPublishedUrl());
|
||||
statusInfo.put("errorMessage", history.getErrorMessage());
|
||||
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.OK,
|
||||
"진행상황을 조회했습니다.", statusInfo));
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"진행상황 조회에 실패했습니다: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 소스별 발행 통계 조회
|
||||
*/
|
||||
@GetMapping("/stats/sources/{blogId}")
|
||||
@ResponseBody
|
||||
public ResponseEntity<RestResponse> getSourcePublishStats(@PathVariable Long blogId) {
|
||||
try {
|
||||
java.util.List<java.util.Map<String, Object>> stats =
|
||||
blogPostingMapper.selectPublishStatsBySource(blogId);
|
||||
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.OK,
|
||||
"소스별 발행 통계를 조회했습니다.", stats));
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"통계 조회에 실패했습니다: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 티스토리 자동 배포
|
||||
*/
|
||||
@PostMapping("/publish/tistory")
|
||||
@ResponseBody
|
||||
public ResponseEntity<RestResponse> publishToTistory(
|
||||
@RequestParam String title,
|
||||
@RequestParam String htmlContent,
|
||||
@RequestParam Long blogId,
|
||||
@RequestParam(required = false) String sourceUrl) {
|
||||
try {
|
||||
BlogPostHistoryVO history;
|
||||
|
||||
if (sourceUrl != null && !sourceUrl.trim().isEmpty()) {
|
||||
history = tistoryPublishService.publishToTistory(title, htmlContent, sourceUrl, blogId);
|
||||
} else {
|
||||
history = tistoryPublishService.publishToTistory(title, htmlContent, blogId);
|
||||
}
|
||||
|
||||
if ("Y".equals(history.getStatus())) {
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.OK,
|
||||
"티스토리에 성공적으로 발행되었습니다.", history));
|
||||
} else {
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"티스토리 발행에 실패했습니다: " + history.getErrorMessage()));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"티스토리 발행 중 오류가 발생했습니다: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Python 블로그 생성 서비스 상태 확인
|
||||
*/
|
||||
@GetMapping("/service/status")
|
||||
@ResponseBody
|
||||
public ResponseEntity<RestResponse> checkServiceStatus() {
|
||||
try {
|
||||
Map<String, Object> status = tistoryPublishService.checkBlogGenerationServiceStatus();
|
||||
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.OK,
|
||||
"서비스 상태를 확인했습니다.", status));
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"서비스 상태 확인에 실패했습니다: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 티스토리 로그인 상태 및 토큰 확인
|
||||
*/
|
||||
@GetMapping("/tistory/check-auth")
|
||||
@ResponseBody
|
||||
public ResponseEntity<RestResponse> checkTistoryAuth() {
|
||||
try {
|
||||
// 티스토리 토큰 획득 시도
|
||||
boolean authValid = true;
|
||||
String errorMessage = null;
|
||||
|
||||
try {
|
||||
// private 메서드를 직접 호출할 수 없으므로 임시 HTML 생성으로 테스트
|
||||
Map<String, Object> result = tistoryPublishService.checkBlogGenerationServiceStatus();
|
||||
// 실제로는 TistoryPublishService에 public 메서드 추가 필요
|
||||
|
||||
Map<String, Object> authStatus = new HashMap<>();
|
||||
authStatus.put("isAuthenticated", authValid);
|
||||
authStatus.put("message", authValid ? "티스토리 인증 상태가 정상입니다." : errorMessage);
|
||||
authStatus.put("checkedAt", java.time.LocalDateTime.now());
|
||||
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.OK,
|
||||
"티스토리 인증 상태를 확인했습니다.", authStatus));
|
||||
|
||||
} catch (Exception e) {
|
||||
authValid = false;
|
||||
errorMessage = e.getMessage();
|
||||
|
||||
Map<String, Object> authStatus = new HashMap<>();
|
||||
authStatus.put("isAuthenticated", false);
|
||||
authStatus.put("message", "티스토리 인증 실패: " + errorMessage);
|
||||
authStatus.put("checkedAt", java.time.LocalDateTime.now());
|
||||
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.OK,
|
||||
"티스토리 인증 상태를 확인했습니다.", authStatus));
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"인증 상태 확인에 실패했습니다: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML과 제목으로 티스토리 자동 배포 (간편 버전)
|
||||
*/
|
||||
@PostMapping("/publish/tistory/simple")
|
||||
@ResponseBody
|
||||
public ResponseEntity<RestResponse> publishToTistorySimple(
|
||||
@RequestBody TistoryPublishRequestDTO request) {
|
||||
try {
|
||||
BlogPostHistoryVO history = tistoryPublishService.publishToTistory(
|
||||
request.getTitle(),
|
||||
request.getHtmlContent(),
|
||||
request.getSourceUrl(),
|
||||
request.getBlogId()
|
||||
);
|
||||
|
||||
if ("Y".equals(history.getStatus())) {
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.OK,
|
||||
"티스토리에 성공적으로 발행되었습니다.", history));
|
||||
} else {
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"티스토리 발행에 실패했습니다: " + history.getErrorMessage()));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"티스토리 발행 중 오류가 발생했습니다: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package com.itn.admin.itn.blog.web;
|
||||
|
||||
import com.itn.admin.itn.blog.service.BlogPostingService;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogAccountWithSourcesDTO;
|
||||
import com.itn.admin.itn.blog.mapper.domain.PostingRequestDTO;
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/blog/posting")
|
||||
@RequiredArgsConstructor
|
||||
public class BlogPostingController {
|
||||
|
||||
private final BlogPostingService blogPostingService;
|
||||
|
||||
@GetMapping("/list")
|
||||
public String getBlogPostingListPage() {
|
||||
return "itn/blog/posting/list";
|
||||
}
|
||||
|
||||
@GetMapping("/form")
|
||||
public String getBlogPostingFormPage() {
|
||||
return "itn/blog/posting/edit";
|
||||
}
|
||||
|
||||
@GetMapping("/manage/{blogId}")
|
||||
public String getBlogPostingManagePage(@PathVariable Long blogId, Model model) {
|
||||
model.addAttribute("blogId", blogId);
|
||||
return "itn/blog/posting/manage";
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
package com.itn.admin.itn.blog.web;
|
||||
|
||||
import com.itn.admin.cmn.config.CustomUserDetails;
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogSourceVO;
|
||||
import com.itn.admin.itn.blog.service.BlogSourceService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/blog/sources")
|
||||
@RequiredArgsConstructor
|
||||
public class BlogSourceController {
|
||||
|
||||
private final BlogSourceService blogSourceService;
|
||||
|
||||
@GetMapping("/list")
|
||||
public String getBlogSourceListPage() {
|
||||
return "itn/blog/source/list";
|
||||
}
|
||||
|
||||
@GetMapping("/detail/{sourceId}")
|
||||
public String getBlogSourceDetailPage(@PathVariable Long sourceId, Model model) {
|
||||
BlogSourceVO detail = blogSourceService.getBlogSourceDetail(sourceId);
|
||||
model.addAttribute("blogSource", detail);
|
||||
return "itn/blog/source/edit";
|
||||
}
|
||||
|
||||
@GetMapping("/edit")
|
||||
public String getBlogSourceEditPage(Model model) {
|
||||
model.addAttribute("blogSource", new BlogSourceVO());
|
||||
return "itn/blog/source/edit";
|
||||
}
|
||||
|
||||
@GetMapping("/api/list")
|
||||
@ResponseBody
|
||||
public ResponseEntity<List<BlogSourceVO>> getBlogSourceList() {
|
||||
List<BlogSourceVO> list = blogSourceService.getBlogSourceList();
|
||||
return ResponseEntity.ok(list);
|
||||
}
|
||||
|
||||
@PostMapping("/api/insert")
|
||||
@ResponseBody
|
||||
public ResponseEntity<RestResponse> insertBlogSource(@RequestBody BlogSourceVO blogSourceVO
|
||||
, @AuthenticationPrincipal CustomUserDetails loginUser) {
|
||||
|
||||
blogSourceVO.setFrstRegisterId(loginUser.getUser().getUniqId());
|
||||
blogSourceVO.setLastUpdusrId(loginUser.getUser().getUniqId());
|
||||
|
||||
return ResponseEntity.ok().body(blogSourceService.insertBlogSource(blogSourceVO));
|
||||
}
|
||||
|
||||
@PutMapping("/api/update")
|
||||
@ResponseBody
|
||||
public ResponseEntity<RestResponse> updateBlogSource(@RequestBody BlogSourceVO blogSourceVO
|
||||
, @AuthenticationPrincipal CustomUserDetails loginUser) {
|
||||
|
||||
blogSourceVO.setLastUpdusrId(loginUser.getUser().getUniqId());
|
||||
|
||||
return ResponseEntity.ok().body(blogSourceService.updateBlogSource(blogSourceVO));
|
||||
}
|
||||
|
||||
@DeleteMapping("/api/delete/{sourceId}")
|
||||
@ResponseBody
|
||||
public ResponseEntity<RestResponse> deleteBlogSource(@PathVariable Long sourceId) {
|
||||
RestResponse restResponse = blogSourceService.deleteBlogSource(sourceId);
|
||||
return ResponseEntity.ok().body(restResponse);
|
||||
}
|
||||
}
|
||||
124
src/main/resources/mapper/itn/blog/BlogAccountMapper.xml
Normal file
124
src/main/resources/mapper/itn/blog/BlogAccountMapper.xml
Normal file
@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.itn.admin.itn.blog.mapper.BlogAccountMapper">
|
||||
|
||||
<sql id="blogAccountColumns">
|
||||
blog_id,
|
||||
platform,
|
||||
blog_name,
|
||||
blog_url,
|
||||
api_key,
|
||||
api_secret,
|
||||
auth_info_1,
|
||||
auth_info_2,
|
||||
status,
|
||||
frst_register_id,
|
||||
frst_regist_pnttm,
|
||||
last_updusr_id,
|
||||
last_updt_pnttm
|
||||
</sql>
|
||||
|
||||
<select id="getBlogAccountList" resultType="com.itn.admin.itn.blog.mapper.domain.BlogAccountVO">
|
||||
SELECT
|
||||
<include refid="blogAccountColumns"/>
|
||||
FROM
|
||||
blog_accounts
|
||||
ORDER BY frst_regist_pnttm DESC
|
||||
</select>
|
||||
|
||||
<!-- 발행 통계를 포함한 블로그 계정 목록 조회 -->
|
||||
<select id="getBlogAccountListWithStats" resultType="com.itn.admin.itn.blog.mapper.domain.BlogAccountVO">
|
||||
SELECT
|
||||
ba.blog_id,
|
||||
ba.platform,
|
||||
ba.blog_name,
|
||||
ba.blog_url,
|
||||
ba.api_key,
|
||||
ba.api_secret,
|
||||
ba.auth_info_1,
|
||||
ba.auth_info_2,
|
||||
ba.status,
|
||||
ba.frst_register_id,
|
||||
ba.frst_regist_pnttm,
|
||||
ba.last_updusr_id,
|
||||
ba.last_updt_pnttm,
|
||||
COALESCE(stats.total_post_count, 0) AS totalPostCount,
|
||||
stats.last_published_at AS lastPublishedAt
|
||||
FROM
|
||||
blog_accounts ba
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
bph.blog_id,
|
||||
COUNT(*) AS total_post_count,
|
||||
MAX(bph.published_at) AS last_published_at
|
||||
FROM
|
||||
blog_post_history bph
|
||||
WHERE
|
||||
bph.status = 'Y'
|
||||
GROUP BY
|
||||
bph.blog_id
|
||||
) stats ON ba.blog_id = stats.blog_id
|
||||
ORDER BY
|
||||
ba.frst_regist_pnttm DESC
|
||||
</select>
|
||||
|
||||
<select id="getBlogAccountDetail" resultType="com.itn.admin.itn.blog.mapper.domain.BlogAccountVO">
|
||||
SELECT
|
||||
<include refid="blogAccountColumns"/>
|
||||
FROM
|
||||
blog_accounts
|
||||
WHERE
|
||||
blog_id = #{blogId}
|
||||
</select>
|
||||
|
||||
<insert id="insertBlogAccount" parameterType="com.itn.admin.itn.blog.mapper.domain.BlogAccountVO">
|
||||
INSERT INTO blog_accounts (
|
||||
platform,
|
||||
blog_name,
|
||||
blog_url,
|
||||
api_key,
|
||||
api_secret,
|
||||
auth_info_1,
|
||||
auth_info_2,
|
||||
frst_register_id,
|
||||
frst_regist_pnttm,
|
||||
last_updusr_id,
|
||||
last_updt_pnttm
|
||||
) VALUES (
|
||||
#{platform},
|
||||
#{blogName},
|
||||
#{blogUrl},
|
||||
#{apiKey},
|
||||
#{apiSecret},
|
||||
#{authInfo1},
|
||||
#{authInfo2},
|
||||
#{frstRegisterId},
|
||||
NOW(),
|
||||
#{lastUpdusrId},
|
||||
NOW()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateBlogAccount" parameterType="com.itn.admin.itn.blog.mapper.domain.BlogAccountVO">
|
||||
UPDATE blog_accounts
|
||||
SET
|
||||
platform = #{platform},
|
||||
blog_name = #{blogName},
|
||||
blog_url = #{blogUrl},
|
||||
api_key = #{apiKey},
|
||||
api_secret = #{apiSecret},
|
||||
auth_info_1 = #{authInfo1},
|
||||
auth_info_2 = #{authInfo2},
|
||||
last_updusr_id = #{lastUpdusrId},
|
||||
last_updt_pnttm = NOW()
|
||||
WHERE
|
||||
blog_id = #{blogId}
|
||||
</update>
|
||||
|
||||
<delete id="deleteBlogAccount" parameterType="Long">
|
||||
DELETE FROM blog_accounts
|
||||
WHERE
|
||||
blog_id = #{blogId}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
@ -0,0 +1,447 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.itn.admin.itn.blog.mapper.BlogScheduleExecutionMapper">
|
||||
|
||||
<sql id="executionColumns">
|
||||
execution_id,
|
||||
schedule_id,
|
||||
executed_at,
|
||||
started_at,
|
||||
completed_at,
|
||||
status,
|
||||
result_message,
|
||||
error_details,
|
||||
attempt_count,
|
||||
published_url,
|
||||
execution_time_ms,
|
||||
server_info,
|
||||
created_at
|
||||
</sql>
|
||||
|
||||
<sql id="executionColumnsWithJoin">
|
||||
bse.execution_id,
|
||||
bse.schedule_id,
|
||||
bse.executed_at,
|
||||
bse.started_at,
|
||||
bse.completed_at,
|
||||
bse.status,
|
||||
bse.result_message,
|
||||
bse.error_details,
|
||||
bse.attempt_count,
|
||||
bse.published_url,
|
||||
bse.execution_time_ms,
|
||||
bse.server_info,
|
||||
bse.created_at,
|
||||
bs.title AS scheduleTitle,
|
||||
ba.blog_name,
|
||||
bs.priority AS schedulePriority
|
||||
</sql>
|
||||
|
||||
<sql id="executionSearchConditions">
|
||||
<where>
|
||||
<if test="searchDTO.status != null and searchDTO.status != ''">
|
||||
AND bse.status = #{searchDTO.status}
|
||||
</if>
|
||||
<if test="searchDTO.lastExecutedFrom != null">
|
||||
AND bse.executed_at >= #{searchDTO.lastExecutedFrom}
|
||||
</if>
|
||||
<if test="searchDTO.lastExecutedTo != null">
|
||||
AND bse.executed_at <= #{searchDTO.lastExecutedTo}
|
||||
</if>
|
||||
<if test="searchDTO.blogId != null">
|
||||
AND bs.blog_id = #{searchDTO.blogId}
|
||||
</if>
|
||||
</where>
|
||||
</sql>
|
||||
|
||||
<!-- 기본 CRUD -->
|
||||
<insert id="insertScheduleExecution" parameterType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleExecutionVO"
|
||||
useGeneratedKeys="true" keyProperty="executionId">
|
||||
INSERT INTO blog_schedule_executions (
|
||||
schedule_id,
|
||||
executed_at,
|
||||
started_at,
|
||||
completed_at,
|
||||
status,
|
||||
result_message,
|
||||
error_details,
|
||||
attempt_count,
|
||||
published_url,
|
||||
execution_time_ms,
|
||||
server_info,
|
||||
created_at
|
||||
) VALUES (
|
||||
#{scheduleId},
|
||||
#{executedAt},
|
||||
#{startedAt},
|
||||
#{completedAt},
|
||||
#{status},
|
||||
#{resultMessage},
|
||||
#{errorDetails},
|
||||
#{attemptCount},
|
||||
#{publishedUrl},
|
||||
#{executionTimeMs},
|
||||
#{serverInfo},
|
||||
NOW()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateScheduleExecution" parameterType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleExecutionVO">
|
||||
UPDATE blog_schedule_executions
|
||||
SET
|
||||
executed_at = #{executedAt},
|
||||
started_at = #{startedAt},
|
||||
completed_at = #{completedAt},
|
||||
status = #{status},
|
||||
result_message = #{resultMessage},
|
||||
error_details = #{errorDetails},
|
||||
attempt_count = #{attemptCount},
|
||||
published_url = #{publishedUrl},
|
||||
execution_time_ms = #{executionTimeMs},
|
||||
server_info = #{serverInfo}
|
||||
WHERE
|
||||
execution_id = #{executionId}
|
||||
</update>
|
||||
|
||||
<select id="getScheduleExecutionDetail" resultType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleExecutionVO">
|
||||
SELECT
|
||||
<include refid="executionColumnsWithJoin"/>
|
||||
FROM
|
||||
blog_schedule_executions bse
|
||||
LEFT JOIN blog_schedules bs ON bse.schedule_id = bs.schedule_id
|
||||
LEFT JOIN blog_accounts ba ON bs.blog_id = ba.blog_id
|
||||
WHERE
|
||||
bse.execution_id = #{executionId}
|
||||
</select>
|
||||
|
||||
<!-- 특정 스케줄의 실행 이력 -->
|
||||
<select id="getExecutionHistory" resultType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleExecutionVO">
|
||||
SELECT
|
||||
<include refid="executionColumnsWithJoin"/>
|
||||
FROM
|
||||
blog_schedule_executions bse
|
||||
LEFT JOIN blog_schedules bs ON bse.schedule_id = bs.schedule_id
|
||||
LEFT JOIN blog_accounts ba ON bs.blog_id = ba.blog_id
|
||||
WHERE
|
||||
bse.schedule_id = #{scheduleId}
|
||||
<if test="searchDTO.status != null and searchDTO.status != ''">
|
||||
AND bse.status = #{searchDTO.status}
|
||||
</if>
|
||||
<if test="searchDTO.lastExecutedFrom != null">
|
||||
AND bse.executed_at >= #{searchDTO.lastExecutedFrom}
|
||||
</if>
|
||||
<if test="searchDTO.lastExecutedTo != null">
|
||||
AND bse.executed_at <= #{searchDTO.lastExecutedTo}
|
||||
</if>
|
||||
ORDER BY
|
||||
bse.executed_at DESC
|
||||
<if test="searchDTO.limit != null and searchDTO.limit > 0">
|
||||
LIMIT #{searchDTO.offset}, #{searchDTO.limit}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<select id="getExecutionHistoryCount" resultType="int">
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
blog_schedule_executions bse
|
||||
LEFT JOIN blog_schedules bs ON bse.schedule_id = bs.schedule_id
|
||||
WHERE
|
||||
bse.schedule_id = #{scheduleId}
|
||||
<if test="searchDTO.status != null and searchDTO.status != ''">
|
||||
AND bse.status = #{searchDTO.status}
|
||||
</if>
|
||||
<if test="searchDTO.lastExecutedFrom != null">
|
||||
AND bse.executed_at >= #{searchDTO.lastExecutedFrom}
|
||||
</if>
|
||||
<if test="searchDTO.lastExecutedTo != null">
|
||||
AND bse.executed_at <= #{searchDTO.lastExecutedTo}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<!-- 전체 실행 이력 (관리자용) -->
|
||||
<select id="getAllExecutionHistory" resultType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleExecutionVO">
|
||||
SELECT
|
||||
<include refid="executionColumnsWithJoin"/>
|
||||
FROM
|
||||
blog_schedule_executions bse
|
||||
LEFT JOIN blog_schedules bs ON bse.schedule_id = bs.schedule_id
|
||||
LEFT JOIN blog_accounts ba ON bs.blog_id = ba.blog_id
|
||||
<include refid="executionSearchConditions"/>
|
||||
ORDER BY
|
||||
bse.executed_at DESC
|
||||
<if test="limit != null and limit > 0">
|
||||
LIMIT #{offset}, #{limit}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<select id="getAllExecutionHistoryCount" resultType="int">
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
blog_schedule_executions bse
|
||||
LEFT JOIN blog_schedules bs ON bse.schedule_id = bs.schedule_id
|
||||
LEFT JOIN blog_accounts ba ON bs.blog_id = ba.blog_id
|
||||
<include refid="executionSearchConditions"/>
|
||||
</select>
|
||||
|
||||
<!-- 상태별 조회 -->
|
||||
<select id="getExecutionsByStatus" resultType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleExecutionVO">
|
||||
SELECT
|
||||
<include refid="executionColumns"/>
|
||||
FROM
|
||||
blog_schedule_executions
|
||||
WHERE
|
||||
status = #{status}
|
||||
ORDER BY
|
||||
executed_at DESC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<select id="getRunningExecutions" resultType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleExecutionVO">
|
||||
SELECT
|
||||
<include refid="executionColumnsWithJoin"/>
|
||||
FROM
|
||||
blog_schedule_executions bse
|
||||
LEFT JOIN blog_schedules bs ON bse.schedule_id = bs.schedule_id
|
||||
LEFT JOIN blog_accounts ba ON bs.blog_id = ba.blog_id
|
||||
WHERE
|
||||
bse.status = 'RUNNING'
|
||||
ORDER BY
|
||||
bse.started_at ASC
|
||||
</select>
|
||||
|
||||
<select id="getPendingExecutions" resultType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleExecutionVO">
|
||||
SELECT
|
||||
<include refid="executionColumnsWithJoin"/>
|
||||
FROM
|
||||
blog_schedule_executions bse
|
||||
LEFT JOIN blog_schedules bs ON bse.schedule_id = bs.schedule_id
|
||||
LEFT JOIN blog_accounts ba ON bs.blog_id = ba.blog_id
|
||||
WHERE
|
||||
bse.status = 'PENDING'
|
||||
ORDER BY
|
||||
bse.executed_at ASC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<select id="getFailedExecutions" resultType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleExecutionVO">
|
||||
SELECT
|
||||
<include refid="executionColumnsWithJoin"/>
|
||||
FROM
|
||||
blog_schedule_executions bse
|
||||
LEFT JOIN blog_schedules bs ON bse.schedule_id = bs.schedule_id
|
||||
LEFT JOIN blog_accounts ba ON bs.blog_id = ba.blog_id
|
||||
WHERE
|
||||
bse.status = 'FAILED'
|
||||
AND bse.executed_at BETWEEN #{fromTime} AND #{toTime}
|
||||
ORDER BY
|
||||
bse.executed_at DESC
|
||||
</select>
|
||||
|
||||
<!-- 성능 통계 -->
|
||||
<select id="getSlowExecutions" resultType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleExecutionVO">
|
||||
SELECT
|
||||
<include refid="executionColumnsWithJoin"/>
|
||||
FROM
|
||||
blog_schedule_executions bse
|
||||
LEFT JOIN blog_schedules bs ON bse.schedule_id = bs.schedule_id
|
||||
LEFT JOIN blog_accounts ba ON bs.blog_id = ba.blog_id
|
||||
WHERE
|
||||
bse.execution_time_ms > #{thresholdMs}
|
||||
AND bse.status = 'SUCCESS'
|
||||
ORDER BY
|
||||
bse.execution_time_ms DESC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<select id="getAverageExecutionTime" resultType="Double">
|
||||
SELECT
|
||||
AVG(execution_time_ms)
|
||||
FROM
|
||||
blog_schedule_executions
|
||||
WHERE
|
||||
schedule_id = #{scheduleId}
|
||||
AND status = 'SUCCESS'
|
||||
AND executed_at >= DATE_SUB(NOW(), INTERVAL #{days} DAY)
|
||||
</select>
|
||||
|
||||
<!-- 상태 업데이트 -->
|
||||
<update id="updateExecutionStatus">
|
||||
UPDATE blog_schedule_executions
|
||||
SET
|
||||
status = #{status},
|
||||
result_message = #{resultMessage}
|
||||
WHERE
|
||||
execution_id = #{executionId}
|
||||
</update>
|
||||
|
||||
<update id="updateExecutionStartTime">
|
||||
UPDATE blog_schedule_executions
|
||||
SET
|
||||
started_at = #{startedAt},
|
||||
status = 'RUNNING'
|
||||
WHERE
|
||||
execution_id = #{executionId}
|
||||
</update>
|
||||
|
||||
<update id="updateExecutionEndTime">
|
||||
UPDATE blog_schedule_executions
|
||||
SET
|
||||
completed_at = #{completedAt},
|
||||
execution_time_ms = #{executionTimeMs},
|
||||
status = 'SUCCESS'
|
||||
WHERE
|
||||
execution_id = #{executionId}
|
||||
</update>
|
||||
|
||||
<update id="updateExecutionError">
|
||||
UPDATE blog_schedule_executions
|
||||
SET
|
||||
error_details = #{errorDetails},
|
||||
result_message = #{resultMessage},
|
||||
status = 'FAILED'
|
||||
WHERE
|
||||
execution_id = #{executionId}
|
||||
</update>
|
||||
|
||||
<!-- 스케줄별 통계 -->
|
||||
<select id="getLastExecutionBySchedule" resultType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleExecutionVO">
|
||||
SELECT
|
||||
<include refid="executionColumns"/>
|
||||
FROM
|
||||
blog_schedule_executions
|
||||
WHERE
|
||||
schedule_id = #{scheduleId}
|
||||
ORDER BY
|
||||
executed_at DESC
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="getSuccessCountBySchedule" resultType="int">
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
blog_schedule_executions
|
||||
WHERE
|
||||
schedule_id = #{scheduleId}
|
||||
AND status = 'SUCCESS'
|
||||
AND executed_at >= DATE_SUB(NOW(), INTERVAL #{days} DAY)
|
||||
</select>
|
||||
|
||||
<select id="getFailureCountBySchedule" resultType="int">
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
blog_schedule_executions
|
||||
WHERE
|
||||
schedule_id = #{scheduleId}
|
||||
AND status = 'FAILED'
|
||||
AND executed_at >= DATE_SUB(NOW(), INTERVAL #{days} DAY)
|
||||
</select>
|
||||
|
||||
<select id="getTotalExecutionCount" resultType="int">
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
blog_schedule_executions
|
||||
WHERE
|
||||
schedule_id = #{scheduleId}
|
||||
</select>
|
||||
|
||||
<!-- 재시도 관련 -->
|
||||
<select id="getRetryableExecutions" resultType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleExecutionVO">
|
||||
SELECT
|
||||
bse.*
|
||||
FROM
|
||||
blog_schedule_executions bse
|
||||
INNER JOIN blog_schedules bs ON bse.schedule_id = bs.schedule_id
|
||||
WHERE
|
||||
bse.status = 'FAILED'
|
||||
AND bse.attempt_count < bs.max_retries
|
||||
AND DATE_ADD(bse.executed_at, INTERVAL bs.retry_interval MINUTE) <= #{currentTime}
|
||||
AND bs.retry_interval <= #{maxRetryInterval}
|
||||
ORDER BY
|
||||
bse.executed_at ASC
|
||||
LIMIT 50
|
||||
</select>
|
||||
|
||||
<update id="incrementAttemptCount">
|
||||
UPDATE blog_schedule_executions
|
||||
SET
|
||||
attempt_count = attempt_count + 1
|
||||
WHERE
|
||||
execution_id = #{executionId}
|
||||
</update>
|
||||
|
||||
<!-- 정리 작업 -->
|
||||
<delete id="deleteOldExecutions">
|
||||
DELETE FROM blog_schedule_executions
|
||||
WHERE
|
||||
created_at < #{beforeDate}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteExecutionsBySchedule">
|
||||
DELETE FROM blog_schedule_executions
|
||||
WHERE
|
||||
schedule_id = #{scheduleId}
|
||||
</delete>
|
||||
|
||||
<!-- 통계용 집계 쿼리 -->
|
||||
<select id="getExecutionStatsGroupByHour" resultType="Object[]">
|
||||
SELECT
|
||||
HOUR(executed_at) AS hour,
|
||||
COUNT(*) AS executionCount,
|
||||
SUM(CASE WHEN status = 'SUCCESS' THEN 1 ELSE 0 END) AS successCount,
|
||||
SUM(CASE WHEN status = 'FAILED' THEN 1 ELSE 0 END) AS failureCount
|
||||
FROM
|
||||
blog_schedule_executions
|
||||
WHERE
|
||||
executed_at BETWEEN #{fromTime} AND #{toTime}
|
||||
GROUP BY
|
||||
HOUR(executed_at)
|
||||
ORDER BY
|
||||
hour
|
||||
</select>
|
||||
|
||||
<select id="getExecutionStatsGroupByDay" resultType="Object[]">
|
||||
SELECT
|
||||
DATE(executed_at) AS date,
|
||||
COUNT(*) AS executionCount,
|
||||
SUM(CASE WHEN status = 'SUCCESS' THEN 1 ELSE 0 END) AS successCount,
|
||||
SUM(CASE WHEN status = 'FAILED' THEN 1 ELSE 0 END) AS failureCount,
|
||||
CASE
|
||||
WHEN COUNT(*) = 0 THEN 0.0
|
||||
ELSE (SUM(CASE WHEN status = 'SUCCESS' THEN 1 ELSE 0 END) * 100.0 / COUNT(*))
|
||||
END AS successRate
|
||||
FROM
|
||||
blog_schedule_executions
|
||||
WHERE
|
||||
executed_at BETWEEN #{fromTime} AND #{toTime}
|
||||
GROUP BY
|
||||
DATE(executed_at)
|
||||
ORDER BY
|
||||
date DESC
|
||||
</select>
|
||||
|
||||
<select id="getExecutionStatsGroupByBlog" resultType="Object[]">
|
||||
SELECT
|
||||
bs.blog_id,
|
||||
ba.blog_name,
|
||||
COUNT(bse.execution_id) AS executionCount,
|
||||
SUM(CASE WHEN bse.status = 'SUCCESS' THEN 1 ELSE 0 END) AS successCount,
|
||||
SUM(CASE WHEN bse.status = 'FAILED' THEN 1 ELSE 0 END) AS failureCount
|
||||
FROM
|
||||
blog_schedule_executions bse
|
||||
INNER JOIN blog_schedules bs ON bse.schedule_id = bs.schedule_id
|
||||
INNER JOIN blog_accounts ba ON bs.blog_id = ba.blog_id
|
||||
WHERE
|
||||
bse.executed_at BETWEEN #{fromTime} AND #{toTime}
|
||||
GROUP BY
|
||||
bs.blog_id, ba.blog_name
|
||||
ORDER BY
|
||||
executionCount DESC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
474
src/main/resources/mapper/itn/blog/BlogScheduleMapper.xml
Normal file
474
src/main/resources/mapper/itn/blog/BlogScheduleMapper.xml
Normal file
@ -0,0 +1,474 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.itn.admin.itn.blog.mapper.BlogScheduleMapper">
|
||||
|
||||
<sql id="blogScheduleColumns">
|
||||
schedule_id,
|
||||
blog_id,
|
||||
url_id,
|
||||
title,
|
||||
content,
|
||||
schedule_type,
|
||||
scheduled_at,
|
||||
repeat_interval,
|
||||
repeat_value,
|
||||
end_at,
|
||||
status,
|
||||
priority,
|
||||
max_retries,
|
||||
retry_interval,
|
||||
notification_email,
|
||||
slack_channel,
|
||||
enable_notification,
|
||||
frst_register_id,
|
||||
frst_regist_dt,
|
||||
last_updusr_id,
|
||||
last_updt_dt
|
||||
</sql>
|
||||
|
||||
<sql id="blogScheduleColumnsWithJoin">
|
||||
bs.schedule_id,
|
||||
bs.blog_id,
|
||||
bs.url_id,
|
||||
bs.title,
|
||||
bs.content,
|
||||
bs.schedule_type,
|
||||
bs.scheduled_at,
|
||||
bs.repeat_interval,
|
||||
bs.repeat_value,
|
||||
bs.end_at,
|
||||
bs.status,
|
||||
bs.priority,
|
||||
bs.max_retries,
|
||||
bs.retry_interval,
|
||||
bs.notification_email,
|
||||
bs.slack_channel,
|
||||
bs.enable_notification,
|
||||
bs.frst_register_id,
|
||||
bs.frst_regist_dt,
|
||||
bs.last_updusr_id,
|
||||
bs.last_updt_dt,
|
||||
ba.blog_name,
|
||||
ba.blog_url,
|
||||
bsu.url AS sourceUrl,
|
||||
bsu.title AS sourceTitle
|
||||
</sql>
|
||||
|
||||
<sql id="searchConditions">
|
||||
<where>
|
||||
<if test="scheduleId != null">
|
||||
AND bs.schedule_id = #{scheduleId}
|
||||
</if>
|
||||
<if test="blogId != null">
|
||||
AND bs.blog_id = #{blogId}
|
||||
</if>
|
||||
<if test="urlId != null">
|
||||
AND bs.url_id = #{urlId}
|
||||
</if>
|
||||
<if test="title != null and title != ''">
|
||||
AND bs.title LIKE CONCAT('%', #{title}, '%')
|
||||
</if>
|
||||
<if test="scheduleType != null and scheduleType != ''">
|
||||
AND bs.schedule_type = #{scheduleType}
|
||||
</if>
|
||||
<if test="status != null and status != ''">
|
||||
AND bs.status = #{status}
|
||||
</if>
|
||||
<if test="statusList != null and statusList.size() > 0">
|
||||
AND bs.status IN
|
||||
<foreach collection="statusList" item="status" open="(" separator="," close=")">
|
||||
#{status}
|
||||
</foreach>
|
||||
</if>
|
||||
<if test="priority != null and priority != ''">
|
||||
AND bs.priority = #{priority}
|
||||
</if>
|
||||
<if test="scheduledAtFrom != null">
|
||||
AND bs.scheduled_at >= #{scheduledAtFrom}
|
||||
</if>
|
||||
<if test="scheduledAtTo != null">
|
||||
AND bs.scheduled_at <= #{scheduledAtTo}
|
||||
</if>
|
||||
<if test="createdAtFrom != null">
|
||||
AND bs.frst_regist_dt >= #{createdAtFrom}
|
||||
</if>
|
||||
<if test="createdAtTo != null">
|
||||
AND bs.frst_regist_dt <= #{createdAtTo}
|
||||
</if>
|
||||
<if test="enableNotification != null">
|
||||
AND bs.enable_notification = #{enableNotification}
|
||||
</if>
|
||||
<if test="frstRegisterId != null and frstRegisterId != ''">
|
||||
AND bs.frst_register_id = #{frstRegisterId}
|
||||
</if>
|
||||
</where>
|
||||
</sql>
|
||||
|
||||
<!-- 기본 CRUD 작업 -->
|
||||
<insert id="insertBlogSchedule" parameterType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleVO"
|
||||
useGeneratedKeys="true" keyProperty="scheduleId">
|
||||
INSERT INTO blog_schedules (
|
||||
blog_id,
|
||||
url_id,
|
||||
title,
|
||||
content,
|
||||
schedule_type,
|
||||
scheduled_at,
|
||||
repeat_interval,
|
||||
repeat_value,
|
||||
end_at,
|
||||
status,
|
||||
priority,
|
||||
max_retries,
|
||||
retry_interval,
|
||||
notification_email,
|
||||
slack_channel,
|
||||
enable_notification,
|
||||
frst_register_id,
|
||||
frst_regist_dt,
|
||||
last_updusr_id,
|
||||
last_updt_dt
|
||||
) VALUES (
|
||||
#{blogId},
|
||||
#{urlId},
|
||||
#{title},
|
||||
#{content},
|
||||
#{scheduleType},
|
||||
#{scheduledAt},
|
||||
#{repeatInterval},
|
||||
#{repeatValue},
|
||||
#{endAt},
|
||||
#{status},
|
||||
#{priority},
|
||||
#{maxRetries},
|
||||
#{retryInterval},
|
||||
#{notificationEmail},
|
||||
#{slackChannel},
|
||||
#{enableNotification},
|
||||
#{frstRegisterId},
|
||||
NOW(),
|
||||
#{lastUpdusrId},
|
||||
NOW()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateBlogSchedule" parameterType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleVO">
|
||||
UPDATE blog_schedules
|
||||
SET
|
||||
title = #{title},
|
||||
content = #{content},
|
||||
scheduled_at = #{scheduledAt},
|
||||
repeat_interval = #{repeatInterval},
|
||||
repeat_value = #{repeatValue},
|
||||
end_at = #{endAt},
|
||||
status = #{status},
|
||||
priority = #{priority},
|
||||
max_retries = #{maxRetries},
|
||||
retry_interval = #{retryInterval},
|
||||
notification_email = #{notificationEmail},
|
||||
slack_channel = #{slackChannel},
|
||||
enable_notification = #{enableNotification},
|
||||
last_updusr_id = #{lastUpdusrId},
|
||||
last_updt_dt = NOW()
|
||||
WHERE
|
||||
schedule_id = #{scheduleId}
|
||||
</update>
|
||||
|
||||
<delete id="deleteBlogSchedule" parameterType="Long">
|
||||
DELETE FROM blog_schedules
|
||||
WHERE schedule_id = #{scheduleId}
|
||||
</delete>
|
||||
|
||||
<select id="getBlogScheduleDetail" resultType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleVO">
|
||||
SELECT
|
||||
<include refid="blogScheduleColumnsWithJoin"/>
|
||||
FROM
|
||||
blog_schedules bs
|
||||
LEFT JOIN blog_accounts ba ON bs.blog_id = ba.blog_id
|
||||
LEFT JOIN blog_source_urls bsu ON bs.url_id = bsu.url_id
|
||||
WHERE
|
||||
bs.schedule_id = #{scheduleId}
|
||||
</select>
|
||||
|
||||
<!-- 목록 조회 -->
|
||||
<select id="getBlogScheduleList" resultType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleVO">
|
||||
SELECT
|
||||
<include refid="blogScheduleColumnsWithJoin"/>
|
||||
FROM
|
||||
blog_schedules bs
|
||||
LEFT JOIN blog_accounts ba ON bs.blog_id = ba.blog_id
|
||||
LEFT JOIN blog_source_urls bsu ON bs.url_id = bsu.url_id
|
||||
<include refid="searchConditions"/>
|
||||
ORDER BY
|
||||
<choose>
|
||||
<when test="sortBy != null and sortBy != ''">
|
||||
${sortBy}
|
||||
<if test="sortOrder != null and sortOrder != ''">
|
||||
${sortOrder}
|
||||
</if>
|
||||
</when>
|
||||
<otherwise>
|
||||
bs.scheduled_at DESC
|
||||
</otherwise>
|
||||
</choose>
|
||||
<if test="limit != null and limit > 0">
|
||||
LIMIT #{offset}, #{limit}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<select id="getBlogScheduleCount" resultType="int">
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
blog_schedules bs
|
||||
LEFT JOIN blog_accounts ba ON bs.blog_id = ba.blog_id
|
||||
LEFT JOIN blog_source_urls bsu ON bs.url_id = bsu.url_id
|
||||
<include refid="searchConditions"/>
|
||||
</select>
|
||||
|
||||
<!-- 통계 포함 목록 조회 -->
|
||||
<select id="getBlogScheduleListWithStats" resultType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleVO">
|
||||
SELECT
|
||||
<include refid="blogScheduleColumnsWithJoin"/>,
|
||||
COALESCE(exec_stats.total_executions, 0) AS totalExecutions,
|
||||
COALESCE(exec_stats.success_executions, 0) AS successExecutions,
|
||||
COALESCE(exec_stats.failed_executions, 0) AS failedExecutions,
|
||||
exec_stats.last_executed_at AS lastExecutedAt
|
||||
FROM
|
||||
blog_schedules bs
|
||||
LEFT JOIN blog_accounts ba ON bs.blog_id = ba.blog_id
|
||||
LEFT JOIN blog_source_urls bsu ON bs.url_id = bsu.url_id
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
bse.schedule_id,
|
||||
COUNT(*) AS total_executions,
|
||||
SUM(CASE WHEN bse.status = 'SUCCESS' THEN 1 ELSE 0 END) AS success_executions,
|
||||
SUM(CASE WHEN bse.status = 'FAILED' THEN 1 ELSE 0 END) AS failed_executions,
|
||||
MAX(bse.executed_at) AS last_executed_at
|
||||
FROM
|
||||
blog_schedule_executions bse
|
||||
GROUP BY
|
||||
bse.schedule_id
|
||||
) exec_stats ON bs.schedule_id = exec_stats.schedule_id
|
||||
<include refid="searchConditions"/>
|
||||
ORDER BY
|
||||
<choose>
|
||||
<when test="sortBy != null and sortBy != ''">
|
||||
${sortBy}
|
||||
<if test="sortOrder != null and sortOrder != ''">
|
||||
${sortOrder}
|
||||
</if>
|
||||
</when>
|
||||
<otherwise>
|
||||
bs.scheduled_at DESC
|
||||
</otherwise>
|
||||
</choose>
|
||||
<if test="limit != null and limit > 0">
|
||||
LIMIT #{offset}, #{limit}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<!-- 스케줄링 전용 조회 -->
|
||||
<select id="getActiveSchedulesByTime" resultType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleVO">
|
||||
SELECT
|
||||
<include refid="blogScheduleColumnsWithJoin"/>
|
||||
FROM
|
||||
blog_schedules bs
|
||||
LEFT JOIN blog_accounts ba ON bs.blog_id = ba.blog_id
|
||||
LEFT JOIN blog_source_urls bsu ON bs.url_id = bsu.url_id
|
||||
WHERE
|
||||
bs.status = 'ACTIVE'
|
||||
AND bs.scheduled_at <= DATE_ADD(#{currentTime}, INTERVAL #{bufferMinutes} MINUTE)
|
||||
AND (bs.end_at IS NULL OR bs.end_at > #{currentTime})
|
||||
ORDER BY
|
||||
bs.priority DESC,
|
||||
bs.scheduled_at ASC
|
||||
LIMIT 100
|
||||
</select>
|
||||
|
||||
<select id="getPendingSchedulesByPriority" resultType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleVO">
|
||||
SELECT
|
||||
<include refid="blogScheduleColumns"/>
|
||||
FROM
|
||||
blog_schedules
|
||||
WHERE
|
||||
status = 'ACTIVE'
|
||||
AND scheduled_at <= NOW()
|
||||
ORDER BY
|
||||
FIELD(priority, 'HIGH', 'NORMAL', 'LOW'),
|
||||
scheduled_at ASC
|
||||
LIMIT 50
|
||||
</select>
|
||||
|
||||
<select id="getFailedSchedulesForRetry" resultType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleVO">
|
||||
SELECT
|
||||
bs.*
|
||||
FROM
|
||||
blog_schedules bs
|
||||
INNER JOIN blog_schedule_executions bse ON bs.schedule_id = bse.schedule_id
|
||||
WHERE
|
||||
bs.status = 'FAILED'
|
||||
AND bse.status = 'FAILED'
|
||||
AND bse.attempt_count < bs.max_retries
|
||||
AND DATE_ADD(bse.executed_at, INTERVAL bs.retry_interval MINUTE) <= #{currentTime}
|
||||
GROUP BY
|
||||
bs.schedule_id
|
||||
HAVING
|
||||
MAX(bse.executed_at) = bse.executed_at
|
||||
ORDER BY
|
||||
bs.priority DESC,
|
||||
bse.executed_at ASC
|
||||
LIMIT 20
|
||||
</select>
|
||||
|
||||
<!-- 상태 업데이트 -->
|
||||
<update id="updateScheduleStatus">
|
||||
UPDATE blog_schedules
|
||||
SET
|
||||
status = #{status},
|
||||
last_updt_dt = NOW()
|
||||
WHERE
|
||||
schedule_id = #{scheduleId}
|
||||
</update>
|
||||
|
||||
<update id="updateScheduleNextExecution">
|
||||
UPDATE blog_schedules
|
||||
SET
|
||||
scheduled_at = #{nextExecuteAt},
|
||||
last_updt_dt = NOW()
|
||||
WHERE
|
||||
schedule_id = #{scheduleId}
|
||||
</update>
|
||||
|
||||
<update id="incrementScheduleExecutionCount">
|
||||
UPDATE blog_schedules
|
||||
SET
|
||||
last_updt_dt = NOW()
|
||||
WHERE
|
||||
schedule_id = #{scheduleId}
|
||||
</update>
|
||||
|
||||
<!-- 락킹 (실제 구현에서는 SELECT FOR UPDATE 사용) -->
|
||||
<update id="lockScheduleForExecution">
|
||||
UPDATE blog_schedules
|
||||
SET
|
||||
last_updt_dt = NOW()
|
||||
WHERE
|
||||
schedule_id = #{scheduleId}
|
||||
AND status = 'ACTIVE'
|
||||
</update>
|
||||
|
||||
<update id="unlockSchedule">
|
||||
UPDATE blog_schedules
|
||||
SET
|
||||
last_updt_dt = NOW()
|
||||
WHERE
|
||||
schedule_id = #{scheduleId}
|
||||
</update>
|
||||
|
||||
<!-- 통계 조회 -->
|
||||
<select id="getOverallStatistics" resultType="com.itn.admin.itn.blog.mapper.domain.ScheduleStatisticsDTO">
|
||||
SELECT
|
||||
COUNT(*) AS totalSchedules,
|
||||
SUM(CASE WHEN status = 'ACTIVE' THEN 1 ELSE 0 END) AS activeSchedules,
|
||||
SUM(CASE WHEN status = 'INACTIVE' THEN 1 ELSE 0 END) AS inactiveSchedules,
|
||||
SUM(CASE WHEN status = 'COMPLETED' THEN 1 ELSE 0 END) AS completedSchedules,
|
||||
SUM(CASE WHEN status = 'FAILED' THEN 1 ELSE 0 END) AS failedSchedules,
|
||||
SUM(CASE WHEN schedule_type = 'ONE_TIME' THEN 1 ELSE 0 END) AS oneTimeSchedules,
|
||||
SUM(CASE WHEN schedule_type = 'RECURRING' THEN 1 ELSE 0 END) AS recurringSchedules,
|
||||
SUM(CASE WHEN priority = 'HIGH' THEN 1 ELSE 0 END) AS highPrioritySchedules,
|
||||
SUM(CASE WHEN priority = 'NORMAL' THEN 1 ELSE 0 END) AS normalPrioritySchedules,
|
||||
SUM(CASE WHEN priority = 'LOW' THEN 1 ELSE 0 END) AS lowPrioritySchedules,
|
||||
SUM(CASE WHEN enable_notification = true THEN 1 ELSE 0 END) AS enabledNotificationCount,
|
||||
SUM(CASE WHEN notification_email IS NOT NULL THEN 1 ELSE 0 END) AS emailNotificationCount,
|
||||
SUM(CASE WHEN slack_channel IS NOT NULL THEN 1 ELSE 0 END) AS slackNotificationCount
|
||||
FROM
|
||||
blog_schedules
|
||||
</select>
|
||||
|
||||
<select id="getBlogScheduleStats" resultType="com.itn.admin.itn.blog.mapper.domain.ScheduleStatisticsDTO$BlogScheduleStatsDTO">
|
||||
SELECT
|
||||
bs.blog_id AS blogId,
|
||||
ba.blog_name AS blogName,
|
||||
COUNT(bs.schedule_id) AS scheduleCount,
|
||||
COALESCE(exec_stats.success_count, 0) AS successCount,
|
||||
COALESCE(exec_stats.failure_count, 0) AS failureCount,
|
||||
CASE
|
||||
WHEN COALESCE(exec_stats.total_count, 0) = 0 THEN 0.0
|
||||
ELSE (COALESCE(exec_stats.success_count, 0) * 100.0 / exec_stats.total_count)
|
||||
END AS successRate
|
||||
FROM
|
||||
blog_schedules bs
|
||||
LEFT JOIN blog_accounts ba ON bs.blog_id = ba.blog_id
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
bs2.blog_id,
|
||||
COUNT(bse.execution_id) AS total_count,
|
||||
SUM(CASE WHEN bse.status = 'SUCCESS' THEN 1 ELSE 0 END) AS success_count,
|
||||
SUM(CASE WHEN bse.status = 'FAILED' THEN 1 ELSE 0 END) AS failure_count
|
||||
FROM
|
||||
blog_schedules bs2
|
||||
LEFT JOIN blog_schedule_executions bse ON bs2.schedule_id = bse.schedule_id
|
||||
GROUP BY
|
||||
bs2.blog_id
|
||||
) exec_stats ON bs.blog_id = exec_stats.blog_id
|
||||
GROUP BY
|
||||
bs.blog_id, ba.blog_name, exec_stats.success_count, exec_stats.failure_count, exec_stats.total_count
|
||||
ORDER BY
|
||||
scheduleCount DESC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<!-- 유지보수 쿼리 -->
|
||||
<delete id="deleteCompletedSchedules">
|
||||
DELETE FROM blog_schedules
|
||||
WHERE
|
||||
status = 'COMPLETED'
|
||||
AND schedule_type = 'ONE_TIME'
|
||||
AND last_updt_dt < #{beforeDate}
|
||||
</delete>
|
||||
|
||||
<delete id="cleanupFailedSchedules">
|
||||
DELETE FROM blog_schedules
|
||||
WHERE
|
||||
status = 'FAILED'
|
||||
AND last_updt_dt < #{beforeDate}
|
||||
AND (
|
||||
SELECT COUNT(*)
|
||||
FROM blog_schedule_executions bse
|
||||
WHERE bse.schedule_id = blog_schedules.schedule_id
|
||||
AND bse.status = 'FAILED'
|
||||
) >= #{maxFailures}
|
||||
</delete>
|
||||
|
||||
<!-- 블로그별/예정 스케줄 조회 -->
|
||||
<select id="getSchedulesByBlogId" resultType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleVO">
|
||||
SELECT
|
||||
<include refid="blogScheduleColumns"/>
|
||||
FROM
|
||||
blog_schedules
|
||||
WHERE
|
||||
blog_id = #{blogId}
|
||||
<if test="status != null and status != ''">
|
||||
AND status = #{status}
|
||||
</if>
|
||||
ORDER BY
|
||||
scheduled_at ASC
|
||||
</select>
|
||||
|
||||
<select id="getUpcomingSchedules" resultType="com.itn.admin.itn.blog.mapper.domain.BlogScheduleVO">
|
||||
SELECT
|
||||
<include refid="blogScheduleColumnsWithJoin"/>
|
||||
FROM
|
||||
blog_schedules bs
|
||||
LEFT JOIN blog_accounts ba ON bs.blog_id = ba.blog_id
|
||||
LEFT JOIN blog_source_urls bsu ON bs.url_id = bsu.url_id
|
||||
WHERE
|
||||
bs.status = 'ACTIVE'
|
||||
AND bs.scheduled_at BETWEEN #{fromTime} AND #{toTime}
|
||||
ORDER BY
|
||||
bs.scheduled_at ASC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
100
src/main/resources/mapper/itn/blog/BlogSourceMapper.xml
Normal file
100
src/main/resources/mapper/itn/blog/BlogSourceMapper.xml
Normal file
@ -0,0 +1,100 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<mapper namespace="com.itn.admin.itn.blog.mapper.BlogSourceMapper">
|
||||
|
||||
<select id="selectBlogSourceList" resultType="com.itn.admin.itn.blog.mapper.domain.BlogSourceVO">
|
||||
SELECT
|
||||
url_id,
|
||||
url,
|
||||
title,
|
||||
category,
|
||||
is_active,
|
||||
frst_regist_pnttm,
|
||||
last_updt_pnttm
|
||||
FROM blog_source_urls
|
||||
ORDER BY frst_regist_pnttm DESC
|
||||
</select>
|
||||
|
||||
<!-- 특정 블로그의 발행 통계를 포함한 소스 목록 조회 -->
|
||||
<select id="selectBlogSourceListWithStats" parameterType="Long" resultType="com.itn.admin.itn.blog.mapper.domain.BlogSourceVO">
|
||||
SELECT
|
||||
bsu.url_id,
|
||||
bsu.url,
|
||||
bsu.title,
|
||||
bsu.category,
|
||||
bsu.is_active,
|
||||
bsu.frst_regist_pnttm AS frstRegistPnttm,
|
||||
bsu.last_updt_pnttm AS lastUpdtPnttm,
|
||||
COALESCE(stats.total_publish_count, 0) AS totalPublishCount,
|
||||
stats.last_published_at AS lastPublishedAt,
|
||||
stats.last_published_url AS lastPublishedUrl
|
||||
FROM
|
||||
blog_source_urls bsu
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
SUBSTRING_INDEX(bph.published_url, '#', 1) AS source_url,
|
||||
COUNT(*) AS total_publish_count,
|
||||
MAX(bph.published_at) AS last_published_at,
|
||||
(SELECT bph2.published_url
|
||||
FROM blog_post_history bph2
|
||||
WHERE bph2.blog_id = #{blogId}
|
||||
AND SUBSTRING_INDEX(bph2.published_url, '#', 1) = SUBSTRING_INDEX(bph.published_url, '#', 1)
|
||||
AND bph2.published_at = MAX(bph.published_at)
|
||||
LIMIT 1) AS last_published_url
|
||||
FROM
|
||||
blog_post_history bph
|
||||
WHERE
|
||||
bph.blog_id = #{blogId}
|
||||
AND bph.status = 'Y'
|
||||
AND bph.published_url IS NOT NULL
|
||||
GROUP BY
|
||||
SUBSTRING_INDEX(bph.published_url, '#', 1)
|
||||
) stats ON bsu.url = stats.source_url
|
||||
ORDER BY
|
||||
bsu.frst_regist_pnttm DESC
|
||||
</select>
|
||||
|
||||
<select id="getBlogSourceDetail" parameterType="Long" resultType="com.itn.admin.itn.blog.mapper.domain.BlogSourceVO">
|
||||
SELECT
|
||||
url_id AS urlId,
|
||||
url,
|
||||
title,
|
||||
category,
|
||||
is_active AS active,
|
||||
frst_regist_pnttm AS frstRegistPnttm,
|
||||
frst_register_id AS frstRegisterId,
|
||||
last_updt_pnttm AS lastUpdtPnttm,
|
||||
last_updusr_id AS lastUpdusrId
|
||||
FROM blog_source_urls
|
||||
WHERE url_id = #{urlId}
|
||||
</select>
|
||||
|
||||
<insert id="insertBlogSource" parameterType="com.itn.admin.itn.blog.mapper.domain.BlogSourceVO">
|
||||
INSERT INTO blog_source_urls (
|
||||
url, title, category, is_active,
|
||||
frst_regist_pnttm, frst_register_id, last_updt_pnttm, last_updusr_id
|
||||
) VALUES (
|
||||
#{url}, #{title}, #{category}, #{isActive},
|
||||
NOW(), #{frstRegisterId}, NOW(), #{lastUpdusrId}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateBlogSource" parameterType="com.itn.admin.itn.blog.mapper.domain.BlogSourceVO">
|
||||
UPDATE blog_source_urls
|
||||
SET
|
||||
url = #{url},
|
||||
title = #{title},
|
||||
category = #{category},
|
||||
is_active = #{isActive},
|
||||
last_updt_pnttm = NOW(),
|
||||
last_updusr_id = 'admin'
|
||||
WHERE url_id = #{urlId}
|
||||
</update>
|
||||
|
||||
<delete id="deleteBlogSource" parameterType="Long">
|
||||
DELETE FROM blog_source_urls
|
||||
WHERE url_id = #{urlId}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
@ -0,0 +1,116 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.itn.admin.itn.blog.mapper.BlogCookieMappingMapper">
|
||||
|
||||
<!-- 블로그 계정별 쿠키 매핑 정보 조회 -->
|
||||
<select id="selectCookieMappingByBlogId" parameterType="long" resultType="blogCookieMappingVO">
|
||||
SELECT
|
||||
bcm.mapping_id AS mappingId,
|
||||
bcm.blog_id,
|
||||
bcm.cookie_file_path,
|
||||
bcm.cookie_file_name,
|
||||
bcm.is_active,
|
||||
bcm.last_validated_at,
|
||||
bcm.cookie_expires_at,
|
||||
bcm.frst_register_id,
|
||||
bcm.frst_regist_pnttm,
|
||||
bcm.last_updusr_id,
|
||||
bcm.last_updt_pnttm,
|
||||
ba.blog_name,
|
||||
ba.blog_url,
|
||||
ba.platform
|
||||
FROM
|
||||
blog_cookie_mapping bcm
|
||||
LEFT JOIN
|
||||
blog_accounts ba ON bcm.blog_id = ba.blog_id
|
||||
WHERE
|
||||
bcm.blog_id = #{blogId}
|
||||
AND bcm.is_active = 'Y'
|
||||
ORDER BY bcm.frst_regist_pnttm DESC
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<!-- 활성화된 쿠키 매핑 정보 조회 -->
|
||||
<select id="selectActiveCookieMappings" resultType="blogCookieMappingVO">
|
||||
SELECT
|
||||
bcm.mapping_id AS mappingId,
|
||||
bcm.blog_id,
|
||||
bcm.cookie_file_path,
|
||||
bcm.cookie_file_name,
|
||||
bcm.is_active,
|
||||
bcm.last_validated_at,
|
||||
bcm.cookie_expires_at,
|
||||
bcm.frst_register_id,
|
||||
bcm.frst_regist_pnttm,
|
||||
bcm.last_updusr_id,
|
||||
bcm.last_updt_pnttm,
|
||||
ba.blog_name,
|
||||
ba.blog_url,
|
||||
ba.platform
|
||||
FROM
|
||||
blog_cookie_mapping bcm
|
||||
LEFT JOIN
|
||||
blog_accounts ba ON bcm.blog_id = ba.blog_id
|
||||
WHERE
|
||||
bcm.is_active = 'Y'
|
||||
AND ba.status = 'Y'
|
||||
ORDER BY bcm.frst_regist_pnttm DESC
|
||||
</select>
|
||||
|
||||
<!-- 쿠키 매핑 정보 저장 -->
|
||||
<insert id="insertCookieMapping" parameterType="blogCookieMappingVO" useGeneratedKeys="true" keyProperty="mappingId">
|
||||
INSERT INTO blog_cookie_mapping (
|
||||
blog_id, cookie_file_path, cookie_file_name, is_active,
|
||||
last_validated_at, cookie_expires_at, frst_register_id, frst_regist_pnttm,
|
||||
last_updusr_id, last_updt_pnttm
|
||||
) VALUES (
|
||||
#{blogId}, #{cookieFilePath}, #{cookieFileName}, #{isActive},
|
||||
#{lastValidatedAt}, #{cookieExpiresAt}, #{frstRegisterId}, NOW(),
|
||||
#{lastUpdusrId}, NOW()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<!-- 쿠키 매핑 정보 업데이트 -->
|
||||
<update id="updateCookieMapping" parameterType="blogCookieMappingVO">
|
||||
UPDATE blog_cookie_mapping
|
||||
SET
|
||||
cookie_file_path = #{cookieFilePath},
|
||||
cookie_file_name = #{cookieFileName},
|
||||
is_active = #{isActive},
|
||||
cookie_expires_at = #{cookieExpiresAt},
|
||||
last_updusr_id = #{lastUpdusrId},
|
||||
last_updt_pnttm = NOW()
|
||||
WHERE mapping_id = #{mappingId}
|
||||
</update>
|
||||
|
||||
<!-- 쿠키 유효성 검증 시간 업데이트 -->
|
||||
<update id="updateLastValidated" parameterType="map">
|
||||
UPDATE blog_cookie_mapping
|
||||
SET
|
||||
last_validated_at = NOW(),
|
||||
last_updusr_id = 'SYSTEM',
|
||||
last_updt_pnttm = NOW()
|
||||
WHERE mapping_id = #{mappingId}
|
||||
</update>
|
||||
|
||||
<!-- 쿠키 만료 시간 업데이트 -->
|
||||
<update id="updateCookieExpiration" parameterType="map">
|
||||
UPDATE blog_cookie_mapping
|
||||
SET
|
||||
cookie_expires_at = #{cookieExpiresAt},
|
||||
last_updusr_id = 'SYSTEM',
|
||||
last_updt_pnttm = NOW()
|
||||
WHERE mapping_id = #{mappingId}
|
||||
</update>
|
||||
|
||||
<!-- 쿠키 매핑 비활성화 -->
|
||||
<update id="deactivateCookieMapping" parameterType="map">
|
||||
UPDATE blog_cookie_mapping
|
||||
SET
|
||||
is_active = 'N',
|
||||
last_updusr_id = 'SYSTEM',
|
||||
last_updt_pnttm = NOW()
|
||||
WHERE blog_id = #{blogId}
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
275
src/main/resources/mapper/itn/itn/blog/BlogPostingMapper.xml
Normal file
275
src/main/resources/mapper/itn/itn/blog/BlogPostingMapper.xml
Normal file
@ -0,0 +1,275 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.itn.admin.itn.blog.mapper.BlogPostingMapper">
|
||||
|
||||
|
||||
<!-- 발행 히스토리 저장 -->
|
||||
<insert id="insertBlogPostHistory" parameterType="com.itn.admin.itn.blog.mapper.domain.BlogPostHistoryVO" useGeneratedKeys="true" keyProperty="postId">
|
||||
INSERT INTO blog_post_history (
|
||||
blog_id, url_id, post_title, post_content, published_url,
|
||||
status, error_message, published_at, html_generated_at, publish_started_at,
|
||||
frst_register_id, frst_regist_pnttm, last_updusr_id, last_updt_pnttm
|
||||
) VALUES (
|
||||
#{blogId}, #{urlId}, #{postTitle}, #{postContent}, #{publishedUrl},
|
||||
#{status}, #{errorMessage}, #{publishedAt}, #{htmlGeneratedAt}, #{publishStartedAt},
|
||||
'SYSTEM', NOW(), 'SYSTEM', NOW()
|
||||
)
|
||||
</insert>
|
||||
|
||||
|
||||
<!-- 특정 블로그-소스의 발행 히스토리 조회 -->
|
||||
<select id="selectBlogPostHistories" resultType="com.itn.admin.itn.blog.mapper.domain.BlogPostHistoryVO">
|
||||
SELECT
|
||||
bph.post_id AS postId,
|
||||
bph.blog_id,
|
||||
bph.url_id,
|
||||
bph.post_title,
|
||||
bph.post_content,
|
||||
bph.published_url,
|
||||
bph.status,
|
||||
bph.error_message,
|
||||
bph.published_at,
|
||||
bph.html_generated_at,
|
||||
bph.publish_started_at,
|
||||
bph.frst_regist_pnttm,
|
||||
bph.last_updt_pnttm,
|
||||
ba.blog_name,
|
||||
bsu.title AS sourceTitle
|
||||
FROM
|
||||
blog_post_history bph
|
||||
LEFT JOIN
|
||||
blog_accounts ba ON bph.blog_id = ba.blog_id
|
||||
LEFT JOIN
|
||||
blog_source_urls bsu ON bph.url_id = bsu.url_id
|
||||
WHERE
|
||||
1=1
|
||||
<if test="blogId != null">
|
||||
AND bph.blog_id = #{blogId}
|
||||
</if>
|
||||
<if test="urlId != null">
|
||||
AND bph.url_id = #{urlId}
|
||||
</if>
|
||||
ORDER BY bph.frst_regist_pnttm DESC
|
||||
LIMIT #{limit} OFFSET #{offset}
|
||||
</select>
|
||||
|
||||
<!-- 워크플로우 단계별 업데이트 쿼리 -->
|
||||
|
||||
<!-- HTML 생성 완료 업데이트 -->
|
||||
<update id="updateHtmlGenerated" parameterType="com.itn.admin.itn.blog.mapper.domain.BlogPostHistoryVO">
|
||||
UPDATE blog_post_history
|
||||
SET
|
||||
post_title = #{postTitle},
|
||||
post_content = #{postContent},
|
||||
html_generated_at = #{htmlGeneratedAt},
|
||||
last_updusr_id = 'SYSTEM',
|
||||
last_updt_pnttm = NOW()
|
||||
WHERE post_id = #{postId}
|
||||
</update>
|
||||
|
||||
<!-- 발행 시작 업데이트 -->
|
||||
<update id="updatePublishStarted" parameterType="com.itn.admin.itn.blog.mapper.domain.BlogPostHistoryVO">
|
||||
UPDATE blog_post_history
|
||||
SET
|
||||
publish_started_at = #{publishStartedAt},
|
||||
last_updusr_id = 'SYSTEM',
|
||||
last_updt_pnttm = NOW()
|
||||
WHERE post_id = #{postId}
|
||||
</update>
|
||||
|
||||
<!-- 발행 완료(성공) 업데이트 -->
|
||||
<update id="updatePublishCompleted" parameterType="com.itn.admin.itn.blog.mapper.domain.BlogPostHistoryVO">
|
||||
UPDATE blog_post_history
|
||||
SET
|
||||
status = #{status},
|
||||
published_url = #{publishedUrl},
|
||||
published_at = #{publishedAt},
|
||||
last_updusr_id = 'SYSTEM',
|
||||
last_updt_pnttm = NOW()
|
||||
WHERE post_id = #{postId}
|
||||
</update>
|
||||
|
||||
<!-- 발행 실패 업데이트 -->
|
||||
<update id="updatePublishFailed" parameterType="com.itn.admin.itn.blog.mapper.domain.BlogPostHistoryVO">
|
||||
UPDATE blog_post_history
|
||||
SET
|
||||
status = #{status},
|
||||
error_message = #{errorMessage},
|
||||
last_updusr_id = 'SYSTEM',
|
||||
last_updt_pnttm = NOW()
|
||||
WHERE post_id = #{postId}
|
||||
</update>
|
||||
|
||||
<!-- 특정 발행 히스토리 조회 (ID로) -->
|
||||
<select id="selectBlogPostHistoryById" parameterType="long" resultType="com.itn.admin.itn.blog.mapper.domain.BlogPostHistoryVO">
|
||||
SELECT
|
||||
bph.post_id AS postId,
|
||||
bph.blog_id,
|
||||
bph.url_id,
|
||||
bph.post_title,
|
||||
bph.post_content,
|
||||
bph.published_url,
|
||||
bph.status,
|
||||
bph.error_message,
|
||||
bph.published_at,
|
||||
bph.html_generated_at,
|
||||
bph.publish_started_at,
|
||||
bph.frst_regist_pnttm,
|
||||
bph.last_updt_pnttm,
|
||||
ba.blog_name,
|
||||
bsu.title AS sourceTitle,
|
||||
bsu.url AS sourceUrl
|
||||
FROM
|
||||
blog_post_history bph
|
||||
LEFT JOIN
|
||||
blog_accounts ba ON bph.blog_id = ba.blog_id
|
||||
LEFT JOIN
|
||||
blog_source_urls bsu ON bph.url_id = bsu.url_id
|
||||
WHERE
|
||||
bph.post_id = #{postId}
|
||||
</select>
|
||||
|
||||
<!-- 소스별 발행 통계 조회 -->
|
||||
<select id="selectPublishStatsBySource" parameterType="map" resultType="map">
|
||||
SELECT
|
||||
bsu.url_id,
|
||||
bsu.title,
|
||||
bsu.url,
|
||||
COUNT(bph.post_id) AS totalPublishCount,
|
||||
COUNT(CASE WHEN bph.status = 'S' THEN 1 END) AS successCount,
|
||||
COUNT(CASE WHEN bph.status = 'F' THEN 1 END) AS failedCount,
|
||||
MAX(CASE WHEN bph.status = 'S' THEN bph.published_at END) AS lastPublishedAt,
|
||||
MAX(CASE WHEN bph.status = 'S' THEN bph.published_url END) AS lastPublishedUrl
|
||||
FROM
|
||||
blog_source_urls bsu
|
||||
LEFT JOIN
|
||||
blog_post_history bph ON bsu.url_id = bph.url_id AND bph.blog_id = #{blogId}
|
||||
WHERE
|
||||
bsu.is_active = 'Y'
|
||||
GROUP BY bsu.url_id, bsu.title, bsu.url
|
||||
ORDER BY bsu.frst_regist_pnttm DESC
|
||||
</select>
|
||||
|
||||
<!-- 워크플로우 히스토리 조회 (필터링 및 페이징 지원) -->
|
||||
<select id="selectWorkflowHistory" parameterType="map" resultType="com.itn.admin.itn.blog.mapper.domain.BlogPostHistoryVO">
|
||||
SELECT
|
||||
bph.post_id AS postId,
|
||||
bph.blog_id,
|
||||
bph.url_id,
|
||||
bph.post_title,
|
||||
bph.post_content,
|
||||
bph.published_url,
|
||||
bph.status,
|
||||
bph.error_message,
|
||||
bph.published_at,
|
||||
bph.html_generated_at,
|
||||
bph.publish_started_at,
|
||||
bph.frst_regist_pnttm,
|
||||
bph.last_updt_pnttm,
|
||||
ba.blog_name,
|
||||
bsu.title AS sourceTitle,
|
||||
bsu.url AS sourceUrl,
|
||||
CASE
|
||||
WHEN bph.html_generated_at IS NOT NULL THEN 'HTML 생성 완료'
|
||||
WHEN bph.status = 'I' THEN 'HTML 생성 중'
|
||||
ELSE 'HTML 생성 대기'
|
||||
END AS htmlStatus,
|
||||
CASE
|
||||
WHEN bph.published_at IS NOT NULL THEN '발행 완료'
|
||||
WHEN bph.publish_started_at IS NOT NULL THEN '발행 중'
|
||||
WHEN bph.html_generated_at IS NOT NULL THEN '발행 대기'
|
||||
ELSE '발행 준비 중'
|
||||
END AS publishStatus,
|
||||
CASE
|
||||
WHEN bph.html_generated_at IS NOT NULL AND bph.frst_regist_pnttm IS NOT NULL
|
||||
THEN TIMESTAMPDIFF(SECOND, bph.frst_regist_pnttm, bph.html_generated_at)
|
||||
ELSE NULL
|
||||
END AS htmlGenerationTime,
|
||||
CASE
|
||||
WHEN bph.published_at IS NOT NULL AND bph.publish_started_at IS NOT NULL
|
||||
THEN TIMESTAMPDIFF(SECOND, bph.publish_started_at, bph.published_at)
|
||||
ELSE NULL
|
||||
END AS publishTime,
|
||||
CASE
|
||||
WHEN bph.published_at IS NOT NULL AND bph.frst_regist_pnttm IS NOT NULL
|
||||
THEN TIMESTAMPDIFF(SECOND, bph.frst_regist_pnttm, bph.published_at)
|
||||
ELSE NULL
|
||||
END AS totalTime
|
||||
FROM
|
||||
blog_post_history bph
|
||||
LEFT JOIN
|
||||
blog_accounts ba ON bph.blog_id = ba.blog_id
|
||||
LEFT JOIN
|
||||
blog_source_urls bsu ON bph.url_id = bsu.url_id
|
||||
WHERE
|
||||
1=1
|
||||
<if test="blogId != null and blogId != ''">
|
||||
AND bph.blog_id = #{blogId}
|
||||
</if>
|
||||
<if test="urlId != null and urlId != ''">
|
||||
AND bph.url_id = #{urlId}
|
||||
</if>
|
||||
<if test="status != null and status != ''">
|
||||
AND bph.status = #{status}
|
||||
</if>
|
||||
ORDER BY bph.frst_regist_pnttm DESC
|
||||
LIMIT #{limit} OFFSET #{offset}
|
||||
</select>
|
||||
|
||||
<!-- 워크플로우 통계 조회 -->
|
||||
<select id="selectWorkflowStatistics" parameterType="map" resultType="map">
|
||||
SELECT
|
||||
COUNT(*) AS totalWorkflows,
|
||||
COUNT(CASE WHEN status = 'I' THEN 1 END) AS inProgressCount,
|
||||
COUNT(CASE WHEN status = 'S' THEN 1 END) AS successCount,
|
||||
COUNT(CASE WHEN status = 'F' THEN 1 END) AS failedCount,
|
||||
ROUND(COUNT(CASE WHEN status = 'S' THEN 1 END) * 100.0 / COUNT(*), 2) AS successRate,
|
||||
AVG(CASE
|
||||
WHEN html_generated_at IS NOT NULL AND frst_regist_pnttm IS NOT NULL
|
||||
THEN TIMESTAMPDIFF(SECOND, frst_regist_pnttm, html_generated_at)
|
||||
ELSE NULL
|
||||
END) AS avgHtmlGenerationTime,
|
||||
AVG(CASE
|
||||
WHEN published_at IS NOT NULL AND publish_started_at IS NOT NULL
|
||||
THEN TIMESTAMPDIFF(SECOND, publish_started_at, published_at)
|
||||
ELSE NULL
|
||||
END) AS avgPublishTime,
|
||||
AVG(CASE
|
||||
WHEN published_at IS NOT NULL AND frst_regist_pnttm IS NOT NULL
|
||||
THEN TIMESTAMPDIFF(SECOND, frst_regist_pnttm, published_at)
|
||||
ELSE NULL
|
||||
END) AS avgTotalTime,
|
||||
COUNT(CASE WHEN DATE(frst_regist_pnttm) = CURDATE() THEN 1 END) AS todayCount,
|
||||
COUNT(CASE WHEN DATE(frst_regist_pnttm) >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) THEN 1 END) AS weekCount
|
||||
FROM
|
||||
blog_post_history
|
||||
WHERE
|
||||
blog_id = #{blogId}
|
||||
</select>
|
||||
|
||||
<!-- 실패 원인별 분석 -->
|
||||
<select id="selectFailureAnalysis" parameterType="map" resultType="map">
|
||||
SELECT
|
||||
CASE
|
||||
WHEN error_message LIKE '%HTML 생성%' THEN 'HTML 생성 실패'
|
||||
WHEN error_message LIKE '%티스토리%' THEN '티스토리 발행 실패'
|
||||
WHEN error_message LIKE '%토큰%' THEN '인증 토큰 실패'
|
||||
WHEN error_message LIKE '%네트워크%' OR error_message LIKE '%timeout%' THEN '네트워크 오류'
|
||||
WHEN error_message IS NOT NULL THEN '기타 오류'
|
||||
ELSE '알 수 없는 오류'
|
||||
END AS failureCategory,
|
||||
COUNT(*) AS failureCount,
|
||||
ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM blog_post_history WHERE blog_id = #{blogId} AND status = 'F' AND DATE(frst_regist_pnttm) >= DATE_SUB(CURDATE(), INTERVAL #{days} DAY)), 2) AS failureRate,
|
||||
MAX(frst_regist_pnttm) AS lastFailureAt,
|
||||
GROUP_CONCAT(DISTINCT SUBSTRING(error_message, 1, 50) ORDER BY frst_regist_pnttm DESC SEPARATOR '; ') AS sampleErrors
|
||||
FROM
|
||||
blog_post_history
|
||||
WHERE
|
||||
blog_id = #{blogId}
|
||||
AND status = 'F'
|
||||
AND DATE(frst_regist_pnttm) >= DATE_SUB(CURDATE(), INTERVAL #{days} DAY)
|
||||
GROUP BY failureCategory
|
||||
ORDER BY failureCount DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
Loading…
Reference in New Issue
Block a user