Compare commits
1 Commits
master
...
client_03_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4518dcb19 |
31
pom.xml
31
pom.xml
@ -103,7 +103,18 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- DB Logback -->
|
||||
<dependency>
|
||||
<groupId>org.bgee.log4jdbc-log4j2</groupId>
|
||||
<artifactId>log4jdbc-log4j2-jdbc4.1</artifactId>
|
||||
<version>1.16</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
|
||||
<dependency>
|
||||
<groupId>org.mybatis.spring.boot</groupId>
|
||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||
<version>3.0.3</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
@ -160,24 +171,6 @@
|
||||
<artifactId>HikariCP</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/com.oracle.database.jdbc/ojdbc8 -->
|
||||
<dependency>
|
||||
<groupId>com.oracle.database.jdbc</groupId>
|
||||
<artifactId>ojdbc8</artifactId>
|
||||
<version>21.17.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/com.oracle.database.nls/orai18n -->
|
||||
<dependency>
|
||||
<groupId>com.oracle.database.nls</groupId>
|
||||
<artifactId>orai18n</artifactId>
|
||||
<version>21.17.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package com.itn.admin;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
|
||||
@ -11,7 +10,6 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@EnableScheduling
|
||||
@SpringBootApplication
|
||||
@MapperScan("com.itn.admin.itn.**.mapper") // 패키지 경로를 정확히 지정
|
||||
//@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
|
||||
public class ItnAdminApplication {
|
||||
|
||||
|
||||
@ -0,0 +1,75 @@
|
||||
package com.itn.admin.agent.client.cmm.service;
|
||||
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
public abstract class AbstractAgentService<T, M> implements AgentService<T> {
|
||||
|
||||
@Autowired
|
||||
protected M mapper; // 매퍼를 protected로 선언하여 서브 클래스에서 접근 가능
|
||||
|
||||
private static final int BATCH_SIZE = 100000;
|
||||
|
||||
@Override
|
||||
public RestResponse send(T agentVO) {
|
||||
List<T> agentVOL = new ArrayList<>();
|
||||
int sendCnt = parseSendCount(agentVO);
|
||||
|
||||
|
||||
for (int i = 0; i < sendCnt; i++) {
|
||||
T paramVO = createCopy(agentVO, i);
|
||||
agentVOL.add(paramVO);
|
||||
}
|
||||
|
||||
int totalSize = agentVOL.size();
|
||||
long startTime = System.currentTimeMillis() / 1000;
|
||||
int totalBatches = (int) Math.ceil((double) totalSize / BATCH_SIZE);
|
||||
for (int i = 0; i < totalSize; i += BATCH_SIZE) {
|
||||
int end = Math.min(totalSize, i + BATCH_SIZE);
|
||||
List<T> batchList = agentVOL.subList(i, end);
|
||||
insertBatch(batchList);
|
||||
logBatchProgress(i, totalBatches);
|
||||
}
|
||||
long endTime = System.currentTimeMillis() / 1000;
|
||||
long totalTime = endTime - startTime;
|
||||
log.info("insert 시간 : [{}]", totalTime);
|
||||
return new RestResponse(HttpStatus.OK, "데이터를 정상적으로 입력했습니다.", totalTime + "초");
|
||||
}
|
||||
|
||||
public RestResponse findByInsertCnt(T agentVO) {
|
||||
int count = countByCondition(agentVO);
|
||||
return new RestResponse(HttpStatus.OK, "", count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestResponse findByLogMoveCntWhereMessage(T agentVO) {
|
||||
int count = countByLogMoveCntWhereMsgTypeAndMessage(agentVO);
|
||||
return new RestResponse(HttpStatus.OK, "", count);
|
||||
}
|
||||
public RestResponse findAllLogMoveCnt(T agentVO) {
|
||||
int count = this.countAllLogMoveCnt(agentVO);
|
||||
return new RestResponse(HttpStatus.OK, "", count);
|
||||
}
|
||||
|
||||
protected abstract int countAllLogMoveCnt(T agentVO);
|
||||
|
||||
protected abstract int countByLogMoveCntWhereMsgTypeAndMessage(T agentVO);
|
||||
|
||||
protected abstract int countByCondition(T agentVO);
|
||||
protected abstract int parseSendCount(T agentVO);
|
||||
protected abstract T createCopy(T originalVO, int index);
|
||||
protected abstract void insertBatch(List<T> batchList);
|
||||
|
||||
private void logBatchProgress(int i, int totalBatches) {
|
||||
int currentBatch = (i / BATCH_SIZE) + 1;
|
||||
log.info("현재 처리 중인 배치: [{}]", currentBatch + "/" + totalBatches);
|
||||
log.info("남은 배치 수: [{}]", (totalBatches - currentBatch));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.itn.admin.agent.client.cmm.service;
|
||||
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
|
||||
public interface AgentService<T> {
|
||||
RestResponse send(T agentVO);
|
||||
|
||||
RestResponse findByLogMoveCntWhereMessage(T agentVO);
|
||||
|
||||
RestResponse findAllLogMoveCnt(T agentVO);
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package com.itn.admin.agent.client.one.mapper;
|
||||
|
||||
import com.itn.admin.agent.client.one.mapper.domain.AgentCOneVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* packageName : com.itn.admin.agent.mapper
|
||||
* fileName : AgentMapper
|
||||
* author : hylee
|
||||
* date : 2024-07-31
|
||||
* description :
|
||||
* ===========================================================
|
||||
* DATE AUTHOR NOTE
|
||||
* -----------------------------------------------------------
|
||||
* 2024-07-31 hylee 최초 생성
|
||||
*/
|
||||
@Mapper
|
||||
public interface AgentCOneMapper {
|
||||
|
||||
List<AgentCOneVO> findAll(AgentCOneVO agentCTwoVO);
|
||||
|
||||
void insertAgents(List<AgentCOneVO> agentCTwoVO);
|
||||
|
||||
int countBySendStatusNotAndMsgType(AgentCOneVO agentCTwoVO);
|
||||
|
||||
int countByLogMoveCntWhereMsgTypeAndMessage(AgentCOneVO agentVO);
|
||||
|
||||
int findAllLogMoveCnt(AgentCOneVO agentVO);
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package com.itn.admin.agent.client.one.mapper.domain;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* packageName : com.itn.admin.agent.mapper.domain
|
||||
* fileName : AgentVO
|
||||
* author : hylee
|
||||
* date : 2024-07-31
|
||||
* description : 에이젼트 테스트발송
|
||||
* ===========================================================
|
||||
* DATE AUTHOR NOTE
|
||||
* -----------------------------------------------------------
|
||||
* 2023-05-09 hylee 최초 생성
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
public class AgentCOneVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private String msgType;
|
||||
private String sendStatus;
|
||||
private String requestSate;
|
||||
private String recvPhone;
|
||||
private String sendPhone;
|
||||
private String subject;
|
||||
private String message;
|
||||
private String sendCnt;
|
||||
private String fileName01;
|
||||
private String fileName02;
|
||||
private String fileName03;
|
||||
|
||||
private String cnt;
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package com.itn.admin.agent.client.one.service;
|
||||
|
||||
import com.itn.admin.agent.client.one.mapper.domain.AgentCOneVO;
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
|
||||
|
||||
public interface AgentCOneService {
|
||||
|
||||
|
||||
|
||||
RestResponse send(AgentCOneVO agentCOneVO);
|
||||
|
||||
RestResponse findByInsertCnt(AgentCOneVO agentCOneVO);
|
||||
|
||||
RestResponse findByLogMoveCntWhereMessage(AgentCOneVO agentCOneVO);
|
||||
|
||||
RestResponse findAllLogMoveCnt(AgentCOneVO agentCOneVO);
|
||||
|
||||
// RestResponse findByReportCnt(AgentCTwoVO agentCTwoVO);
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
package com.itn.admin.agent.client.one.service.impl;
|
||||
|
||||
import com.itn.admin.agent.client.cmm.service.AbstractAgentService;
|
||||
import com.itn.admin.agent.client.one.mapper.AgentCOneMapper;
|
||||
import com.itn.admin.agent.client.one.mapper.domain.AgentCOneVO;
|
||||
import com.itn.admin.agent.client.one.service.AgentCOneService;
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AgentCOneServiceImpl extends AbstractAgentService<AgentCOneVO, AgentCOneMapper> implements AgentCOneService {
|
||||
|
||||
@Autowired
|
||||
private AgentCOneMapper agentCOneMapper;
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
this.mapper = agentCOneMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int countAllLogMoveCnt(AgentCOneVO agentVO) {
|
||||
return mapper.findAllLogMoveCnt(agentVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int countByLogMoveCntWhereMsgTypeAndMessage(AgentCOneVO agentVO) {
|
||||
return mapper.countByLogMoveCntWhereMsgTypeAndMessage(agentVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int countByCondition(AgentCOneVO agentVO) {
|
||||
return mapper.countBySendStatusNotAndMsgType(agentVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int parseSendCount(AgentCOneVO agentVO) {
|
||||
try {
|
||||
return (agentVO.getSendCnt() != null && !agentVO.getSendCnt().isEmpty()) ? Integer.parseInt(agentVO.getSendCnt()) : 0;
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AgentCOneVO createCopy(AgentCOneVO originalVO, int index) {
|
||||
|
||||
if (!originalVO.getMessage().startsWith("ITN")) {
|
||||
return originalVO;
|
||||
}
|
||||
|
||||
AgentCOneVO paramVO = new AgentCOneVO();
|
||||
String msgType = originalVO.getMsgType();
|
||||
|
||||
paramVO.setMsgType(msgType);
|
||||
paramVO.setSendStatus(originalVO.getSendStatus());
|
||||
paramVO.setRecvPhone(modifyPhoneNumber(originalVO.getRecvPhone(), index));
|
||||
paramVO.setSendPhone(modifyPhoneNumber(originalVO.getSendPhone(), index));
|
||||
|
||||
paramVO.setFileName01(originalVO.getFileName01());
|
||||
paramVO.setFileName02(originalVO.getFileName02());
|
||||
paramVO.setFileName03(originalVO.getFileName03());
|
||||
|
||||
paramVO.setMessage(originalVO.getMessage() + " " + (index + 1));
|
||||
if (!"S".equals(msgType)) {
|
||||
paramVO.setSubject(originalVO.getSubject() + " " + (index + 1));
|
||||
}
|
||||
return paramVO;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void insertBatch(List<AgentCOneVO> batchList) {
|
||||
mapper.insertAgents(batchList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private String modifyPhoneNumber(String phone, int index) {
|
||||
// 휴대폰 번호는 010-XXXX-YYYY 형식으로 가정
|
||||
String prefix = phone.substring(0, 4); // "010-" 부분
|
||||
String middle = phone.substring(4, 8); // "XXXX" 부분
|
||||
String suffix = phone.substring(8); // "YYYY" 부분
|
||||
|
||||
// 중간 부분 숫자 수정
|
||||
int middleNumber = Integer.parseInt(middle);
|
||||
middleNumber = (middleNumber + index) % 10000; // 0000~9999 사이의 값으로 제한
|
||||
middle = String.format("%04d", middleNumber); // 네 자리 숫자로 포맷
|
||||
|
||||
return prefix + middle + suffix;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package com.itn.admin.agent.client.one.web;
|
||||
|
||||
import com.itn.admin.agent.client.one.mapper.domain.AgentCOneVO;
|
||||
import com.itn.admin.agent.client.one.service.AgentCOneService;
|
||||
import com.itn.admin.agent.client.two.mapper.domain.AgentCTwoVO;
|
||||
import com.itn.admin.agent.client.two.service.AgentCTwoService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
|
||||
@Controller
|
||||
public class AgentCOneController {
|
||||
|
||||
private AgentCOneService agentCOneService;
|
||||
|
||||
@Value("${spring.mjagent.client.one.userid}")
|
||||
private String ONE_USER_ID;
|
||||
|
||||
@Value("${spring.mjagent.client.two.userid}")
|
||||
private String TOW_USER_ID;
|
||||
|
||||
|
||||
@Autowired
|
||||
public void setAgentCOneService(AgentCOneService agentCOneService) {
|
||||
this.agentCOneService = agentCOneService;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,121 @@
|
||||
package com.itn.admin.agent.client.one.web;
|
||||
|
||||
import com.itn.admin.agent.client.one.mapper.domain.AgentCOneVO;
|
||||
import com.itn.admin.agent.client.one.service.AgentCOneService;
|
||||
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.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
public class AgentCOneRestController {
|
||||
|
||||
private AgentCOneService agentCOneService;
|
||||
|
||||
private final String UPLOAD_DIR = "/home/mjon_client_agent_1/mmsfile";
|
||||
|
||||
|
||||
@Autowired
|
||||
public void setAgentService(AgentCOneService agentCOneService) {
|
||||
this.agentCOneService = agentCOneService;
|
||||
}
|
||||
|
||||
/*
|
||||
* client db에 insert
|
||||
* */
|
||||
@PostMapping("/agent/one/send")
|
||||
public ResponseEntity<RestResponse> send(@RequestBody AgentCOneVO agentCOneVO) throws Exception {
|
||||
return ResponseEntity.ok().body(agentCOneService.send(agentCOneVO));
|
||||
}
|
||||
|
||||
/*
|
||||
* client db에 insert 됐는지 확인 count
|
||||
* */
|
||||
@PostMapping("/agent/one/findByInsertCnt")
|
||||
public ResponseEntity<RestResponse> findByInsertCnt(@RequestBody AgentCOneVO agentCOneVO) throws Exception {
|
||||
return ResponseEntity.ok().body(agentCOneService.findByInsertCnt(agentCOneVO));
|
||||
}
|
||||
|
||||
/*
|
||||
* client LOG TB에 insert 됐는지 확인 count
|
||||
* 리포트할때 ''현재'' 데이터가 LOG 테이블에 이동됐는지 확인
|
||||
* select cnt MESSAGE LIKE CONCAT(#{message}, '%')
|
||||
* */
|
||||
@PostMapping("/agent/one/findByLogMoveCntWhereMessage")
|
||||
public ResponseEntity<RestResponse> findByLogMoveCntWhereMessage(@RequestBody AgentCOneVO agentCOneVO) throws Exception {
|
||||
return ResponseEntity.ok().body(agentCOneService.findByLogMoveCntWhereMessage(agentCOneVO));
|
||||
}
|
||||
|
||||
/*
|
||||
* client LOG TB에 insert 됐는지 확인 count
|
||||
* 리포트할때 동일 타입 데이터가 LOG 테이블에 이동됐는지 확인
|
||||
* select cnt WHERE msgType = #{msgType}
|
||||
* */
|
||||
@PostMapping("/agent/one/findByLogMoveCnt")
|
||||
public ResponseEntity<RestResponse> findByLogMoveCnt(@RequestBody AgentCOneVO agentCOneVO) throws Exception {
|
||||
return ResponseEntity.ok().body(agentCOneService.findAllLogMoveCnt(agentCOneVO));
|
||||
}
|
||||
|
||||
@PostMapping("/agent/one/uploadFiles")
|
||||
public ResponseEntity<RestResponse> uploadFiles(@RequestParam("fileName01") MultipartFile file1,
|
||||
@RequestParam("fileName02") MultipartFile file2,
|
||||
@RequestParam("fileName03") MultipartFile file3) {
|
||||
try {
|
||||
Map<String, String> fileNames = new HashMap<>();
|
||||
|
||||
// 각 파일을 업로드하고 파일명을 수집
|
||||
String fileName = "";
|
||||
if (!file1.isEmpty()) {
|
||||
fileName = uploadSingleFile(file1);
|
||||
fileNames.put("fileName01", fileName);
|
||||
}
|
||||
|
||||
if (!file2.isEmpty()) {
|
||||
fileName = uploadSingleFile(file2);
|
||||
fileNames.put("fileName02", fileName);
|
||||
}
|
||||
|
||||
if (!file3.isEmpty()) {
|
||||
fileName = uploadSingleFile(file3);
|
||||
fileNames.put("fileName03", fileName);
|
||||
}
|
||||
// 경로와 파일명 반환
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("fileNames", fileNames);
|
||||
response.put("status", "OK");
|
||||
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.OK, "",response) );
|
||||
|
||||
} catch (IOException ex) {
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR, "저장실패","") );
|
||||
}
|
||||
}
|
||||
|
||||
private String uploadSingleFile(MultipartFile file) throws IOException {
|
||||
String fileName = StringUtils.cleanPath(file.getOriginalFilename());
|
||||
Path uploadPath = Paths.get(UPLOAD_DIR);
|
||||
|
||||
if (!Files.exists(uploadPath)) {
|
||||
Files.createDirectories(uploadPath);
|
||||
}
|
||||
|
||||
Path filePath = uploadPath.resolve(fileName);
|
||||
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package com.itn.admin.agent.client.two.mapper;
|
||||
|
||||
import com.itn.admin.agent.client.two.mapper.domain.AgentCTwoVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* packageName : com.itn.admin.agent.mapper
|
||||
* fileName : AgentMapper
|
||||
* author : hylee
|
||||
* date : 2024-07-31
|
||||
* description :
|
||||
* ===========================================================
|
||||
* DATE AUTHOR NOTE
|
||||
* -----------------------------------------------------------
|
||||
* 2024-07-31 hylee 최초 생성
|
||||
*/
|
||||
@Mapper
|
||||
public interface AgentCTwoMapper {
|
||||
|
||||
List<AgentCTwoVO> findAll(AgentCTwoVO agentCTwoVO);
|
||||
|
||||
void insertAgents(List<AgentCTwoVO> agentCTwoVO);
|
||||
|
||||
int countBySendStatusNotAndMsgType(AgentCTwoVO agentCTwoVO);
|
||||
|
||||
int countByLogMoveCntWhereMsgTypeAndMessage(AgentCTwoVO agentVO);
|
||||
|
||||
int findAllLogMoveCnt(AgentCTwoVO agentVO);
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package com.itn.admin.agent.client.two.mapper.domain;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* packageName : com.itn.admin.agent.mapper.domain
|
||||
* fileName : AgentVO
|
||||
* author : hylee
|
||||
* date : 2024-07-31
|
||||
* description : 에이젼트 테스트발송
|
||||
* ===========================================================
|
||||
* DATE AUTHOR NOTE
|
||||
* -----------------------------------------------------------
|
||||
* 2023-05-09 hylee 최초 생성
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
public class AgentCTwoVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private String msgType;
|
||||
private String sendStatus;
|
||||
private String requestSate;
|
||||
private String recvPhone;
|
||||
private String sendPhone;
|
||||
private String subject;
|
||||
private String message;
|
||||
private String sendCnt;
|
||||
private String fileName01;
|
||||
private String fileName02;
|
||||
private String fileName03;
|
||||
|
||||
private String cnt;
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package com.itn.admin.agent.client.two.service;
|
||||
|
||||
import com.itn.admin.agent.client.two.mapper.domain.AgentCTwoVO;
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
|
||||
|
||||
public interface AgentCTwoService {
|
||||
|
||||
|
||||
|
||||
RestResponse send(AgentCTwoVO agentCTwoVO);
|
||||
|
||||
RestResponse findByInsertCnt(AgentCTwoVO agentCTwoVO);
|
||||
|
||||
RestResponse findByLogMoveCntWhereMessage(AgentCTwoVO agentCTwoVO);
|
||||
|
||||
RestResponse findAllLogMoveCnt(AgentCTwoVO agentCTwoVO);
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
package com.itn.admin.agent.client.two.service.impl;
|
||||
|
||||
import com.itn.admin.agent.client.cmm.service.AbstractAgentService;
|
||||
import com.itn.admin.agent.client.two.mapper.domain.AgentCTwoVO;
|
||||
import com.itn.admin.agent.client.two.mapper.AgentCTwoMapper;
|
||||
import com.itn.admin.agent.client.two.service.AgentCTwoService;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AgentCTwoServiceImpl extends AbstractAgentService<AgentCTwoVO, AgentCTwoMapper> implements AgentCTwoService {
|
||||
|
||||
|
||||
@Autowired
|
||||
private AgentCTwoMapper agentCTwoMapper;
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
this.mapper = agentCTwoMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int countByLogMoveCntWhereMsgTypeAndMessage(AgentCTwoVO agentVO) {
|
||||
return mapper.countByLogMoveCntWhereMsgTypeAndMessage(agentVO);
|
||||
// return mapper.countByLogMoveCntWhereMsgTypeAndMessage(agentVO);
|
||||
}
|
||||
@Override
|
||||
protected int countAllLogMoveCnt(AgentCTwoVO agentVO) {
|
||||
return mapper.findAllLogMoveCnt(agentVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int countByCondition(AgentCTwoVO agentVO) {
|
||||
return mapper.countBySendStatusNotAndMsgType(agentVO);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected int parseSendCount(AgentCTwoVO agentVO) {
|
||||
try {
|
||||
return (agentVO.getSendCnt() != null && !agentVO.getSendCnt().isEmpty()) ? Integer.parseInt(agentVO.getSendCnt()) : 0;
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AgentCTwoVO createCopy(AgentCTwoVO originalVO, int index) {
|
||||
|
||||
if (!originalVO.getMessage().startsWith("ITN")) {
|
||||
return originalVO;
|
||||
}
|
||||
|
||||
AgentCTwoVO paramVO = new AgentCTwoVO();
|
||||
String msgType = originalVO.getMsgType();
|
||||
|
||||
paramVO.setMsgType(msgType);
|
||||
paramVO.setSendStatus(originalVO.getSendStatus());
|
||||
paramVO.setRecvPhone(modifyPhoneNumber(originalVO.getRecvPhone(), index));
|
||||
paramVO.setSendPhone(modifyPhoneNumber(originalVO.getSendPhone(), index));
|
||||
|
||||
paramVO.setFileName01(originalVO.getFileName01());
|
||||
paramVO.setFileName02(originalVO.getFileName02());
|
||||
paramVO.setFileName03(originalVO.getFileName03());
|
||||
|
||||
paramVO.setMessage(originalVO.getMessage() + " " + (index + 1));
|
||||
if (!"S".equals(msgType)) {
|
||||
paramVO.setSubject(originalVO.getSubject() + " " + (index + 1));
|
||||
}
|
||||
return paramVO;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void insertBatch(List<AgentCTwoVO> batchList) {
|
||||
mapper.insertAgents(batchList);
|
||||
}
|
||||
|
||||
|
||||
private String modifyPhoneNumber(String phone, int index) {
|
||||
// 휴대폰 번호는 010-XXXX-YYYY 형식으로 가정
|
||||
String prefix = phone.substring(0, 4); // "010-" 부분
|
||||
String middle = phone.substring(4, 8); // "XXXX" 부분
|
||||
String suffix = phone.substring(8); // "YYYY" 부분
|
||||
|
||||
// 중간 부분 숫자 수정
|
||||
int middleNumber = Integer.parseInt(middle);
|
||||
middleNumber = (middleNumber + index) % 10000; // 0000~9999 사이의 값으로 제한
|
||||
middle = String.format("%04d", middleNumber); // 네 자리 숫자로 포맷
|
||||
|
||||
return prefix + middle + suffix;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package com.itn.admin.agent.client.two.web;
|
||||
|
||||
import com.itn.admin.agent.client.two.service.AgentCTwoService;
|
||||
import com.itn.admin.agent.client.two.mapper.domain.AgentCTwoVO;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
|
||||
@Controller
|
||||
public class AgentCTwoController {
|
||||
|
||||
private AgentCTwoService agentCTwoService;
|
||||
|
||||
@Value("${spring.mjagent.client.one.userid}")
|
||||
private String ONE_USER_ID;
|
||||
|
||||
@Value("${spring.mjagent.client.two.userid}")
|
||||
private String TOW_USER_ID;
|
||||
|
||||
|
||||
@Autowired
|
||||
public void setAgentCTwoService(AgentCTwoService agentCTwoService) {
|
||||
this.agentCTwoService = agentCTwoService;
|
||||
}
|
||||
|
||||
@GetMapping(value = "/agent/view")
|
||||
public String list(@ModelAttribute("agentVO") AgentCTwoVO agentCTwoVO, Model model) {
|
||||
|
||||
|
||||
model.addAttribute("oneUserId", ONE_USER_ID);
|
||||
model.addAttribute("twoUserId", TOW_USER_ID);
|
||||
|
||||
return "agent/view";
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,117 @@
|
||||
package com.itn.admin.agent.client.two.web;
|
||||
|
||||
import com.itn.admin.agent.client.two.mapper.domain.AgentCTwoVO;
|
||||
import com.itn.admin.agent.client.two.service.AgentCTwoService;
|
||||
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.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
public class AgentCTwoRestController {
|
||||
|
||||
private AgentCTwoService agentCTwoService;
|
||||
|
||||
private final String UPLOAD_DIR = "/home/mjon_client_agent_2/mmsfile";
|
||||
|
||||
|
||||
@Autowired
|
||||
public void setAgentService(AgentCTwoService agentCTwoService) {
|
||||
this.agentCTwoService = agentCTwoService;
|
||||
}
|
||||
|
||||
/*
|
||||
* client db에 insert
|
||||
* */
|
||||
@PostMapping("/agent/two/send")
|
||||
public ResponseEntity<RestResponse> send(@RequestBody AgentCTwoVO agentCTwoVO) throws Exception {
|
||||
return ResponseEntity.ok().body(agentCTwoService.send(agentCTwoVO));
|
||||
}
|
||||
|
||||
/*
|
||||
* client db에 insert 됐는지 확인 count
|
||||
* */
|
||||
@PostMapping("/agent/two/findByInsertCnt")
|
||||
public ResponseEntity<RestResponse> findByInsertCnt(@RequestBody AgentCTwoVO agentCTwoVO) throws Exception {
|
||||
System.out.println(" :: /agent/two/findByInsertCnt :: ");
|
||||
return ResponseEntity.ok().body(agentCTwoService.findByInsertCnt(agentCTwoVO));
|
||||
}
|
||||
|
||||
/*
|
||||
* client LOG TB에 insert 됐는지 확인 count
|
||||
* 리포트할때 ''현재'' 데이터가 LOG 테이블에 이동됐는지 확인
|
||||
* select cnt WHERE MESSAGE LIKE CONCAT(#{message}, '%')
|
||||
* */
|
||||
@PostMapping("/agent/two/findByLogMoveCntWhereMessage")
|
||||
public ResponseEntity<RestResponse> findByLogMoveCntWhereMessage(@RequestBody AgentCTwoVO agentCTwoVO) throws Exception {
|
||||
return ResponseEntity.ok().body(agentCTwoService.findByLogMoveCntWhereMessage(agentCTwoVO));
|
||||
}
|
||||
|
||||
/*
|
||||
* client LOG TB에 insert 됐는지 확인 count
|
||||
* 리포트 update한 모든 데이터가 LOG 테이블에 이동됐는지 확인
|
||||
* */
|
||||
@PostMapping("/agent/two/findByLogMoveCnt")
|
||||
public ResponseEntity<RestResponse> findByLogMoveCnt(@RequestBody AgentCTwoVO agentCTwoVO) throws Exception {
|
||||
return ResponseEntity.ok().body(agentCTwoService.findAllLogMoveCnt(agentCTwoVO));
|
||||
}
|
||||
|
||||
@PostMapping("/agent/two/uploadFiles")
|
||||
public ResponseEntity<RestResponse> uploadFiles(@RequestParam("fileName01") MultipartFile file1,
|
||||
@RequestParam("fileName02") MultipartFile file2,
|
||||
@RequestParam("fileName03") MultipartFile file3) {
|
||||
try {
|
||||
Map<String, String> fileNames = new HashMap<>();
|
||||
|
||||
// 각 파일을 업로드하고 파일명을 수집
|
||||
String fileName = "";
|
||||
if (!file1.isEmpty()) {
|
||||
fileName = uploadSingleFile(file1);
|
||||
fileNames.put("fileName01", fileName);
|
||||
}
|
||||
|
||||
if (!file2.isEmpty()) {
|
||||
fileName = uploadSingleFile(file2);
|
||||
fileNames.put("fileName02", fileName);
|
||||
}
|
||||
|
||||
if (!file3.isEmpty()) {
|
||||
fileName = uploadSingleFile(file3);
|
||||
fileNames.put("fileName03", fileName);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.OK, "",fileNames) );
|
||||
|
||||
} catch (IOException ex) {
|
||||
return ResponseEntity.ok(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR, "저장실패","") );
|
||||
}
|
||||
}
|
||||
|
||||
private String uploadSingleFile(MultipartFile file) throws IOException {
|
||||
String fileName = StringUtils.cleanPath(file.getOriginalFilename());
|
||||
Path uploadPath = Paths.get(UPLOAD_DIR);
|
||||
|
||||
if (!Files.exists(uploadPath)) {
|
||||
Files.createDirectories(uploadPath);
|
||||
}
|
||||
|
||||
Path filePath = uploadPath.resolve(fileName);
|
||||
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package com.itn.admin.agent.server.mapper;
|
||||
|
||||
import com.itn.admin.agent.server.mapper.domain.AgentSVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* packageName : com.itn.admin.agent.mapper
|
||||
* fileName : AgentMapper
|
||||
* author : hylee
|
||||
* date : 2024-07-31
|
||||
* description :
|
||||
* ===========================================================
|
||||
* DATE AUTHOR NOTE
|
||||
* -----------------------------$------------------------------
|
||||
* 2024-07-31 hylee 최초 생성
|
||||
*/
|
||||
@Mapper
|
||||
public interface AgentSMapper {
|
||||
int countByCurStateAndUserId(AgentSVO agentSVO);
|
||||
|
||||
int updateReportWhereUserId(AgentSVO agentSVO);
|
||||
|
||||
String findByCurStateAndUserIdAndSmsTxt(AgentSVO agentSVO);
|
||||
|
||||
int updateReportWhereUserIdAndMassage(AgentSVO agentSVO);
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
package com.itn.admin.agent.server.mapper.domain;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* packageName : com.itn.admin.agent.server.mapper.domain
|
||||
* fileName : AgentSVO
|
||||
* author : hylee
|
||||
* date : 2024-07-31
|
||||
* description : 에이젼트 테스트발송
|
||||
* ===========================================================
|
||||
* DATE AUTHOR NOTE
|
||||
* -----------------------------------------------------------
|
||||
* 2023-05-09 hylee 최초 생성
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
public class AgentSVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
private String msgId; // 문자ID
|
||||
private String userId; // 문자온 일반회원ID
|
||||
private Integer userdata; // 사용자 정의 코드
|
||||
private Long msgSeq; // 메시지의 고유번호
|
||||
private String agentMsgId; // 고객전송 문자ID
|
||||
private Integer curState; // 상태 값
|
||||
private Date sentDate; // 메시지를 전송한 시각
|
||||
private Date rsltDate; // 핸드폰에 전달된 시간
|
||||
private Date reportDate; // 레포트 처리한 시간
|
||||
private Date reqDate; // 예약일시
|
||||
private Integer rsltCode; // 결과처리코드
|
||||
private String rsltCode2; // 결과처리 상세코드
|
||||
private String rsltNet; // 결과처리 통신사
|
||||
private String callTo; // 수신번호
|
||||
private String callFrom; // 발신번호
|
||||
private String subject; // MMS경우 제목
|
||||
private String smsTxt; // SMS용 메시지본문
|
||||
private String msgType; // 메시지의 종류
|
||||
private String msgPayCode; // 최종전송콘텐트 종류 저장
|
||||
private Integer contSeq; // MMS의 콘텐츠 Key
|
||||
private Integer msgTypeResend; // 재전송할 문자 타입
|
||||
private Integer centerSeqResend; // 재전송할 센터
|
||||
private String msgNoticetalkSenderKey; // 카카오 알림톡에 등록된 사용자 고유키
|
||||
private String msgNoticetalkTmpKey; // 카카오 알림톡에 등록된 문자 템플릿 고유키
|
||||
private Integer msgResendCount; // 재전송한 카운트
|
||||
private Date msgResendDate; // 재전송된 시간
|
||||
private Date sentDatePre; // 이전 메시지를 전송한 시각
|
||||
private Date rsltDatePre; // 이전 핸드폰에 전달된 시간
|
||||
private Date reportDatePre; // 이전 레포트 처리한 시간
|
||||
private Integer rsltCodePre; // 이전 결과처리코드
|
||||
private String rsltCode2Pre; // 이전 결과처리 상세코드
|
||||
private String rsltNetPre; // 이전 결과처리 통신사
|
||||
private String conectMthd; // 접속한 기기
|
||||
private String agentCode; // 전송사
|
||||
private String delFlag; // 사용자 삭제여부
|
||||
private Integer fileCnt; // 첨부파일 갯수
|
||||
private String filePath1; // 첨부파일 1 경로
|
||||
private String filePath2; // 첨부파일 2 경로
|
||||
private String filePath3; // 첨부파일 3 경로
|
||||
private String msgGroupId; // 문자그룹ID
|
||||
private String neoType; // NEO MMS메세지 타입
|
||||
private String reserveCYn; // 예약 취소 유무
|
||||
private String refundYn; // 문자발송 실패의 환불처리 유무
|
||||
private Date resultLogUpdtPnttm; // LOG 테이블의 결과를 업데이트한 시간
|
||||
private String resellerCode; // 발송사업자 식별코드 정의
|
||||
private String bizKakaoResendType; // 카카오 재전송 type
|
||||
private String bizKakaoResendData; // 카카오 재전송 내용
|
||||
private String bizKakaoJsonFile; // 카카오 json 첨부파일
|
||||
private String bizKakaoResendYn; // 카카오 재전송 여부
|
||||
private String bizKakaoTitle; // 카카오 강조유형 타이틀
|
||||
private String bizUmid; // 비즈뿌리오 서버에서 정의한 ID
|
||||
private String message; //
|
||||
|
||||
// client 컬럼
|
||||
private String sendStatus;
|
||||
private String requestSate;
|
||||
private String recvPhone;
|
||||
private String sendPhone;
|
||||
private String sendCnt;
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package com.itn.admin.agent.server.service;
|
||||
|
||||
import com.itn.admin.agent.server.mapper.domain.AgentSVO;
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
|
||||
|
||||
public interface AgentSService {
|
||||
|
||||
|
||||
RestResponse findByTransferCnt(AgentSVO agentSVO);
|
||||
|
||||
RestResponse serverReport(AgentSVO agentSVO);
|
||||
|
||||
RestResponse nowDataReport(AgentSVO agentSVO);
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package com.itn.admin.agent.server.service.impl;
|
||||
|
||||
import com.itn.admin.agent.server.mapper.AgentSMapper;
|
||||
import com.itn.admin.agent.server.mapper.domain.AgentSVO;
|
||||
import com.itn.admin.agent.server.service.AgentSService;
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
import io.micrometer.common.util.StringUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AgentSServiceImpl implements AgentSService {
|
||||
|
||||
|
||||
@Autowired
|
||||
AgentSMapper agentSMapper;
|
||||
|
||||
@Override
|
||||
public RestResponse findByTransferCnt(AgentSVO agentSVO) {
|
||||
|
||||
// int cnt = agentSMapper.countByCurStateAndUserId(agentSVO);
|
||||
String cntTxt = agentSMapper.findByCurStateAndUserIdAndSmsTxt(agentSVO);
|
||||
|
||||
if(StringUtils.isNotEmpty(cntTxt) ){
|
||||
cntTxt = cntTxt.replace(agentSVO.getMessage(),"").trim();
|
||||
}
|
||||
return new RestResponse(HttpStatus.OK,"", cntTxt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestResponse serverReport(AgentSVO agentSVO) {
|
||||
int cnt = agentSMapper.updateReportWhereUserId(agentSVO);
|
||||
String msg = agentSVO.getUserId()+ "관련 모든 데이터 report 시작합니다.";
|
||||
return new RestResponse(HttpStatus.OK,msg, cnt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestResponse nowDataReport(AgentSVO agentSVO) {
|
||||
|
||||
int cnt = agentSMapper.updateReportWhereUserIdAndMassage(agentSVO);
|
||||
|
||||
log.info(" + nowDataReport cnt : [{}]", cnt);
|
||||
String msg = agentSVO.getMessage() + "관련 데이터 report 시작합니다.";
|
||||
|
||||
return new RestResponse(HttpStatus.OK,msg, cnt);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package com.itn.admin.agent.server.web;
|
||||
|
||||
import com.itn.admin.agent.client.two.service.AgentCTwoService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
@Controller
|
||||
public class AgentSController {
|
||||
|
||||
private AgentCTwoService agentCTwoService;
|
||||
|
||||
@Autowired
|
||||
public void setCommuteService(AgentCTwoService agentCTwoService) {
|
||||
this.agentCTwoService = agentCTwoService;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
package com.itn.admin.agent.server.web;
|
||||
|
||||
import com.itn.admin.agent.server.mapper.domain.AgentSVO;
|
||||
import com.itn.admin.agent.server.service.AgentSService;
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class AgentSRestController {
|
||||
|
||||
private AgentSService agentSService;
|
||||
|
||||
@Autowired
|
||||
public void setAgentService(AgentSService agentSService) {
|
||||
this.agentSService = agentSService;
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
* */
|
||||
@PostMapping("/agent/server/findByTransferCnt")
|
||||
public ResponseEntity<RestResponse> findByTransferCnt(@RequestBody AgentSVO agentSVO) throws Exception {
|
||||
return ResponseEntity.ok().body(agentSService.findByTransferCnt(agentSVO));
|
||||
}
|
||||
|
||||
/*
|
||||
* 전송사가 리턴해준것처럼
|
||||
* server DB에 update 함
|
||||
* @@ 모든 데이터 대량 없뎃
|
||||
* */
|
||||
@PostMapping("/agent/server/allReport")
|
||||
public ResponseEntity<RestResponse> serverReport(@RequestBody AgentSVO agentSVO) throws Exception {
|
||||
return ResponseEntity.ok().body(agentSService.serverReport(agentSVO));
|
||||
}
|
||||
|
||||
/*
|
||||
* 전송사가 리턴해준것처럼
|
||||
* server DB에 update 함
|
||||
* @@ 현재 화면 기준 data만 없뎃
|
||||
* */
|
||||
@PostMapping("/agent/server/nowDataReport")
|
||||
public ResponseEntity<RestResponse> serverNowDataReport(@RequestBody AgentSVO agentSVO) throws Exception {
|
||||
return ResponseEntity.ok().body(agentSService.nowDataReport(agentSVO));
|
||||
}
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
package com.itn.admin.cmn.code.common;
|
||||
|
||||
public class CommonCodeConstants {
|
||||
|
||||
// 연차 계획 상태
|
||||
public static final String LEAVE_STATUS_PENDING = "10";
|
||||
public static final String LEAVE_STATUS_APPROVED = "20";
|
||||
public static final String LEAVE_STATUS_REJECTED = "30";
|
||||
|
||||
// 연차 변경 요청 상태
|
||||
public static final String LEAVE_CHANGE_REQUEST_STATUS_NONE = "0";
|
||||
public static final String LEAVE_CHANGE_REQUEST_STATUS_PENDING = "10";
|
||||
public static final String LEAVE_CHANGE_REQUEST_STATUS_APPROVED = "20";
|
||||
public static final String LEAVE_CHANGE_REQUEST_STATUS_REJECTED = "30";
|
||||
}
|
||||
@ -1,39 +1,47 @@
|
||||
package com.itn.admin.cmn.config;
|
||||
|
||||
import com.itn.admin.itn.user.mapper.domain.UserVO;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
|
||||
public class CustomUserDetails implements UserDetails {
|
||||
|
||||
private final UserVO user;
|
||||
private final String username;
|
||||
private final String password;
|
||||
private final String userId;
|
||||
private final String id;
|
||||
private final Collection<? extends GrantedAuthority> authorities;
|
||||
|
||||
public CustomUserDetails(UserVO user) {
|
||||
this.user = user;
|
||||
public CustomUserDetails(String username, String password, String userId, String id, Collection<? extends GrantedAuthority> authorities) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.userId = userId;
|
||||
this.id = id;
|
||||
this.authorities = authorities;
|
||||
}
|
||||
|
||||
public UserVO getUser() {
|
||||
return this.user;
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return Collections.singletonList(new SimpleGrantedAuthority(user.getRole().name()));
|
||||
return authorities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return user.getPassword();
|
||||
return password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return user.getUserId(); // userName으로 바꿔도 무관
|
||||
return username;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -53,11 +61,6 @@ public class CustomUserDetails implements UserDetails {
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return "Y".equalsIgnoreCase(Optional.ofNullable(user.getActiveYn()).orElse("N"));
|
||||
}
|
||||
|
||||
// 도메인 정보 쉽게 접근할 수 있게 추가적인 getter
|
||||
public String getId() {
|
||||
return user.getUniqId();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,39 +2,35 @@ package com.itn.admin.cmn.config;
|
||||
|
||||
import com.itn.admin.itn.user.mapper.UserMapper;
|
||||
import com.itn.admin.itn.user.mapper.domain.UserVO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class CustomUserDetailsService implements UserDetailsService {
|
||||
|
||||
@Autowired
|
||||
private UserMapper userMapper;
|
||||
|
||||
// @Autowired
|
||||
// private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
|
||||
UserVO user = userMapper.getLoadUserByUsername(userId);
|
||||
UserVO user = userMapper.getUserById(userId);
|
||||
if (user == null) {
|
||||
throw new UsernameNotFoundException("User not found");
|
||||
}
|
||||
// log.info("DB Password: {}", user.getPassword());
|
||||
// log.info("Encoded password: {}", passwordEncoder.encode("내가 입력한 비밀번호"));
|
||||
// log.info("match? {}", passwordEncoder.matches("내가 입력한 비밀번호", user.getPassword()));
|
||||
|
||||
return new CustomUserDetails(user);
|
||||
// 사용자 권한을 SimpleGrantedAuthority로 변환
|
||||
GrantedAuthority authority = new SimpleGrantedAuthority(user.getRole().name());
|
||||
Collection<? extends GrantedAuthority> authorities = Collections.singletonList(authority);
|
||||
|
||||
// CustomUserDetails 객체 생성
|
||||
return new CustomUserDetails(user.getUserId(), user.getPassword(), user.getUserId(), user.getUniqId(), authorities);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,19 @@
|
||||
package com.itn.admin.cmn.config;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import org.mybatis.spring.SqlSessionFactoryBean;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.jdbc.DataSourceBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
@ -20,7 +24,6 @@ import javax.sql.DataSource;
|
||||
,"com.itn.admin.itn.mjon.spam.mapper"
|
||||
,"com.itn.admin.itn.user.mapper"
|
||||
,"com.itn.admin.itn.code.mapper"
|
||||
,"com.itn.admin.itn.blog.mapper"
|
||||
}
|
||||
, sqlSessionFactoryRef = "factory")
|
||||
class MainDatabaseConfig {
|
||||
@ -44,8 +47,7 @@ class MainDatabaseConfig {
|
||||
|
||||
SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
|
||||
sqlSessionFactory.setDataSource(dataSource);
|
||||
// sqlSessionFactory.setTypeAliasesPackage("com.itn.admin.itn.*");
|
||||
// sqlSessionFactory.setTypeAliasesPackage("com.itn.admin.itn");
|
||||
sqlSessionFactory.setTypeAliasesPackage("com.itn.admin.itn.*");
|
||||
sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/itn/**/*Mapper.xml"));
|
||||
sqlSessionFactory.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:mybatis-config.xml"));
|
||||
return sqlSessionFactory.getObject();
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
package com.itn.admin.cmn.config;
|
||||
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import org.mybatis.spring.SqlSessionFactoryBean;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.jdbc.DataSourceBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
|
||||
@Configuration
|
||||
@MapperScan(value = "com.itn.admin.agent.client.one.mapper", sqlSessionFactoryRef = "factory5")
|
||||
class MjonAgentCOneDatabaseConfig {
|
||||
|
||||
private final String COMMUTE_DATA_SOURCE = "MjagentClienOneDatabase";
|
||||
|
||||
// A database DataSource
|
||||
@Bean(COMMUTE_DATA_SOURCE)
|
||||
@ConfigurationProperties(prefix = "spring.mjagent.client.one.datasource")
|
||||
public DataSource CommuteDataSource() {
|
||||
return DataSourceBuilder.create()
|
||||
// .type(HikariDataSource.class)
|
||||
.build();
|
||||
}
|
||||
|
||||
// SqlSessionTemplate 에서 사용할 SqlSession 을 생성하는 Factory
|
||||
@Bean(name = "factory5")
|
||||
public SqlSessionFactory MjonAgentCOneSqlSessionFactory(@Qualifier(COMMUTE_DATA_SOURCE) DataSource dataSource) throws Exception {
|
||||
SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
|
||||
sqlSessionFactory.setDataSource(dataSource);
|
||||
sqlSessionFactory.setTypeAliasesPackage("com.itn.admin.agent.client.one.*");
|
||||
sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/agent/client/one/*Mapper.xml"));
|
||||
sqlSessionFactory.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:mybatis-config.xml"));
|
||||
return sqlSessionFactory.getObject();
|
||||
}
|
||||
|
||||
// DataSource 에서 Transaction 관리를 위한 Manager 클래스 등록
|
||||
@Bean(name = "sqlSession5")
|
||||
public SqlSessionTemplate sqlSession(SqlSessionFactory sqlSessionFactory) {
|
||||
return new SqlSessionTemplate(sqlSessionFactory);
|
||||
}
|
||||
}
|
||||
@ -15,14 +15,14 @@ import javax.sql.DataSource;
|
||||
|
||||
|
||||
@Configuration
|
||||
@MapperScan(value = "com.itn.admin.gw.holiday.mapper", sqlSessionFactoryRef = "factory3")
|
||||
class GwDatabaseConfig {
|
||||
@MapperScan(value = "com.itn.admin.agent.client.two.mapper", sqlSessionFactoryRef = "factory3")
|
||||
class MjonAgentCTwoDatabaseConfig {
|
||||
|
||||
private final String GW_DATA_SOURCE = "GwDatabase";
|
||||
private final String COMMUTE_DATA_SOURCE = "MjagentClienTwoDatabase";
|
||||
|
||||
// A database DataSource
|
||||
@Bean(GW_DATA_SOURCE)
|
||||
@ConfigurationProperties(prefix = "spring.gw.datasource")
|
||||
@Bean(COMMUTE_DATA_SOURCE)
|
||||
@ConfigurationProperties(prefix = "spring.mjagent.client.two.datasource")
|
||||
public DataSource CommuteDataSource() {
|
||||
return DataSourceBuilder.create()
|
||||
// .type(HikariDataSource.class)
|
||||
@ -31,11 +31,11 @@ class GwDatabaseConfig {
|
||||
|
||||
// SqlSessionTemplate 에서 사용할 SqlSession 을 생성하는 Factory
|
||||
@Bean(name = "factory3")
|
||||
public SqlSessionFactory CommuteSqlSessionFactory(@Qualifier(GW_DATA_SOURCE) DataSource dataSource) throws Exception {
|
||||
public SqlSessionFactory MjonAgentCTwoSqlSessionFactory(@Qualifier(COMMUTE_DATA_SOURCE) DataSource dataSource) throws Exception {
|
||||
SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
|
||||
sqlSessionFactory.setDataSource(dataSource);
|
||||
sqlSessionFactory.setTypeAliasesPackage("com.itn.admin.gw");
|
||||
sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/gw/**/*Mapper.xml"));
|
||||
sqlSessionFactory.setTypeAliasesPackage("com.itn.admin.agent.client.two.*");
|
||||
sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/agent/client/two/*Mapper.xml"));
|
||||
sqlSessionFactory.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:mybatis-config.xml"));
|
||||
return sqlSessionFactory.getObject();
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package com.itn.admin.cmn.config;
|
||||
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import org.mybatis.spring.SqlSessionFactoryBean;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.jdbc.DataSourceBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
|
||||
@Configuration
|
||||
@MapperScan(value = "com.itn.admin.agent.server.mapper", sqlSessionFactoryRef = "factory4")
|
||||
class MjonAgentSDatabaseConfig {
|
||||
|
||||
private final String AGENT_S_DATA_SOURCE = "MjagentServerDatabase";
|
||||
|
||||
// A database DataSource
|
||||
@Bean(AGENT_S_DATA_SOURCE)
|
||||
@ConfigurationProperties(prefix = "spring.mjagent.server.datasource")
|
||||
public DataSource MjagentServerSource() {
|
||||
return DataSourceBuilder.create()
|
||||
.type(com.zaxxer.hikari.HikariDataSource.class) // HikariDataSource를 명시적으로 사용
|
||||
.build();
|
||||
}
|
||||
|
||||
// SqlSessionTemplate 에서 사용할 SqlSession 을 생성하는 Factory
|
||||
@Bean(name = "factory4")
|
||||
public SqlSessionFactory MjonAgentSSqlSessionFactory(@Qualifier(AGENT_S_DATA_SOURCE) DataSource dataSource) throws Exception {
|
||||
SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
|
||||
sqlSessionFactory.setDataSource(dataSource);
|
||||
sqlSessionFactory.setTypeAliasesPackage("com.itn.admin.agent.server.*");
|
||||
sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/agent/server/*Mapper.xml"));
|
||||
sqlSessionFactory.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:mybatis-config.xml"));
|
||||
return sqlSessionFactory.getObject();
|
||||
}
|
||||
|
||||
// DataSource 에서 Transaction 관리를 위한 Manager 클래스 등록
|
||||
@Bean(name = "sqlSession4")
|
||||
public SqlSessionTemplate sqlSession(SqlSessionFactory sqlSessionFactory) {
|
||||
return new SqlSessionTemplate(sqlSessionFactory);
|
||||
}
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
package com.itn.admin.cmn.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class PasswordEncoderConfig {
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,9 @@
|
||||
package com.itn.admin.cmn.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.itn.admin.itn.user.mapper.UserMapper;
|
||||
import com.itn.admin.itn.user.mapper.domain.UserVO;
|
||||
import com.itn.admin.itn.user.service.UserService;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@ -25,197 +22,125 @@ import org.springframework.security.web.session.SessionInformationExpiredStrateg
|
||||
import java.net.URL;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
@Slf4j
|
||||
@Configuration // Spring의 설정 클래스임을 나타냄
|
||||
@EnableWebSecurity // Spring Security의 웹 보안 기능을 활성화
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
// 사용자 정보를 로드하는 커스텀 서비스 (UserDetailsService 구현체)
|
||||
private final CustomUserDetailsService customUserDetailsService;
|
||||
// 사용자 관련 비즈니스 로직을 처리하는 서비스
|
||||
private final UserService userService;
|
||||
|
||||
private final UserMapper userMapper;
|
||||
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
// 생성자를 통해 의존성 주입
|
||||
public SecurityConfig(CustomUserDetailsService customUserDetailsService
|
||||
, UserService userService
|
||||
, UserMapper userMapper
|
||||
, PasswordEncoder passwordEncoder) {
|
||||
public SecurityConfig(CustomUserDetailsService customUserDetailsService, UserService userService) {
|
||||
this.customUserDetailsService = customUserDetailsService;
|
||||
this.userService = userService;
|
||||
this.userMapper = userMapper;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
/*
|
||||
// 비밀번호 암호화를 위한 BCryptPasswordEncoder 빈 생성
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
PasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||
log.info(" + UserRestController 등록된 PasswordEncoder instance: {}", encoder);
|
||||
PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
return encoder;
|
||||
}*/
|
||||
|
||||
// Spring Security의 보안 필터 체인을 설정하는 핵심 메서드
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// CSRF(Cross-Site Request Forgery) 보호 비활성화 (주로 API 서버나 폼 기반 인증에서 사용)
|
||||
.csrf((csrfConfig) -> csrfConfig.disable())
|
||||
// X-Frame-Options 헤더 비활성화 (iframe 사용 허용)
|
||||
.headers((headerConfig) -> headerConfig.frameOptions(frameOptionsConfig -> frameOptionsConfig.disable()))
|
||||
// HTTP 요청에 대한 권한 설정
|
||||
.authorizeHttpRequests((authorizeRequests) -> authorizeRequests
|
||||
.requestMatchers("/user/register").permitAll() // 회원가입 페이지는 인증 없이 접근 가능
|
||||
.requestMatchers("/accessDenied").permitAll() // 접근 거부 페이지는 인증 없이 접근 가능
|
||||
.requestMatchers("/static/**", "/plugins/**", "/dist/**").permitAll() // 정적 리소스는 인증 없이 접근 가능
|
||||
.requestMatchers("/api/**").permitAll() // API 엔드포인트는 인증 없이 접근 가능
|
||||
.requestMatchers("/admin/**").hasRole("ADMIN") // /admin/** 경로는 ADMIN 역할 필요
|
||||
.anyRequest().authenticated() // 나머지 모든 요청은 인증 필요
|
||||
.requestMatchers("/user/register").permitAll()
|
||||
.requestMatchers("/accessDenied").permitAll()
|
||||
.requestMatchers("/static/**", "/plugins/**", "/dist/**").permitAll()
|
||||
.requestMatchers("/api/**").permitAll()
|
||||
.requestMatchers("/admin/**").hasRole("ADMIN")
|
||||
.anyRequest().authenticated() // 모든 요청에 대해 인증 요구
|
||||
)
|
||||
// 예외 처리 설정
|
||||
.exceptionHandling((exceptionConfig) -> exceptionConfig
|
||||
.authenticationEntryPoint(customUnauthorizedEntryPoint()) // 인증 실패 시 처리
|
||||
.accessDeniedHandler(customAccessDeniedHandler()) // 권한 부족 시 처리
|
||||
.authenticationEntryPoint(customUnauthorizedEntryPoint())
|
||||
.accessDeniedHandler(customAccessDeniedHandler())
|
||||
)
|
||||
// 폼 로그인 설정
|
||||
.formLogin((formLogin) -> formLogin
|
||||
.loginPage("/user/login") // 커스텀 로그인 페이지 경로
|
||||
.usernameParameter("username") // 로그인 폼의 사용자 이름 파라미터 이름
|
||||
.passwordParameter("password") // 로그인 폼의 비밀번호 파라미터 이름
|
||||
.loginProcessingUrl("/login/login-proc") // 로그인 요청을 처리하는 URL
|
||||
.successHandler(customAuthenticationSuccessHandler()) // 로그인 성공 시 핸들러
|
||||
.failureHandler(customAuthenticationFailureHandler()) // 로그인 실패 시 핸들러
|
||||
.permitAll() // 로그인 페이지는 누구나 접근 가능
|
||||
.loginPage("/user/login")
|
||||
.usernameParameter("username")
|
||||
.passwordParameter("password")
|
||||
.loginProcessingUrl("/login/login-proc")
|
||||
.successHandler(customAuthenticationSuccessHandler())
|
||||
.failureHandler(customAuthenticationFailureHandler())
|
||||
.permitAll()
|
||||
)
|
||||
|
||||
// 로그아웃 설정
|
||||
.logout((logoutConfig) -> logoutConfig
|
||||
.logoutUrl("/logout") // 로그아웃 요청 URL
|
||||
.logoutSuccessUrl("/user/login") // 로그아웃 성공 시 리다이렉트 URL
|
||||
.invalidateHttpSession(true) // 로그아웃 시 세션 무효화
|
||||
// .deleteCookies("JSESSIONID") // JSESSIONID 쿠키 삭제
|
||||
.permitAll() // 로그아웃은 누구나 요청 가능
|
||||
.logoutUrl("/logout")
|
||||
.logoutSuccessUrl("/user/login")
|
||||
.invalidateHttpSession(true) // 세션 무효화
|
||||
.deleteCookies("JSESSIONID") // 쿠키 삭제
|
||||
.permitAll()
|
||||
)
|
||||
// 세션 관리 설정
|
||||
.sessionManagement((sessionManagement) -> sessionManagement
|
||||
.sessionFixation().migrateSession() // 세션 고정 공격 방지 (새 세션 생성 후 기존 데이터 복사)
|
||||
.invalidSessionUrl("/user/login?invalidSession=true") // 세션이 유효하지 않을 때 리다이렉트
|
||||
.maximumSessions(1) // 한 사용자당 최대 동시 세션 수 제한 (1개)
|
||||
.maxSessionsPreventsLogin(false) // 최대 세션 초과 시 새 로그인 허용 (기존 세션 만료)
|
||||
.expiredSessionStrategy(sessionExpiredStrategy()) // 세션 만료 시 처리 전략
|
||||
.sessionFixation().migrateSession() // 세션 고정 공격 방지
|
||||
.invalidSessionUrl("/user/login?invalidSession=true") // 세션이 유효하지 않을 때 리다이렉트할 URL
|
||||
.maximumSessions(1) // 동시 세션 수 제한
|
||||
.maxSessionsPreventsLogin(false) // 최대 세션 수 초과 시 추가 로그인을 방지
|
||||
.expiredSessionStrategy(sessionExpiredStrategy()) // 세션 만료 전략 설정
|
||||
)
|
||||
// 사용자 정보 로드를 위한 서비스 설정
|
||||
.userDetailsService(customUserDetailsService);
|
||||
|
||||
// 설정을 기반으로 SecurityFilterChain 객체 생성 및 반환
|
||||
return http.build();
|
||||
}
|
||||
|
||||
// 세션 만료 시 커스텀 전략을 정의하는 빈
|
||||
@Bean
|
||||
public SessionInformationExpiredStrategy sessionExpiredStrategy() {
|
||||
return new CustomSessionExpiredStrategy("/user/login?expired"); // 세션 만료 시 리다이렉트 경로 지정
|
||||
return new CustomSessionExpiredStrategy("/user/login?expired");
|
||||
}
|
||||
|
||||
// 로그인 성공 시 동작을 커스터마이징하는 핸들러
|
||||
@Bean
|
||||
public AuthenticationSuccessHandler customAuthenticationSuccessHandler() {
|
||||
return (request, response, authentication) -> {
|
||||
// 인증된 사용자 정보를 가져옴
|
||||
// 디버깅 정보를 콘솔에 출력
|
||||
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
|
||||
// String userId = userDetails.getUserId();
|
||||
|
||||
// session 설정
|
||||
// 보안이슈로인해 필요한 데이터만
|
||||
UserVO loginVO = new UserVO();
|
||||
loginVO.setUserName( userDetails.getUser().getUserName());
|
||||
loginVO.setBiostarId( userDetails.getUser().getBiostarId());
|
||||
loginVO.setUniqId( userDetails.getUser().getUniqId());
|
||||
request.getSession().setAttribute("loginVO", loginVO);
|
||||
|
||||
// 요청 URL의 호스트를 추출 (디버깅용)
|
||||
// 개발이 아니면 로그 등록하게 수정 20240522
|
||||
String host_url = new URL(request.getRequestURL().toString()).getHost();
|
||||
// System.out.println("host_url : " + host_url);
|
||||
|
||||
// 로컬 환경이 아닌 경우 로그인 로그를 기록 (2024-05-22 수정)
|
||||
if (!"localhost".equals(host_url)) {
|
||||
|
||||
System.out.println("host_url : "+ host_url);
|
||||
if(!"localhost".equals(host_url)){
|
||||
String id = userDetails.getId();
|
||||
userService.loginLog(id); // 로그인 기록 저장
|
||||
userService.loginLog(id);
|
||||
}
|
||||
|
||||
//
|
||||
// var rememberMeServices = new org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices(
|
||||
// "secureAndRandomKey", customUserDetailsService
|
||||
// );
|
||||
// rememberMeServices.setAlwaysRemember(true);
|
||||
// rememberMeServices.onLoginSuccess(request, response, authentication);
|
||||
|
||||
|
||||
response.setStatus(HttpStatus.OK.value()); // HTTP 상태 코드 200 설정
|
||||
response.sendRedirect("/"); // 루트 경로로 리다이렉트
|
||||
response.setStatus(HttpStatus.OK.value());
|
||||
response.sendRedirect("/");
|
||||
};
|
||||
}
|
||||
|
||||
// 로그인 실패 시 동작을 커스터마이징하는 핸들러
|
||||
@Bean
|
||||
public AuthenticationFailureHandler customAuthenticationFailureHandler() {
|
||||
return (request, response, exception) -> {
|
||||
// 실패 원인을 콘솔에 출력 (디버깅용)
|
||||
String username = request.getParameter("username"); // 입력한 ID
|
||||
String rawPassword = request.getParameter("password"); // 입력한 비번
|
||||
|
||||
log.info("로그인 실패 - 입력한 ID: [{}]", username);
|
||||
log.info("로그인 실패 - 입력한 비번(raw): [{}]", rawPassword);
|
||||
|
||||
// DB 비밀번호 가져오기 (직접 조회)
|
||||
UserVO user = userMapper.getLoadUserByUsername(username); // userMapper로 직접 조회해도 OK
|
||||
if (user != null) {
|
||||
log.info("로그인 실패 - DB 비번(encoded): [{}]", user.getPassword());
|
||||
|
||||
// 비번 매칭 테스트
|
||||
boolean match = passwordEncoder.matches(rawPassword, user.getPassword());
|
||||
log.info("비밀번호 일치 여부: [{}]", match);
|
||||
} else {
|
||||
log.warn("로그인 실패 - 아이디에 해당하는 사용자 없음");
|
||||
}
|
||||
|
||||
// 디버깅 정보를 콘솔에 출력
|
||||
System.out.println("Authentication failed. Exception: " + exception.getMessage());
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value()); // HTTP 상태 코드 401 설정
|
||||
response.sendRedirect("/user/login?error=true"); // 로그인 페이지로 리다이렉트 (에러 표시)
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
response.sendRedirect("/user/login?error=true");
|
||||
};
|
||||
}
|
||||
|
||||
// 인증되지 않은 사용자가 보호된 리소스에 접근 시 처리
|
||||
@Bean
|
||||
public AuthenticationEntryPoint customUnauthorizedEntryPoint() {
|
||||
return (request, response, authException) -> {
|
||||
response.sendRedirect("/user/login"); // 로그인 페이지로 리다이렉트
|
||||
response.sendRedirect("/user/login");
|
||||
};
|
||||
}
|
||||
|
||||
// 권한이 부족한 경우 처리
|
||||
@Bean
|
||||
public AccessDeniedHandler customAccessDeniedHandler() {
|
||||
return (request, response, accessDeniedException) -> {
|
||||
response.sendRedirect("/accessDenied"); // 접근 거부 페이지로 리다이렉트
|
||||
response.sendRedirect("/accessDenied");
|
||||
};
|
||||
}
|
||||
|
||||
// 세션 이벤트 발행을 위한 빈 (세션 생성/소멸 감지)
|
||||
@Bean
|
||||
public HttpSessionEventPublisher httpSessionEventPublisher() {
|
||||
return new HttpSessionEventPublisher();
|
||||
}
|
||||
|
||||
// 에러 응답 객체를 정의하는 내부 클래스
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public static class ErrorResponse {
|
||||
private final HttpStatus status; // HTTP 상태 코드
|
||||
private final String message; // 에러 메시지
|
||||
private final HttpStatus status;
|
||||
private final String message;
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,11 +25,9 @@ public class UserInterceptor implements HandlerInterceptor {
|
||||
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
|
||||
|
||||
log.info(" :: postHandle :: ");
|
||||
log.info(" :: Request URL: " + request.getRequestURL());
|
||||
log.info(" :: Request Method: " + request.getMethod());
|
||||
log.info(" :: Remote Address: " + request.getRemoteAddr());
|
||||
|
||||
log.info(" :: modelAndView: " + modelAndView);
|
||||
log.info("Request URL: " + request.getRequestURL());
|
||||
log.info("Request Method: " + request.getMethod());
|
||||
log.info("Remote Address: " + request.getRemoteAddr());
|
||||
|
||||
if (modelAndView != null) {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
@ -42,7 +40,7 @@ public class UserInterceptor implements HandlerInterceptor {
|
||||
if (principal instanceof UserVO) { // principal이 UserVO 타입인 경우
|
||||
UserVO loggedInUser = (UserVO) principal; // principal 객체를 UserVO 타입으로 변환
|
||||
id = loggedInUser.getUserId(); // UserVO 객체에서 userId 값을 가져옴
|
||||
userName = loggedInUser.getUserName(); // UserVO 객체에서 username 값을 가져옴
|
||||
userName = loggedInUser.getUsername(); // UserVO 객체에서 username 값을 가져옴
|
||||
} else if (principal instanceof User) { // principal이 User 타입인 경우
|
||||
User loggedInUser = (User) principal; // principal 객체를 User 타입으로 변환
|
||||
id = loggedInUser.getUsername(); // User 객체에서 username 값을 가져옴 (userId로 사용)
|
||||
|
||||
@ -37,11 +37,6 @@ public class RestResponse {
|
||||
this.msg = msg;
|
||||
this.data = data;
|
||||
}
|
||||
@Builder
|
||||
public RestResponse(HttpStatus status, String msg) {
|
||||
this.status = status;
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -1,79 +0,0 @@
|
||||
package com.itn.admin.cmn.util;
|
||||
|
||||
import com.itn.admin.commute.mapper.domain.CommuteVO;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class DateUtils {
|
||||
|
||||
|
||||
/**
|
||||
* 전 영업일을 계산하여 년, 월, 일을 Map 형태로 반환하는 메서드.
|
||||
* 현재 날짜에서 하루를 빼고, 주말(토요일 또는 일요일)인 경우 금요일로 조정함.
|
||||
* @return Map<String,String> - "year", "month", "day" 키로 구성된 날짜 정보
|
||||
*/
|
||||
public static Map<String,String> getPreviousBusinessDay(){
|
||||
Map<String,String> map = new HashMap<>();
|
||||
|
||||
|
||||
// 현재 날짜 구하기
|
||||
LocalDate now = LocalDate.now().minusDays(1);
|
||||
|
||||
// 만약 전날이 토요일(7) 또는 일요일(1)이면 금요일로 설정
|
||||
if (now.getDayOfWeek() == DayOfWeek.SATURDAY) {
|
||||
now = now.minusDays(1); // 토요일이면 금요일로
|
||||
} else if (now.getDayOfWeek() == DayOfWeek.SUNDAY) {
|
||||
now = now.minusDays(2); // 일요일이면 금요일로
|
||||
}
|
||||
// 년도 구하기
|
||||
String year = String.valueOf(now.getYear());
|
||||
|
||||
String month = now.format(DateTimeFormatter.ofPattern("M"));
|
||||
|
||||
String day = now.format(DateTimeFormatter.ofPattern("d"));
|
||||
|
||||
|
||||
// 결과 출력
|
||||
// System.out.println("Year: " + year);
|
||||
// System.out.println("Month: " + month);
|
||||
// System.out.println("Day: " + day);
|
||||
|
||||
map.put("year",year);
|
||||
map.put("month",month);
|
||||
map.put("day",day);
|
||||
|
||||
return map;
|
||||
|
||||
}
|
||||
|
||||
public static String getFormatToStandardDate(String workDt) {
|
||||
|
||||
// LocalDate로 변환
|
||||
LocalDate date = LocalDate.parse(workDt, DateTimeFormatter.ofPattern("yyyy-M-d"));
|
||||
|
||||
// 원하는 형식으로 변환
|
||||
return date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
||||
}
|
||||
public static String getYesterdayDate(CommuteVO commuteVO) {
|
||||
// commuteVO에서 년, 월, 일을 가져옴
|
||||
String year = commuteVO.getSearchYear();
|
||||
String month = commuteVO.getSearchMonth();
|
||||
String day = commuteVO.getSearchDay();
|
||||
|
||||
// commuteVO의 값으로 LocalDate 생성
|
||||
LocalDate date = LocalDate.of(Integer.parseInt(year), Integer.parseInt(month), Integer.parseInt(day));
|
||||
|
||||
// 어제 날짜 계산
|
||||
// LocalDate yesterday = date.minusDays(5);
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd");
|
||||
System.out.println("date.format(formatter) :: "+ date.format(formatter));
|
||||
return date.format(formatter);
|
||||
// return yesterday.format(formatter);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,11 +1,7 @@
|
||||
package com.itn.admin.cmn.util.scheduled;
|
||||
|
||||
import com.itn.admin.commute.mapper.domain.CommuteVO;
|
||||
import com.itn.admin.commute.service.CommuteService;
|
||||
import com.itn.admin.etc.crawling.morak.service.MorakService;
|
||||
import com.itn.admin.itn.commute.service.ItnCommuteService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@ -23,27 +19,15 @@ import java.io.IOException;
|
||||
* 2023-08-28 hylee 최초 생성
|
||||
*/
|
||||
@Service
|
||||
@ConditionalOnProperty(name = "spring.profiles.active", havingValue = "prod")
|
||||
public class ScheduledTasks {
|
||||
|
||||
@Autowired
|
||||
private MorakService morakService;
|
||||
|
||||
@Autowired
|
||||
private CommuteService commuteService;
|
||||
|
||||
@Autowired
|
||||
private ItnCommuteService itnCommuteService;
|
||||
|
||||
// @Scheduled(cron = "*/50 * * * * MON-FRI")
|
||||
@Scheduled(cron = "0 40 10 * * MON-FRI")
|
||||
public void noonJob() throws IOException {
|
||||
System.out.println("It's noon!");
|
||||
morakService.morakMenu();
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 0 10 * * MON-FRI")
|
||||
public void commuteJob() throws IOException {
|
||||
CommuteVO commuteVO = new CommuteVO();
|
||||
itnCommuteService.transfer(commuteVO);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,327 +0,0 @@
|
||||
package com.itn.admin.cmn.util.slack;
|
||||
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogScheduleVO;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogScheduleExecutionVO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 범용 Slack 알림 서비스
|
||||
* 블로그 예약 발행 시스템을 위한 다양한 알림 기능을 제공
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SlackNotificationService {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
@Value("${slack.notification.enabled:false}")
|
||||
private boolean notificationEnabled;
|
||||
|
||||
@Value("${slack.webhook.default:}")
|
||||
private String defaultWebhookUrl;
|
||||
|
||||
@Value("${slack.webhook.blog-schedule:}")
|
||||
private String blogScheduleWebhookUrl;
|
||||
|
||||
@Value("${slack.webhook.system-alert:}")
|
||||
private String systemAlertWebhookUrl;
|
||||
|
||||
@Value("${slack.channel.default:general}")
|
||||
private String defaultChannel;
|
||||
|
||||
@Value("${slack.channel.blog-schedule:blog-alerts}")
|
||||
private String blogScheduleChannel;
|
||||
|
||||
@Value("${slack.channel.system-alert:system-alerts}")
|
||||
private String systemAlertChannel;
|
||||
|
||||
@Value("${slack.username:ITN-Admin}")
|
||||
private String username;
|
||||
|
||||
@Value("${slack.icon.emoji::robot_face:}")
|
||||
private String iconEmoji;
|
||||
|
||||
private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
public SlackNotificationService(RestTemplate restTemplate) {
|
||||
this.restTemplate = restTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 예약 생성 알림
|
||||
*/
|
||||
@Async
|
||||
public void sendScheduleCreatedNotification(BlogScheduleVO schedule) {
|
||||
if (!notificationEnabled || !schedule.isNotificationEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String title = "📅 새로운 블로그 예약이 생성되었습니다";
|
||||
String message = buildScheduleCreatedMessage(schedule);
|
||||
String webhookUrl = getWebhookUrl("blog-schedule");
|
||||
String channel = getChannelForSchedule(schedule);
|
||||
|
||||
sendNotification(webhookUrl, channel, title, message, ":calendar:");
|
||||
}
|
||||
|
||||
/**
|
||||
* 발행 성공 알림
|
||||
*/
|
||||
@Async
|
||||
public void sendPublishSuccessNotification(BlogScheduleVO schedule, BlogScheduleExecutionVO execution) {
|
||||
if (!notificationEnabled || !schedule.isNotificationEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String title = "✅ 블로그 발행이 성공했습니다";
|
||||
String message = buildPublishSuccessMessage(schedule, execution);
|
||||
String webhookUrl = getWebhookUrl("blog-schedule");
|
||||
String channel = getChannelForSchedule(schedule);
|
||||
|
||||
sendNotification(webhookUrl, channel, title, message, ":white_check_mark:");
|
||||
}
|
||||
|
||||
/**
|
||||
* 발행 실패 알림
|
||||
*/
|
||||
@Async
|
||||
public void sendPublishFailureNotification(BlogScheduleVO schedule, BlogScheduleExecutionVO execution) {
|
||||
if (!notificationEnabled || !schedule.isNotificationEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String title = "❌ 블로그 발행이 실패했습니다";
|
||||
String message = buildPublishFailureMessage(schedule, execution);
|
||||
String webhookUrl = getWebhookUrl("blog-schedule");
|
||||
String channel = getChannelForSchedule(schedule);
|
||||
|
||||
sendNotification(webhookUrl, channel, title, message, ":x:");
|
||||
}
|
||||
|
||||
/**
|
||||
* 재시도 알림
|
||||
*/
|
||||
@Async
|
||||
public void sendRetryNotification(BlogScheduleVO schedule, BlogScheduleExecutionVO execution, int retryCount) {
|
||||
if (!notificationEnabled || !schedule.isNotificationEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String title = "🔄 블로그 발행 재시도 중";
|
||||
String message = buildRetryMessage(schedule, execution, retryCount);
|
||||
String webhookUrl = getWebhookUrl("blog-schedule");
|
||||
String channel = getChannelForSchedule(schedule);
|
||||
|
||||
sendNotification(webhookUrl, channel, title, message, ":arrows_counterclockwise:");
|
||||
}
|
||||
|
||||
/**
|
||||
* 시스템 오류 알림
|
||||
*/
|
||||
@Async
|
||||
public void sendSystemErrorNotification(String component, String errorMessage, Exception exception) {
|
||||
if (!notificationEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
String title = "🚨 시스템 오류 발생";
|
||||
String message = buildSystemErrorMessage(component, errorMessage, exception);
|
||||
String webhookUrl = getWebhookUrl("system-alert");
|
||||
|
||||
sendNotification(webhookUrl, systemAlertChannel, title, message, ":rotating_light:");
|
||||
}
|
||||
|
||||
/**
|
||||
* 스케줄러 상태 알림
|
||||
*/
|
||||
@Async
|
||||
public void sendSchedulerStatusNotification(String status, int activeSchedules, int runningTasks) {
|
||||
if (!notificationEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
String title = "📊 스케줄러 상태 보고";
|
||||
String message = buildSchedulerStatusMessage(status, activeSchedules, runningTasks);
|
||||
String webhookUrl = getWebhookUrl("system-alert");
|
||||
|
||||
sendNotification(webhookUrl, systemAlertChannel, title, message, ":bar_chart:");
|
||||
}
|
||||
|
||||
/**
|
||||
* 커스텀 메시지 발송
|
||||
*/
|
||||
@Async
|
||||
public void sendCustomMessage(String channel, String title, String message, String emoji) {
|
||||
if (!notificationEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
String webhookUrl = getWebhookUrl("default");
|
||||
sendNotification(webhookUrl, channel, title, message, emoji);
|
||||
}
|
||||
|
||||
/**
|
||||
* 핵심 알림 발송 메서드
|
||||
*/
|
||||
private void sendNotification(String webhookUrl, String channel, String title, String message, String emoji) {
|
||||
if (webhookUrl == null || webhookUrl.trim().isEmpty() || webhookUrl.contains("YOUR_")) {
|
||||
log.warn("Slack webhook URL이 설정되지 않았습니다. 알림을 건너뜁니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("channel", channel);
|
||||
payload.put("username", username);
|
||||
payload.put("icon_emoji", emoji != null ? emoji : iconEmoji);
|
||||
payload.put("text", title + "\n" + message);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
HttpEntity<Map<String, Object>> request = new HttpEntity<>(payload, headers);
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(webhookUrl, request, String.class);
|
||||
|
||||
if (response.getStatusCode() == HttpStatus.OK) {
|
||||
log.debug("Slack 알림 발송 성공: {} to {}", title, channel);
|
||||
} else {
|
||||
log.warn("Slack 알림 발송 실패: {} - {}", response.getStatusCode(), response.getBody());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Slack 알림 발송 중 오류 발생: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 메시지 빌더 메서드들
|
||||
private String buildScheduleCreatedMessage(BlogScheduleVO schedule) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("**제목:** ").append(schedule.getTitle()).append("\n");
|
||||
sb.append("**블로그:** ").append(schedule.getBlogName()).append("\n");
|
||||
sb.append("**유형:** ").append(getScheduleTypeKorean(schedule.getScheduleType())).append("\n");
|
||||
sb.append("**예약 시간:** ").append(schedule.getScheduledAt().format(DATETIME_FORMATTER)).append("\n");
|
||||
sb.append("**우선순위:** ").append(getPriorityKorean(schedule.getPriority())).append("\n");
|
||||
if (schedule.isRecurring()) {
|
||||
sb.append("**반복 설정:** ").append(getRepeatIntervalKorean(schedule.getRepeatInterval())).append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String buildPublishSuccessMessage(BlogScheduleVO schedule, BlogScheduleExecutionVO execution) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("**제목:** ").append(schedule.getTitle()).append("\n");
|
||||
sb.append("**블로그:** ").append(schedule.getBlogName()).append("\n");
|
||||
sb.append("**실행 시간:** ").append(execution.getExecutedAt().format(DATETIME_FORMATTER)).append("\n");
|
||||
if (execution.getExecutionTimeMs() != null) {
|
||||
sb.append("**소요 시간:** ").append(execution.getExecutionTimeMs()).append("ms\n");
|
||||
}
|
||||
if (execution.getPublishedUrl() != null) {
|
||||
sb.append("**발행 URL:** ").append(execution.getPublishedUrl()).append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String buildPublishFailureMessage(BlogScheduleVO schedule, BlogScheduleExecutionVO execution) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("**제목:** ").append(schedule.getTitle()).append("\n");
|
||||
sb.append("**블로그:** ").append(schedule.getBlogName()).append("\n");
|
||||
sb.append("**실행 시간:** ").append(execution.getExecutedAt().format(DATETIME_FORMATTER)).append("\n");
|
||||
sb.append("**시도 횟수:** ").append(execution.getAttemptCount()).append("/").append(schedule.getMaxRetries()).append("\n");
|
||||
if (execution.getErrorDetails() != null) {
|
||||
sb.append("**오류 내용:** ").append(execution.getErrorDetails()).append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String buildRetryMessage(BlogScheduleVO schedule, BlogScheduleExecutionVO execution, int retryCount) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("**제목:** ").append(schedule.getTitle()).append("\n");
|
||||
sb.append("**블로그:** ").append(schedule.getBlogName()).append("\n");
|
||||
sb.append("**재시도 횟수:** ").append(retryCount).append("/").append(schedule.getMaxRetries()).append("\n");
|
||||
sb.append("**다음 재시도:** ").append(schedule.getRetryInterval()).append("분 후\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String buildSystemErrorMessage(String component, String errorMessage, Exception exception) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("**컴포넌트:** ").append(component).append("\n");
|
||||
sb.append("**오류 메시지:** ").append(errorMessage).append("\n");
|
||||
if (exception != null) {
|
||||
sb.append("**예외 유형:** ").append(exception.getClass().getSimpleName()).append("\n");
|
||||
sb.append("**상세 내용:** ").append(exception.getMessage()).append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String buildSchedulerStatusMessage(String status, int activeSchedules, int runningTasks) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("**스케줄러 상태:** ").append(status).append("\n");
|
||||
sb.append("**활성 스케줄:** ").append(activeSchedules).append("개\n");
|
||||
sb.append("**실행 중인 작업:** ").append(runningTasks).append("개\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// 유틸리티 메서드들
|
||||
private String getWebhookUrl(String type) {
|
||||
switch (type) {
|
||||
case "blog-schedule":
|
||||
return blogScheduleWebhookUrl.isEmpty() ? defaultWebhookUrl : blogScheduleWebhookUrl;
|
||||
case "system-alert":
|
||||
return systemAlertWebhookUrl.isEmpty() ? defaultWebhookUrl : systemAlertWebhookUrl;
|
||||
default:
|
||||
return defaultWebhookUrl;
|
||||
}
|
||||
}
|
||||
|
||||
private String getChannelForSchedule(BlogScheduleVO schedule) {
|
||||
if (schedule.getSlackChannel() != null && !schedule.getSlackChannel().trim().isEmpty()) {
|
||||
return schedule.getSlackChannel();
|
||||
}
|
||||
return blogScheduleChannel;
|
||||
}
|
||||
|
||||
private String getScheduleTypeKorean(String scheduleType) {
|
||||
switch (scheduleType) {
|
||||
case "ONE_TIME": return "일회성";
|
||||
case "RECURRING": return "반복";
|
||||
default: return scheduleType;
|
||||
}
|
||||
}
|
||||
|
||||
private String getPriorityKorean(String priority) {
|
||||
switch (priority) {
|
||||
case "HIGH": return "높음";
|
||||
case "NORMAL": return "보통";
|
||||
case "LOW": return "낮음";
|
||||
default: return priority;
|
||||
}
|
||||
}
|
||||
|
||||
private String getRepeatIntervalKorean(String interval) {
|
||||
switch (interval) {
|
||||
case "DAILY": return "매일";
|
||||
case "WEEKLY": return "매주";
|
||||
case "MONTHLY": return "매월";
|
||||
default: return interval;
|
||||
}
|
||||
}
|
||||
|
||||
// 설정 확인 메서드
|
||||
public boolean isNotificationEnabled() {
|
||||
return notificationEnabled;
|
||||
}
|
||||
|
||||
public void setNotificationEnabled(boolean enabled) {
|
||||
this.notificationEnabled = enabled;
|
||||
log.info("Slack 알림 상태 변경: {}", enabled ? "활성화" : "비활성화");
|
||||
}
|
||||
}
|
||||
@ -1,20 +1,14 @@
|
||||
package com.itn.admin.cmn.util.slack;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.apache.commons.httpclient.HttpClient;
|
||||
import org.apache.commons.httpclient.HttpStatus;
|
||||
import org.apache.commons.httpclient.methods.PostMethod;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 레거시 Slack 유틸리티 클래스
|
||||
*
|
||||
* @deprecated 새로운 프로젝트에서는 {@link SlackNotificationService}를 사용하세요.
|
||||
* 기존 코드 호환성을 위해 유지됩니다.
|
||||
*
|
||||
* packageName : com.itn.mjonApi.util.slack
|
||||
* fileName : Slack
|
||||
* author : hylee
|
||||
@ -24,74 +18,40 @@ import java.io.IOException;
|
||||
* DATE AUTHOR NOTE
|
||||
* -----------------------------------------------------------
|
||||
* 2023-08-28 hylee 최초 생성
|
||||
* 2025-01-16 claude SlackNotificationService 통합을 위한 리팩토링
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Deprecated
|
||||
|
||||
public class SlackUtil {
|
||||
|
||||
/**
|
||||
* 모락 메뉴 알림 발송 (레거시 메서드)
|
||||
* @deprecated 새로운 알림은 {@link SlackNotificationService#sendCustomMessage}를 사용하세요.
|
||||
*/
|
||||
@Deprecated
|
||||
public static void sendMorakMenuToSlack(String sendMsg) {
|
||||
|
||||
String url = "https://hooks.slack.com/services/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";
|
||||
sendSlackMessage(url, "모락메뉴api", sendMsg, "모락 메뉴", ":bento:");
|
||||
}
|
||||
|
||||
/**
|
||||
* 범용 슬랙 메시지 발송 메서드
|
||||
* @param webhookUrl 웹훅 URL
|
||||
* @param channel 채널명
|
||||
* @param message 메시지 내용
|
||||
* @param username 발송자명
|
||||
* @param iconEmoji 아이콘 이모지
|
||||
*/
|
||||
public static void sendSlackMessage(String webhookUrl, String channel, String message,
|
||||
String username, String iconEmoji) {
|
||||
HttpClient client = new HttpClient();
|
||||
PostMethod post = new PostMethod(webhookUrl);
|
||||
PostMethod post = new PostMethod(url);
|
||||
JSONObject json = new JSONObject();
|
||||
|
||||
try {
|
||||
json.put("channel", channel);
|
||||
json.put("text", message);
|
||||
json.put("username", username);
|
||||
if (iconEmoji != null && !iconEmoji.trim().isEmpty()) {
|
||||
json.put("icon_emoji", iconEmoji);
|
||||
}
|
||||
String munjaText = sendMsg;
|
||||
json.put("channel", "모락메뉴api");
|
||||
|
||||
json.put("text", munjaText);
|
||||
// json.put("icon_emoji", ":원하는 아이콘:"); //커스터마이징으로 아이콘 만들수도 있다!
|
||||
json.put("username", "모락 메뉴");
|
||||
|
||||
|
||||
post.addParameter("payload", json.toString());
|
||||
// 처음에 utf-8로 content-type안넣어주니까 한글은 깨져서 content-type넣어줌
|
||||
post.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
|
||||
|
||||
int responseCode = client.executeMethod(post);
|
||||
String response = post.getResponseBodyAsString();
|
||||
|
||||
if (responseCode == HttpStatus.SC_OK) {
|
||||
log.debug("Slack 메시지 발송 성공: {} to {}", message, channel);
|
||||
} else {
|
||||
log.warn("Slack 메시지 발송 실패 - Code: {}, Response: {}", responseCode, response);
|
||||
if (responseCode != HttpStatus.SC_OK) {
|
||||
System.out.println("Response: " + response);
|
||||
}
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Slack 발송 중 잘못된 인자 오류: {}", e.getMessage(), e);
|
||||
System.out.println("IllegalArgumentException posting to Slack " + e);
|
||||
} catch (IOException e) {
|
||||
log.error("Slack 발송 중 IO 오류: {}", e.getMessage(), e);
|
||||
} catch (Exception e) {
|
||||
log.error("Slack 발송 중 예상치 못한 오류: {}", e.getMessage(), e);
|
||||
System.out.println("IOException posting to Slack " + e);
|
||||
} finally {
|
||||
post.releaseConnection();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 단순 메시지 발송 (기본 설정 사용)
|
||||
* @param webhookUrl 웹훅 URL
|
||||
* @param message 메시지 내용
|
||||
*/
|
||||
public static void sendSimpleMessage(String webhookUrl, String message) {
|
||||
sendSlackMessage(webhookUrl, "general", message, "ITN-Admin", ":robot_face:");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,66 +0,0 @@
|
||||
package com.itn.admin.cmn.util.thymeleafUtils;
|
||||
|
||||
import com.itn.admin.itn.code.mapper.domain.CodeDetailVO;
|
||||
import com.itn.admin.itn.code.server.CodeDetailService;
|
||||
import com.itn.admin.itn.user.mapper.UserMapper;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
// Tag Code Utils
|
||||
@Component
|
||||
public class TCodeUtils {
|
||||
|
||||
@Autowired
|
||||
private CodeDetailService codeDetailService;
|
||||
@Autowired
|
||||
private UserMapper userMapper;
|
||||
|
||||
public String getCodeName(String codeGroupId, String codeValue) {
|
||||
if(StringUtils.isEmpty(codeValue)){
|
||||
return codeValue;
|
||||
};
|
||||
return codeDetailService.getCodeName(codeGroupId, codeValue);
|
||||
}
|
||||
|
||||
public List<CodeDetailVO> getCodeList(String codeGroupId) {
|
||||
return codeDetailService.getDetailsByGroupId(codeGroupId);
|
||||
|
||||
}
|
||||
|
||||
public String getUserName(String uniqId) {
|
||||
if(StringUtils.isEmpty(uniqId)){
|
||||
return uniqId;
|
||||
}
|
||||
String userName = userMapper.findById(uniqId).getUserName();
|
||||
return StringUtils.isEmpty(userName) ? uniqId : userName ;
|
||||
}
|
||||
public String getUserDeptTxt(String uniqId) {
|
||||
String deptCd = userMapper.findById(uniqId).getDeptCd();
|
||||
String dept = "";
|
||||
if(StringUtils.isNotEmpty(deptCd)){
|
||||
dept = codeDetailService.getCodeName("DEPT", deptCd);
|
||||
}
|
||||
return StringUtils.isEmpty(dept) ? "-" : dept ;
|
||||
}
|
||||
|
||||
public String getUserRankTxt(String uniqId) {
|
||||
String rankCd = userMapper.findById(uniqId).getRankCd();
|
||||
String rank = "";
|
||||
if(StringUtils.isNotEmpty(rankCd)){
|
||||
rank = codeDetailService.getCodeName("RANK", rankCd);
|
||||
}
|
||||
return StringUtils.isEmpty(rank) ? "-" : rank ;
|
||||
}
|
||||
|
||||
public String getUserMobilePhone(String uniqId) {
|
||||
String mobilePhone = userMapper.findById(uniqId).getMobilePhone();
|
||||
|
||||
return StringUtils.isEmpty(mobilePhone) ? uniqId : mobilePhone ;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -1,212 +0,0 @@
|
||||
package com.itn.admin.cmn.util.tistory;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogCookieMappingVO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TistoryCookieUtil {
|
||||
|
||||
private static final String COOKIE_FILE_PATH = "docs/munjaon_tstory_state.json";
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 쿠키 상태 JSON 파일에서 쿠키 문자열을 생성합니다.
|
||||
*
|
||||
* @return 쿠키 문자열 (예: "name1=value1; name2=value2")
|
||||
*/
|
||||
public String getCookieString() {
|
||||
try {
|
||||
ClassPathResource resource = new ClassPathResource(COOKIE_FILE_PATH);
|
||||
JsonNode rootNode = objectMapper.readTree(resource.getInputStream());
|
||||
JsonNode cookiesNode = rootNode.get("cookies");
|
||||
|
||||
if (cookiesNode == null || !cookiesNode.isArray()) {
|
||||
log.warn("쿠키 정보를 찾을 수 없습니다.");
|
||||
return "";
|
||||
}
|
||||
|
||||
List<String> cookieList = new ArrayList<>();
|
||||
for (JsonNode cookieNode : cookiesNode) {
|
||||
String name = cookieNode.get("name").asText();
|
||||
String value = cookieNode.get("value").asText();
|
||||
|
||||
// 유효한 쿠키만 추가 (빈 값이나 null 제외)
|
||||
if (name != null && !name.trim().isEmpty() &&
|
||||
value != null && !value.trim().isEmpty()) {
|
||||
cookieList.add(name + "=" + value);
|
||||
}
|
||||
}
|
||||
|
||||
String cookieString = String.join("; ", cookieList);
|
||||
log.info("티스토리 쿠키 문자열 생성 완료: {} 개 쿠키", cookieList.size());
|
||||
|
||||
return cookieString;
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("쿠키 파일 읽기 실패: {}", COOKIE_FILE_PATH, e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 도메인의 쿠키만 필터링하여 반환합니다.
|
||||
*
|
||||
* @param domain 필터링할 도메인 (예: "tistory.com")
|
||||
* @return 해당 도메인의 쿠키 문자열
|
||||
*/
|
||||
public String getCookieStringForDomain(String domain) {
|
||||
try {
|
||||
ClassPathResource resource = new ClassPathResource(COOKIE_FILE_PATH);
|
||||
JsonNode rootNode = objectMapper.readTree(resource.getInputStream());
|
||||
JsonNode cookiesNode = rootNode.get("cookies");
|
||||
|
||||
if (cookiesNode == null || !cookiesNode.isArray()) {
|
||||
log.warn("쿠키 정보를 찾을 수 없습니다.");
|
||||
return "";
|
||||
}
|
||||
|
||||
List<String> cookieList = new ArrayList<>();
|
||||
for (JsonNode cookieNode : cookiesNode) {
|
||||
String cookieDomain = cookieNode.get("domain").asText();
|
||||
String name = cookieNode.get("name").asText();
|
||||
String value = cookieNode.get("value").asText();
|
||||
|
||||
// 도메인이 일치하고 유효한 쿠키만 추가
|
||||
if (cookieDomain.contains(domain) &&
|
||||
name != null && !name.trim().isEmpty() &&
|
||||
value != null && !value.trim().isEmpty()) {
|
||||
cookieList.add(name + "=" + value);
|
||||
}
|
||||
}
|
||||
|
||||
String cookieString = String.join("; ", cookieList);
|
||||
log.info("티스토리 도메인({}) 쿠키 문자열 생성 완료: {} 개 쿠키", domain, cookieList.size());
|
||||
|
||||
return cookieString;
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("쿠키 파일 읽기 실패: {}", COOKIE_FILE_PATH, e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 티스토리 전용 쿠키 문자열을 반환합니다.
|
||||
*
|
||||
* @return 티스토리 관련 쿠키 문자열
|
||||
*/
|
||||
public String getTistoryCookieString() {
|
||||
return getCookieStringForDomain("tistory.com");
|
||||
}
|
||||
|
||||
/**
|
||||
* 블로그 계정별 쿠키 파일에서 쿠키 문자열 반환
|
||||
*/
|
||||
public String getCookieStringForBlog(BlogCookieMappingVO cookieMapping) {
|
||||
return getCookieStringForBlog(cookieMapping, "tistory.com");
|
||||
}
|
||||
|
||||
/**
|
||||
* 블로그 계정별 쿠키 파일에서 특정 도메인의 쿠키 문자열 반환
|
||||
*/
|
||||
public String getCookieStringForBlog(BlogCookieMappingVO cookieMapping, String domain) {
|
||||
if (cookieMapping == null || !cookieMapping.isActiveCookie()) {
|
||||
throw new RuntimeException("유효하지 않은 쿠키 매핑 정보입니다.");
|
||||
}
|
||||
|
||||
if (cookieMapping.isCookieExpired()) {
|
||||
throw new RuntimeException("쿠키가 만료되었습니다. 재로그인이 필요합니다.");
|
||||
}
|
||||
|
||||
try {
|
||||
String fullPath = cookieMapping.getFullCookieFilePath();
|
||||
File cookieFile = new File(fullPath);
|
||||
|
||||
if (!cookieFile.exists()) {
|
||||
throw new RuntimeException("쿠키 파일을 찾을 수 없습니다: " + fullPath);
|
||||
}
|
||||
|
||||
JsonNode rootNode = objectMapper.readTree(cookieFile);
|
||||
return parseCookieStringFromJson(rootNode, domain);
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("쿠키 파일 읽기 실패: " + cookieMapping.getFullCookieFilePath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿠키 파일 경로로부터 직접 쿠키 문자열 반환
|
||||
*/
|
||||
public String getCookieStringFromFile(String cookieFilePath, String domain) {
|
||||
try {
|
||||
File cookieFile = new File(cookieFilePath);
|
||||
|
||||
if (!cookieFile.exists()) {
|
||||
throw new RuntimeException("쿠키 파일을 찾을 수 없습니다: " + cookieFilePath);
|
||||
}
|
||||
|
||||
JsonNode rootNode = objectMapper.readTree(cookieFile);
|
||||
return parseCookieStringFromJson(rootNode, domain);
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("쿠키 파일 읽기 실패: " + cookieFilePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿠키 파일 유효성 검증
|
||||
*/
|
||||
public boolean validateCookieFile(BlogCookieMappingVO cookieMapping) {
|
||||
try {
|
||||
String cookieString = getCookieStringForBlog(cookieMapping);
|
||||
return cookieString != null && !cookieString.trim().isEmpty() &&
|
||||
cookieString.contains("TSSESSION"); // 티스토리 세션 쿠키 필수
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("쿠키 파일 유효성 검증 실패: {}", cookieMapping.getFullCookieFilePath(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 노드에서 쿠키 문자열 파싱
|
||||
*/
|
||||
private String parseCookieStringFromJson(JsonNode rootNode, String domain) {
|
||||
JsonNode cookiesNode = rootNode.get("cookies");
|
||||
|
||||
if (cookiesNode == null || !cookiesNode.isArray()) {
|
||||
throw new RuntimeException("쿠키 데이터 형식이 올바르지 않습니다.");
|
||||
}
|
||||
|
||||
List<String> cookieList = new ArrayList<>();
|
||||
|
||||
for (JsonNode cookieNode : cookiesNode) {
|
||||
String cookieDomain = cookieNode.get("domain").asText();
|
||||
String cookieName = cookieNode.get("name").asText();
|
||||
String cookieValue = cookieNode.get("value").asText();
|
||||
|
||||
// 도메인 매칭 (점으로 시작하는 도메인 포함)
|
||||
if (cookieDomain.contains(domain) || cookieDomain.endsWith("." + domain)) {
|
||||
if (cookieName != null && !cookieName.trim().isEmpty() &&
|
||||
cookieValue != null && !cookieValue.trim().isEmpty()) {
|
||||
cookieList.add(cookieName + "=" + cookieValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String cookieString = String.join("; ", cookieList);
|
||||
log.info("도메인({}) 쿠키 문자열 생성 완료: {} 개 쿠키", domain, cookieList.size());
|
||||
|
||||
return cookieString;
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,9 @@
|
||||
package com.itn.admin.cmn.vo;
|
||||
|
||||
import lombok.*;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@SuperBuilder
|
||||
@Getter
|
||||
@Setter
|
||||
public class CmnVO {
|
||||
@ -21,15 +17,6 @@ public class CmnVO {
|
||||
private int totalPageCount; // 총 페이지 수
|
||||
private int startPage; // 페이지네이션의 시작 페이지 번호
|
||||
private int endPage; // 페이지네이션의 끝 페이지 번호
|
||||
private String createdBy;
|
||||
private LocalDateTime frstRegistPnttm;
|
||||
private String updatedBy;
|
||||
private LocalDateTime lastUpdtPnttm;
|
||||
|
||||
// 등록자/수정자 정보
|
||||
private String frstRegisterId; // 최초 등록자 ID
|
||||
private String lastUpdusrId; // 최종 수정자 ID
|
||||
|
||||
|
||||
// 페이징을 위한 offset과 limit을 계산하는 메서드
|
||||
public void calculatePaging(int totalRecordCount) {
|
||||
|
||||
@ -20,6 +20,4 @@ import java.util.List;
|
||||
public interface CommuteMapper {
|
||||
|
||||
List<CommuteVO> findAll(CommuteVO commuteVO);
|
||||
|
||||
CommuteVO findByUsridAndSrvdtBetween(CommuteVO commuteVO);
|
||||
}
|
||||
|
||||
@ -26,7 +26,6 @@ public class CommuteVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private int evtlguid;
|
||||
private String biostarId;
|
||||
private String srvdt;
|
||||
private int devdt;
|
||||
private int devuid;
|
||||
@ -68,12 +67,4 @@ public class CommuteVO implements Serializable {
|
||||
private String searchDay;
|
||||
|
||||
private String tableNm;
|
||||
|
||||
private String uniqId;
|
||||
|
||||
|
||||
private String div1;
|
||||
private String div2;
|
||||
private String div3;
|
||||
private Boolean pstnIsNumber;
|
||||
}
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
package com.itn.admin.commute.mapper.domain;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
public class ItnCommuteGroupVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Integer commuteGroupId; // 그룹 아이디
|
||||
private String workDt; // 근무 날짜
|
||||
private String approver; // 승인자
|
||||
private String approverDt; // 승인 일지
|
||||
private String frstRegisterId; // 최초 등록자 ID
|
||||
private String frstRegistPnttm; // 최초 등록 일자
|
||||
private String lastUpdusrId; // 최종 수정자 ID
|
||||
private String lastUpdtPnttm; // 최종 수정 일자
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
package com.itn.admin.commute.mapper.domain;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
public class ItnCommuteVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Integer commuteId; // 아이디
|
||||
private Integer commuteGroupId; // 그룹 아이디
|
||||
private String uniqId;
|
||||
private String userName; // 이름
|
||||
private String rankCd; // 직위
|
||||
private String category; // 구분
|
||||
private String workDt; // 근무일자
|
||||
private String startTime; // 출근시간
|
||||
private String startRslt; // 출근결과
|
||||
private String endTime; // 퇴근시간
|
||||
private String endRslt; // 퇴근결과
|
||||
private String transferDt; // 이관일시
|
||||
private String approver; // 승인자
|
||||
private String approverDt; // 승인일시
|
||||
private String holiCode; // 승인일시
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -8,7 +8,7 @@ import java.util.stream.Stream;
|
||||
public enum UserEnum {
|
||||
user1("&@~PYfUBsF+m99kduT53j1Stw==","조용준", "본부장")
|
||||
,user2("&@~C33DuWpcSL7Krvh2zAByUQ==","박진순", "팀장")
|
||||
,user3("&@~9+BQUtRi1cuWOaIqeCYdAA==","우영두", "차장")
|
||||
,user3("&@~9+BQUtRi1cuWOaIqeCYdAA==","우영두", "팀장")
|
||||
,user4("&@~peUfyxpLvs6RN9X4waktzQ==","원영현", "과장")
|
||||
,user5("&@~tBRefZ81JCbrXNyRkjZNGQ==","이호영", "대리")
|
||||
,user6("&@~X0eEqUF71/pD/Z0KPKysrA==","이지우", "대리")
|
||||
@ -16,15 +16,15 @@ public enum UserEnum {
|
||||
,user8("&@~Z/Uti3tzksl96ByRRZT7AQ==","유인식", "대표")
|
||||
,user9("&@~zO4oUG4dNtiVfnDdfCWM2A==","장영익", "실장")
|
||||
,user10("&@~O+736xoPn0FzMuPQufff1w==","김상훈", "본부장")
|
||||
// ,user11("&@~E8RB3p27IfRVEhNefMu2Vw==","김보미", "대리")
|
||||
// ,user12("&@~47amAycYJ4ZT8BZDi6a2sA==","이설희", "주임")
|
||||
,user13("&@~tuIqQtGVVzz6rRunF/jHUQ==","정다은", "대리")
|
||||
,user14("&@~KPBL+GIy7i2agV7V57MZWg==","조현희", "주임")
|
||||
,user11("&@~E8RB3p27IfRVEhNefMu2Vw==","김보미", "대리")
|
||||
,user12("&@~47amAycYJ4ZT8BZDi6a2sA==","이설희", "주임")
|
||||
,user13("&@~KPBL+GIy7i2agV7V57MZWg==","정다은", "대리")
|
||||
,user15("&@~S6vaRrMJmeRjp0T8z+/ybg==","강민경", "팀장")
|
||||
,user16("&@~7mpJXFU+euFUNEdHmHLQVQ==","정수빈", "대리")
|
||||
// ,user17("&@~+BL2FKvmIQc/jIyHvz0jew==","박은지", "주임")
|
||||
,user17("&@~+BL2FKvmIQc/jIyHvz0jew==","박은지", "주임")
|
||||
|
||||
// ,user18("&@~l+Zs/I7piSIFu9WBQaI1eg==","박세희R", "알바")
|
||||
,user18("&@~/GE8OIrOSbOevc+sViEvbg==","이수현R", "알바")
|
||||
,user19("&@~l+Zs/I7piSIFu9WBQaI1eg==","임선율R", "알바")
|
||||
|
||||
,user99("&@~6MWcZ9FnikUCQ8NTFDqHHQ==","itn6", "")
|
||||
// ,user19("&@~8v+/5tt7+aiF/GuAhT37Xw==","S고수빈")
|
||||
@ -34,9 +34,6 @@ public enum UserEnum {
|
||||
// ,user23("&@~1M3p31Kvc+zVi2IMgvazHw==","S한승아")
|
||||
// ,user24("&@~/GE8OIrOSbOevc+sViEvbg==","S장수진")
|
||||
// ,user14("&@~HM2dbaHcmk8bIMYKO2uIRg==","장건영")
|
||||
|
||||
// ,user18("&@~/GE8OIrOSbOevc+sViEvbg==","이수현R", "알바")
|
||||
// ,user19("&@~l+Zs/I7piSIFu9WBQaI1eg==","임선율R", "알바")
|
||||
;
|
||||
|
||||
private final String label;
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package com.itn.admin.commute.service;
|
||||
|
||||
import com.itn.admin.commute.mapper.domain.CommuteVO;
|
||||
import com.itn.admin.commute.mapper.domain.ItnCommuteVO;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@ -10,6 +9,4 @@ public interface CommuteService {
|
||||
|
||||
|
||||
Map<String, Object> getList(CommuteVO commuteVO);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -1,68 +0,0 @@
|
||||
package com.itn.admin.commute.service;
|
||||
|
||||
import com.itn.admin.gw.holiday.mapper.domain.HolidayVO;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
public class HolidayAnalyzer {
|
||||
|
||||
// 휴가 유형 상수
|
||||
private static final int FULL_DAY_MINUTES = 480; // 연차
|
||||
private static final int HALF_DAY_MINUTES = 240; // 반차
|
||||
private static final int WORK_START = 510; // 08:30
|
||||
private static final int WORK_END = 1050; // 17:30
|
||||
private static final int HALF_DAY_BREAK = 810; // 13:30
|
||||
|
||||
|
||||
|
||||
// 휴가 유형 판단 메서드
|
||||
public static String determineHolidayType(HolidayVO holi) {
|
||||
// Long 타입을 int로 변환
|
||||
int reqCnt = holi.getReqCnt().intValue();
|
||||
int startTm = holi.getStartTm().intValue();
|
||||
int endTm = holi.getEndTm().intValue();
|
||||
|
||||
// 날짜 차이 계산
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd");
|
||||
LocalDate startDate = LocalDate.parse(holi.getStartDt(), formatter);
|
||||
LocalDate endDate = LocalDate.parse(holi.getEndDt(), formatter);
|
||||
long daysBetween = ChronoUnit.DAYS.between(startDate, endDate) + 1; // 포함된 날짜 수
|
||||
|
||||
// 1일 신청
|
||||
if (daysBetween == 1) {
|
||||
if (reqCnt == FULL_DAY_MINUTES) {
|
||||
return "30"; // "연차";
|
||||
} else if (reqCnt == HALF_DAY_MINUTES) {
|
||||
if (startTm == WORK_START && endTm == HALF_DAY_BREAK) {
|
||||
return "10"; // "오후 반차";
|
||||
} else if (startTm == HALF_DAY_BREAK && endTm == WORK_END) {
|
||||
return "20"; // "오전 반차";
|
||||
} else {
|
||||
return "반차 (시간 확인 필요: " + startTm + " ~ " + endTm + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2일 이상 신청
|
||||
else if (daysBetween > 1) {
|
||||
// 하루 평균 요청 시간 계산
|
||||
int avgMinutesPerDay = reqCnt / (int) daysBetween;
|
||||
|
||||
if (avgMinutesPerDay == FULL_DAY_MINUTES) {
|
||||
return "30"; // "연차 " + daysBetween + "일";
|
||||
} else if (avgMinutesPerDay == HALF_DAY_MINUTES) {
|
||||
if (startTm == WORK_START && endTm == HALF_DAY_BREAK) {
|
||||
return "10"; // "오후 반차 " + daysBetween + "일";
|
||||
} else if (startTm == HALF_DAY_BREAK && endTm == WORK_END) {
|
||||
return "20"; // "오전 반차 " + daysBetween + "일";
|
||||
} else {
|
||||
return "반차 (시간 확인 필요: " + startTm + " ~ " + endTm + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "기타 (" + reqCnt + "분, " + daysBetween + "일)";
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,25 +1,13 @@
|
||||
package com.itn.admin.commute.service.impl;
|
||||
|
||||
import com.itn.admin.cmn.util.DateUtils;
|
||||
import com.itn.admin.commute.mapper.CommuteMapper;
|
||||
import com.itn.admin.commute.mapper.domain.CommuteVO;
|
||||
import com.itn.admin.commute.mapper.domain.ItnCommuteGroupVO;
|
||||
import com.itn.admin.commute.mapper.domain.ItnCommuteVO;
|
||||
import com.itn.admin.commute.mapper.domain.UserEnum;
|
||||
import com.itn.admin.commute.service.CommuteService;
|
||||
import com.itn.admin.commute.service.HolidayAnalyzer;
|
||||
import com.itn.admin.gw.holiday.mapper.HolidayMapper;
|
||||
import com.itn.admin.gw.holiday.mapper.domain.HolidayVO;
|
||||
import com.itn.admin.itn.code.mapper.domain.CodeDetailVO;
|
||||
import com.itn.admin.itn.code.server.CodeDetailService;
|
||||
import com.itn.admin.itn.commute.mapper.ItnCommuteMapper;
|
||||
import com.itn.admin.itn.user.mapper.UserMapper;
|
||||
import com.itn.admin.itn.user.mapper.domain.UserVO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.text.Collator;
|
||||
import java.time.DayOfWeek;
|
||||
@ -39,90 +27,86 @@ public class CommuteServiceImpl implements CommuteService {
|
||||
@Autowired
|
||||
CommuteMapper commuteMapper;
|
||||
|
||||
@Autowired
|
||||
HolidayMapper holidayMapper;
|
||||
|
||||
@Autowired
|
||||
private UserMapper userMapper;
|
||||
|
||||
|
||||
private static final int PAGE_SIZE = 5;
|
||||
|
||||
public Map<String, Object> getList(CommuteVO commuteVO) {
|
||||
|
||||
|
||||
List<CommuteVO> commuteList = makeList(commuteVO);
|
||||
|
||||
// controller에 return
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
if(StringUtils.isNotEmpty(commuteVO.getSearchYear())){
|
||||
|
||||
}else{
|
||||
// 현재 날짜 구하기
|
||||
LocalDate now = LocalDate.now().minusDays(1);
|
||||
|
||||
// 만약 전날이 토요일(7) 또는 일요일(1)이면 금요일로 설정
|
||||
if (now.getDayOfWeek() == DayOfWeek.SATURDAY) {
|
||||
now = now.minusDays(1); // 토요일이면 금요일로
|
||||
} else if (now.getDayOfWeek() == DayOfWeek.SUNDAY) {
|
||||
now = now.minusDays(2); // 일요일이면 금요일로
|
||||
}
|
||||
// 년도 구하기
|
||||
String year = String.valueOf(now.getYear());
|
||||
|
||||
String month = now.format(DateTimeFormatter.ofPattern("M"));
|
||||
|
||||
String day = now.format(DateTimeFormatter.ofPattern("d"));
|
||||
|
||||
|
||||
for (CommuteVO row : commuteList) {
|
||||
String pstn = row.getPstn();
|
||||
row.setPstnIsNumber(pstn != null && pstn.trim().matches("\\d+"));
|
||||
// 결과 출력
|
||||
System.out.println("Year: " + year);
|
||||
System.out.println("Month: " + month);
|
||||
System.out.println("Day: " + day);
|
||||
|
||||
commuteVO.setSearchYear(year);
|
||||
commuteVO.setSearchMonth(month);
|
||||
commuteVO.setSearchDay(day);
|
||||
}
|
||||
|
||||
map.put("resultList", commuteList);
|
||||
map.put("commuteVO", commuteVO);
|
||||
return map;
|
||||
}
|
||||
|
||||
|
||||
commuteVO.setStartDate(commuteVO.getSearchYear()+"-"+commuteVO.getSearchMonth()+"-"+commuteVO.getSearchDay()+" 06:00:00");
|
||||
commuteVO.setEndDate(commuteVO.getSearchYear()+"-"+commuteVO.getSearchMonth()+"-"+commuteVO.getSearchDay()+" 23:59:59");
|
||||
|
||||
// 테이블명 생성
|
||||
String tableNmM = commuteVO.getSearchMonth().length() <2 ? "0"+commuteVO.getSearchMonth() : commuteVO.getSearchMonth();
|
||||
commuteVO.setTableNm("t_lg"+commuteVO.getSearchYear()+tableNmM);
|
||||
|
||||
|
||||
|
||||
public List<CommuteVO> makeList(CommuteVO commuteVO){
|
||||
|
||||
|
||||
if(StringUtils.isEmpty(commuteVO.getSearchYear())){
|
||||
|
||||
Map<String,String> map = DateUtils.getPreviousBusinessDay();
|
||||
commuteVO.setSearchYear(map.get("year"));
|
||||
commuteVO.setSearchMonth(map.get("month"));
|
||||
commuteVO.setSearchDay(map.get("day"));
|
||||
List<CommuteVO> commuteList = new ArrayList<>();
|
||||
try {
|
||||
// commuteList = commuteMapper.findAll(commuteVO);
|
||||
commuteList = new ArrayList<>(commuteMapper.findAll(commuteVO));
|
||||
}catch (Exception e){
|
||||
// 월별로 테이블이 생성되는데
|
||||
// 없는 테이블을 select하면 Exception이 발생함
|
||||
// 해당 exception 방지용 try-catch
|
||||
}
|
||||
|
||||
CommuteVO searchVO = getSearchVO(commuteVO);
|
||||
|
||||
List<CommuteVO> commuteList = commuteMapper.findAll(searchVO);
|
||||
|
||||
List<HolidayVO> holidayList = holidayMapper.selectHolidaysByDateRange(DateUtils.getYesterdayDate(commuteVO));
|
||||
log.info("holidayList.size() :: [{}] : "+holidayList.size());
|
||||
holidayList.forEach(holidayVO -> {
|
||||
log.info("holidayVO : [{}]",holidayVO);
|
||||
});
|
||||
List<UserVO> userAllVO = userMapper.findByBiostarIdIsNotNull();
|
||||
userAllVO.forEach(userVO -> log.info("userVO : [{}][{}]", userVO.getBiostarId(), userVO) );
|
||||
|
||||
commuteList.stream().forEach(t->{
|
||||
|
||||
|
||||
|
||||
// 지각 체크
|
||||
t.setFirstActivityTimeMemo(this.getLateChk(t.getFirstActivityTime()));
|
||||
// 조기퇴근 체크
|
||||
t.setLastActivityTimeMemo(this.getLeaveWorkEarly(t.getLastActivityTime()));
|
||||
|
||||
|
||||
|
||||
if(StringUtils.isNotEmpty(t.getUsrid())){
|
||||
UserVO matchedUser = userAllVO.stream()
|
||||
.filter(user -> user.getBiostarId().equals(t.getUsrid()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
log.info("===============================================");
|
||||
log.info("matchedUser : [{}] : [{}]", matchedUser, t.getUsrid());
|
||||
|
||||
if( matchedUser != null ){
|
||||
t.setUsrid(matchedUser.getUserName());
|
||||
t.setPstn(matchedUser.getRankCd());
|
||||
t.setUniqId(matchedUser.getUniqId());
|
||||
try {
|
||||
UserEnum userEnum = UserEnum.valueOfLabel(t.getUsrid());
|
||||
t.setUsrid(userEnum.userName());
|
||||
t.setPstn(userEnum.pstn());
|
||||
}catch (Exception e){
|
||||
t.setUsrid("힣등록해야함");
|
||||
}
|
||||
|
||||
}
|
||||
t.setFirstActivityTime(t.getFirstActivityTime().split(" ")[1]);
|
||||
t.setLastActivityTime(t.getLastActivityTime().split(" ")[1]);
|
||||
});
|
||||
|
||||
commuteList.forEach(t -> log.info("userVO : [{}][{}]", t.getBiostarId(), t) );
|
||||
commuteList.forEach(t-> {
|
||||
t.setFirstActivityTime(t.getFirstActivityTime().split(" ")[1]);
|
||||
t.setLastActivityTime(t.getLastActivityTime().split(" ")[1]);
|
||||
});
|
||||
|
||||
// 출근안한사람 체크하기
|
||||
for (UserEnum user : UserEnum.values()) {
|
||||
@ -143,37 +127,22 @@ public class CommuteServiceImpl implements CommuteService {
|
||||
commuteTempVO.setFirstActivityTime("-"); // 기본값 설정
|
||||
commuteTempVO.setLastActivityTime("-"); // 기본값 설정
|
||||
commuteList.add(commuteTempVO); // 수정된 리스트에 추가
|
||||
log.info(" : commuteTempVO : [{}]", commuteTempVO);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
commuteList.forEach(commute -> System.out.println(commute.toString()));
|
||||
|
||||
commuteList.removeIf(t -> "&@~Z/Uti3tzksl96ByRRZT7AQ==".equals(t.getUsrid())
|
||||
|| "itn6".equals(t.getUsrid())
|
||||
commuteList.removeIf(t -> "유인식".equals(t.getUsrid())
|
||||
|| "itn6".equals(t.getUsrid())
|
||||
);
|
||||
|
||||
return commuteList;
|
||||
}
|
||||
|
||||
// controller에 return
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
|
||||
|
||||
|
||||
private static CommuteVO getSearchVO(CommuteVO commuteVO) {
|
||||
commuteVO.setStartDate(commuteVO.getSearchYear()+"-"+ commuteVO.getSearchMonth()+"-"+ commuteVO.getSearchDay()+" 06:00:00");
|
||||
commuteVO.setEndDate(commuteVO.getSearchYear()+"-"+ commuteVO.getSearchMonth()+"-"+ commuteVO.getSearchDay()+" 23:59:59");
|
||||
|
||||
// 테이블명 생성
|
||||
// biostar(관리자콘솔)은 테이블명이 "t_lg년월"로 이루어져 있음.
|
||||
String tableNmM = commuteVO.getSearchMonth().length() <2
|
||||
? "0"+ commuteVO.getSearchMonth()
|
||||
: commuteVO.getSearchMonth();
|
||||
|
||||
commuteVO.setTableNm("t_lg"+ commuteVO.getSearchYear()+tableNmM);
|
||||
|
||||
return commuteVO;
|
||||
map.put("resultList", commuteList);
|
||||
map.put("commuteVO", commuteVO);
|
||||
return map;
|
||||
}
|
||||
|
||||
public static String getRandomTime(String start, String end) {
|
||||
@ -195,43 +164,39 @@ public class CommuteServiceImpl implements CommuteService {
|
||||
return randomTime.format(DateTimeFormatter.ofPattern("HH:mm:ss"));
|
||||
}
|
||||
|
||||
// 입력된 시간 문자열을 LocalTime 객체로 변환하는 함수
|
||||
private static LocalTime parseActivityTime(String activityTimeString) {
|
||||
// "yyyy-MM-dd HH:mm:ss" 패턴의 포매터 생성
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
// 문자열을 LocalDateTime으로 파싱
|
||||
LocalDateTime dateTime = LocalDateTime.parse(activityTimeString, formatter);
|
||||
// LocalDateTime에서 시간 부분만 추출하여 반환
|
||||
return dateTime.toLocalTime();
|
||||
}
|
||||
|
||||
// 조기 퇴근 여부를 확인하는 함수
|
||||
private String getLeaveWorkEarly(String p_lastActivityTime) {
|
||||
// 마지막 활동 시간을 LocalTime으로 변환
|
||||
LocalTime activityTime = parseActivityTime(p_lastActivityTime);
|
||||
// 기준 시간(18:30) 설정
|
||||
// DateTimeFormatter를 사용하여 String을 LocalDateTime으로 파싱
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
LocalDateTime firstActivityTime = LocalDateTime.parse(p_lastActivityTime, formatter);
|
||||
|
||||
// 체크하고자 하는 시간 설정 (09:30)
|
||||
LocalTime checkTime = LocalTime.of(18, 30);
|
||||
|
||||
// 활동 시간이 기준 시간보다 이전이면 조기 퇴근
|
||||
// firstActivityTime의 시간 부분만 추출
|
||||
LocalTime activityTime = firstActivityTime.toLocalTime();
|
||||
|
||||
// activityTime이 checkTime보다 이후인지 확인
|
||||
if (activityTime.isBefore(checkTime)) {
|
||||
return "90"; //""조기퇴근";
|
||||
return "조기퇴근";
|
||||
}
|
||||
// 그렇지 않으면 빈 문자열 반환
|
||||
return "";
|
||||
}
|
||||
|
||||
// 지각 여부를 확인하는 함수
|
||||
private static String getLateChk(String p_firstActivityTime) {
|
||||
// 첫 활동 시간을 LocalTime으로 변환
|
||||
LocalTime activityTime = parseActivityTime(p_firstActivityTime);
|
||||
// 기준 시간(09:30) 설정
|
||||
// DateTimeFormatter를 사용하여 String을 LocalDateTime으로 파싱
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
LocalDateTime firstActivityTime = LocalDateTime.parse(p_firstActivityTime, formatter);
|
||||
|
||||
// 체크하고자 하는 시간 설정 (09:30)
|
||||
LocalTime checkTime = LocalTime.of(9, 30);
|
||||
|
||||
// 활동 시간이 기준 시간보다 이후면 지각
|
||||
// firstActivityTime의 시간 부분만 추출
|
||||
LocalTime activityTime = firstActivityTime.toLocalTime();
|
||||
|
||||
// activityTime이 checkTime보다 이후인지 확인
|
||||
if (activityTime.isAfter(checkTime)) {
|
||||
return "70"; // 지각 시 70 반환 (공통코드 COMMUTE)
|
||||
return "지각";
|
||||
}
|
||||
// 그렇지 않으면 빈 문자열 반환
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
@ -1,15 +1,12 @@
|
||||
package com.itn.admin.commute.web;
|
||||
|
||||
import com.itn.admin.cmn.util.DateUtils;
|
||||
import com.itn.admin.commute.mapper.domain.CommuteVO;
|
||||
import com.itn.admin.commute.mapper.domain.ItnCommuteVO;
|
||||
import com.itn.admin.commute.service.CommuteService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@ -23,7 +20,7 @@ public class CommuteController {
|
||||
this.commuteService = commuteService;
|
||||
}
|
||||
|
||||
@GetMapping(value = "/commute/list_temp")
|
||||
@GetMapping(value = "/commute/list")
|
||||
public String list(@ModelAttribute("commuteVO") CommuteVO commuteVO, Model model) {
|
||||
|
||||
|
||||
@ -32,7 +29,23 @@ public class CommuteController {
|
||||
model.addAttribute("list", resultMap.get("resultList"));
|
||||
model.addAttribute("commuteVO", resultMap.get("commuteVO"));
|
||||
|
||||
return "commute/list_temp";
|
||||
return "commute/list";
|
||||
}
|
||||
|
||||
// @GetMapping(value = "/{pageNumber}")
|
||||
// public String list(@ModelAttribute CommuteVO commuteVO, Model model) {
|
||||
// Page<CommuteVO> page = commuteService.getList(commuteVO);
|
||||
//
|
||||
// int current = page.getNumber() + 1;
|
||||
// int begin = Math.max(1, current - 5);
|
||||
// int end = Math.min(begin + 10, page.getTotalPages());
|
||||
//
|
||||
// model.addAttribute("list", page);
|
||||
// model.addAttribute("beginIndex", begin);
|
||||
// model.addAttribute("endIndex", end);
|
||||
// model.addAttribute("currentIndex", current);
|
||||
//
|
||||
// return "customers/list";
|
||||
//
|
||||
// }
|
||||
}
|
||||
|
||||
@ -3,8 +3,6 @@ package com.itn.admin.commute.web;
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
import com.itn.admin.commute.mapper.domain.CommuteVO;
|
||||
import com.itn.admin.commute.service.CommuteService;
|
||||
import com.itn.admin.itn.commute.service.ItnCommuteService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@ -14,168 +12,44 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.YearMonth;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
public class CommuteRestController {
|
||||
|
||||
private CommuteService commuteService;
|
||||
|
||||
private ItnCommuteService itnCommuteService;
|
||||
|
||||
@Autowired
|
||||
public void setCommuteService(CommuteService commuteService) {
|
||||
this.commuteService = commuteService;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setItnCommuteService(ItnCommuteService itnCommuteService) {
|
||||
this.itnCommuteService = itnCommuteService;
|
||||
}
|
||||
|
||||
@GetMapping(value = "/api/commute/transfer")
|
||||
@GetMapping(value = "/api/commute/list")
|
||||
public ResponseEntity<RestResponse> list(@ModelAttribute("commuteVO") CommuteVO commuteVO, Model model) {
|
||||
// 서비스 메서드 호출
|
||||
Map<String, Object> resultMap = itnCommuteService.transfer(commuteVO);
|
||||
|
||||
|
||||
Map<String, Object> resultMap = commuteService.getList(commuteVO);
|
||||
|
||||
// model.addAttribute("list", resultMap.get("resultList"));
|
||||
// model.addAttribute("commuteVO", resultMap.get("commuteVO"));
|
||||
|
||||
return ResponseEntity.ok().body(new RestResponse(HttpStatus.OK,"성공적으로 조회했습니다.",resultMap));
|
||||
/*
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
@GetMapping(value = "/api/commute/transferMonth")
|
||||
public ResponseEntity<RestResponse> transferMonth(@ModelAttribute("commuteVO") CommuteVO commuteVO, Model model) {
|
||||
// 서비스 메서드 호출
|
||||
|
||||
// 연도와 월 추출
|
||||
String year = commuteVO.getSearchYear();
|
||||
String month = commuteVO.getSearchMonth();
|
||||
|
||||
// 입력 데이터 로그 출력
|
||||
log.info(":: commuteVO.getSearchYear() :: [{}]", year);
|
||||
log.info(":: commuteVO.getSearchMonth() :: [{}]", month);
|
||||
log.info(":: commuteVO.getSearchDay() :: [{}]", commuteVO.getSearchDay());
|
||||
|
||||
// 결과 Map 초기화
|
||||
Map<String, Map<String, Object>> resultMap = new HashMap<>();
|
||||
|
||||
try {
|
||||
// YearMonth 객체 생성 (해당 월의 정보를 관리)
|
||||
YearMonth yearMonth = YearMonth.of(Integer.parseInt(year), Integer.parseInt(month));
|
||||
int daysInMonth = yearMonth.lengthOfMonth(); // 해당 월의 총 일수 (예: 3월 → 31일)
|
||||
|
||||
// 날짜 포맷터 (키로 사용할 날짜 형식: "yyyy-MM-dd")
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
// 1일부터 마지막 날까지 순회
|
||||
for (int day = 1; day <= daysInMonth; day++) {
|
||||
// 날짜 설정
|
||||
LocalDate date = yearMonth.atDay(day);
|
||||
String formattedDate = date.format(formatter); // 예: "2025-03-01"
|
||||
|
||||
// commuteVO에 날짜 설정
|
||||
commuteVO.setSearchYear(String.valueOf(date.getYear()));
|
||||
commuteVO.setSearchMonth(String.format("%02d", date.getMonthValue())); // 2자리 형식 (예: "03")
|
||||
commuteVO.setSearchDay(String.format("%02d", date.getDayOfMonth())); // 2자리 형식 (예: "01")
|
||||
|
||||
// commuteService.transfer 호출
|
||||
try {
|
||||
Map<String, Object> dailyResult = itnCommuteService.transfer(commuteVO);
|
||||
resultMap.put(formattedDate, dailyResult);
|
||||
log.info(":: Processed date :: [{}] - Result: [{}]", formattedDate, dailyResult);
|
||||
} catch (Exception e) {
|
||||
log.error(":: Error processing date :: [{}] - Error: [{}]", formattedDate, e.getMessage());
|
||||
resultMap.put(formattedDate, Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
// 성공 응답 반환
|
||||
return ResponseEntity.ok()
|
||||
.body(new RestResponse(HttpStatus.OK, "성공적으로 조회했습니다.", resultMap));
|
||||
|
||||
} catch (NumberFormatException e) {
|
||||
log.error("Invalid year or month: year=[{}], month=[{}]", year, month);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(new RestResponse(HttpStatus.BAD_REQUEST, "연도 또는 월 형식이 잘못되었습니다.", null));
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing commute transfer: {}", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR, "조회 중 오류가 발생했습니다.", null));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@GetMapping(value = "/api/commute/transferYear")
|
||||
public ResponseEntity<RestResponse> transferYear(@ModelAttribute("commuteVO") CommuteVO commuteVO, Model model) {
|
||||
// 서비스 메서드 호출
|
||||
String year = commuteVO.getSearchYear();
|
||||
|
||||
// 입력 데이터 로그 출력
|
||||
log.info(":: commuteVO.getSearchYear() :: [{}]", year);
|
||||
log.info(":: commuteVO.getSearchMonth() :: [{}]", commuteVO.getSearchMonth());
|
||||
log.info(":: commuteVO.getSearchDay() :: [{}]", commuteVO.getSearchDay());
|
||||
|
||||
// 결과 Map 초기화
|
||||
Map<String, Map<String, Object>> resultMap = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 1월부터 12월까지 순회
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
for (int month = 1; month <= 12; month++) {
|
||||
// YearMonth 객체 생성 (해당 월의 정보를 관리)
|
||||
YearMonth yearMonth = YearMonth.of(Integer.parseInt(year), month);
|
||||
int daysInMonth = yearMonth.lengthOfMonth(); // 해당 월의 총 일수 (예: 3월 → 31일)
|
||||
|
||||
// 날짜 포맷터 (키로 사용할 날짜 형식: "yyyy-MM-dd")
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
// 1일부터 마지막 날까지 순회
|
||||
for (int day = 1; day <= daysInMonth; day++) {
|
||||
// 날짜 설정
|
||||
LocalDate date = yearMonth.atDay(day);
|
||||
String formattedDate = date.format(formatter); // 예: "2025-03-01"
|
||||
|
||||
// commuteVO에 날짜 설정
|
||||
commuteVO.setSearchYear(String.valueOf(date.getYear()));
|
||||
commuteVO.setSearchMonth(String.format("%02d", date.getMonthValue())); // 2자리 형식 (예: "03")
|
||||
commuteVO.setSearchDay(String.format("%02d", date.getDayOfMonth())); // 2자리 형식 (예: "01")
|
||||
|
||||
// commuteService.transfer 호출
|
||||
try {
|
||||
Map<String, Object> dailyResult = itnCommuteService.transfer(commuteVO);
|
||||
resultMap.put(formattedDate, dailyResult);
|
||||
log.info(":: Processed date :: [{}] - Result: [{}]", formattedDate, dailyResult);
|
||||
} catch (Exception e) {
|
||||
log.error(":: Error processing date :: [{}] - Error: [{}]", formattedDate, e.getMessage());
|
||||
resultMap.put(formattedDate, Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
// 월별 데이터 추가 (기존 day 대신 month 사용)
|
||||
map.put("month" + month, month);
|
||||
}
|
||||
|
||||
log.info("map :: [{}]", map.toString());
|
||||
// 성공 응답 반환
|
||||
return ResponseEntity.ok()
|
||||
.body(new RestResponse(HttpStatus.OK, "성공적으로 조회했습니다.", resultMap));
|
||||
|
||||
} catch (NumberFormatException e) {
|
||||
log.error("Invalid year: year=[{}]", year);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(new RestResponse(HttpStatus.BAD_REQUEST, "연도 형식이 잘못되었습니다.", null));
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing commute transfer: {}", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(new RestResponse(HttpStatus.INTERNAL_SERVER_ERROR, "조회 중 오류가 발생했습니다.", null));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// @GetMapping(value = "/{pageNumber}")
|
||||
// public String list(@ModelAttribute CommuteVO commuteVO, Model model) {
|
||||
// Page<CommuteVO> page = commuteService.getList(commuteVO);
|
||||
//
|
||||
// int current = page.getNumber() + 1;
|
||||
// int begin = Math.max(1, current - 5);
|
||||
// int end = Math.min(begin + 10, page.getTotalPages());
|
||||
//
|
||||
// model.addAttribute("list", page);
|
||||
// model.addAttribute("beginIndex", begin);
|
||||
// model.addAttribute("endIndex", end);
|
||||
// model.addAttribute("currentIndex", current);
|
||||
//
|
||||
// return "customers/list";
|
||||
//
|
||||
// }
|
||||
}
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
package com.itn.admin.gw.holiday.mapper;
|
||||
|
||||
import com.itn.admin.gw.holiday.mapper.domain.HolidayVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* packageName : com.itn.admin.gw.holiday.mapper
|
||||
* fileName : HolidayMapper
|
||||
* author : hylee
|
||||
* date : 2025-03-28
|
||||
* description :
|
||||
* ===========================================================
|
||||
* DATE AUTHOR NOTE
|
||||
* -----------------------------------------------------------
|
||||
* 2025-03-28 hylee 최초 생성
|
||||
*/
|
||||
@Mapper
|
||||
public interface HolidayMapper {
|
||||
|
||||
List<HolidayVO> selectHolidaysByDateRange(String targetDate);
|
||||
|
||||
String findByUserIdFromUsrGlobalWhereName(String userName);
|
||||
}
|
||||
@ -1,112 +0,0 @@
|
||||
package com.itn.admin.gw.holiday.mapper.domain;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* packageName : com.itn.admin.itn.commute.mapper.domain
|
||||
* fileName : MjonMsgVO
|
||||
* author : hylee
|
||||
* date : 2025-02-13
|
||||
* description : 출퇴근 log
|
||||
* ===========================================================
|
||||
* DATE AUTHOR NOTE
|
||||
* -----------------------------------------------------------
|
||||
* 2023-05-09 hylee 최초 생성
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
public class HolidayVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
// 휴가 키 (Primary Key, Not Null)
|
||||
private String holiSeq;
|
||||
|
||||
// 사원 코드
|
||||
private String empCode;
|
||||
|
||||
// 사용자 ID
|
||||
private String userId;
|
||||
|
||||
// 이름
|
||||
private String name;
|
||||
|
||||
// 기안 사원 코드
|
||||
private String gianEmpCode;
|
||||
|
||||
// 기안자 이름
|
||||
private String gianName;
|
||||
|
||||
// 직급 ID
|
||||
private String posId;
|
||||
|
||||
// 직급 이름
|
||||
private String posName;
|
||||
|
||||
// 부서 ID
|
||||
private String deptId;
|
||||
|
||||
// 부서 이름
|
||||
private String deptName;
|
||||
|
||||
// 승인 상태
|
||||
private String apprStat;
|
||||
|
||||
// 승인 날짜
|
||||
private LocalDate apprDate;
|
||||
|
||||
// 시작 날짜 (yyyy-MM-dd 형식)
|
||||
private String startDt;
|
||||
|
||||
// 종료 날짜 (yyyy-MM-dd 형식)
|
||||
private String endDt;
|
||||
|
||||
// 시작 시간
|
||||
private Long startTm;
|
||||
|
||||
// 종료 시간
|
||||
private Long endTm;
|
||||
|
||||
// 휴가 코드
|
||||
private String holiCode;
|
||||
|
||||
// 휴가 내용
|
||||
private String holiCont;
|
||||
|
||||
// 목적지
|
||||
private String dest;
|
||||
|
||||
// 연락처
|
||||
private String phone;
|
||||
|
||||
// 구분 1
|
||||
private String divi1;
|
||||
|
||||
// 구분 2
|
||||
private String divi2;
|
||||
|
||||
// 구분 3
|
||||
private String divi3;
|
||||
|
||||
// 선택 연도
|
||||
private String selYear;
|
||||
|
||||
// 요청 횟수
|
||||
private Long reqCnt;
|
||||
|
||||
// 기타 횟수
|
||||
private Long etcCnt;
|
||||
|
||||
// 등록 날짜
|
||||
private LocalDate insDate;
|
||||
|
||||
// 휴가 상세 내용
|
||||
private String holiDetail;
|
||||
|
||||
}
|
||||
@ -1,49 +1,17 @@
|
||||
package com.itn.admin.init.web;
|
||||
|
||||
import com.itn.admin.cmn.config.CustomUserDetails;
|
||||
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripVO;
|
||||
import com.itn.admin.itn.bizTrip.service.BizTripService;
|
||||
import com.itn.admin.itn.user.mapper.domain.UserVO;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
public class DashboardController {
|
||||
|
||||
@Autowired
|
||||
private BizTripService bizTripService;
|
||||
|
||||
@GetMapping("/")
|
||||
public String index(HttpServletRequest request
|
||||
, Model model
|
||||
, @AuthenticationPrincipal CustomUserDetails loginUser) {
|
||||
|
||||
|
||||
|
||||
log.info("loginUser :: [{}]", loginUser.getUser().getBiostarId());
|
||||
|
||||
List<BizTripVO> myApprovalslist = bizTripService.getMyPendingApprovals(loginUser.getUser().getUniqId());
|
||||
model.addAttribute("myApprovalslist", myApprovalslist);
|
||||
|
||||
|
||||
|
||||
// Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
// System.out.println("authentication.getAuthorities(); : "+ authentication.getAuthorities());
|
||||
// HttpSession session = request.getSession();
|
||||
// Object loginVO = session.getAttribute("loginVO");
|
||||
// log.info("세션에 저장된 사용자 정보: {}", loginVO instanceof UserVO);
|
||||
// log.info("세션에 저장된 사용자 정보: {}", loginVO);
|
||||
public String index() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
System.out.println("authentication.getAuthorities(); : "+ authentication.getAuthorities());
|
||||
return "dashboard/index";
|
||||
}
|
||||
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
package com.itn.admin.itn.bizTrip.mapper;
|
||||
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripApprovalVO;
|
||||
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripMemberVO;
|
||||
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface BizTripMapper {
|
||||
void insertBizTrip(BizTripVO trip);
|
||||
|
||||
void insertTripMember(BizTripMemberVO member);
|
||||
|
||||
void insertApprovalLine(BizTripApprovalVO approval);
|
||||
|
||||
List<BizTripVO> selectTripList();
|
||||
|
||||
BizTripVO getBizTripWithDetail(String tripId);
|
||||
|
||||
List<BizTripVO> getMyPendingApprovals(String uniqId);
|
||||
|
||||
RestResponse saveApproval(BizTripApprovalVO bizTripApprovalVO);
|
||||
|
||||
void updateTripStatus(BizTripVO bizTripVO);
|
||||
|
||||
String getNextApproverId(int tripId, String approverId);
|
||||
|
||||
void updateBizTrip(BizTripVO trip);
|
||||
|
||||
void deleteTripMembers(Integer tripId);
|
||||
|
||||
void deleteApprovalLines(Integer tripId);
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
package com.itn.admin.itn.bizTrip.mapper.domain;
|
||||
|
||||
import com.itn.admin.cmn.vo.CmnVO;
|
||||
import lombok.*;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.util.Date;
|
||||
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@SuperBuilder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BizTripApprovalVO extends CmnVO {
|
||||
|
||||
private Integer id;
|
||||
private Integer tripId; // 출장 ID (FK)
|
||||
private String approverId; // 결재자 uniq_id
|
||||
private Integer orderNo; // 결재 순서
|
||||
private String approveStatus; // 결재 상태 (WAIT, APPROVED, REJECTED)
|
||||
private Date approveDt; // 결재 일시
|
||||
private String comment; // 결재 의견
|
||||
|
||||
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
package com.itn.admin.itn.bizTrip.mapper.domain;
|
||||
|
||||
import com.itn.admin.cmn.vo.CmnVO;
|
||||
import lombok.*;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@SuperBuilder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BizTripMemberVO extends CmnVO {
|
||||
|
||||
private Integer id;
|
||||
private Integer tripId; // 출장 ID (FK)
|
||||
private String uniqId; // 유저 고유 ID (FK)
|
||||
private String role; // 역할 (기안자: 0, 동행자: 1 등)
|
||||
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
package com.itn.admin.itn.bizTrip.mapper.domain;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class BizTripRequestDTO {
|
||||
private BizTripVO tripInfo;
|
||||
private List<BizTripMemberVO> tripMembers;
|
||||
private List<BizTripApprovalVO> approvalLines;
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
package com.itn.admin.itn.bizTrip.mapper.domain;
|
||||
|
||||
import com.itn.admin.cmn.vo.CmnVO;
|
||||
import lombok.*;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@SuperBuilder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BizTripVO extends CmnVO {
|
||||
|
||||
private Integer tripId; // 출장 고유 ID
|
||||
private String tripTypeCd; // 출장 구분 (TRIP_TYPE 공통코드)
|
||||
private String locationCd; // 출장지 (TRIP_LOCATION 공통코드)
|
||||
private String locationTxt; // 출장지 (TRIP_LOCATION 공통코드)
|
||||
private String purpose; // 출장 목적
|
||||
private String moveCd; // 이동 수단 (TRIP_MOVE 공통코드)
|
||||
private LocalDate tripDt; // 출장일자
|
||||
private LocalTime startTime; // 출장 시작시간
|
||||
private LocalTime endTime; // 출장 종료시간
|
||||
private String status; // 결재 상태 (ING, DONE 등)
|
||||
private String useYn; // 사용여부
|
||||
|
||||
|
||||
private String latestApproveStatus; // 최신 결재 상태
|
||||
private String currentApproverId; // 현재 결제 대기자
|
||||
|
||||
private List<BizTripMemberVO> memberList;
|
||||
private List<BizTripApprovalVO> approvalList;
|
||||
|
||||
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
package com.itn.admin.itn.bizTrip.service;
|
||||
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripApprovalVO;
|
||||
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripRequestDTO;
|
||||
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripVO;
|
||||
import com.itn.admin.itn.user.mapper.domain.UserVO;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface BizTripService {
|
||||
RestResponse register(BizTripRequestDTO dto);
|
||||
|
||||
List<BizTripVO> selectTripList();
|
||||
|
||||
Map<String, Object> getBizTripWithDetail(String tripId);
|
||||
|
||||
List<BizTripVO> getMyPendingApprovals(String uniqId);
|
||||
|
||||
RestResponse approval(BizTripApprovalVO bizTripApprovalVO, UserVO userVO);
|
||||
|
||||
Map<String, Object> getBizTripWithEdit(String tripId);
|
||||
|
||||
RestResponse update(BizTripRequestDTO dto);
|
||||
}
|
||||
@ -1,197 +0,0 @@
|
||||
package com.itn.admin.itn.bizTrip.service.impl;
|
||||
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
import com.itn.admin.itn.bizTrip.mapper.BizTripMapper;
|
||||
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripApprovalVO;
|
||||
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripMemberVO;
|
||||
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripRequestDTO;
|
||||
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripVO;
|
||||
import com.itn.admin.itn.bizTrip.service.BizTripService;
|
||||
import com.itn.admin.itn.code.mapper.CodeMapper;
|
||||
import com.itn.admin.itn.code.mapper.domain.CodeVO;
|
||||
import com.itn.admin.itn.code.server.CodeService;
|
||||
import com.itn.admin.itn.user.mapper.domain.UserVO;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
public class BizTripServiceImpl implements BizTripService {
|
||||
|
||||
@Autowired
|
||||
private BizTripMapper bizTripMapper;
|
||||
|
||||
@Override
|
||||
public RestResponse register(BizTripRequestDTO dto) {
|
||||
// 1. BizTripVO insert (자동 생성된 tripId 얻기)
|
||||
BizTripVO trip = dto.getTripInfo();
|
||||
bizTripMapper.insertBizTrip(trip); // insert 후 trip.tripId에 PK 자동 세팅됨
|
||||
|
||||
Integer tripId = trip.getTripId();
|
||||
|
||||
// 2. 출장 참여 인원 등록
|
||||
List<BizTripMemberVO> memberList = dto.getTripMembers();
|
||||
if (memberList != null && !memberList.isEmpty()) {
|
||||
for (BizTripMemberVO member : memberList) {
|
||||
member.setTripId(tripId);
|
||||
bizTripMapper.insertTripMember(member);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 결재 라인 등록
|
||||
List<BizTripApprovalVO> approvalList = dto.getApprovalLines();
|
||||
if (approvalList != null && !approvalList.isEmpty()) {
|
||||
for (BizTripApprovalVO approval : approvalList) {
|
||||
approval.setTripId(tripId);
|
||||
bizTripMapper.insertApprovalLine(approval);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return new RestResponse(HttpStatus.OK, "등록되었습니다",tripId);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public RestResponse update(BizTripRequestDTO dto) {
|
||||
BizTripVO trip = dto.getTripInfo();
|
||||
Integer tripId = trip.getTripId();
|
||||
|
||||
// 1. 출장 기본 정보 수정
|
||||
bizTripMapper.updateBizTrip(trip); // tripId 포함되어 있어야 함
|
||||
|
||||
// 2. 기존 출장 인원 삭제 후 재등록
|
||||
bizTripMapper.deleteTripMembers(tripId);
|
||||
List<BizTripMemberVO> memberList = dto.getTripMembers();
|
||||
if (memberList != null && !memberList.isEmpty()) {
|
||||
for (BizTripMemberVO member : memberList) {
|
||||
member.setTripId(tripId);
|
||||
bizTripMapper.insertTripMember(member);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 기존 결재라인 삭제 후 재등록
|
||||
bizTripMapper.deleteApprovalLines(tripId);
|
||||
List<BizTripApprovalVO> approvalList = dto.getApprovalLines();
|
||||
if (approvalList != null && !approvalList.isEmpty()) {
|
||||
for (BizTripApprovalVO approval : approvalList) {
|
||||
approval.setTripId(tripId);
|
||||
bizTripMapper.insertApprovalLine(approval);
|
||||
}
|
||||
}
|
||||
|
||||
return new RestResponse(HttpStatus.OK, "수정되었습니다");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public List<BizTripVO> selectTripList() {
|
||||
return bizTripMapper.selectTripList();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getBizTripWithDetail(String tripId) {
|
||||
Map<String, Object> returnMap = new HashMap<>();
|
||||
BizTripVO bizTrip = bizTripMapper.getBizTripWithDetail(tripId);
|
||||
|
||||
// 상세화면
|
||||
String firstWaitingApproverId = bizTrip.getApprovalList().stream()
|
||||
.filter(a -> "10".equals(a.getApproveStatus()))
|
||||
.map(BizTripApprovalVO::getApproverId)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
|
||||
/*더 필요한 데이터있으면 추가해서 returnMap에 put 해서 넘겨주기*/
|
||||
returnMap.put("bizTrip", bizTrip);
|
||||
|
||||
returnMap.put("firstWaitingApproverId", firstWaitingApproverId);
|
||||
return returnMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getBizTripWithEdit(String tripId) {
|
||||
Map<String, Object> returnMap = new HashMap<>();
|
||||
BizTripVO bizTrip = bizTripMapper.getBizTripWithDetail(tripId);
|
||||
|
||||
// 검토1, 검토2, 결제를 구현하기 위한 로직
|
||||
List<BizTripApprovalVO> finalList = new ArrayList<>();
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
// 복사해서 final 변수로 만듦
|
||||
// 불변이라고 선언을 해야 람다에서 사용가능
|
||||
final int order = i;
|
||||
Optional<BizTripApprovalVO> found = bizTrip.getApprovalList().stream()
|
||||
.filter(vo -> vo.getOrderNo() == order)
|
||||
.findFirst();
|
||||
|
||||
if (found.isPresent()) {
|
||||
finalList.add(found.get());
|
||||
} else {
|
||||
BizTripApprovalVO empty = new BizTripApprovalVO();
|
||||
empty.setOrderNo(i);
|
||||
finalList.add(empty);
|
||||
}
|
||||
}
|
||||
bizTrip.setApprovalList(finalList);
|
||||
// -- 검토1, 검토2, 결제를 구현하기 위한 로직
|
||||
|
||||
/*더 필요한 데이터있으면 추가해서 returnMap에 put 해서 넘겨주기*/
|
||||
returnMap.put("bizTrip", bizTrip);
|
||||
|
||||
return returnMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BizTripVO> getMyPendingApprovals(String uniqId) {
|
||||
return bizTripMapper.getMyPendingApprovals(uniqId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public RestResponse approval(BizTripApprovalVO bizTripApprovalVO, UserVO userVO) {
|
||||
|
||||
|
||||
// '결재 상태(APPR_STS - 대기:10, 승인:30, 거절:40)'
|
||||
String approveStatus = bizTripApprovalVO.getApproveStatus();
|
||||
int tripId = bizTripApprovalVO.getTripId();
|
||||
|
||||
bizTripMapper.saveApproval(bizTripApprovalVO);
|
||||
|
||||
|
||||
// 다음 결제자 있는지 확인하는 로직
|
||||
// 있으면 uniqId 가져옴
|
||||
String nextApproverId = bizTripMapper.getNextApproverId(tripId, bizTripApprovalVO.getApproverId());
|
||||
BizTripVO bizTripVO = BizTripVO.builder()
|
||||
.tripId(tripId)
|
||||
.status(approveStatus)
|
||||
.lastUpdusrId(userVO.getUniqId())
|
||||
.build();
|
||||
|
||||
if (StringUtils.isNotEmpty(nextApproverId) && "30".equals(approveStatus)) {
|
||||
bizTripVO.setStatus("20");
|
||||
// 다음 결재자 있음 & 현재 승인 → '진행중'
|
||||
bizTripMapper.updateTripStatus(bizTripVO);
|
||||
|
||||
//TODO [slack] nextApproverId를 참고해서 알려줘야함
|
||||
|
||||
|
||||
} else {
|
||||
|
||||
// 마지막 결재자 or 반려 → 본인 상태 그대로 반영
|
||||
bizTripMapper.updateTripStatus(bizTripVO);
|
||||
|
||||
//TODO [slack] 결제 완료되었다는걸 알려줘야함
|
||||
|
||||
}
|
||||
|
||||
|
||||
return new RestResponse(HttpStatus.OK,"결제가 완료되었습니다.");
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,78 +0,0 @@
|
||||
package com.itn.admin.itn.bizTrip.web;
|
||||
|
||||
import com.itn.admin.cmn.config.CustomUserDetails;
|
||||
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripMemberVO;
|
||||
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripVO;
|
||||
import com.itn.admin.itn.bizTrip.service.BizTripService;
|
||||
import com.itn.admin.itn.user.mapper.domain.UserVO;
|
||||
import com.itn.admin.itn.user.service.UserService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
public class BizTripController {
|
||||
|
||||
@Autowired
|
||||
private BizTripService bizTripService;
|
||||
|
||||
|
||||
@GetMapping("/itn/bizTrip/reg")
|
||||
public String list(@AuthenticationPrincipal CustomUserDetails loginUser
|
||||
,Model model
|
||||
) {
|
||||
log.info(" + loginUser :: [{}]", loginUser.getUser());
|
||||
model.addAttribute("loginUser", loginUser.getUser());
|
||||
|
||||
return "itn/bizTrip/reg";
|
||||
}
|
||||
|
||||
@GetMapping("/itn/bizTrip/edit/{tripId}")
|
||||
public String edit(@PathVariable String tripId
|
||||
, @AuthenticationPrincipal CustomUserDetails loginUser
|
||||
,Model model
|
||||
) {
|
||||
|
||||
Map<String, Object> returnMap = bizTripService.getBizTripWithEdit(tripId);
|
||||
model.addAttribute("trip", returnMap.get("bizTrip"));
|
||||
model.addAttribute("loginUser", loginUser.getUser());
|
||||
|
||||
return "itn/bizTrip/edit";
|
||||
}
|
||||
@GetMapping("/itn/bizTrip/list")
|
||||
public String bizTripList(Model model
|
||||
,@AuthenticationPrincipal CustomUserDetails loginUser) {
|
||||
List<BizTripVO> list = bizTripService.selectTripList();
|
||||
model.addAttribute("list", list);
|
||||
for (BizTripVO vo : list) {
|
||||
log.info(" + vo :: [{}]", vo.getCurrentApproverId());
|
||||
}
|
||||
model.addAttribute("loginUser", loginUser.getUser());
|
||||
log.info(" + loginUser :: [{}]", loginUser.getUser());
|
||||
return "itn/bizTrip/list"; // Thymeleaf HTML 파일 경로
|
||||
}
|
||||
|
||||
@GetMapping("/itn/bizTrip/detail/{tripId}")
|
||||
public String tripDetail(@PathVariable String tripId
|
||||
,@AuthenticationPrincipal CustomUserDetails loginUser
|
||||
, Model model
|
||||
) {
|
||||
Map<String, Object> returnMap = bizTripService.getBizTripWithDetail(tripId);
|
||||
model.addAttribute("trip", returnMap.get("bizTrip"));
|
||||
model.addAttribute("loginUser", loginUser.getUser());
|
||||
model.addAttribute("firstWaitingApproverId", returnMap.get("firstWaitingApproverId"));
|
||||
return "itn/bizTrip/tripDetail";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
package com.itn.admin.itn.bizTrip.web;
|
||||
|
||||
import com.itn.admin.cmn.config.CustomUserDetails;
|
||||
import com.itn.admin.cmn.msg.RestResponse;
|
||||
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripApprovalVO;
|
||||
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripRequestDTO;
|
||||
import com.itn.admin.itn.bizTrip.service.BizTripService;
|
||||
import com.itn.admin.itn.code.mapper.domain.CodeDetailVO;
|
||||
import com.itn.admin.itn.user.mapper.domain.UserVO;
|
||||
import com.itn.admin.itn.user.service.UserService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.*;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
public class BizTripRestController {
|
||||
|
||||
@Autowired
|
||||
private BizTripService bizTripService;
|
||||
|
||||
@PostMapping("/api/bizTrip/register")
|
||||
public ResponseEntity<RestResponse> registerBizTrip(@RequestBody BizTripRequestDTO dto
|
||||
, @AuthenticationPrincipal CustomUserDetails loginUser) {
|
||||
dto.getTripInfo().setFrstRegisterId(loginUser.getUser().getUniqId());
|
||||
log.info("dto: [{}]", dto);
|
||||
return ResponseEntity.ok().body(bizTripService.register(dto));
|
||||
}
|
||||
|
||||
@PostMapping("/api/bizTrip/approval")
|
||||
public ResponseEntity<RestResponse> approval(@RequestBody BizTripApprovalVO bizTripApprovalVO
|
||||
, @AuthenticationPrincipal CustomUserDetails loginUser) {
|
||||
log.info("bizTripApprovalVO: [{}]", bizTripApprovalVO);
|
||||
// bizTripService.register(dto);
|
||||
return ResponseEntity.ok().body(bizTripService.approval(bizTripApprovalVO, loginUser.getUser()));
|
||||
}
|
||||
|
||||
@PutMapping("/api/bizTrip/update")
|
||||
public ResponseEntity<RestResponse> updateBizTrip(@RequestBody BizTripRequestDTO dto,
|
||||
@AuthenticationPrincipal CustomUserDetails loginUser) {
|
||||
dto.getTripInfo().setLastUpdusrId(loginUser.getUser().getUniqId());
|
||||
return ResponseEntity.ok().body(bizTripService.update(dto));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@ -1,96 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
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; // 마지막 발행 시간
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,114 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
@ -1,84 +0,0 @@
|
||||
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 "알 수 없는 상태";
|
||||
}
|
||||
}
|
||||
@ -1,71 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
package com.itn.admin.itn.blog.mapper.domain;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
public class PostingRequestDTO {
|
||||
private String blogId;
|
||||
private List<String> sourceIds;
|
||||
private String action; // "save" or "execute"
|
||||
}
|
||||
@ -1,89 +0,0 @@
|
||||
package com.itn.admin.itn.blog.mapper.domain;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
public class ScheduleCreateRequestDTO {
|
||||
|
||||
@NotNull(message = "블로그 ID는 필수입니다")
|
||||
private Long blogId; // 블로그 계정 ID
|
||||
|
||||
@NotNull(message = "소스 URL ID는 필수입니다")
|
||||
private Long urlId; // 소스 URL ID
|
||||
|
||||
@NotBlank(message = "제목은 필수입니다")
|
||||
@Size(max = 255, message = "제목은 255자를 초과할 수 없습니다")
|
||||
private String title; // 예약 제목
|
||||
|
||||
@Size(max = 65535, message = "내용이 너무 깁니다")
|
||||
private String content; // 예약 내용
|
||||
|
||||
// 스케줄링 설정
|
||||
@NotBlank(message = "스케줄 유형은 필수입니다")
|
||||
@Pattern(regexp = "^(ONE_TIME|RECURRING)$", message = "스케줄 유형은 ONE_TIME 또는 RECURRING이어야 합니다")
|
||||
private String scheduleType; // 스케줄 유형
|
||||
|
||||
@NotNull(message = "예약 실행 시간은 필수입니다")
|
||||
@Future(message = "예약 실행 시간은 현재 시간보다 이후여야 합니다")
|
||||
private LocalDateTime scheduledAt; // 예약 실행 시간
|
||||
|
||||
@Pattern(regexp = "^(DAILY|WEEKLY|MONTHLY)$", message = "반복 간격은 DAILY, WEEKLY, MONTHLY 중 하나여야 합니다")
|
||||
private String repeatInterval; // 반복 간격 (RECURRING일 때만)
|
||||
|
||||
@Min(value = 1, message = "반복 주기 값은 1 이상이어야 합니다")
|
||||
@Max(value = 365, message = "반복 주기 값은 365 이하여야 합니다")
|
||||
private Integer repeatValue; // 반복 주기 값
|
||||
|
||||
private LocalDateTime endAt; // 반복 종료 시간 (RECURRING일 때만)
|
||||
|
||||
// 상태 및 우선순위
|
||||
@Pattern(regexp = "^(ACTIVE|INACTIVE)$", message = "상태는 ACTIVE 또는 INACTIVE여야 합니다")
|
||||
private String status = "ACTIVE"; // 스케줄 상태 (기본값: ACTIVE)
|
||||
|
||||
@Pattern(regexp = "^(HIGH|NORMAL|LOW)$", message = "우선순위는 HIGH, NORMAL, LOW 중 하나여야 합니다")
|
||||
private String priority = "NORMAL"; // 실행 우선순위 (기본값: NORMAL)
|
||||
|
||||
// 재시도 설정
|
||||
@Min(value = 0, message = "최대 재시도 횟수는 0 이상이어야 합니다")
|
||||
@Max(value = 10, message = "최대 재시도 횟수는 10 이하여야 합니다")
|
||||
private Integer maxRetries = 3; // 최대 재시도 횟수 (기본값: 3)
|
||||
|
||||
@Min(value = 1, message = "재시도 간격은 1분 이상이어야 합니다")
|
||||
@Max(value = 1440, message = "재시도 간격은 1440분(24시간) 이하여야 합니다")
|
||||
private Integer retryInterval = 5; // 재시도 간격 (분, 기본값: 5)
|
||||
|
||||
// 알림 설정
|
||||
@Email(message = "올바른 이메일 형식이 아닙니다")
|
||||
@Size(max = 255, message = "이메일은 255자를 초과할 수 없습니다")
|
||||
private String notificationEmail; // 알림 이메일
|
||||
|
||||
@Size(max = 100, message = "슬랙 채널명은 100자를 초과할 수 없습니다")
|
||||
@Pattern(regexp = "^[#@]?[a-z0-9-_]+$", message = "올바른 슬랙 채널명 형식이 아닙니다", flags = Pattern.Flag.CASE_INSENSITIVE)
|
||||
private String slackChannel; // 슬랙 채널명
|
||||
|
||||
private Boolean enableNotification = false; // 알림 활성화 여부 (기본값: false)
|
||||
|
||||
// 검증 메서드들
|
||||
@AssertTrue(message = "RECURRING 스케줄의 경우 반복 간격이 필요합니다")
|
||||
private boolean isRepeatIntervalValidForRecurring() {
|
||||
if ("RECURRING".equals(scheduleType)) {
|
||||
return repeatInterval != null && !repeatInterval.trim().isEmpty();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@AssertTrue(message = "반복 종료 시간은 예약 실행 시간보다 이후여야 합니다")
|
||||
private boolean isEndAtAfterScheduledAt() {
|
||||
if (endAt != null && scheduledAt != null) {
|
||||
return endAt.isAfter(scheduledAt);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -1,107 +0,0 @@
|
||||
package com.itn.admin.itn.blog.mapper.domain;
|
||||
|
||||
import com.itn.admin.cmn.vo.CmnVO;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
public class ScheduleSearchDTO extends CmnVO {
|
||||
|
||||
// 기본 검색 조건
|
||||
private Long scheduleId; // 특정 스케줄 ID
|
||||
private Long blogId; // 블로그 계정 ID
|
||||
private Long urlId; // 소스 URL ID
|
||||
private String title; // 제목 (부분 일치)
|
||||
|
||||
// 스케줄 유형 및 상태
|
||||
private String scheduleType; // ONE_TIME 또는 RECURRING
|
||||
private String status; // ACTIVE, INACTIVE, COMPLETED, FAILED
|
||||
private String priority; // HIGH, NORMAL, LOW
|
||||
private List<String> statusList; // 여러 상태 조건 (IN 절용)
|
||||
|
||||
// 시간 범위 검색
|
||||
private LocalDateTime scheduledAtFrom; // 예약 시간 시작
|
||||
private LocalDateTime scheduledAtTo; // 예약 시간 종료
|
||||
private LocalDateTime createdAtFrom; // 생성일 시작
|
||||
private LocalDateTime createdAtTo; // 생성일 종료
|
||||
|
||||
// 실행 관련 검색
|
||||
private Boolean hasExecutions; // 실행 이력 존재 여부
|
||||
private String lastExecutionStatus; // 마지막 실행 상태
|
||||
private LocalDateTime lastExecutedFrom; // 마지막 실행일 시작
|
||||
private LocalDateTime lastExecutedTo; // 마지막 실행일 종료
|
||||
|
||||
// 알림 설정 검색
|
||||
private Boolean enableNotification; // 알림 활성화 여부
|
||||
private String notificationEmail; // 알림 이메일 (부분 일치)
|
||||
private String slackChannel; // 슬랙 채널 (부분 일치)
|
||||
|
||||
// 등록자/수정자 검색
|
||||
private String frstRegisterId; // 등록자 ID
|
||||
private String lastUpdusrId; // 수정자 ID
|
||||
|
||||
// 정렬 조건
|
||||
private String sortBy = "scheduled_at"; // 정렬 기준 (scheduled_at, created_at, priority 등)
|
||||
private String sortOrder = "DESC"; // 정렬 순서 (ASC, DESC)
|
||||
|
||||
// 통계 조건
|
||||
private Boolean includeStatistics = false; // 통계 정보 포함 여부
|
||||
private Boolean includeExecutionCount = false; // 실행 횟수 포함 여부
|
||||
|
||||
// 편의 메서드들
|
||||
public boolean hasTimeRange() {
|
||||
return scheduledAtFrom != null || scheduledAtTo != null;
|
||||
}
|
||||
|
||||
public boolean hasCreatedAtRange() {
|
||||
return createdAtFrom != null || createdAtTo != null;
|
||||
}
|
||||
|
||||
public boolean hasLastExecutedRange() {
|
||||
return lastExecutedFrom != null || lastExecutedTo != null;
|
||||
}
|
||||
|
||||
public boolean hasMultipleStatus() {
|
||||
return statusList != null && !statusList.isEmpty();
|
||||
}
|
||||
|
||||
public boolean hasNotificationFilter() {
|
||||
return enableNotification != null ||
|
||||
(notificationEmail != null && !notificationEmail.trim().isEmpty()) ||
|
||||
(slackChannel != null && !slackChannel.trim().isEmpty());
|
||||
}
|
||||
|
||||
// 검색 조건 검증
|
||||
public boolean isValidDateRange() {
|
||||
if (scheduledAtFrom != null && scheduledAtTo != null) {
|
||||
return !scheduledAtFrom.isAfter(scheduledAtTo);
|
||||
}
|
||||
if (createdAtFrom != null && createdAtTo != null) {
|
||||
return !createdAtFrom.isAfter(createdAtTo);
|
||||
}
|
||||
if (lastExecutedFrom != null && lastExecutedTo != null) {
|
||||
return !lastExecutedFrom.isAfter(lastExecutedTo);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 정렬 조건 검증
|
||||
public boolean isValidSortBy() {
|
||||
if (sortBy == null) return false;
|
||||
List<String> validSortFields = List.of(
|
||||
"schedule_id", "blog_id", "title", "scheduled_at",
|
||||
"status", "priority", "frst_regist_dt", "last_updt_dt"
|
||||
);
|
||||
return validSortFields.contains(sortBy.toLowerCase());
|
||||
}
|
||||
|
||||
public boolean isValidSortOrder() {
|
||||
return "ASC".equalsIgnoreCase(sortOrder) || "DESC".equalsIgnoreCase(sortOrder);
|
||||
}
|
||||
}
|
||||
@ -1,147 +0,0 @@
|
||||
package com.itn.admin.itn.blog.mapper.domain;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
public class ScheduleStatisticsDTO {
|
||||
|
||||
// 전체 통계
|
||||
private Integer totalSchedules; // 전체 스케줄 수
|
||||
private Integer activeSchedules; // 활성 스케줄 수
|
||||
private Integer inactiveSchedules; // 비활성 스케줄 수
|
||||
private Integer completedSchedules; // 완료된 스케줄 수
|
||||
private Integer failedSchedules; // 실패한 스케줄 수
|
||||
|
||||
// 스케줄 유형별 통계
|
||||
private Integer oneTimeSchedules; // 일회성 스케줄 수
|
||||
private Integer recurringSchedules; // 반복 스케줄 수
|
||||
|
||||
// 우선순위별 통계
|
||||
private Integer highPrioritySchedules; // 높은 우선순위 스케줄 수
|
||||
private Integer normalPrioritySchedules; // 일반 우선순위 스케줄 수
|
||||
private Integer lowPrioritySchedules; // 낮은 우선순위 스케줄 수
|
||||
|
||||
// 실행 통계
|
||||
private Integer totalExecutions; // 전체 실행 횟수
|
||||
private Integer successfulExecutions; // 성공한 실행 횟수
|
||||
private Integer failedExecutions; // 실패한 실행 횟수
|
||||
private Integer pendingExecutions; // 대기 중인 실행 횟수
|
||||
private Integer runningExecutions; // 실행 중인 작업 수
|
||||
|
||||
// 성공률 통계
|
||||
private Double successRate; // 전체 성공률 (%)
|
||||
private Double todaySuccessRate; // 오늘 성공률 (%)
|
||||
private Double weeklySuccessRate; // 주간 성공률 (%)
|
||||
private Double monthlySuccessRate; // 월간 성공률 (%)
|
||||
|
||||
// 시간 관련 통계
|
||||
private LocalDateTime lastExecutionTime; // 마지막 실행 시간
|
||||
private LocalDateTime nextScheduledTime; // 다음 예정 실행 시간
|
||||
private Integer schedulesTodayCount; // 오늘 예정된 스케줄 수
|
||||
private Integer schedulesThisWeekCount; // 이번 주 예정된 스케줄 수
|
||||
|
||||
// 성능 통계
|
||||
private Double averageExecutionTimeMs; // 평균 실행 시간 (밀리초)
|
||||
private Long maxExecutionTimeMs; // 최대 실행 시간 (밀리초)
|
||||
private Long minExecutionTimeMs; // 최소 실행 시간 (밀리초)
|
||||
|
||||
// 재시도 통계
|
||||
private Integer totalRetries; // 전체 재시도 횟수
|
||||
private Double averageRetriesPerFailure; // 실패당 평균 재시도 횟수
|
||||
|
||||
// 알림 통계
|
||||
private Integer enabledNotificationCount; // 알림 활성화된 스케줄 수
|
||||
private Integer emailNotificationCount; // 이메일 알림 설정 수
|
||||
private Integer slackNotificationCount; // 슬랙 알림 설정 수
|
||||
|
||||
// 블로그별 통계 (상위 N개)
|
||||
private List<BlogScheduleStatsDTO> topBlogStats; // 블로그별 통계
|
||||
|
||||
// 시간대별 실행 통계 (24시간)
|
||||
private List<HourlyExecutionStatsDTO> hourlyStats; // 시간대별 실행 통계
|
||||
|
||||
// 일별 실행 통계 (최근 7일)
|
||||
private List<DailyExecutionStatsDTO> dailyStats; // 일별 실행 통계
|
||||
|
||||
// 내부 클래스들
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
public static class BlogScheduleStatsDTO {
|
||||
private Long blogId;
|
||||
private String blogName;
|
||||
private Integer scheduleCount;
|
||||
private Integer successCount;
|
||||
private Integer failureCount;
|
||||
private Double successRate;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
public static class HourlyExecutionStatsDTO {
|
||||
private Integer hour; // 시간 (0-23)
|
||||
private Integer executionCount; // 실행 횟수
|
||||
private Integer successCount; // 성공 횟수
|
||||
private Integer failureCount; // 실패 횟수
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
public static class DailyExecutionStatsDTO {
|
||||
private String date; // 날짜 (YYYY-MM-DD)
|
||||
private Integer executionCount; // 실행 횟수
|
||||
private Integer successCount; // 성공 횟수
|
||||
private Integer failureCount; // 실패 횟수
|
||||
private Double successRate; // 성공률
|
||||
}
|
||||
|
||||
// 편의 메서드들
|
||||
public double calculateSuccessRate() {
|
||||
if (totalExecutions == null || totalExecutions == 0) {
|
||||
return 0.0;
|
||||
}
|
||||
return (successfulExecutions != null ? successfulExecutions : 0) * 100.0 / totalExecutions;
|
||||
}
|
||||
|
||||
public double calculateFailureRate() {
|
||||
if (totalExecutions == null || totalExecutions == 0) {
|
||||
return 0.0;
|
||||
}
|
||||
return (failedExecutions != null ? failedExecutions : 0) * 100.0 / totalExecutions;
|
||||
}
|
||||
|
||||
public boolean hasActiveSchedules() {
|
||||
return activeSchedules != null && activeSchedules > 0;
|
||||
}
|
||||
|
||||
public boolean hasFailedSchedules() {
|
||||
return failedSchedules != null && failedSchedules > 0;
|
||||
}
|
||||
|
||||
public boolean hasRunningExecutions() {
|
||||
return runningExecutions != null && runningExecutions > 0;
|
||||
}
|
||||
|
||||
public String getPerformanceSummary() {
|
||||
if (averageExecutionTimeMs == null) {
|
||||
return "성능 데이터 없음";
|
||||
}
|
||||
return String.format("평균: %.1fms, 최대: %dms, 최소: %dms",
|
||||
averageExecutionTimeMs, maxExecutionTimeMs, minExecutionTimeMs);
|
||||
}
|
||||
}
|
||||
@ -1,96 +0,0 @@
|
||||
package com.itn.admin.itn.blog.mapper.domain;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
public class ScheduleUpdateRequestDTO {
|
||||
|
||||
@NotNull(message = "스케줄 ID는 필수입니다")
|
||||
private Long scheduleId; // 예약 ID (수정 대상)
|
||||
|
||||
@Size(max = 255, message = "제목은 255자를 초과할 수 없습니다")
|
||||
private String title; // 예약 제목
|
||||
|
||||
@Size(max = 65535, message = "내용이 너무 깁니다")
|
||||
private String content; // 예약 내용
|
||||
|
||||
// 스케줄링 설정 (수정 가능한 항목들만)
|
||||
private LocalDateTime scheduledAt; // 예약 실행 시간
|
||||
|
||||
@Pattern(regexp = "^(DAILY|WEEKLY|MONTHLY)$", message = "반복 간격은 DAILY, WEEKLY, MONTHLY 중 하나여야 합니다")
|
||||
private String repeatInterval; // 반복 간격
|
||||
|
||||
@Min(value = 1, message = "반복 주기 값은 1 이상이어야 합니다")
|
||||
@Max(value = 365, message = "반복 주기 값은 365 이하여야 합니다")
|
||||
private Integer repeatValue; // 반복 주기 값
|
||||
|
||||
private LocalDateTime endAt; // 반복 종료 시간
|
||||
|
||||
// 상태 및 우선순위
|
||||
@Pattern(regexp = "^(ACTIVE|INACTIVE|COMPLETED|FAILED)$", message = "상태는 ACTIVE, INACTIVE, COMPLETED, FAILED 중 하나여야 합니다")
|
||||
private String status; // 스케줄 상태
|
||||
|
||||
@Pattern(regexp = "^(HIGH|NORMAL|LOW)$", message = "우선순위는 HIGH, NORMAL, LOW 중 하나여야 합니다")
|
||||
private String priority; // 실행 우선순위
|
||||
|
||||
// 재시도 설정
|
||||
@Min(value = 0, message = "최대 재시도 횟수는 0 이상이어야 합니다")
|
||||
@Max(value = 10, message = "최대 재시도 횟수는 10 이하여야 합니다")
|
||||
private Integer maxRetries; // 최대 재시도 횟수
|
||||
|
||||
@Min(value = 1, message = "재시도 간격은 1분 이상이어야 합니다")
|
||||
@Max(value = 1440, message = "재시도 간격은 1440분(24시간) 이하여야 합니다")
|
||||
private Integer retryInterval; // 재시도 간격 (분)
|
||||
|
||||
// 알림 설정
|
||||
@Email(message = "올바른 이메일 형식이 아닙니다")
|
||||
@Size(max = 255, message = "이메일은 255자를 초과할 수 없습니다")
|
||||
private String notificationEmail; // 알림 이메일
|
||||
|
||||
@Size(max = 100, message = "슬랙 채널명은 100자를 초과할 수 없습니다")
|
||||
@Pattern(regexp = "^[#@]?[a-z0-9-_]+$", message = "올바른 슬랙 채널명 형식이 아닙니다", flags = Pattern.Flag.CASE_INSENSITIVE)
|
||||
private String slackChannel; // 슬랙 채널명
|
||||
|
||||
private Boolean enableNotification; // 알림 활성화 여부
|
||||
|
||||
// 검증 메서드들
|
||||
@AssertTrue(message = "반복 종료 시간은 예약 실행 시간보다 이후여야 합니다")
|
||||
private boolean isEndAtAfterScheduledAt() {
|
||||
if (endAt != null && scheduledAt != null) {
|
||||
return endAt.isAfter(scheduledAt);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 수정 가능한 필드인지 확인하는 편의 메서드들
|
||||
public boolean hasTitle() {
|
||||
return title != null && !title.trim().isEmpty();
|
||||
}
|
||||
|
||||
public boolean hasContent() {
|
||||
return content != null;
|
||||
}
|
||||
|
||||
public boolean hasScheduledAt() {
|
||||
return scheduledAt != null;
|
||||
}
|
||||
|
||||
public boolean hasStatus() {
|
||||
return status != null && !status.trim().isEmpty();
|
||||
}
|
||||
|
||||
public boolean hasPriority() {
|
||||
return priority != null && !priority.trim().isEmpty();
|
||||
}
|
||||
|
||||
public boolean hasNotificationSettings() {
|
||||
return notificationEmail != null || slackChannel != null || enableNotification != null;
|
||||
}
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
package com.itn.admin.itn.blog.mapper.domain;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
public class TistoryPublishRequestDTO {
|
||||
private String title; // 포스트 제목
|
||||
private String htmlContent; // HTML 내용
|
||||
private String sourceUrl; // 원본 소스 URL (선택사항)
|
||||
private Long blogId; // 블로그 ID (blog_accounts 테이블의 blog_id)
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
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);
|
||||
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
package com.itn.admin.itn.blog.service;
|
||||
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogPostHistoryVO;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 티스토리 자동 배포 서비스 인터페이스
|
||||
*/
|
||||
public interface TistoryPublishService {
|
||||
|
||||
/**
|
||||
* 티스토리 블로그에 포스트를 자동 발행합니다.
|
||||
*
|
||||
* @param title HTML 제목
|
||||
* @param htmlContent HTML 내용
|
||||
* @param blogId 블로그 ID (blog_accounts 테이블의 blog_id)
|
||||
* @return 발행 결과 (BlogPostHistoryVO)
|
||||
*/
|
||||
BlogPostHistoryVO publishToTistory(String title, String htmlContent, Long blogId);
|
||||
|
||||
/**
|
||||
* 티스토리 블로그에 포스트를 자동 발행합니다. (소스 URL 포함)
|
||||
*
|
||||
* @param title HTML 제목
|
||||
* @param htmlContent HTML 내용
|
||||
* @param sourceUrl 원본 소스 URL
|
||||
* @param blogId 블로그 ID
|
||||
* @return 발행 결과 (BlogPostHistoryVO)
|
||||
*/
|
||||
BlogPostHistoryVO publishToTistory(String title, String htmlContent, String sourceUrl, Long blogId);
|
||||
|
||||
/**
|
||||
* HTML 생성부터 티스토리 발행까지 통합 워크플로우로 처리합니다.
|
||||
*
|
||||
* @param blogId 블로그 ID
|
||||
* @param urlId 소스 URL ID
|
||||
* @param sourceUrl 원본 소스 URL
|
||||
* @return 발행 결과 (BlogPostHistoryVO)
|
||||
*/
|
||||
BlogPostHistoryVO publishWithHtmlGeneration(Long blogId, Long urlId, String sourceUrl);
|
||||
|
||||
/**
|
||||
* Python 블로그 생성 서비스 상태를 확인합니다.
|
||||
*
|
||||
* @return 서비스 상태 정보 (가용성, 응답시간, 메시지 등)
|
||||
*/
|
||||
Map<String, Object> checkBlogGenerationServiceStatus();
|
||||
}
|
||||
@ -1,78 +0,0 @@
|
||||
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, "삭제에 실패했습니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,118 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,220 +0,0 @@
|
||||
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";
|
||||
}
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
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, "삭제에 실패했습니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,889 +0,0 @@
|
||||
package com.itn.admin.itn.blog.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.itn.admin.cmn.util.tistory.TistoryCookieUtil;
|
||||
import com.itn.admin.itn.blog.mapper.BlogPostingMapper;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogCookieMappingVO;
|
||||
import com.itn.admin.itn.blog.mapper.domain.BlogPostHistoryVO;
|
||||
import com.itn.admin.itn.blog.service.BlogCookieService;
|
||||
import com.itn.admin.itn.blog.service.TistoryPublishService;
|
||||
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.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class TistoryPublishServiceImpl implements TistoryPublishService {
|
||||
|
||||
private final TistoryCookieUtil cookieUtil;
|
||||
private final BlogPostingMapper blogPostingMapper;
|
||||
private final BlogCookieService blogCookieService;
|
||||
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;
|
||||
|
||||
// 티스토리 관리 페이지 URL
|
||||
private static final String TISTORY_ADMIN_URL = "https://munjaon.tistory.com/admin/entry/post";
|
||||
private static final String TISTORY_POST_SAVE_URL = "https://munjaon.tistory.com/admin/entry/post";
|
||||
|
||||
@Override
|
||||
public BlogPostHistoryVO publishToTistory(String title, String htmlContent, Long blogId) {
|
||||
return publishToTistory(title, htmlContent, null, blogId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlogPostHistoryVO publishToTistory(String title, String htmlContent, String sourceUrl, Long blogId) {
|
||||
log.info("티스토리 포스트 발행 시작: title={}, blogId={}, sourceUrl={}", title, blogId, sourceUrl);
|
||||
|
||||
BlogPostHistoryVO historyVO = new BlogPostHistoryVO();
|
||||
historyVO.setBlogId(blogId);
|
||||
historyVO.setPostTitle(title);
|
||||
historyVO.setPostContent(htmlContent);
|
||||
historyVO.setPublishedAt(LocalDateTime.now());
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
// 1. 티스토리 관리 페이지에서 필요한 토큰들을 가져옵니다 (DB 기반 쿠키 사용)
|
||||
String[] tokens = getTistoryTokens(blogId);
|
||||
if (tokens == null || tokens.length < 2) {
|
||||
log.warn("블로그별 쿠키로 토큰 획득 실패, 기본 쿠키로 재시도: blogId={}", blogId);
|
||||
tokens = getTistoryTokens(null); // 기본 쿠키로 폴백
|
||||
if (tokens == null || tokens.length < 2) {
|
||||
throw new RuntimeException("티스토리 토큰 획득 실패");
|
||||
}
|
||||
}
|
||||
|
||||
String xsrfToken = tokens[0];
|
||||
String blogIdFromPage = tokens[1];
|
||||
|
||||
// 2. 포스트 데이터를 티스토리에 전송합니다 (DB 기반 쿠키 사용)
|
||||
String publishedUrl = publishPost(title, htmlContent, xsrfToken, blogIdFromPage, blogId);
|
||||
|
||||
if (StringUtils.hasText(publishedUrl)) {
|
||||
// 성공
|
||||
historyVO.setStatus(BlogPostHistoryVO.STATUS_SUCCESS);
|
||||
historyVO.setPublishedUrl(publishedUrl);
|
||||
|
||||
log.info("티스토리 포스트 발행 성공: title={}, publishedUrl={}", title, publishedUrl);
|
||||
} else {
|
||||
throw new RuntimeException("발행 URL을 가져올 수 없습니다.");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("티스토리 포스트 발행 실패: title={}, error={}", title, e.getMessage(), e);
|
||||
|
||||
// 실패
|
||||
historyVO.setStatus(BlogPostHistoryVO.STATUS_FAILED);
|
||||
historyVO.setErrorMessage(e.getMessage());
|
||||
}
|
||||
|
||||
// 3. 히스토리 저장
|
||||
blogPostingMapper.insertBlogPostHistory(historyVO);
|
||||
|
||||
long endTime = System.currentTimeMillis();
|
||||
log.info("티스토리 포스트 발행 완료: title={}, status={}, 소요시간={}ms",
|
||||
title, historyVO.getStatus(), (endTime - startTime));
|
||||
|
||||
return historyVO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Python 블로그 생성 서비스 상태를 확인합니다.
|
||||
*/
|
||||
public Map<String, Object> checkBlogGenerationServiceStatus() {
|
||||
Map<String, Object> status = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 간단한 헬스 체크 요청
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
HttpEntity<String> requestEntity = new HttpEntity<>("{\"test\": true}", headers);
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
blogGenerateUrl,
|
||||
HttpMethod.POST,
|
||||
requestEntity,
|
||||
String.class
|
||||
);
|
||||
long responseTime = System.currentTimeMillis() - startTime;
|
||||
|
||||
status.put("available", true);
|
||||
status.put("responseTime", responseTime);
|
||||
status.put("statusCode", response.getStatusCode().value());
|
||||
status.put("message", "서비스가 정상적으로 응답합니다.");
|
||||
|
||||
} catch (Exception e) {
|
||||
status.put("available", false);
|
||||
status.put("responseTime", -1);
|
||||
status.put("error", e.getMessage());
|
||||
|
||||
if (e.getMessage() != null && e.getMessage().contains("Connection")) {
|
||||
status.put("message", "블로그 생성 서비스에 연결할 수 없습니다. 네트워크 또는 서비스 상태를 확인해주세요.");
|
||||
} else {
|
||||
status.put("message", "블로그 생성 서비스에서 오류가 발생했습니다: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
status.put("serviceUrl", blogGenerateUrl);
|
||||
status.put("checkedAt", LocalDateTime.now());
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 티스토리 관리 페이지에서 필요한 토큰들을 추출합니다.
|
||||
*
|
||||
* @return [xsrfToken, blogId]
|
||||
*/
|
||||
private String[] getTistoryTokens() {
|
||||
return getTistoryTokens(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 블로그 계정별 쿠키를 사용하여 티스토리 관리 페이지에서 필요한 토큰들을 추출합니다.
|
||||
*
|
||||
* @param blogAccountId 블로그 계정 ID (null이면 기본 쿠키 사용)
|
||||
* @return [xsrfToken, blogId]
|
||||
*/
|
||||
private String[] getTistoryTokens(Long blogAccountId) {
|
||||
try {
|
||||
// 최대 2번 시도 (첫 번째 실패시 쿠키 갱신 후 재시도)
|
||||
for (int attempt = 1; attempt <= 2; attempt++) {
|
||||
log.info("티스토리 토큰 획득 시도 {}/2", attempt);
|
||||
|
||||
HttpHeaders headers;
|
||||
if (blogAccountId != null) {
|
||||
headers = createHeadersForBlog(blogAccountId);
|
||||
log.info("블로그별 쿠키로 티스토리 토큰 요청: blogAccountId={}", blogAccountId);
|
||||
} else {
|
||||
headers = createHeaders();
|
||||
log.info("기본 쿠키로 티스토리 토큰 요청");
|
||||
}
|
||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||
|
||||
log.info("티스토리 관리 페이지 접속: {}", TISTORY_ADMIN_URL);
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
TISTORY_ADMIN_URL,
|
||||
HttpMethod.GET,
|
||||
entity,
|
||||
String.class
|
||||
);
|
||||
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
log.error("티스토리 관리 페이지 접속 실패: {}", response.getStatusCode());
|
||||
if (attempt == 2) return null;
|
||||
continue;
|
||||
}
|
||||
|
||||
String pageContent = response.getBody();
|
||||
if (pageContent == null) {
|
||||
log.error("티스토리 관리 페이지 내용이 비어있습니다.");
|
||||
if (attempt == 2) return null;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 로그인 페이지로 리다이렉션 되었는지 먼저 확인
|
||||
if (isLoginPage(pageContent)) {
|
||||
log.warn("티스토리 로그인 페이지로 리다이렉션됨. 쿠키가 만료된 것으로 보입니다.");
|
||||
|
||||
if (attempt == 1) {
|
||||
log.info("쿠키 갱신을 시도합니다...");
|
||||
if (blogAccountId != null) {
|
||||
blogCookieService.validateAndUpdateCookie(blogAccountId);
|
||||
}
|
||||
continue; // 재시도
|
||||
} else {
|
||||
log.error("쿠키 갱신 후에도 로그인 페이지로 리다이렉션됩니다. 수동 로그인이 필요할 수 있습니다.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// XSRF 토큰 추출
|
||||
String xsrfToken = extractXsrfToken(pageContent);
|
||||
if (xsrfToken == null) {
|
||||
log.error("XSRF 토큰을 찾을 수 없습니다. HTML 내용 디버깅을 시작합니다.");
|
||||
debugTokenExtraction(pageContent);
|
||||
if (attempt == 2) return null;
|
||||
continue; // 재시도
|
||||
}
|
||||
|
||||
// 블로그 ID 추출
|
||||
String blogIdFromPage = extractBlogId(pageContent);
|
||||
if (blogIdFromPage == null) {
|
||||
log.error("블로그 ID를 찾을 수 없습니다.");
|
||||
if (attempt == 2) return null;
|
||||
continue; // 재시도
|
||||
}
|
||||
|
||||
log.info("티스토리 토큰 추출 성공: xsrfToken={}, blogId={}",
|
||||
xsrfToken.substring(0, Math.min(10, xsrfToken.length())) + "...", blogIdFromPage);
|
||||
|
||||
return new String[]{xsrfToken, blogIdFromPage};
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("티스토리 토큰 획득 중 오류 발생", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그인 페이지인지 확인합니다.
|
||||
*/
|
||||
private boolean isLoginPage(String html) {
|
||||
String[] loginIndicators = {
|
||||
"login", "sign-in", "로그인", "accounts.kakao.com",
|
||||
"oauth", "authentication", "tistory.com/auth",
|
||||
"password", "비밀번호", "로그인하기"
|
||||
};
|
||||
|
||||
String lowerHtml = html.toLowerCase();
|
||||
for (String indicator : loginIndicators) {
|
||||
if (lowerHtml.contains(indicator.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML에서 XSRF 토큰을 추출합니다.
|
||||
*/
|
||||
private String extractXsrfToken(String html) {
|
||||
// 다양한 XSRF 토큰 패턴들을 시도합니다
|
||||
Pattern[] patterns = {
|
||||
// 기본 input 필드 패턴들
|
||||
Pattern.compile("name=[\"'](?:XSRF-TOKEN|_token|csrf-token)[\"']\\s+value=[\"']([^\"']+)[\"']", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("value=[\"']([^\"']+)[\"']\\s+name=[\"'](?:XSRF-TOKEN|_token|csrf-token)[\"']", Pattern.CASE_INSENSITIVE),
|
||||
|
||||
// meta 태그 패턴들
|
||||
Pattern.compile("<meta\\s+name=[\"'](?:csrf-token|_token|XSRF-TOKEN)[\"']\\s+content=[\"']([^\"']+)[\"']", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("<meta\\s+content=[\"']([^\"']+)[\"']\\s+name=[\"'](?:csrf-token|_token|XSRF-TOKEN)[\"']", Pattern.CASE_INSENSITIVE),
|
||||
|
||||
// JavaScript 변수 패턴들
|
||||
Pattern.compile("(?:csrf_token|xsrfToken|_token)\\s*[:=]\\s*[\"']([^\"']+)[\"']", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("window\\._token\\s*=\\s*[\"']([^\"']+)[\"']", Pattern.CASE_INSENSITIVE),
|
||||
|
||||
// 티스토리 특화 패턴들
|
||||
Pattern.compile("data-csrf-token=[\"']([^\"']+)[\"']", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("csrf[\"']?\\s*[:=]\\s*[\"']([^\"']+)[\"']", Pattern.CASE_INSENSITIVE)
|
||||
};
|
||||
|
||||
for (Pattern pattern : patterns) {
|
||||
Matcher matcher = pattern.matcher(html);
|
||||
if (matcher.find()) {
|
||||
String token = matcher.group(1);
|
||||
log.info("XSRF 토큰 발견: 패턴={}, 토큰={}", pattern.pattern(), token.substring(0, Math.min(10, token.length())) + "...");
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* XSRF 토큰 추출 실패 시 디버깅을 위한 메서드
|
||||
*/
|
||||
private void debugTokenExtraction(String html) {
|
||||
log.debug("HTML 응답 길이: {} characters", html.length());
|
||||
|
||||
// HTML의 일부분을 로깅 (보안상 전체는 X)
|
||||
if (html.length() > 0) {
|
||||
String preview = html.length() > 500 ? html.substring(0, 500) + "..." : html;
|
||||
log.debug("HTML 응답 미리보기: {}", preview);
|
||||
}
|
||||
|
||||
// 가능한 토큰 관련 키워드들 검색
|
||||
String[] keywords = {"token", "csrf", "xsrf", "_token", "csrf-token", "XSRF-TOKEN"};
|
||||
for (String keyword : keywords) {
|
||||
if (html.toLowerCase().contains(keyword.toLowerCase())) {
|
||||
log.info("키워드 '{}' 발견됨", keyword);
|
||||
|
||||
// 해당 키워드 주변 내용 추출
|
||||
int index = html.toLowerCase().indexOf(keyword.toLowerCase());
|
||||
if (index >= 0) {
|
||||
int start = Math.max(0, index - 50);
|
||||
int end = Math.min(html.length(), index + keyword.length() + 100);
|
||||
String context = html.substring(start, end);
|
||||
log.info("키워드 '{}' 주변 내용: {}", keyword, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 로그인 페이지로 리다이렉션 되었는지 확인
|
||||
if (html.contains("login") || html.contains("sign-in") || html.contains("로그인")) {
|
||||
log.error("티스토리 로그인 페이지로 리다이렉션된 것으로 보입니다. 쿠키가 만료되었을 가능성이 있습니다.");
|
||||
}
|
||||
|
||||
// 페이지 제목 확인
|
||||
Pattern titlePattern = Pattern.compile("<title>([^<]+)</title>", Pattern.CASE_INSENSITIVE);
|
||||
Matcher titleMatcher = titlePattern.matcher(html);
|
||||
if (titleMatcher.find()) {
|
||||
log.info("페이지 제목: {}", titleMatcher.group(1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML에서 블로그 ID를 추출합니다.
|
||||
*/
|
||||
private String extractBlogId(String html) {
|
||||
// 다양한 패턴으로 블로그 ID를 찾습니다
|
||||
Pattern[] patterns = {
|
||||
Pattern.compile("blogId[\"']?\\s*[:=]\\s*[\"']?(\\d+)[\"']?"),
|
||||
Pattern.compile("blog_id[\"']?\\s*[:=]\\s*[\"']?(\\d+)[\"']?"),
|
||||
Pattern.compile("data-blog-id=[\"']?(\\d+)[\"']?"),
|
||||
Pattern.compile("/admin/entry/post\\?(.*&)?blogId=(\\d+)")
|
||||
};
|
||||
|
||||
for (Pattern pattern : patterns) {
|
||||
Matcher matcher = pattern.matcher(html);
|
||||
if (matcher.find()) {
|
||||
// 그룹 2가 있으면 그것을 사용, 없으면 그룹 1 사용
|
||||
return matcher.groupCount() >= 2 && matcher.group(2) != null ?
|
||||
matcher.group(2) : matcher.group(1);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 실제로 포스트를 발행합니다.
|
||||
*/
|
||||
private String publishPost(String title, String htmlContent, String xsrfToken, String blogId) {
|
||||
return publishPost(title, htmlContent, xsrfToken, blogId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 블로그 계정별 쿠키를 사용하여 실제로 포스트를 발행합니다.
|
||||
*/
|
||||
private String publishPost(String title, String htmlContent, String xsrfToken, String blogId, Long blogAccountId) {
|
||||
try {
|
||||
HttpHeaders headers;
|
||||
if (blogAccountId != null) {
|
||||
headers = createHeadersForBlog(blogAccountId);
|
||||
} else {
|
||||
headers = createHeaders();
|
||||
}
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
|
||||
// 폼 데이터 구성
|
||||
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
|
||||
formData.add("XSRF-TOKEN", xsrfToken);
|
||||
formData.add("blogId", blogId);
|
||||
formData.add("title", title);
|
||||
formData.add("content", htmlContent);
|
||||
formData.add("visibility", "3"); // 공개 발행
|
||||
formData.add("acceptComment", "1"); // 댓글 허용
|
||||
formData.add("category", "0"); // 기본 카테고리
|
||||
formData.add("tag", ""); // 태그 없음
|
||||
formData.add("published", "1"); // 즉시 발행
|
||||
|
||||
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(formData, headers);
|
||||
|
||||
log.info("티스토리 포스트 저장 요청: title={}, blogId={}", title, blogId);
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
TISTORY_POST_SAVE_URL,
|
||||
HttpMethod.POST,
|
||||
entity,
|
||||
String.class
|
||||
);
|
||||
|
||||
if (response.getStatusCode() == HttpStatus.OK ||
|
||||
response.getStatusCode() == HttpStatus.FOUND) {
|
||||
|
||||
// 성공 시 발행된 URL 추출
|
||||
String publishedUrl = extractPublishedUrl(response);
|
||||
if (publishedUrl == null) {
|
||||
// 기본 URL 패턴으로 생성
|
||||
publishedUrl = "https://munjaon.tistory.com/entry/" +
|
||||
title.replaceAll("[^a-zA-Z0-9가-힣]", "-");
|
||||
}
|
||||
|
||||
return publishedUrl;
|
||||
} else {
|
||||
log.error("티스토리 포스트 저장 실패: status={}", response.getStatusCode());
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("티스토리 포스트 저장 중 오류 발생", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 응답에서 발행된 URL을 추출합니다.
|
||||
*/
|
||||
private String extractPublishedUrl(ResponseEntity<String> response) {
|
||||
// Location 헤더에서 리다이렉트 URL 확인
|
||||
String location = response.getHeaders().getFirst(HttpHeaders.LOCATION);
|
||||
if (StringUtils.hasText(location)) {
|
||||
return location;
|
||||
}
|
||||
|
||||
// 응답 본문에서 URL 패턴 찾기
|
||||
String body = response.getBody();
|
||||
if (StringUtils.hasText(body)) {
|
||||
Pattern pattern = Pattern.compile("https://munjaon\\.tistory\\.com/\\d+");
|
||||
Matcher matcher = pattern.matcher(body);
|
||||
if (matcher.find()) {
|
||||
return matcher.group();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 헤더를 생성합니다.
|
||||
*/
|
||||
private HttpHeaders createHeaders() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
// 쿠키 설정 (기본 방식 - 하위 호환성)
|
||||
String cookieString = cookieUtil.getTistoryCookieString();
|
||||
if (StringUtils.hasText(cookieString)) {
|
||||
headers.set(HttpHeaders.COOKIE, cookieString);
|
||||
}
|
||||
|
||||
return createHeaders(headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 블로그 계정별 쿠키를 사용하여 HTTP 헤더를 생성합니다.
|
||||
*/
|
||||
private HttpHeaders createHeadersForBlog(Long blogId) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
try {
|
||||
// 블로그별 쿠키 매핑 정보 조회
|
||||
BlogCookieMappingVO cookieMapping = blogCookieService.getCookieMappingByBlogId(blogId);
|
||||
|
||||
if (cookieMapping != null && cookieMapping.isActiveCookie()) {
|
||||
// 쿠키 유효성 검증 (필요시)
|
||||
if (cookieMapping.needsValidation()) {
|
||||
boolean isValid = blogCookieService.validateAndUpdateCookie(blogId);
|
||||
if (!isValid) {
|
||||
log.warn("쿠키 유효성 검증 실패: blogId={}", blogId);
|
||||
// 유효하지 않은 쿠키지만 일단 시도해보기 위해 계속 진행
|
||||
}
|
||||
}
|
||||
|
||||
// 블로그별 쿠키 문자열 생성
|
||||
String cookieString = cookieUtil.getCookieStringForBlog(cookieMapping);
|
||||
if (StringUtils.hasText(cookieString)) {
|
||||
headers.set(HttpHeaders.COOKIE, cookieString);
|
||||
log.info("블로그별 쿠키 헤더 설정 완료: blogId={}", blogId);
|
||||
} else {
|
||||
log.warn("블로그별 쿠키 문자열이 비어있음: blogId={}", blogId);
|
||||
}
|
||||
} else {
|
||||
log.warn("유효한 쿠키 매핑 정보를 찾을 수 없음: blogId={}", blogId);
|
||||
// 기본 쿠키로 폴백
|
||||
String defaultCookie = cookieUtil.getTistoryCookieString();
|
||||
if (StringUtils.hasText(defaultCookie)) {
|
||||
headers.set(HttpHeaders.COOKIE, defaultCookie);
|
||||
log.info("기본 쿠키로 폴백: blogId={}", blogId);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("블로그별 쿠키 헤더 생성 실패: blogId={}, 기본 쿠키로 폴백", blogId, e);
|
||||
// 오류 발생 시 기본 쿠키로 폴백
|
||||
String defaultCookie = cookieUtil.getTistoryCookieString();
|
||||
if (StringUtils.hasText(defaultCookie)) {
|
||||
headers.set(HttpHeaders.COOKIE, defaultCookie);
|
||||
}
|
||||
}
|
||||
|
||||
return createHeaders(headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통 헤더 설정 완료
|
||||
*/
|
||||
private HttpHeaders createHeaders(HttpHeaders headers) {
|
||||
|
||||
// 기본 헤더 설정
|
||||
headers.set(HttpHeaders.USER_AGENT,
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36");
|
||||
headers.set(HttpHeaders.ACCEPT,
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8");
|
||||
headers.set(HttpHeaders.ACCEPT_LANGUAGE, "ko-KR,ko;q=0.9,en;q=0.8");
|
||||
headers.set(HttpHeaders.ACCEPT_ENCODING, "gzip, deflate, br");
|
||||
headers.set("Sec-Fetch-Dest", "document");
|
||||
headers.set("Sec-Fetch-Mode", "navigate");
|
||||
headers.set("Sec-Fetch-Site", "same-origin");
|
||||
headers.set("Upgrade-Insecure-Requests", "1");
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlogPostHistoryVO publishWithHtmlGeneration(Long blogId, Long urlId, String sourceUrl) {
|
||||
log.info("통합 발행 워크플로우 시작: blogId={}, urlId={}, sourceUrl={}", blogId, urlId, sourceUrl);
|
||||
|
||||
long workflowStartTime = System.currentTimeMillis();
|
||||
BlogPostHistoryVO historyVO = null;
|
||||
|
||||
try {
|
||||
// 1. 초기 히스토리 생성 (I: In Progress 상태)
|
||||
historyVO = createInitialHistory(blogId, urlId, sourceUrl);
|
||||
|
||||
// 2. HTML 생성 단계
|
||||
try {
|
||||
generateHtmlFromSource(historyVO, sourceUrl);
|
||||
log.info("2단계 HTML 생성 완료, 3단계 티스토리 발행 진행: postId={}", historyVO.getPostId());
|
||||
} catch (Exception htmlError) {
|
||||
log.error("2단계 HTML 생성 실패로 워크플로우 중단: postId={}, error={}",
|
||||
historyVO.getPostId(), htmlError.getMessage());
|
||||
|
||||
// HTML 생성 실패 시 워크플로우 즉시 중단
|
||||
updatePublishFailure(historyVO, htmlError.getMessage());
|
||||
throw new RuntimeException("HTML 생성 단계 실패: " + htmlError.getMessage(), htmlError);
|
||||
}
|
||||
|
||||
// 3. 티스토리 발행 단계 (2단계 성공 시에만 실행)
|
||||
try {
|
||||
publishToTistoryInternal(historyVO);
|
||||
log.info("3단계 티스토리 발행 완료, 최종 성공 처리 진행: postId={}", historyVO.getPostId());
|
||||
} catch (Exception publishError) {
|
||||
log.error("3단계 티스토리 발행 실패: postId={}, error={}",
|
||||
historyVO.getPostId(), publishError.getMessage());
|
||||
|
||||
// 티스토리 발행 실패 시 워크플로우 중단
|
||||
updatePublishFailure(historyVO, publishError.getMessage());
|
||||
throw new RuntimeException("티스토리 발행 단계 실패: " + publishError.getMessage(), publishError);
|
||||
}
|
||||
|
||||
// 4. 최종 성공 처리 (모든 단계 성공 시에만 실행)
|
||||
updatePublishSuccess(historyVO);
|
||||
|
||||
long workflowEndTime = System.currentTimeMillis();
|
||||
log.info("통합 발행 워크플로우 완료: postId={}, 총 소요시간={}ms",
|
||||
historyVO.getPostId(), (workflowEndTime - workflowStartTime));
|
||||
|
||||
} catch (RuntimeException e) {
|
||||
// 이미 구체적으로 처리된 예외들은 그대로 재throw
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
// 예상치 못한 일반적인 예외 처리
|
||||
log.error("통합 발행 워크플로우 예상치 못한 오류: blogId={}, urlId={}, error={}",
|
||||
blogId, urlId, e.getMessage(), e);
|
||||
|
||||
if (historyVO != null) {
|
||||
updatePublishFailure(historyVO, "시스템 오류: " + e.getMessage());
|
||||
}
|
||||
|
||||
throw new RuntimeException("워크플로우 시스템 오류: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
return historyVO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1단계: 초기 히스토리 생성 (I: In Progress 상태)
|
||||
*/
|
||||
private BlogPostHistoryVO createInitialHistory(Long blogId, Long urlId, String sourceUrl) {
|
||||
log.info("1단계: 초기 히스토리 생성 시작");
|
||||
|
||||
BlogPostHistoryVO historyVO = new BlogPostHistoryVO();
|
||||
historyVO.setBlogId(blogId);
|
||||
historyVO.setUrlId(urlId);
|
||||
historyVO.setStatus(BlogPostHistoryVO.STATUS_IN_PROGRESS);
|
||||
historyVO.setPostTitle("HTML 생성 중...");
|
||||
historyVO.setPostContent("");
|
||||
|
||||
// 초기 히스토리 저장
|
||||
blogPostingMapper.insertBlogPostHistory(historyVO);
|
||||
|
||||
log.info("1단계 완료: 초기 히스토리 생성됨, postId={}", historyVO.getPostId());
|
||||
return historyVO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 2단계: HTML 생성
|
||||
*/
|
||||
private void generateHtmlFromSource(BlogPostHistoryVO historyVO, String sourceUrl) {
|
||||
log.info("2단계: HTML 생성 시작, sourceUrl={}", sourceUrl);
|
||||
|
||||
long htmlStartTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
// Python 서비스에 HTML 생성 요청
|
||||
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 HTML 생성 서비스 호출: URL={}, sourceUrl={}", blogGenerateUrl, sourceUrl);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
blogGenerateUrl,
|
||||
HttpMethod.POST,
|
||||
requestEntity,
|
||||
String.class
|
||||
);
|
||||
|
||||
log.info("Python 서비스 응답: status={}, bodyLength={}",
|
||||
response.getStatusCode(),
|
||||
response.getBody() != null ? response.getBody().length() : 0);
|
||||
|
||||
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
|
||||
String htmlContent = response.getBody();
|
||||
String extractedTitle = extractTitleFromHtml(htmlContent);
|
||||
|
||||
// HTML 생성 완료 상태 업데이트
|
||||
historyVO.setPostTitle(extractedTitle);
|
||||
historyVO.setPostContent(htmlContent);
|
||||
historyVO.setHtmlGeneratedAt(LocalDateTime.now());
|
||||
|
||||
blogPostingMapper.updateHtmlGenerated(historyVO);
|
||||
|
||||
long htmlEndTime = System.currentTimeMillis();
|
||||
log.info("2단계 완료: HTML 생성 성공, title={}, 소요시간={}ms",
|
||||
extractedTitle, (htmlEndTime - htmlStartTime));
|
||||
} else {
|
||||
throw new RuntimeException("Python 서비스에서 HTML 생성 실패: " + response.getStatusCode());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("2단계 실패: HTML 생성 중 오류 발생", e);
|
||||
|
||||
// 구체적인 오류 분석 및 사용자 친화적 메시지 생성
|
||||
String errorMessage = analyzeHtmlGenerationError(e, sourceUrl);
|
||||
throw new RuntimeException(errorMessage, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML 생성 오류를 분석하여 사용자 친화적인 메시지를 생성합니다.
|
||||
*/
|
||||
private String analyzeHtmlGenerationError(Exception e, String sourceUrl) {
|
||||
String errorMessage = e.getMessage();
|
||||
|
||||
if (errorMessage != null) {
|
||||
// Google AI API 500 Internal Server Error
|
||||
if (errorMessage.contains("500") && errorMessage.toLowerCase().contains("internal")) {
|
||||
return String.format(
|
||||
"[2단계 HTML 생성 실패] Google AI API 서버 내부 오류가 발생했습니다. " +
|
||||
"AI 서비스가 일시적으로 불안정하거나 과부하 상태일 수 있습니다. " +
|
||||
"몇 분 후 다시 시도하거나 수동으로 HTML을 작성해서 티스토리에 직접 발행해주세요. " +
|
||||
"(오류: Google AI 500 Internal Error, 소스: %s)",
|
||||
sourceUrl
|
||||
);
|
||||
}
|
||||
|
||||
// Google AI API finish_reason 오류
|
||||
if (errorMessage.contains("finish_reason") && errorMessage.contains("1")) {
|
||||
return String.format(
|
||||
"[2단계 HTML 생성 실패] AI가 콘텐츠를 처리할 수 없습니다. " +
|
||||
"소스 URL의 내용이 AI 정책에 위배되거나 너무 복잡할 수 있습니다. " +
|
||||
"다른 URL을 시도하거나 수동으로 HTML을 작성해주세요. " +
|
||||
"(오류코드: finish_reason=1, 소스: %s)",
|
||||
sourceUrl
|
||||
);
|
||||
}
|
||||
|
||||
// HTTP 상태 코드 관련 오류
|
||||
if (errorMessage.contains("status") && errorMessage.contains("50")) {
|
||||
return String.format(
|
||||
"[2단계 HTML 생성 실패] AI 서비스에서 서버 오류가 발생했습니다. " +
|
||||
"API 서버 문제일 가능성이 높으므로 잠시 후 다시 시도하거나 " +
|
||||
"수동으로 HTML을 작성해서 발행해주세요. " +
|
||||
"(오류: %s, 소스: %s)",
|
||||
errorMessage, sourceUrl
|
||||
);
|
||||
}
|
||||
|
||||
// 네트워크 연결 오류
|
||||
if (errorMessage.contains("Connection") || errorMessage.contains("timeout")) {
|
||||
return String.format(
|
||||
"[2단계 HTML 생성 실패] 블로그 생성 서비스에 연결할 수 없습니다. " +
|
||||
"네트워크 상태를 확인하거나 잠시 후 다시 시도해주세요. " +
|
||||
"또는 수동으로 HTML을 작성해서 발행해주세요. " +
|
||||
"(서비스: %s, 소스: %s)",
|
||||
blogGenerateUrl, sourceUrl
|
||||
);
|
||||
}
|
||||
|
||||
// 잘못된 URL 오류
|
||||
if (errorMessage.contains("404") || errorMessage.contains("NOT_FOUND")) {
|
||||
return String.format(
|
||||
"[2단계 HTML 생성 실패] 소스 URL에 접근할 수 없습니다. " +
|
||||
"URL이 올바른지 확인하고 접근 권한이 있는지 확인해주세요. " +
|
||||
"(오류: 404 Not Found, 소스: %s)",
|
||||
sourceUrl
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 기본 오류 메시지
|
||||
return String.format(
|
||||
"[2단계 HTML 생성 실패] 예상치 못한 오류가 발생했습니다. " +
|
||||
"수동으로 HTML을 작성해서 티스토리에 직접 발행하거나 관리자에게 문의해주세요. " +
|
||||
"(오류: %s, 소스: %s)",
|
||||
errorMessage != null ? errorMessage : "알 수 없는 오류",
|
||||
sourceUrl
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 3단계: 티스토리 발행
|
||||
*/
|
||||
private void publishToTistoryInternal(BlogPostHistoryVO historyVO) {
|
||||
log.info("3단계: 티스토리 발행 시작, title={}", historyVO.getPostTitle());
|
||||
|
||||
// 발행 시작 시간 기록
|
||||
historyVO.setPublishStartedAt(LocalDateTime.now());
|
||||
blogPostingMapper.updatePublishStarted(historyVO);
|
||||
|
||||
long publishStartTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
// 티스토리 토큰 획득 (DB 기반 쿠키 사용)
|
||||
String[] tokens = getTistoryTokens(historyVO.getBlogId());
|
||||
if (tokens == null || tokens.length < 2) {
|
||||
log.warn("블로그별 쿠키로 토큰 획득 실패, 기본 쿠키로 재시도: blogId={}", historyVO.getBlogId());
|
||||
tokens = getTistoryTokens(null); // 기본 쿠키로 폴백
|
||||
if (tokens == null || tokens.length < 2) {
|
||||
throw new RuntimeException("티스토리 토큰 획득 실패");
|
||||
}
|
||||
}
|
||||
|
||||
String xsrfToken = tokens[0];
|
||||
String blogIdFromPage = tokens[1];
|
||||
|
||||
// 포스트 발행 (DB 기반 쿠키 사용)
|
||||
String publishedUrl = publishPost(historyVO.getPostTitle(), historyVO.getPostContent(),
|
||||
xsrfToken, blogIdFromPage, historyVO.getBlogId());
|
||||
|
||||
if (StringUtils.hasText(publishedUrl)) {
|
||||
historyVO.setPublishedUrl(publishedUrl);
|
||||
historyVO.setPublishedAt(LocalDateTime.now());
|
||||
|
||||
long publishEndTime = System.currentTimeMillis();
|
||||
log.info("3단계 완료: 티스토리 발행 성공, publishedUrl={}, 소요시간={}ms",
|
||||
publishedUrl, (publishEndTime - publishStartTime));
|
||||
} else {
|
||||
throw new RuntimeException("티스토리 발행 URL을 가져올 수 없습니다.");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("3단계 실패: 티스토리 발행 중 오류 발생", e);
|
||||
throw new RuntimeException("티스토리 발행 실패: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 4단계: 최종 성공 처리
|
||||
*/
|
||||
private void updatePublishSuccess(BlogPostHistoryVO historyVO) {
|
||||
log.info("4단계: 최종 성공 처리 시작");
|
||||
|
||||
historyVO.setStatus(BlogPostHistoryVO.STATUS_SUCCESS);
|
||||
blogPostingMapper.updatePublishCompleted(historyVO);
|
||||
|
||||
log.info("4단계 완료: 최종 성공 처리 완료, status={}", historyVO.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 실패 처리
|
||||
*/
|
||||
private void updatePublishFailure(BlogPostHistoryVO historyVO, String errorMessage) {
|
||||
log.info("실패 처리 시작: errorMessage={}", errorMessage);
|
||||
|
||||
historyVO.setStatus(BlogPostHistoryVO.STATUS_FAILED);
|
||||
historyVO.setErrorMessage(errorMessage);
|
||||
blogPostingMapper.updatePublishFailed(historyVO);
|
||||
|
||||
log.info("실패 처리 완료: status={}", historyVO.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML 컨텐츠에서 타이틀 추출
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
log.warn("HTML에서 타이틀을 찾을 수 없습니다. 기본값을 사용합니다.");
|
||||
return "제목 없음";
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("HTML 타이틀 추출 중 오류 발생: {}", e.getMessage(), e);
|
||||
return "제목 추출 실패";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 타이틀을 데이터베이스 제한 길이에 맞게 자르기
|
||||
*/
|
||||
private String truncateTitle(String title) {
|
||||
if (title == null) {
|
||||
return "제목 없음";
|
||||
}
|
||||
|
||||
// 데이터베이스 VARCHAR(255) 제한에 맞춰 자르기
|
||||
if (title.length() > 255) {
|
||||
return title.substring(0, 252) + "...";
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@ -1,179 +0,0 @@
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,337 +0,0 @@
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
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";
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -11,8 +11,4 @@ public interface CodeDetailMapper {
|
||||
void insert(CodeDetailVO codeDetailVO);
|
||||
void update(CodeDetailVO codeDetailVO);
|
||||
void delete(String codeGroupId, String codeId);
|
||||
|
||||
int countSortOrder(CodeDetailVO codeDetail);
|
||||
|
||||
void pushBackSortOrder(CodeDetailVO codeDetail);
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import lombok.*;
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CodeVO extends CodeDetailVO {
|
||||
public class CodeVO {
|
||||
private String codeGroupId;
|
||||
private String codeGroupName;
|
||||
private String description;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user