스팸단어 체크

This commit is contained in:
hylee 2024-09-03 14:57:51 +09:00
parent 7caaa99ae9
commit b5c1cba2c3
24 changed files with 744 additions and 108 deletions

View File

@ -166,6 +166,10 @@
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
</dependencies>

View File

@ -23,9 +23,9 @@ class MjonAgentSDatabaseConfig {
// A database DataSource
@Bean(AGENT_S_DATA_SOURCE)
@ConfigurationProperties(prefix = "spring.mjagent.server.datasource")
public DataSource CommuteDataSource() {
public DataSource MjagentServerSource() {
return DataSourceBuilder.create()
// .type(HikariDataSource.class)
.type(com.zaxxer.hikari.HikariDataSource.class) // HikariDataSource를 명시적으로 사용
.build();
}

View File

@ -7,8 +7,8 @@ import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CodeDetailMapper {
List<CodeDetailVO> findByGroupId(String codeGroupId);
CodeDetailVO findById(String codeId);
CodeDetailVO findById(String codeGroupId, String codeId);
void insert(CodeDetailVO codeDetailVO);
void update(CodeDetailVO codeDetailVO);
void delete(String codeId);
void delete(String codeGroupId, String codeId);
}

View File

@ -1,12 +1,10 @@
package com.itn.admin.itn.code.mapper.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.*;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class CodeDetailVO {

View File

@ -12,14 +12,14 @@ public interface CodeDetailService {
List<CodeDetailVO> getDetailsByGroupId(String codeGroupId);
// 특정 코드 상세를 ID로 가져오는 메서드
CodeDetailVO getCodeDetailById(String codeId);
CodeDetailVO getCodeDetailById(String codeGroupId, String codeId);
// 코드 상세 추가 메서드
RestResponse addCodeDetail(CodeDetailVO codeDetail);
// 코드 상세 수정 메서드
void updateCodeDetail(CodeDetailVO codeDetail);
RestResponse updateCodeDetail(CodeDetailVO codeDetail);
// 코드 상세 삭제 메서드
void deleteCodeDetail(String codeId);
void deleteCodeDetail(String codeGroupId, String codeId);
}

View File

@ -2,14 +2,17 @@ package com.itn.admin.itn.code.server.impl;
import java.util.List;
import com.itn.admin.cmn.config.SecurityUtil;
import com.itn.admin.cmn.msg.RestResponse;
import com.itn.admin.itn.code.mapper.CodeDetailMapper;
import com.itn.admin.itn.code.mapper.domain.CodeDetailVO;
import com.itn.admin.itn.code.server.CodeDetailService;
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 CodeDetailServiceImpl implements CodeDetailService {
@ -22,23 +25,51 @@ public class CodeDetailServiceImpl implements CodeDetailService {
}
@Override
public CodeDetailVO getCodeDetailById(String codeId) {
return codeDetailMapper.findById(codeId);
public CodeDetailVO getCodeDetailById(String codeGroupId, String codeId) {
CodeDetailVO codeDetailVO = new CodeDetailVO();
return codeDetailMapper.findById(codeGroupId, codeId);
}
@Override
public RestResponse addCodeDetail(CodeDetailVO codeDetail) {
codeDetailMapper.insert(codeDetail);
// 현재 인증된 사용자 정보 가져오기
String userId = SecurityUtil.getCurrentUserId();
if (userId == null) {
log.warn("Failed to retrieve current user ID.");
throw new IllegalStateException("Current user ID is not available");
}
log.info("Updating by user: [{}]", userId);
codeDetail.setFrstRegisterId(userId);
codeDetail.setLastUpdusrId(userId);
try {
codeDetailMapper.insert(codeDetail);
}catch (Exception e) {
return new RestResponse(HttpStatus.BAD_REQUEST, "코드 ID:"+ codeDetail.getCodeId() + " 를 확인해주세요. \n이미 등록되어 있을 수 있습니다." , codeDetail.getCodeName());
}
return new RestResponse(HttpStatus.OK, "등록되었습니다", codeDetail.getCodeName());
}
@Override
public void updateCodeDetail(CodeDetailVO codeDetail) {
public RestResponse updateCodeDetail(CodeDetailVO codeDetail) {
// 현재 인증된 사용자 정보 가져오기
String userId = SecurityUtil.getCurrentUserId();
if (userId == null) {
log.warn("Failed to retrieve current user ID.");
throw new IllegalStateException("Current user ID is not available");
}
log.info("Updating by user: [{}]", userId);
codeDetail.setLastUpdusrId(userId);
codeDetailMapper.update(codeDetail);
return new RestResponse(HttpStatus.OK, "등록되었습니다", codeDetail.getCodeName());
}
@Override
public void deleteCodeDetail(String codeId) {
codeDetailMapper.delete(codeId);
public void deleteCodeDetail(String codeGroupId, String codeId){
codeDetailMapper.delete(codeGroupId, codeId);
}
}

View File

@ -3,13 +3,16 @@ package com.itn.admin.itn.code.web;
import com.itn.admin.cmn.msg.RestResponse;
import com.itn.admin.itn.code.mapper.domain.CodeDetailVO;
import com.itn.admin.itn.code.server.CodeDetailService;
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.List;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/api/code-details") // URL 경로를 '/api/code-details' 변경
public class CodeDetailRestController {
@ -24,9 +27,9 @@ public class CodeDetailRestController {
}
// 특정 코드 상세를 ID로 가져오는 메서드
@GetMapping("/detail/{codeId}")
public CodeDetailVO getCodeDetailById(@PathVariable String codeId) {
return codeDetailService.getCodeDetailById(codeId);
@GetMapping("/detail/{codeGroupId}/{codeId}")
public CodeDetailVO getCodeDetailById(@PathVariable String codeGroupId,@PathVariable String codeId) {
return codeDetailService.getCodeDetailById(codeGroupId, codeId);
}
// 코드 상세 추가 메서드
@ -37,14 +40,16 @@ public class CodeDetailRestController {
// 코드 상세 수정 메서드
@PutMapping("/detail/{codeId}")
public void updateCodeDetail(@PathVariable String codeId, @RequestBody CodeDetailVO codeDetail) {
codeDetail.setCodeId(codeId);
codeDetailService.updateCodeDetail(codeDetail);
public ResponseEntity<RestResponse> updateCodeDetail(@PathVariable String codeId, @RequestBody CodeDetailVO codeDetail) {
// log.info("codeDetail :: [{}]", codeDetail.toString());
return ResponseEntity.ok().body(codeDetailService.updateCodeDetail(codeDetail));
}
// 코드 상세 삭제 메서드
@DeleteMapping("/detail/{codeId}")
public void deleteCodeDetail(@PathVariable String codeId) {
codeDetailService.deleteCodeDetail(codeId);
@DeleteMapping("/detail/{codeGroupId}/{codeId}")
public void deleteCodeDetail(@PathVariable String codeGroupId,@PathVariable String codeId) {
codeDetailService.deleteCodeDetail(codeGroupId, codeId);
}
}

View File

@ -20,4 +20,8 @@ public interface SpamMapper {
List<SpamVO> getSpamList(SpamVO spamVO);
int getTotalRecordCount();
SpamVO findById(String spamId);
void updateByKeywordsAndChcKeywordsWhereSpamId(SpamVO spamVO);
}

View File

@ -1,6 +1,7 @@
package com.itn.admin.itn.mjon.spam.mapper.domain;
import com.itn.admin.cmn.vo.CmnVO;
import com.itn.admin.itn.code.mapper.domain.CodeDetailVO;
import lombok.*;
import java.io.Serializable;
@ -30,9 +31,9 @@ public class SpamVO extends CmnVO implements Serializable {
private static final long serialVersionUID = 1L;
private int spamId; // 스팸 ID
private String msgGroupId; // 문자그룹ID
private String smsTxt; // 문자내용
private String spamRsn; // 스팸사유
private String spamRsnCode01; // 스팸코드01
private String spamRsnCode02; // 스팸코드02
private String keywords; // 문자에 포함된 단어
private String chcKeywords; // 스팸에 해당하는 단어
private LocalDateTime timestamp; // 요청 시간
@ -43,6 +44,11 @@ public class SpamVO extends CmnVO implements Serializable {
private LocalDateTime lastUpdtPnttm; // 최종수정일자
private List<CodeDetailVO> CodeDetailList; // 최종수정일자
// spam_keywords TB 필드들
private List<String> keywordList; // 키워드 리스트
private List<String> chcKeywordList; // 키워드 리스트
}

View File

@ -1,5 +1,7 @@
package com.itn.admin.itn.mjon.spam.service;
import com.itn.admin.cmn.msg.RestResponse;
import com.itn.admin.itn.code.mapper.domain.CodeDetailVO;
import com.itn.admin.itn.mjon.spam.mapper.domain.SpamVO;
import java.util.Map;
@ -13,4 +15,8 @@ public interface SpamService {
void analyze();
Map<String, Object> getSpamList(SpamVO spamVO);
RestResponse updateKeyword(String spamId, String word);
RestResponse updateChcKeyword(String spamId, String word);
}

View File

@ -1,9 +1,13 @@
package com.itn.admin.itn.mjon.spam.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.itn.admin.cmn.msg.RestResponse;
import com.itn.admin.itn.code.mapper.domain.CodeDetailVO;
import com.itn.admin.itn.code.server.CodeDetailService;
import com.itn.admin.itn.mjon.spam.mapper.SpamMapper;
import com.itn.admin.itn.mjon.spam.mapper.domain.SpamVO;
import com.itn.admin.itn.mjon.spam.service.SpamService;
import jakarta.servlet.ServletOutputStream;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
@ -11,6 +15,7 @@ import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
@ -20,11 +25,13 @@ public class SpamServiceImpl implements SpamService {
@Autowired
SpamMapper spamMapper;
@Autowired
private CodeDetailService codeDetailService;
@Autowired
private RestTemplate restTemplate;
final static private String PYTHON_ANALYZE_URL= "http://192.168.0.78:5000/word_analyze";
public SpamServiceImpl(RestTemplate restTemplate) {
@ -40,7 +47,6 @@ public class SpamServiceImpl implements SpamService {
map.put("spamList", spamList);
return map;
}
@ -114,19 +120,90 @@ public class SpamServiceImpl implements SpamService {
List<SpamVO> spamList = spamMapper.getSpamList(spamVO);
// 키워드를 버튼용 배열로 변환
for (SpamVO spam : spamList) {
if (spam.getKeywords() != null) {
spam.setKeywordList(Arrays.asList(spam.getKeywords().split(", ")));
}
if (spam.getChcKeywords() != null) {
spam.setChcKeywordList(Arrays.asList(spam.getChcKeywords().split(", ")));
}
}
List<CodeDetailVO> rsnCode02List = new ArrayList<>();
// 키워드를 버튼용 배열로 변환
for (SpamVO spam : spamList) {
if (spam.getSpamRsnCode01() != null) {
spam.setCodeDetailList(codeDetailService.getDetailsByGroupId("SPAM"+spam.getSpamRsnCode01()));
}
}
List<CodeDetailVO> rsnCode01List = codeDetailService.getDetailsByGroupId("SPAM");
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("spamList", spamList);
resultMap.put("totalRecordCount", totalRecordCount);
resultMap.put("totalPageCount", spamVO.getTotalPageCount());
resultMap.put("rsnCode01List", rsnCode01List);
return resultMap;
}
@Override
public RestResponse updateChcKeyword(String spamId, String word) {
SpamVO spamVO = spamMapper.findById(spamId);
spamVO.setKeywords(removeString(spamVO.getKeywords(),word));
spamVO.setChcKeywords(addString(spamVO.getChcKeywords(),word));
spamMapper.updateByKeywordsAndChcKeywordsWhereSpamId(spamVO);
return new RestResponse(HttpStatus.OK, word+" 단어가 선택되었습니다.","");
}
@Override
public RestResponse updateKeyword(String spamId, String word) {
SpamVO spamVO = spamMapper.findById(spamId);
spamVO.setChcKeywords(removeString(spamVO.getChcKeywords(),word));
spamVO.setKeywords(addString(spamVO.getKeywords(),word));
spamMapper.updateByKeywordsAndChcKeywordsWhereSpamId(spamVO);
return new RestResponse(HttpStatus.OK, word+" 단어가 취소되었습니다.","");
}
private String removeString(String keywords, String word) {
String[] keywordArray = keywords.split(", ");
return Arrays.stream(keywordArray)
.filter(s -> !s.equals(word))
.collect(Collectors.joining(", "));
}
private String addString(String keywords, String word) {
if (keywords == null) {
keywords = "";
}
String[] keywordArray = keywords.split(", ");
boolean wordExists = Arrays.stream(keywordArray).anyMatch(kw -> kw.equals(word));
if (!wordExists) {
// keywords 문자열이 비어있지 않은 경우, 쉼표를 추가한 새로운 단어를 추가합니다.
if (!keywords.isEmpty()) {
keywords += ", " + word;
} else {
// keywords가 비어있으면, 쉼표 없이 새로운 단어를 추가합니다.
keywords = word;
}
}
return keywords;
}
}

View File

@ -1,10 +1,15 @@
package com.itn.admin.itn.mjon.spam.web;
import com.itn.admin.cmn.msg.RestResponse;
import com.itn.admin.itn.code.mapper.domain.CodeDetailVO;
import com.itn.admin.itn.mjon.spam.service.SpamService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@ -21,15 +26,21 @@ public class RestSpamController {
@GetMapping(value = "/mjon/spam/analyze")
public void analyze(Model model) {
spamService.analyze();
}
// 키워드 -> 핵심키워드 :: keywords -> chc_keywords
@PutMapping("/mjon/spam/chcKeyword/{spamId}/{word}")
public ResponseEntity<RestResponse> updateChcKeyword(@PathVariable String spamId, @PathVariable String word) {
return ResponseEntity.ok().body(spamService.updateChcKeyword(spamId, word));
}
// 핵심키워드 -> 키워드 :: chc_keywords -> keywords
@PutMapping("/mjon/spam/keyword/{spamId}/{word}")
public ResponseEntity<RestResponse> updateKeyword(@PathVariable String spamId, @PathVariable String word) {
return ResponseEntity.ok().body(spamService.updateKeyword(spamId, word));
}
}

View File

@ -30,9 +30,15 @@ public class SpamController {
model.addAttribute("spamList", map.get("spamList"));
model.addAttribute("pagingVO", spamVO);
model.addAttribute("rsnCode01List", map.get("rsnCode01List"));
return "mjon/spam/select";
}
@GetMapping(value = "/mjon/spam/chk")
public String chk(@ModelAttribute("spamVO") SpamVO spamVO, Model model) {
return "mjon/spam/chk";
}

View File

@ -32,6 +32,7 @@ spring.main.datasource.password=itntest123
#
spring.commute.datasource.driverClassName=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.commute.datasource.jdbc-url=jdbc:log4jdbc:mysql://192.168.0.200:3312/biostar2_ac?serverTimezone=Asia/Seoul
#spring.commute.datasource.jdbc-url=jdbc:log4jdbc:mysql://192.168.0.30:3312/biostar2_ac?serverTimezone=Asia/Seoul
spring.commute.datasource.username=root
spring.commute.datasource.password=itntest!
@ -54,7 +55,8 @@ spring.mjagent.server.datasource.driverClassName=net.sf.log4jdbc.sql.jdbcapi.Dri
spring.mjagent.server.datasource.jdbc-url=jdbc:log4jdbc:mysql://119.193.215.98:3306/mjon_agent_back?serverTimezone=Asia/Seoul
spring.mjagent.server.datasource.username=mjonUr_agent
spring.mjagent.server.datasource.password=mjagent123$
spring.mjagent.server.datasource.hikari.minimum-idle=2
spring.mjagent.server.datasource.hikari.maximum-pool-size=4

View File

@ -60,6 +60,7 @@
MUNJAON_MSG
WHERE SEND_STATUS != 1000
and MSG_TYPE = #{msgType}
and MESSAGE LIKE CONCAT(#{message}, '%')
</select>
<!-- 리포트할때 현재 데이터가 LOG 테이블에 이동됐는지 확인 -->

View File

@ -12,7 +12,7 @@
<!-- 특정 코드 상세를 ID로 조회하는 쿼리 -->
<select id="findById" parameterType="String" resultType="codeDetailVO">
SELECT * FROM common_code_detail WHERE code_id = #{codeId}
SELECT * FROM common_code_detail WHERE code_id = #{codeId} AND code_group_id = #{codeGroupId}
</select>
<!-- 코드 상세를 추가하는 쿼리 -->
@ -35,6 +35,6 @@
<!-- 코드 상세를 삭제하는 쿼리 -->
<delete id="delete" parameterType="String">
DELETE FROM common_code_detail WHERE code_id = #{codeId}
DELETE FROM common_code_detail WHERE code_id = #{codeId} AND code_group_id = #{codeGroupId}
</delete>
</mapper>

View File

@ -12,9 +12,9 @@
<select id="getSpamList" parameterType="spamVO" resultType="SpamVO">
SELECT
SPAM_ID,
MSG_GROUP_ID,
SMS_TXT,
SPAM_RSN,
SPAM_RSN_CODE_01,
SPAM_RSN_CODE_02,
KEYWORDS,
CHC_KEYWORDS,
TIMESTAMP,
@ -25,6 +25,7 @@
LAST_UPDT_PNTTM
FROM
spam_keywords
/*where chc_keywords is null*/
ORDER BY
SPAM_ID
LIMIT #{limit} OFFSET #{offset}
@ -35,9 +36,9 @@
<select id="getList" parameterType="spamVO" resultType="spamVO">
SELECT
SPAM_ID,
MSG_GROUP_ID,
SMS_TXT,
SPAM_RSN,
SPAM_RSN_CODE_01,
SPAM_RSN_CODE_02,
KEYWORDS,
CHC_KEYWORDS,
TIMESTAMP,
@ -73,9 +74,9 @@
<select id="getListWhereKeywordsIsNull" parameterType="spamVO" resultType="spamVO">
SELECT
SPAM_ID,
MSG_GROUP_ID,
SMS_TXT,
SPAM_RSN,
SPAM_RSN_CODE_01,
SPAM_RSN_CODE_02,
KEYWORDS,
CHC_KEYWORDS,
TIMESTAMP,
@ -90,10 +91,52 @@
KEYWORDS IS NULL
</select>
<select id="findById" parameterType="String" resultType="spamVO">
SELECT
*
FROM
spam_keywords
WHERE
spam_id = #{spamId}
</select>
<update id="update">
UPDATE spam_keywords
SET keywords = #{keywords}
WHERE spam_id = #{spamId}
</update>
<!-- 정확히 일치하는 단어와 그 개수를 검색하는 쿼리 -->
<select id="selectByExactWordsWithCount" resultType="map">
SELECT
*,
(
<foreach item="word" index="index" collection="words" separator="+">
CASE WHEN FIND_IN_SET(#{word}, REPLACE(chc_keywords, ', ', ',')) THEN 1 ELSE 0 END
</foreach>
) AS match_count,
CONCAT(
<foreach item="word" index="index" collection="words" separator=", ">
IF(FIND_IN_SET(#{word}, REPLACE(chc_keywords, ', ', ',')), #{word}, NULL)
</foreach>
) AS matched_words
FROM spam_keywords
WHERE
<foreach item="word" index="index" collection="words" separator=" OR ">
FIND_IN_SET(#{word}, REPLACE(chc_keywords, ', ', ',')) > 0
</foreach>
</select>
<update id="updateByKeywordsAndChcKeywordsWhereSpamId" parameterType="spamVO">
UPDATE spam_keywords
SET keywords = #{keywords},
chc_keywords = #{chcKeywords}
WHERE spam_id = #{spamId}
</update>
</mapper>

View File

@ -216,14 +216,16 @@ function oneStopReporingTimer() {
}
function fn_oneReportCntAndTime(){
function fn_oneReportCntAndTime(userId){
// 폼 데이터를 수집
var formData = new FormData($("#divOneSms .sendForm")[0]);
var jsonObject = {};
formData.forEach((value, key) => {
jsonObject[key] = value;
if (!(value instanceof File)) {
jsonObject[key] = value;
}
});
jsonObject['userId'] = userId;
@ -239,9 +241,15 @@ function fn_oneReportCntAndTime(){
if (data.status == 'OK') {
var cnt = data.data;
$('#divOneSmsCard .reportStartCnt').text(cnt);
$('#divOneSmsCard .reportSndCnt').text(cnt);
if(cnt == 0){
var transferCnt = $('#divOneSmsCard .insertCnt').text();
console.log('cnt : ', cnt);
console.log('reportStartCnt : ', transferCnt);
console.log('');
if(cnt >= Number(transferCnt)){
oneStopReporingTimer();
}
}

View File

@ -245,8 +245,9 @@ function fn_twoReportCntAndTime(userId){
if (data.status == 'OK') {
var cnt = data.data;
console.log('cnt : ', cnt);
// 리포트 영역에 cnt 추가
$('#divTwoSmsCard .reportSndCnt').text(cnt);
$('#divTwoSmsCard .reportStartCnt').text(cnt);
// server DB에 update한 건수와 cnt비교
var transferCnt = $('#divTwoSmsCard .insertCnt').text();

View File

@ -117,7 +117,7 @@
title: title,
subtitle: '',
autohide : true,
delay: 3000,
delay: 6000,
body: msg
})
}

View File

@ -113,6 +113,14 @@
</a>
</li>
</ul>
<ul class="nav nav-treeview">
<li class="nav-item">
<a th:href="@{/mjon/spam/chk}" class="nav-link">
<i class="fas fa-filter nav-icon"></i>
<p>스팸비교</p>
</a>
</li>
</ul>
</li>
</ul>
</nav>

View File

@ -150,7 +150,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">닫기</button>
<button type="button" class="btn btn-primary" onclick="updateCodeGroup()">저장</button>
<button type="button" class="btn btn-primary" onclick="updateCodeGroup()">수정</button>
</div>
</div>
</div>
@ -239,6 +239,56 @@
</div>
</div>
<!-- 공통 코드 상세 수정 모달 -->
<div class="modal fade" id="editCodeDetailModal" tabindex="-1" role="dialog" aria-labelledby="editCodeDetailModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editCodeDetailModalLabel">공통 코드 상세 수정</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form id="editCodeDetailForm">
<div class="form-group">
<label for="editDetailCodeId">코드 ID</label>
<input type="text" class="form-control" id="editDetailCodeId" name="codeId" readonly>
</div>
<div class="form-group">
<label for="editDetailCodeGroupId">코드 그룹 ID</label>
<input type="text" class="form-control" id="editDetailCodeGroupId" name="codeGroupId" readonly>
</div>
<div class="form-group">
<label for="editDetailCodeName">코드 이름</label>
<input type="text" class="form-control" id="editDetailCodeName" name="codeName" required>
</div>
<div class="form-group">
<label for="editDetailCodeValue">코드 값</label>
<input type="text" class="form-control" id="editDetailCodeValue" name="codeValue" required>
</div>
<div class="form-group">
<label for="editDetailSortOrder">정렬 순서</label>
<input type="number" class="form-control" id="editDetailSortOrder" name="sortOrder">
</div>
<div class="form-group">
<label for="editDetailUseYn">사용 여부</label>
<select class="form-control" id="editDetailUseYn" name="useYn">
<option value="Y">Yes</option>
<option value="N">No</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">닫기</button>
<button type="button" class="btn btn-primary" onclick="updateCodeDetail()">수정</button>
</div>
</div>
</div>
</div>
<footer class="main-footer" th:insert="~{fragments/footer :: footerFragment}"></footer>
<!-- Control Sidebar -->
@ -373,8 +423,8 @@
'<td>' + detail.codeValue + '</td>' +
'<td>' + detail.sortOrder + '</td>' +
'<td>' + detail.useYn + '</td>' +
'<td><button class="btn btn-primary btn-sm" onclick="editCodeDetail(\'' + detail.codeId + '\')">수정</button></td>' +
'<td><button class="btn btn-danger btn-sm" onclick="deleteCodeDetail(\'' + detail.codeId + '\')">삭제</button></td>' +
'<td><button class="btn btn-primary btn-sm" onclick="editCodeDetail(\'' + codeGroupId + '\', \'' + detail.codeId + '\')">수정</button></td>' +
'<td><button class="btn btn-danger btn-sm" onclick="deleteCodeDetail(\'' + codeGroupId+'\', \'' + detail.codeId + '\')">삭제</button></td>' +
'</tr>');
});
$('#commonCodeDetailModal').modal('show');
@ -425,9 +475,17 @@
contentType: 'application/json; charset=utf-8',
dataType: 'json',
success: function(data) {
$('#addCodeDetailModal').modal('hide'); // 모달 닫기
showDetails(formData.codeGroupId); // 상세 보기 갱신
fn_successAlert("성공", data.data +'가 '+data.msg);
if(data.status === 'OK'){
$('#addCodeDetailForm')[0].reset(); // form reset
$('#addCodeDetailModal').modal('hide'); // 모달 닫기
showDetails(formData.codeGroupId); // 상세 보기 갱신
fn_successAlert("성공", data.data +'가 '+data.msg);
}else{
fn_failedAlert("실패", data.msg);
}
},
error: function(xhr) {
alert('공통 코드 상세 추가 중 오류가 발생했습니다.');
@ -435,29 +493,64 @@
});
};
// 코드 상세 추가 함수
window.addCodeDetail = function() {
// 코드 상세 추가 모달을 띄우거나, 필요한 로직 추가
// 코드 상세 수정 모달을 표시하는 함수
window.editCodeDetail = function(codeGroupId, codeId) {
$.ajax({
url: '/api/code-details/detail/' + codeGroupId + '/' + codeId, // API 엔드포인트
type: 'GET',
success: function(data) {
$('#editDetailCodeId').val(data.codeId);
$('#editDetailCodeGroupId').val(data.codeGroupId);
$('#editDetailCodeName').val(data.codeName);
$('#editDetailCodeValue').val(data.codeValue);
$('#editDetailSortOrder').val(data.sortOrder);
$('#editDetailUseYn').val(data.useYn);
$('#editCodeDetailModal').modal('show'); // 수정 모달 표시
},
error: function(xhr) {
alert('코드 상세 정보를 불러오는 데 실패했습니다.');
}
});
};
// 코드 상세 수정 함수
window.editCodeDetail = function(codeId) {
// 코드 상세 수정 로직 추가
window.updateCodeDetail = function() {
var formDataArray = $("#editCodeDetailForm").serializeArray();
var formData = {};
// 배열 데이터를 JSON 객체로 변환
$.each(formDataArray, function(_, field) {
formData[field.name] = field.value;
});
$.ajax({
url: '/api/code-details/detail/' + formData.codeId, // API 엔드포인트
type: 'PUT',
data: JSON.stringify(formData),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
success: function(data) {
$('#editCodeDetailModal').modal('hide'); // 모달 닫기
showDetails(formData.codeGroupId); // 상세 보기 갱신
fn_successAlert("성공", data.data +'가 '+data.msg);
},
error: function(xhr) {
alert('코드 상세 수정 중 오류가 발생했습니다.');
console.log('xhr : ', xhr);
}
});
};
// 코드 상세 삭제 함수
window.deleteCodeDetail = function(codeId) {
window.deleteCodeDetail = function(codeGroupId, codeId) {
if(confirm('삭제하시겠습니까?')) {
$.ajax({
url: '/api/code-details/detail/' + codeId, // 변경된 URL
url: '/api/code-details/detail/' + codeGroupId + '/' + codeId, // 변경된 URL
type: 'DELETE',
success: function(response) {
showDetails($('#detailTableBody').data('group-id')); // 갱신을 위해 다시 로드
showDetails(codeGroupId); // 갱신을 위해 다시 로드
},
error: function(xhr) {
alert('오류가 발생했습니다.');

View File

@ -0,0 +1,199 @@
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="layout">
<head>
<title>스팸 단어 선택</title>
<link rel="stylesheet" th:href="@{/plugins/datatables-bs4/css/dataTables.bootstrap4.min.css}">
<link rel="stylesheet" th:href="@{/plugins/datatables-responsive/css/responsive.bootstrap4.min.css}">
<link rel="stylesheet" th:href="@{/plugins/datatables-buttons/css/buttons.bootstrap4.min.css}">
<style>
.table-fixed thead {
position: sticky;
top: 0;
z-index: 1020;
background-color: #fff;
}
.table-fixed tbody td, .table-fixed thead th {
text-align: center;
vertical-align: middle;
}
/* 중앙 정렬을 위한 추가 스타일 */
.center-align {
display: flex;
justify-content: center;
align-items: center;
margin: 10px 0;
}
/* 통계 정보 구분 */
.stats-info {
text-align: center;
margin-bottom: 20px;
}
/* 메시지 입력과 버튼 정렬을 위한 스타일 */
.input-group {
display: flex;
align-items: stretch; /* 세로 높이 맞추기 */
}
.input-group textarea {
flex: 1;
}
.input-group button {
align-self: stretch; /* 버튼 높이를 textarea에 맞춤 */
}
</style>
</head>
<body layout:fragment="body">
<div class="wrapper">
<div th:replace="~{fragments/top_nav :: topFragment}"></div>
<!-- Main Sidebar Container -->
<aside class="main-sidebar sidebar-dark-primary elevation-4"
th:insert="~{fragments/mainsidebar :: sidebarFragment}">
</aside>
<!-- Content Wrapper. Contains page content -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0">스팸 비교</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="#">Home</a></li>
<li class="breadcrumb-item active">스팸 비교</li>
</ol>
</div>
</div>
</div>
</div>
<!-- /.content-header -->
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">스팸 비교</h3>
</div>
<!-- /.card-header -->
<div class="card-body p-0">
<div class="container-fluid">
<!-- 메시지 입력과 버튼 영역 -->
<div class="row">
<div class="col-12">
<div class="input-group">
<textarea class="form-control" id="inputMessage" rows="3" placeholder="메시지를 입력하세요" style="width: 90%;"></textarea>
<button type="button" class="btn btn-primary" style="width: 10%;">조회</button>
</div>
</div>
</div>
<!-- 불법/성인 버튼 중앙 정렬 -->
<div class="row">
<div class="col-12 d-flex justify-content-center flex-wrap">
<button type="button" class="btn btn-primary mx-2 my-2">불법 145건</button>
<button type="button" class="btn btn-secondary mx-2 my-2">성인 43건</button>
<button type="button" class="btn btn-success mx-2 my-2">광고 78건</button>
<button type="button" class="btn btn-danger mx-2 my-2">금융 20건</button>
<button type="button" class="btn btn-warning mx-2 my-2">스팸 30건</button>
<button type="button" class="btn btn-info mx-2 my-2">게임 15건</button>
<button type="button" class="btn btn-light mx-2 my-2">의료 9건</button>
<button type="button" class="btn btn-dark mx-2 my-2">기타 12건</button>
<button type="button" class="btn btn-outline-primary mx-2 my-2">교육 5건</button>
<button type="button" class="btn btn-outline-secondary mx-2 my-2">이벤트 22건</button>
</div>
</div>
<!-- 통계 정보 영역 -->
<div class="row stats-info">
<div class="col-12">
<span>문자 개수 총 300건</span>
</div>
</div>
<!-- 스팸 비교 테이블 영역 -->
<div class="row mt-3">
<div class="col-12">
<div class="table-responsive">
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>번호</th>
<th>스팸문자</th>
<th>핵심단어</th>
<th>매칭단어 개수</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>(광고)♥봄맞이 특별이벤트 진행중♥ ...</td>
<td>바다, 서비스, 갯수...</td>
<td>3</td>
</tr>
<!-- 추가 행들 필요 시 여기 추가 -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- /.card-body -->
</div>
<!-- /.card -->
</div>
<!-- /.col -->
</div>
<!-- /.row -->
</div>
<!-- /.container-fluid -->
</section>
<!-- /Main content -->
</div>
<!-- /.content-wrapper -->
<footer class="main-footer"
th:insert="~{fragments/footer :: footerFragment}">
</footer>
<!-- Control Sidebar -->
<aside class="control-sidebar control-sidebar-dark">
<!-- Control sidebar content goes here -->
</aside>
<!-- /.control-sidebar -->
</div>
<!-- ./wrapper -->
<script>
$(document).ready(function () {
// 조회 버튼 클릭 시 이벤트 핸들러
$('button.btn-primary').on('click', function () {
var inputMessage = $('#inputMessage').val();
// AJAX 호출이나 데이터 처리 로직 추가 가능
alert('조회 버튼 클릭! 입력된 메시지: ' + inputMessage);
});
});
</script>
</body>
</html>

View File

@ -111,28 +111,30 @@
<thead>
<tr>
<th style="width: 5%;">번호</th>
<th style="width: 25%;">스팸문자</th>
<th style="width: 35%;">복합명사</th>
<th style="width: 25%;">핵심 단어</th>
<th style="width: 10%;">스팸분류</th>
<th style="width: 20%;">스팸문자</th>
<th style="width: 25%;">복합명사</th>
<th style="width: 20%;">핵심 단어</th>
<th style="width: 10%;">스팸분류01</th>
<th style="width: 10%;">스팸분류02</th>
<!-- <th style="width: 10%;">확정</th>-->
</tr>
</thead>
<tbody>
<tr th:each="spam, iterStat : ${spamList}">
<td th:text="${iterStat.index + 1}"></td>
<td th:text="${(pagingVO.pageNum - 1) * pagingVO.pageSize + iterStat.index + 1}"></td>
<td th:text="${spam.smsTxt}">스팸문자 내용</td>
<td>
<span th:each="word : ${spam.keywordList}">
<!--
- adminLTE3 css 참고
btn: 버튼 스타일을 적용합니다.
btn-primary: 파란색 배경의 버튼을 만듭니다. 다른 색상은 btn-secondary, btn-success, btn-danger 등으로 변경할 수 있습니다.
btn-sm: 작은 크기의 버튼을 만듭니다.
rounded-pill: 모서리를 둥글게 처리하여 pill 형태의 버튼을 만듭니다.
<!--
- adminLTE3 css 참고
btn: 버튼 스타일을 적용합니다.
btn-primary: 파란색 배경의 버튼을 만듭니다. 다른 색상은 btn-secondary, btn-success, btn-danger 등으로 변경할 수 있습니다.
btn-sm: 작은 크기의 버튼을 만듭니다.
rounded-pill: 모서리를 둥글게 처리하여 pill 형태의 버튼을 만듭니다.
mx-1: 좌우에 0.25rem(약 4px)의 여백 추가
my-1: 상하에 0.25rem(약 4px)의 여백 추가
-->
mx-1: 좌우에 0.25rem(약 4px)의 여백 추가
my-1: 상하에 0.25rem(약 4px)의 여백 추가
-->
<span th:each="word : ${spam.keywordList}">
<button class="btn btn-primary rounded-pill mx-1 my-1"
th:text="${word}"
th:data-spamid="${spam.spamId}"
@ -141,15 +143,43 @@
</span>
</td>
<!-- <td th:text="${spam.keywords}">복합명사 목록</td>-->
<td th:text="${spam.chcKeywords}">핵심 단어 목록</td>
<td>
<select class="form-control" th:value="${spam.categ}">
<option>선택</option>
<option value="성인">성인</option>
<option value="불법">불법</option>
<option value="해당없음">해당없음</option>
<span th:each="word : ${spam.chcKeywordList}">
<button class="btn btn-success rounded-pill mx-1 my-1"
th:text="${word}"
th:data-spamid="${spam.spamId}"
th:data-word="${word}"
th:data-original-td="2"
th:data-current-td="3"
onclick="handleClick(this)"></button>
</span>
</td>
<td>
<select class="form-control select2 rsnCode01"
onchange="updateCode02Options(this)">
<option value="">선택해주세요.</option>
<option th:each="code : ${rsnCode01List}"
th:value="${code.codeId}"
th:text="${code.codeName}"
th:selected="${code.codeId} == ${spam.spamRsnCode01}">
Code
</option>
</select>
</td>
<td>
<select class="form-control select2 rsnCode02">
<option value="">선택해주세요.</option>
<option th:each="code : ${spam.codeDetailList}"
th:value="${code.codeId}"
th:text="${code.codeName}"
th:selected="${code.codeId} == ${spam.spamRsnCode02}">
Code
</option>
</select>
</td>
<!-- <td>-->
<!-- <button class="btn btn-success btn-confirm" data-spamid="${spam.spamId}" onclick="confirmSpam(this)">확정</button>-->
<!-- </td>-->
</tr>
</tbody>
</table>
@ -209,34 +239,137 @@
</div>
<!-- ./wrapper -->
<!-- &lt;!&ndash; DataTables & Plugins &ndash;&gt;-->
<!-- <script th:src="@{/plugins/datatables/jquery.dataTables.min.js}"></script>-->
<!-- <script th:src="@{/plugins/datatables-bs4/js/dataTables.bootstrap4.min.js}"></script>-->
<!-- <script th:src="@{/plugins/datatables-responsive/js/dataTables.responsive.min.js}"></script>-->
<!-- <script th:src="@{/plugins/datatables-responsive/js/responsive.bootstrap4.min.js}"></script>-->
<!-- <script th:src="@{/plugins/datatables-buttons/js/dataTables.buttons.min.js}"></script>-->
<!-- <script th:src="@{/plugins/datatables-buttons/js/buttons.bootstrap4.min.js}"></script>-->
<!-- <script th:src="@{/plugins/jszip/jszip.min.js}"></script>-->
<!-- <script th:src="@{/plugins/pdfmake/pdfmake.min.js}"></script>-->
<!-- <script th:src="@{/plugins/pdfmake/vfs_fonts.js}"></script>-->
<!-- <script th:src="@{/plugins/datatables-buttons/js/buttons.html5.min.js}"></script>-->
<!-- <script th:src="@{/plugins/datatables-buttons/js/buttons.print.min.js}"></script>-->
<!-- <script th:src="@{/plugins/datatables-buttons/js/buttons.colVis.min.js}"></script>-->
<script>
$(function () {
$(document).ready(function() {
// 초기화 이후에 값을 출력
// $('.rsnCode01').each(function() {
// console.log($(this).val());
// });
});
function handleClick(button) {
var spamId = button.getAttribute('data-spamid');
var word = button.getAttribute('data-word');
console.log("Spam ID:", spamId, "Word:", word);
// 여기에 추가적인 로직을 구현하세요.
var currentTd = button.parentElement.parentElement; // 현재 td 요소
var nextTd;
// 부모 요소인 span을 찾아서 제거
var span = button.parentElement;
span.remove();
// 버튼이 처음 클릭된 상태인지 확인하기 위해 data-original-td 속성을 체크
if (!button.hasAttribute('data-original-td')) {
// 초기 클릭 시, 원래의 td를 data-original-td 속성에 저장
button.setAttribute('data-original-td', currentTd.cellIndex);
button.setAttribute('data-current-td', currentTd.cellIndex);
}
var originalTdIndex = button.getAttribute('data-original-td'); // 원래 td의 인덱스
var currentTdIndex = button.getAttribute('data-current-td'); // 현재 td의 인덱스
if (currentTdIndex == originalTdIndex) {
// 버튼이 원래 위치에 있는 경우 다음 td로 이동
nextTd = currentTd.nextElementSibling;
button.setAttribute('data-current-td', nextTd.cellIndex);
} else {
// 버튼이 다음 위치에 있는 경우 원래 위치로 이동
nextTd = currentTd.previousElementSibling;
button.setAttribute('data-current-td', originalTdIndex);
}
if (nextTd) {
// 버튼을 새 span으로 감싸서 다음 또는 이전 td에 추가
var newSpan = document.createElement('span');
newSpan.appendChild(button);
// 버튼의 CSS 클래스 변경 (색상 변경)
if (currentTdIndex == originalTdIndex) {
button.classList.remove('btn-primary'); // 기존 색상 클래스 제거
button.classList.add('btn-success'); // 새 색상 클래스 추가
fn_chcKeywordUpdate(spamId, word);
} else {
button.classList.remove('btn-success'); // 기존 색상 클래스 제거
button.classList.add('btn-primary'); // 원래 색상 클래스 추가
fn_keywordUpdate(spamId, word);
}
// 다음 또는 이전 td에 새로운 span 추가
nextTd.appendChild(newSpan);
}
}
//rsnCode01 onchange
function updateCode02Options(selectElement) {
var codeGroup = $(selectElement).val(); // 선택된 spam_rsn_code_01 값 가져오기
// selectElement의 부모 <td>의 다음 <td>를 찾고, 그 안의 .rsnCode02 클래스를 가진 <select> 요소를 선택
var code02Select = $(selectElement).closest('td').next('td').find('.rsnCode02');
if (codeGroup) {
// 서버에 AJAX 요청 보내기
$.ajax({
url: '/api/code-details/' + "SPAM"+codeGroup,
method: 'GET',
success: function(data) {
// 현재의 옵션들을 제거하고 새로운 옵션으로 대체
code02Select.empty();
code02Select.append('<option value="">선택해주세요.</option>');
data.forEach(function(code) {
code02Select.append('<option value="' + code.codeId + '">' + code.codeName + '</option>');
});
// spam.spamRsnCode02에 대한 초기 선택 설정 (만약 필요한 경우)
var selectedCode02 = code02Select.data('selected-value');
if (selectedCode02) {
code02Select.val(selectedCode02);
}
},
error: function(xhr) {
console.error("Failed to fetch code details.");
}
});
} else {
code02Select.empty();
code02Select.append('<option value="">선택해주세요.</option>');
}
}
function fn_keywordUpdate(spamId, word){
// 한글이 포함된 word를 URL 인코딩합니다.
var encodedWord = encodeURIComponent(word);
$.ajax({
url: '/mjon/spam/keyword/' + spamId + '/' + encodedWord,
type: 'PUT',
success: function(data) {
fn_successAlert("성공", data.msg);
},
error: function(xhr, data) {
fn_failedAlert("실패", data.msg);
}
});
}
function fn_chcKeywordUpdate(spamId, word){
// 한글이 포함된 word를 URL 인코딩합니다.
var encodedWord = encodeURIComponent(word);
$.ajax({
url: '/mjon/spam/chcKeyword/' + spamId + '/' + encodedWord,
type: 'PUT',
success: function(data) {
fn_successAlert("성공", data.msg);
},
error: function(xhr, data) {
fn_failedAlert("실패", data.msg);
}
});
}
// fn_failedAlert("실패", data.msg);
// fn_successAlert("성공", data.data +'가 '+data.msg);
</script>