김의진 3 months ago
commit be5bed3ef7

@ -18,3 +18,4 @@ create table tb_actn_info
DLTR varchar(11) null comment '삭제자'
)
comment '조치 정보';

@ -8,13 +8,13 @@ create table tb_crdn_photo
CRDN_PHOTO_PATH varchar(100) null comment '단속 사진 경로',
CRDN_PHOTO_NM varchar(500) null comment '단속 사진 명',
CRDN_PHOTO_SE_CD char not null comment '단속 사진 구분 코드',
AACTN_INFO_ID varchar(10) null comment '조치 정보 ID';
ORGNL_PHOTO_NM varchar(500) null comment '원본 사진 명',
REG_DT datetime null comment '등록 일시',
RGTR varchar(11) null comment '등록자',
DEL_YN char not null comment '삭제 여부',
DEL_DT datetime null comment '삭제 일시',
DLTR varchar(11) null comment '삭제자',
ACTN_INFO_ID varchar(10) null comment '조치 정보 ID',
primary key (ACT_INFO_ID, CRDN_PHOTO_SN)
)
comment '단속 사진';

@ -29,7 +29,9 @@ import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
*
@ -325,6 +327,7 @@ public class CrdnActInfoController {
@GetMapping("/photoView.do")
public String photoView(
@Parameter(description = "행위정보ID") @RequestParam String actInfoId,
@Parameter(description = "조치정보ID") @RequestParam(required = false) String actnInfoId,
@Parameter(description = "사진순번") @RequestParam String crdnPhotoSn,
@Parameter(description = "단속조치사진구분") @RequestParam String crdnPhotoSeCd,
Model model) {
@ -334,6 +337,7 @@ public class CrdnActInfoController {
// 중요한 로직 주석: 해당 행위정보의 모든 사진 목록 조회
CrdnPhotoVO searchVO = CrdnPhotoVO.builder()
.actInfoId(actInfoId)
.actnInfoId(actnInfoId)
.crdnPhotoSeCd(crdnPhotoSeCd) // 단속 사진
.build();
@ -433,4 +437,229 @@ public class CrdnActInfoController {
return ApiResponseUtil.error("사진 삭제에 실패했습니다.");
}
}
// ======================================= 여기서부터 조치 정보 ==========================================
/**
* (View)
* : .
* @param actInfoId ID
* @param crdnYr
* @param crdnNo
* @param mode
* @param model
* @return JSP
*/
@Operation(summary = "조치정보 관리 팝업", description = "조치정보 관리 팝업을 반환합니다.")
@GetMapping("/crdnActnInfoRegistPopup.do")
public ModelAndView actnInfoManage(
@Parameter(description = "행위정보ID") @RequestParam String actInfoId,
@Parameter(description = "단속연도") @RequestParam String crdnYr,
@Parameter(description = "단속번호") @RequestParam String crdnNo,
@Parameter(description = "위치정보ID") @RequestParam String pstnInfoId,
@Parameter(description = "화면모드") @RequestParam(required = false, defaultValue = "U") String mode,
Model model) {
log.debug("조치정보 관리 팝업 요청 - actInfoId: {}, crdnYr: {}, crdnNo: {}, mode: {}", actInfoId, crdnYr, crdnNo, mode);
ModelAndView mav = new ModelAndView("crdn/crndRegistAndView/crdnActInfo/crdnActnInfoRegistPopup" + TilesConstants.POPUP);
mav.addObject("mode", mode);
mav.addObject("actInfoId", actInfoId);
mav.addObject("pstnInfoId", pstnInfoId);
mav.addObject("crdnYr", crdnYr);
mav.addObject("crdnNo", crdnNo);
return mav;
}
/**
* (AJAX)
* : .
* @param vo (actInfoId )
* @return
*/
@Operation(summary = "조치정보 목록 조회", description = "특정 행위정보에 속한 조치정보 목록을 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "조회 성공"),
@ApiResponse(responseCode = "500", description = "서버 오류")
})
@PostMapping("/actnInfoList.ajax")
public ResponseEntity<?> actnInfoListAjax(@ModelAttribute CrdnActnInfoVO vo) {
log.debug("조치정보 목록 조회 요청: {}", vo);
// 1. 총 개수 조회
int totalCount = service.selectActnInfoListTotalCount(vo);
// 2. 응답 데이터 구성
vo.setTotalCount(totalCount);
List<CrdnActnInfoVO> list = service.selectActnInfoList(vo);
return ApiResponseUtil.successWithGrid(list, vo);
}
/**
* (AJAX)
* : ID .
* @param actnInfoId ID
* @return
*/
@Operation(summary = "조치정보 상세 조회", description = "조치정보 ID로 상세 정보를 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "조회 성공"),
@ApiResponse(responseCode = "500", description = "서버 오류")
})
@PostMapping("/actnInfoDetail.ajax")
public ResponseEntity<?> actnInfoDetailAjax(@RequestParam String actnInfoId) {
log.debug("조치정보 상세 조회 요청: {}", actnInfoId);
CrdnActnInfoVO result = service.selectActnInfoByPk(actnInfoId);
if (result != null) {
return ApiResponseUtil.success(result);
} else {
return ApiResponseUtil.error("조치정보를 찾을 수 없습니다.");
}
}
/**
* (AJAX)
* : .
* @param vo
* @param actnPhotoFiles ()
* @return
*/
@Operation(summary = "조치정보 저장", description = "조치정보를 등록합니다. 조치사진도 함께 처리 가능합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "등록 성공"),
@ApiResponse(responseCode = "500", description = "서버 오류")
})
@PostMapping("/saveActnInfo.ajax")
public ResponseEntity<?> actnInfoInsertAjax(
@ModelAttribute CrdnActnInfoVO vo,
@RequestParam(value = "actnPhotoFiles", required = false) List<MultipartFile> actnPhotoFiles) {
log.debug("조치정보 등록 요청: {}, 조치사진 개수: {}",
vo, actnPhotoFiles != null ? actnPhotoFiles.size() : 0);
CrdnRegistAndViewVO crdnVO = new CrdnRegistAndViewVO();
crdnVO.setCrdnYr(vo.getCrdnYr());
crdnVO.setCrdnNo(vo.getCrdnNo());
CrdnRegistAndViewVO data = crdnRegistAndViewService.selectOne(crdnVO);
vo.setSggCd(data.getSggCd());
vo.setRgtr(SessionUtil.getUserId());
vo.setMdfr(SessionUtil.getUserId());
int result = 0;
if( vo.getActnInfoId() == null || vo.getActnInfoId().isEmpty() ){
//등록
result = service.insertActnInfoWithFiles(vo, actnPhotoFiles);
} else{
result = service.updateActnInfoWithFiles(vo, actnPhotoFiles);
}
if (result > 0) {
return ApiResponseUtil.success("조치정보가 성공적으로 저장되었습니다.");
} else {
return ApiResponseUtil.error("조치정보 저장에 실패했습니다.");
}
}
/**
* (AJAX)
* : .
* @param actnInfoIds ID
* @return
*/
@Operation(summary = "조치정보 삭제", description = "조치정보를 삭제합니다. 관련된 조치사진도 함께 삭제됩니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "삭제 성공"),
@ApiResponse(responseCode = "500", description = "서버 오류")
})
@PostMapping("/deleteActnInfo.ajax")
public ResponseEntity<?> actnInfoDeleteAjax(@RequestParam List<String> actnInfoIds) {
log.debug("조치정보 삭제 요청: {}", actnInfoIds);
int result = service.deleteActnInfos(actnInfoIds);
if (result > 0) {
return ApiResponseUtil.success("조치정보 " + result + "건이 성공적으로 삭제되었습니다.");
} else {
return ApiResponseUtil.error("조치정보 삭제에 실패했습니다.");
}
}
/**
* (AJAX)
* : .
* @param actnInfoId ID
* @return
*/
@Operation(summary = "조치사진 목록 조회", description = "특정 조치정보에 속한 조치사진 목록을 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "조회 성공"),
@ApiResponse(responseCode = "500", description = "서버 오류")
})
@GetMapping("/selectActnPhotos.ajax")
public ResponseEntity<?> selectActnPhotosAjax(@RequestParam String actnInfoId) {
log.debug("조치사진 목록 조회 요청: {}", actnInfoId);
try {
// 중요한 로직 주석: 조치사진 조회 조건 생성 (CRDN_PHOTO_SE_CD = '2': 조치사진)
CrdnPhotoVO photoVO = CrdnPhotoVO.builder()
.actnInfoId(actnInfoId) // 조치정보 ID
.crdnPhotoSeCd("2") // 조치사진 구분코드
.build();
List<CrdnPhotoVO> photoList = photoService.selectPhotoList(photoVO);
return ApiResponseUtil.success(photoList);
} catch (Exception e) {
log.error("조치사진 목록 조회 중 오류 발생: {}", actnInfoId, e);
return ApiResponseUtil.error("조치사진 목록 조회에 실패했습니다: " + e.getMessage());
}
}
/**
* (AJAX)
* : Ajax , DB .
* @param actnInfoId ID
* @param crdnPhotoSn
* @return
*/
@Operation(summary = "조치사진 개별 삭제", description = "조치사진을 개별적으로 삭제합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "삭제 성공"),
@ApiResponse(responseCode = "500", description = "서버 오류")
})
@PostMapping("/deleteActnPhoto.ajax")
public ResponseEntity<?> deleteActnPhotoAjax(
@Parameter(description = "조치정보ID") @RequestParam String actnInfoId,
@Parameter(description = "사진순번") @RequestParam String crdnPhotoSn) {
log.debug("조치사진 개별 삭제 요청 - actnInfoId: {}, crdnPhotoSn: {}", actnInfoId, crdnPhotoSn);
try {
// 중요한 로직 주석: 삭제할 조치사진 정보 생성
CrdnPhotoVO photoVO = CrdnPhotoVO.builder()
.actnInfoId(actnInfoId) // 조치정보 ID
.crdnPhotoSn(crdnPhotoSn)
.dltr(SessionUtil.getUserId())
.build();
// 중요한 로직 주석: 조치사진 삭제 (논리삭제 + 파일삭제)
int result = photoService.deletePhotoWithFile(photoVO);
if (result > 0) {
return ApiResponseUtil.success("조치사진이 성공적으로 삭제되었습니다.");
} else {
return ApiResponseUtil.error("조치사진 삭제에 실패했습니다.");
}
} catch (Exception e) {
log.error("조치사진 삭제 중 오류 발생: actnInfoId={}, crdnPhotoSn={}", actnInfoId, crdnPhotoSn, e);
return ApiResponseUtil.error("조치사진 삭제 중 오류가 발생했습니다: " + e.getMessage());
}
}
}

@ -90,4 +90,48 @@ public interface CrdnActInfoMapper {
int existsLevyInfoList(List<String> pstnInfoIds);
// ================================ 조치정보 관련 메서드 ================================
/**
*
* @param vo (actInfoId )
* @return
*/
List<CrdnActnInfoVO> selectActnInfoList(CrdnActnInfoVO vo);
/**
*
* @param vo (actInfoId )
* @return
*/
int selectActnInfoListTotalCount(CrdnActnInfoVO vo);
/**
* (PK )
* @param actnInfoId ID
* @return
*/
CrdnActnInfoVO selectActnInfoByPk(String actnInfoId);
/**
*
* @param vo
* @return
*/
int insertActnInfo(CrdnActnInfoVO vo);
/**
*
* @param vo
* @return
*/
int updateActnInfo(CrdnActnInfoVO vo);
/**
* ()
* @param vo (actnInfoId, dltr )
* @return
*/
int deleteActnInfo(CrdnActnInfoVO vo);
}

@ -77,4 +77,14 @@ public interface CrdnPhotoMapper {
* @return (1: , 0: )
*/
int existsPhoto(CrdnPhotoVO photoVO);
// ================================ 조치사진 관련 메서드 ================================
/**
*
* : AACTN_INFO_ID .
* @param photoVO ID (actnInfoId)
* @return
*/
List<CrdnPhotoVO> selectPhotoListByActnInfoId(CrdnPhotoVO photoVO);
}

@ -0,0 +1,56 @@
package go.kr.project.crdn.crndRegistAndView.crdnActInfo.model;
import com.fasterxml.jackson.annotation.JsonFormat;
import go.kr.project.common.model.PagingVO;
import lombok.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* VO
* : tb_actn_info , .
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CrdnActnInfoVO extends PagingVO {
// ============ 테이블 컬럼 ============
private String actnInfoId; // 조치 정보 ID (PK)
private String sggCd; // 시군구 코드
private String crdnYr; // 단속 연도
private String crdnNo; // 단속 번호
private String actInfoId; // 행위 정보 ID (FK - tb_act_info)
private String actnYmd; // 조치 일자 (YYYYMMDD)
private BigDecimal actnArea; // 조치 면적
private String actnRmrk; // 조치 비고
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime regDt; // 등록 일시
private String rgtr; // 등록자
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime mdfcnDt; // 수정 일시
private String mdfr; // 수정자
private String delYn; // 삭제 여부
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime delDt; // 삭제 일시
private String dltr; // 삭제자
// ============ 조회용 추가 필드 ============
private int rownum; // 행 번호 (목록 조회용)
// ============ 검색 조건 필드 ============
private String searchActInfoId; // 검색용 행위정보 ID
private String searchStartYmd; // 검색 시작일자
private String searchEndYmd; // 검색 종료일자
}

@ -30,6 +30,7 @@ public class CrdnPhotoVO {
private String crdnPhotoNm; // 단속 사진 명
private String crdnPhotoSeCd; // 단속 사진 구분 코드 (01:단속, 02:조치)
private String orgnlPhotoNm; // 원본 사진 명
private String actnInfoId; // 조치정보ID
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")

@ -96,4 +96,61 @@ public interface CrdnActInfoService {
*/
List<CrdnPstnIdxVO> getAllPstnIdx();
// ================================ 조치정보 관련 메서드 ================================
/**
*
* : .
* @param vo (actInfoId )
* @return
*/
List<CrdnActnInfoVO> selectActnInfoList(CrdnActnInfoVO vo);
/**
*
* @param vo (actInfoId )
* @return
*/
int selectActnInfoListTotalCount(CrdnActnInfoVO vo);
/**
* (PK )
* @param actnInfoId ID
* @return
*/
CrdnActnInfoVO selectActnInfoByPk(String actnInfoId);
/**
* ( )
* : . .
* @param vo
* @param actnPhotoFiles ()
* @return
*/
int insertActnInfoWithFiles(CrdnActnInfoVO vo, List<MultipartFile> actnPhotoFiles);
/**
* ( )
* : . .
* @param vo
* @param actnPhotoFiles ()
* @return
*/
int updateActnInfoWithFiles(CrdnActnInfoVO vo, List<MultipartFile> actnPhotoFiles);
/**
* ()
* : .
* @param vo (actnInfoId, dltr )
* @return
*/
int deleteActnInfo(CrdnActnInfoVO vo);
/**
* ()
* @param actnInfoIds ID
* @return
*/
int deleteActnInfos(List<String> actnInfoIds);
}

@ -1,6 +1,7 @@
package go.kr.project.crdn.crndRegistAndView.crdnActInfo.service;
import go.kr.project.common.model.FileVO;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.model.CrdnActnInfoVO;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.model.CrdnPhotoVO;
import org.springframework.web.multipart.MultipartFile;
@ -93,4 +94,31 @@ public interface CrdnPhotoService {
* @return (01, 02, 03 )
*/
String getNextPhotoSn(String actInfoId);
// ================================ 조치사진 관련 메서드 ================================
/**
*
* : AACTN_INFO_ID ID .
* @param files
* @param actnInfoVO
* @return
*/
int insertActnPhotosWithFiles(List<MultipartFile> files, CrdnActnInfoVO actnInfoVO);
/**
*
* @param photoVO ID
* @return
*/
List<CrdnPhotoVO> selectPhotoList(CrdnPhotoVO photoVO);
/**
*
* : .
* @param actnInfoId ID
* @param dltr ID
* @return
*/
int deleteActnPhotosByActnInfoId(String actnInfoId, String dltr);
}

@ -263,4 +263,173 @@ public class CrdnActInfoServiceImpl extends EgovAbstractServiceImpl implements C
log.info("불법위반행위정보 일괄 삭제 완료. 요청: {}건, 삭제: {}건", actInfoIds.size(), deletedCount);
return deletedCount;
}
// ================================ 조치정보 관련 메서드 구현 ================================
@Override
public List<CrdnActnInfoVO> selectActnInfoList(CrdnActnInfoVO vo) {
log.debug("조치정보 목록 조회: {}", vo);
return mapper.selectActnInfoList(vo);
}
@Override
public int selectActnInfoListTotalCount(CrdnActnInfoVO vo) {
log.debug("조치정보 총 개수 조회: {}", vo);
return mapper.selectActnInfoListTotalCount(vo);
}
@Override
public CrdnActnInfoVO selectActnInfoByPk(String actnInfoId) {
log.debug("조치정보 상세 조회: {}", actnInfoId);
return mapper.selectActnInfoByPk(actnInfoId);
}
@Override
@Transactional
public int insertActnInfoWithFiles(CrdnActnInfoVO vo, List<MultipartFile> actnPhotoFiles) {
log.debug("조치정보 등록 (파일 포함): {}, 조치사진 개수: {}",
vo, actnPhotoFiles != null ? actnPhotoFiles.size() : 0);
try {
// 중요한 로직 주석: 먼저 조치정보를 등록한다 (insertActnInfo 메서드에서 actnInfoId가 자동 설정됨)
int result = mapper.insertActnInfo(vo);
if (result > 0) {
log.debug("조치정보 등록 완료: actnInfoId={}", vo.getActnInfoId());
// 중요한 로직 주석: 조치사진이 있는 경우 처리 (CRDN_PHOTO_SE_CD = '2')
// 조치사진은 AACTN_INFO_ID 컬럼에 조치정보 ID를 저장한다
if (actnPhotoFiles != null && !actnPhotoFiles.isEmpty()) {
// 중요한 로직 주석: 조치사진을 행위정보에 연결하되, ACTN_INFO_ID에는 조치정보 ID를 저장
int actnPhotoResult = photoService.insertActnPhotosWithFiles(actnPhotoFiles, vo);
log.debug("조치사진 파일 처리 완료: actnInfoId={}, 등록된 조치사진 수={}", vo.getActnInfoId(), actnPhotoResult);
}
} else {
log.error("조치정보 등록 실패: {}", vo);
throw new MessageException("조치정보 등록에 실패했습니다.");
}
return result;
} catch (Exception e) {
log.error("조치정보 등록 (파일 포함) 중 오류 발생: {}", vo, e);
throw new MessageException("조치정보 등록 중 오류가 발생했습니다: " + e.getMessage());
}
}
@Override
@Transactional
public int updateActnInfoWithFiles(CrdnActnInfoVO vo, List<MultipartFile> actnPhotoFiles) {
log.debug("조치정보 수정 (파일 포함): {}, 조치사진 개수: {}",
vo, actnPhotoFiles != null ? actnPhotoFiles.size() : 0);
try {
// 중요한 로직 주석: 먼저 조치정보를 수정한다
int result = mapper.updateActnInfo(vo);
if (result > 0) {
log.debug("조치정보 수정 완료: actnInfoId={}", vo.getActnInfoId());
if (actnPhotoFiles != null && !actnPhotoFiles.isEmpty()) {
// 중요한 로직 주석: 조치사진을 행위정보에 연결하되, ACTN_INFO_ID에는 조치정보 ID를 저장
int actnPhotoResult = photoService.insertActnPhotosWithFiles(actnPhotoFiles, vo);
log.debug("조치사진 파일 처리 완료: actnInfoId={}, 등록된 조치사진 수={}", vo.getActnInfoId(), actnPhotoResult);
}
} else {
log.error("조치정보 수정 실패: {}", vo);
throw new MessageException("조치정보 수정에 실패했습니다.");
}
return result;
} catch (Exception e) {
log.error("조치정보 수정 (파일 포함) 중 오류 발생: {}", vo, e);
throw new MessageException("조치정보 수정 중 오류가 발생했습니다: " + e.getMessage());
}
}
@Override
@Transactional
public int deleteActnInfo(CrdnActnInfoVO vo) {
log.debug("조치정보 삭제: {}", vo);
// 중요한 로직 주석: 조치정보 삭제 시 관련된 모든 조치사진도 함께 삭제한다.
if (vo.getActnInfoId() != null) {
try {
// 중요한 로직 주석: 조치사진 삭제 (AACTN_INFO_ID = 조치정보ID, CRDN_PHOTO_SE_CD = '2')
photoService.deleteActnPhotosByActnInfoId(vo.getActnInfoId(), vo.getDltr());
log.debug("조치정보 관련 조치사진 삭제 완료: actnInfoId={}", vo.getActnInfoId());
} catch (Exception e) {
log.warn("조치정보 관련 사진 삭제 중 오류 발생: actnInfoId={}", vo.getActnInfoId(), e);
// 사진 삭제 실패해도 조치정보 삭제는 진행
}
}
return mapper.deleteActnInfo(vo);
}
/**
* ()
* : .
* ID , .
* @param actnInfoIds ID
* @return
*/
@Override
@Transactional
public int deleteActnInfos(List<String> actnInfoIds) {
log.debug("조치정보 일괄 삭제: {}", actnInfoIds);
// 중요한 로직 주석: 삭제할 ID 목록이 비어있는 경우 처리 중단
if (actnInfoIds == null || actnInfoIds.isEmpty()) {
log.warn("삭제할 조치정보ID 목록이 비어있습니다.");
return 0;
}
// 중요한 로직 주석: 세션에서 현재 사용자 정보를 가져와 삭제자로 설정
String userId = SessionUtil.getUserId();
if (userId == null || userId.trim().isEmpty()) {
log.error("삭제자 정보를 가져올 수 없습니다.");
throw new MessageException("사용자 정보를 확인할 수 없습니다.");
}
// 중요한 로직 주석: 각 조치정보 ID에 대해 유효성 검증 후 논리 삭제 수행
int deletedCount = 0;
for (String actnInfoId : actnInfoIds) {
if (actnInfoId != null && !actnInfoId.trim().isEmpty()) {
// 중요한 로직 주석: 삭제할 데이터 존재 여부 확인
CrdnActnInfoVO existingData = mapper.selectActnInfoByPk(actnInfoId);
if (existingData == null) {
log.warn("삭제할 조치정보를 찾을 수 없습니다. actnInfoId: {}", actnInfoId);
continue;
}
// 중요한 로직 주석: 해당 조치정보의 관련 사진들을 먼저 삭제한다.
try {
photoService.deleteActnPhotosByActnInfoId(actnInfoId, userId);
log.debug("조치정보 관련 사진 삭제 완료: actnInfoId={}", actnInfoId);
} catch (Exception e) {
log.warn("조치정보 관련 사진 삭제 중 오류 발생: actnInfoId={}", actnInfoId, e);
// 사진 삭제 실패해도 조치정보 삭제는 진행
}
// 중요한 로직 주석: 삭제 VO 생성 및 삭제자 정보 설정
CrdnActnInfoVO deleteVO = CrdnActnInfoVO.builder()
.actnInfoId(actnInfoId)
.dltr(userId)
.build();
int result = mapper.deleteActnInfo(deleteVO);
if (result > 0) {
deletedCount++;
log.debug("조치정보 삭제 완료. actnInfoId: {}", actnInfoId);
} else {
log.warn("조치정보 삭제 실패. actnInfoId: {}", actnInfoId);
}
}
}
log.info("조치정보 일괄 삭제 완료. 요청: {}건, 삭제: {}건", actnInfoIds.size(), deletedCount);
return deletedCount;
}
}

@ -6,6 +6,7 @@ import egovframework.util.SessionUtil;
import go.kr.project.common.model.FileVO;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.mapper.CrdnPhotoMapper;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.model.CrdnActInfoVO;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.model.CrdnActnInfoVO;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.model.CrdnPhotoVO;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.service.CrdnPhotoService;
import lombok.RequiredArgsConstructor;
@ -150,8 +151,7 @@ public class CrdnPhotoServiceImpl extends EgovAbstractServiceImpl implements Crd
}
// 중요한 로직 주석: 새 파일 업로드
List<MultipartFile> fileList = Arrays.asList(file);
List<FileVO> uploadedFiles = fileUtil.uploadFiles(fileList, "crdn-act-photo");
List<FileVO> uploadedFiles = fileUtil.uploadFiles(Arrays.asList(file), "crdn-act-photo");
if (!uploadedFiles.isEmpty()) {
FileVO newFileVO = uploadedFiles.get(0);
@ -291,4 +291,140 @@ public class CrdnPhotoServiceImpl extends EgovAbstractServiceImpl implements Crd
log.debug("다음 사진 순번 조회: actInfoId={}", actInfoId);
return mapper.selectNextPhotoSn(actInfoId);
}
// ================================ 조치사진 관련 메서드 구현 ================================
@Override
@Transactional
public int insertActnPhotosWithFiles(List<MultipartFile> files, CrdnActnInfoVO actnInfoVO) {
log.debug("조치사진 파일 업로드 및 등록: actnInfoId={}, 파일 수={}", files != null ? files.size() : 0);
if (files == null || files.isEmpty()) {
log.debug("업로드할 조치사진 파일이 없습니다.");
return 0;
}
try {
int savedCount = 0;
// 중요한 로직 주석: 각 파일을 순회하며 저장 처리
for (MultipartFile file : files) {
if (file.isEmpty()) {
log.warn("빈 파일이 포함되어 있어 건너뜁니다: {}", file.getOriginalFilename());
continue;
}
try {
// 중요한 로직 주석: FileUtil을 통한 파일 저장 (단일 파일을 리스트로 변환하여 처리)
List<MultipartFile> singleFileList = Arrays.asList(file);
List<FileVO> uploadedFiles = fileUtil.uploadFiles(singleFileList, "crdn-act-photo");
if (uploadedFiles.isEmpty()) {
log.error("파일 저장 실패: {}", file.getOriginalFilename());
continue;
}
FileVO fileVO = uploadedFiles.get(0);
// 중요한 로직 주석: 다음 사진 순번 생성
String nextPhotoSn = getNextPhotoSn(actnInfoVO.getActInfoId());
// 중요한 로직 주석: 조치사진 정보 생성 (CRDN_PHOTO_SE_CD = '2', AACTN_INFO_ID에 조치정보 ID 저장)
CrdnPhotoVO photoVO = CrdnPhotoVO.builder()
.actInfoId(actnInfoVO.getActInfoId()) // 부모 행위정보 ID
.crdnPhotoSn(nextPhotoSn) // 사진 순번
.sggCd(actnInfoVO.getSggCd()) // 시군구 코드
.crdnYr(actnInfoVO.getCrdnYr()) // 단속 연도
.crdnNo(actnInfoVO.getCrdnNo()) // 단속 번호
.crdnPhotoPath(fileVO.getFilePath()) // 파일 경로
.crdnPhotoNm(fileVO.getStoredFileNm()) // 저장된 파일명
.crdnPhotoSeCd("2") // 조치사진 구분코드
.actnInfoId(actnInfoVO.getActnInfoId()) // 조치정보 ID (조치사진 연결용)
.orgnlPhotoNm(fileVO.getOriginalFileNm()) // 원본 파일명
.rgtr(SessionUtil.getUserId()) // 등록자
.delYn("N") // 삭제 여부
.build();
int result = mapper.insertPhoto(photoVO);
if (result > 0) {
savedCount++;
log.debug("조치사진 등록 완료: actInfoId={}, photoSn={}, fileName={}",
actnInfoVO.getActInfoId(), nextPhotoSn, file.getOriginalFilename());
}
} catch (Exception e) {
log.error("조치사진 파일 처리 중 오류: {}", file.getOriginalFilename(), e);
// 개별 파일 오류는 전체 처리를 중단하지 않음
}
}
log.info("조치사진 파일 업로드 완료: actnInfoId={}, 총 파일 수={}, 저장된 파일 수={}", actnInfoVO.getActnInfoId(), files.size(), savedCount);
return savedCount;
} catch (Exception e) {
log.error("조치사진 파일 업로드 중 오류 발생: actnInfoId={}", actnInfoVO.getActnInfoId(), e);
throw new MessageException("조치사진 업로드에 실패했습니다: " + e.getMessage());
}
}
@Override
public List<CrdnPhotoVO> selectPhotoList(CrdnPhotoVO photoVO) {
log.debug("조치사진 목록 조회: {}", photoVO);
return mapper.selectPhotoListByActnInfoId(photoVO);
}
@Override
@Transactional
public int deleteActnPhotosByActnInfoId(String actnInfoId, String dltr) {
log.debug("조치정보 관련 모든 조치사진 삭제: actnInfoId={}", actnInfoId);
if (actnInfoId == null || actnInfoId.trim().isEmpty()) {
log.warn("조치정보 ID가 없어서 조치사진을 삭제할 수 없습니다.");
return 0;
}
try {
// 중요한 로직 주석: 삭제할 조치사진 목록 조회 (ACTN_INFO_ID 기준)
CrdnPhotoVO searchVO = CrdnPhotoVO.builder()
.actnInfoId(actnInfoId)
.crdnPhotoSeCd("2") // 조치사진
.build();
List<CrdnPhotoVO> photoList = mapper.selectPhotoListByActnInfoId(searchVO);
int deletedCount = 0;
for (CrdnPhotoVO photo : photoList) {
// 중요한 로직 주석: 논리삭제 처리
CrdnPhotoVO deleteVO = CrdnPhotoVO.builder()
.actInfoId(photo.getActInfoId())
.crdnPhotoSn(photo.getCrdnPhotoSn())
.dltr(dltr)
.delYn("Y")
.delDt(LocalDateTime.now())
.build();
int result = mapper.deletePhoto(deleteVO);
if (result > 0) {
// 중요한 로직 주석: 실제 파일도 삭제
try {
FileVO fileVO = new FileVO();
fileVO.setFilePath(photo.getCrdnPhotoPath());
fileVO.setStoredFileNm(photo.getCrdnPhotoNm());
fileVO.setOriginalFileNm(photo.getOrgnlPhotoNm());
fileUtil.deleteFile(fileVO);
log.debug("조치사진 파일 삭제 완료: {}", photo.getCrdnPhotoPath());
} catch (Exception e) {
log.warn("조치사진 파일 삭제 실패 (DB는 삭제됨): {}", photo.getCrdnPhotoPath(), e);
}
deletedCount++;
}
}
log.info("조치정보 관련 모든 조치사진 삭제 완료: actnInfoId={}, 삭제된 사진 수={}", actnInfoId, deletedCount);
return deletedCount;
} catch (Exception e) {
log.error("조치사진 일괄 삭제 중 오류 발생: actnInfoId={}", actnInfoId, e);
throw new MessageException("조치사진 삭제에 실패했습니다: " + e.getMessage());
}
}
}

@ -107,6 +107,12 @@ public class CrdnRegistAndViewVO extends PagingVO {
/* 단속 처리 일자 */
private String crdnPrcsYmd;
/* 행위 유형 코드 */
private String actTypeCd;
/* 용도 지수 코드 */
private String usgIdxCd;
/** 등록 일시 */
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ -150,6 +156,15 @@ public class CrdnRegistAndViewVO extends PagingVO {
/** 소유자명들 (쉼표로 구분) */
private String ownrNams;
/** 행위자명들 (쉼표로 구분) */
private String actrNams;
/** 행위 유형 코드명 */
private String actTypeCdNm;
/** 용도 지수 코드명 */
private String usgIdxCdNm;
/** 지번 전체 주소 */
private String lotnoWholAddr;
@ -159,27 +174,7 @@ public class CrdnRegistAndViewVO extends PagingVO {
/** 역순 행 번호 (그리드 표시용) */
private Integer rowNum;
/* 사전처분 시작일 */
private String impltBgngYmd;
/* 사전처분 종료일 */
private String impltEndYmd;
/* 사전처분 상태 */
private String impltTaskSeCd;
/* 시정명령 시작일 */
private String impltBgngYmd2;
/* 시정명령 종료일 */
private String impltEndYmd2;
/* 시정명령 상태 */
private String impltTaskSeCd2;
/* 시정촉구 시작일 */
private String impltBgngYmd3;
/* 시정촉구 종료일 */
private String impltEndYmd3;
/* 시정촉구 상태 */
private String impltTaskSeCd3;
// ==================== 검색 조건 ====================

@ -339,4 +339,155 @@
AND DEL_YN = 'N'
</select>
<!-- ================================ 조치정보 관련 쿼리 ================================ -->
<!-- 조치정보 목록 조회 -->
<select id="selectActnInfoList" parameterType="CrdnActnInfoVO" resultType="CrdnActnInfoVO">
/* ActInfoMapper.selectActnInfoList : 조치정보 목록 조회 */
SELECT
(@ROWNUM := @ROWNUM + 1) AS ROWNUM,
a.ACTN_INFO_ID,
a.SGG_CD,
a.CRDN_YR,
a.CRDN_NO,
a.ACT_INFO_ID,
a.ACTN_YMD,
a.ACTN_AREA,
a.ACTN_RMRK,
a.REG_DT,
a.RGTR,
regUser.USER_NM AS RGTR_NM,
a.MDFCN_DT,
a.MDFR,
modUser.USER_NM AS MDFR_NM
FROM tb_actn_info a
LEFT JOIN tb_user regUser ON regUser.USER_ID = a.RGTR
LEFT JOIN tb_user modUser ON modUser.USER_ID = a.MDFR
CROSS JOIN (SELECT @ROWNUM := 0) r
WHERE a.DEL_YN = 'N'
<if test="actInfoId != null and actInfoId != ''">
AND a.ACT_INFO_ID = #{actInfoId}
</if>
<if test="searchActInfoId != null and searchActInfoId != ''">
AND a.ACT_INFO_ID = #{searchActInfoId}
</if>
<if test="searchStartYmd != null and searchStartYmd != ''">
AND a.ACTN_YMD >= #{searchStartYmd}
</if>
<if test="searchEndYmd != null and searchEndYmd != ''">
AND a.ACTN_YMD <![CDATA[<=]]> #{searchEndYmd}
</if>
ORDER BY a.REG_DT DESC, a.ACTN_INFO_ID DESC
<if test="pagingYn == 'Y'">
LIMIT #{firstIndex}, #{recordCountPerPage}
</if>
</select>
<!-- 조치정보 목록 총 개수 조회 -->
<select id="selectActnInfoListTotalCount" parameterType="CrdnActnInfoVO" resultType="int">
/* ActInfoMapper.selectActnInfoListTotalCount : 조치정보 목록 총 개수 조회 */
SELECT COUNT(*)
FROM tb_actn_info a
WHERE a.DEL_YN = 'N'
<if test="actInfoId != null and actInfoId != ''">
AND a.ACT_INFO_ID = #{actInfoId}
</if>
<if test="searchActInfoId != null and searchActInfoId != ''">
AND a.ACT_INFO_ID = #{searchActInfoId}
</if>
<if test="searchStartYmd != null and searchStartYmd != ''">
AND a.ACTN_YMD >= #{searchStartYmd}
</if>
<if test="searchEndYmd != null and searchEndYmd != ''">
AND a.ACTN_YMD <![CDATA[<=]]> #{searchEndYmd}
</if>
</select>
<!-- 조치정보 상세 조회 (PK 기준) -->
<select id="selectActnInfoByPk" parameterType="String" resultType="CrdnActnInfoVO">
/* ActInfoMapper.selectActnInfoByPk : 조치정보 상세 조회 */
SELECT
a.ACTN_INFO_ID,
a.SGG_CD,
a.CRDN_YR,
a.CRDN_NO,
a.ACT_INFO_ID,
a.ACTN_YMD,
a.ACTN_AREA,
a.ACTN_RMRK,
a.REG_DT,
a.RGTR,
regUser.USER_NM AS RGTR_NM,
a.MDFCN_DT,
a.MDFR,
modUser.USER_NM AS MDFR_NM,
a.DEL_YN
FROM tb_actn_info a
LEFT JOIN tb_user regUser ON regUser.USER_ID = a.RGTR
LEFT JOIN tb_user modUser ON modUser.USER_ID = a.MDFR
WHERE a.ACTN_INFO_ID = #{actnInfoId}
AND a.DEL_YN = 'N'
</select>
<!-- 조치정보 등록 -->
<insert id="insertActnInfo" parameterType="CrdnActnInfoVO">
/* ActInfoMapper.insertActnInfo : 조치정보 등록 */
<selectKey keyProperty="actnInfoId" resultType="String" order="BEFORE">
SELECT LPAD(NEXTVAL(seq_actn_info_id), 10, '0') FROM DUAL
</selectKey>
INSERT INTO tb_actn_info (
ACTN_INFO_ID,
SGG_CD,
CRDN_YR,
CRDN_NO,
ACT_INFO_ID,
ACTN_YMD,
ACTN_AREA,
ACTN_RMRK,
REG_DT,
RGTR,
MDFCN_DT,
MDFR,
DEL_YN
) VALUES (
#{actnInfoId},
#{sggCd},
#{crdnYr},
#{crdnNo},
#{actInfoId},
replace(#{actnYmd},'-',''),
#{actnArea},
#{actnRmrk},
NOW(),
#{rgtr},
NOW(),
#{mdfr},
'N'
)
</insert>
<!-- 조치정보 수정 -->
<update id="updateActnInfo" parameterType="CrdnActnInfoVO">
/* ActInfoMapper.updateActnInfo : 조치정보 수정 */
UPDATE tb_actn_info SET
ACTN_YMD = replace(#{actnYmd},'-',''),
ACTN_AREA = #{actnArea},
ACTN_RMRK = #{actnRmrk},
MDFCN_DT = NOW(),
MDFR = #{mdfr}
WHERE ACTN_INFO_ID = #{actnInfoId}
AND DEL_YN = 'N'
</update>
<!-- 조치정보 삭제 (논리삭제) -->
<update id="deleteActnInfo" parameterType="CrdnActnInfoVO">
/* ActInfoMapper.deleteActnInfo : 조치정보 삭제 (논리삭제) */
UPDATE tb_actn_info SET
DEL_YN = 'Y',
DEL_DT = NOW(),
DLTR = #{dltr}
WHERE ACTN_INFO_ID = #{actnInfoId}
AND DEL_YN = 'N'
</update>
</mapper>

@ -85,6 +85,7 @@
CRDN_PHOTO_NM,
CRDN_PHOTO_SE_CD,
ORGNL_PHOTO_NM,
ACTN_INFO_ID,
REG_DT,
RGTR,
DEL_YN
@ -98,6 +99,7 @@
#{crdnPhotoNm},
#{crdnPhotoSeCd},
#{orgnlPhotoNm},
#{actnInfoId},
NOW(),
#{rgtr},
'N'
@ -157,4 +159,44 @@
AND DEL_YN = 'N'
</select>
<!-- ================================ 조치사진 관련 쿼리 ================================ -->
<!-- 조치정보별 조치사진 목록 조회 -->
<select id="selectPhotoListByActnInfoId" parameterType="CrdnPhotoVO" resultType="CrdnPhotoVO">
/* CrdnPhotoMapper.selectPhotoListByActnInfoId : 조치정보별 조치사진 목록 조회 */
SELECT
p.ACT_INFO_ID,
p.CRDN_PHOTO_SN,
p.SGG_CD,
p.CRDN_YR,
p.CRDN_NO,
p.CRDN_PHOTO_PATH,
p.CRDN_PHOTO_NM,
p.CRDN_PHOTO_SE_CD,
p.ORGNL_PHOTO_NM,
p.ACTN_INFO_ID,
p.REG_DT,
p.RGTR,
p.DEL_YN,
p.DEL_DT,
p.DLTR
FROM tb_crdn_photo p
WHERE p.DEL_YN = 'N'
<choose>
<!-- actnInfoId가 있는 경우: 조치정보 ID로 조치사진 조회 -->
<when test="actnInfoId != null and actnInfoId != ''">
AND p.ACTN_INFO_ID = #{actnInfoId}
AND p.CRDN_PHOTO_SE_CD = '2'
</when>
<!-- 일반적인 경우: 행위정보 ID로 조회 -->
<otherwise>
AND p.ACT_INFO_ID = #{actInfoId}
<if test="crdnPhotoSeCd != null and crdnPhotoSeCd != ''">
AND p.CRDN_PHOTO_SE_CD = #{crdnPhotoSeCd}
</if>
</otherwise>
</choose>
ORDER BY p.CRDN_PHOTO_SN
</select>
</mapper>

@ -7,41 +7,41 @@
<select id="selectList" parameterType="CrdnRegistAndViewVO" resultType="CrdnRegistAndViewVO">
/* CrdnRegistAndViewMapper.selectList : 단속 목록 조회 */
SELECT
c.CRDN_YR,
c.CRDN_NO,
c.SGG_CD,
c.CRDN_YR, /* 단속 연도 */
c.CRDN_NO, /* 단속 번호 */
c.SGG_CD, /* 시군구 코드 */
sgg.CD_NM AS SGG_CD_NM,
c.RGN_SE_CD,
c.RGN_SE_CD, /* 지역 구분 코드 */
rgn.CD_NM AS RGN_SE_CD_NM,
c.DSCL_MTHD_CD,
c.DSCL_MTHD_CD, /* 단속 방법 코드 */
dscl.CD_NM AS DSCL_MTHD_CD_NM,
c.DSCL_YMD,
c.EXMNR,
c.RMRK,
c.DSPS_BFHD_BGNG_YMD,
c.DSPS_BFHD_END_YMD,
c.CRC_CMD_BGNG_YMD,
c.CRC_CMD_END_YMD,
c.CRC_URG_BGNG_YMD,
c.CRC_URG_END_YMD,
c.LEVY_PRVNTC_BGNG_YMD,
c.LEVY_PRVNTC_END_YMD,
c.LEVY_BGNG_YMD,
c.LEVY_END_YMD,
c.PAY_URG_BGNG_YMD,
c.PAY_URG_END_YMD,
c.FRST_CRDN_YR,
c.FRST_CRDN_NO,
c.RELEVY_YN,
c.AGRVTN_LEVY_TRGT_YN,
c.CRDN_PRCS_STTS_CD,
c.DSCL_YMD, /* 적발 일자 */
c.EXMNR, /* 조사원 */
c.RMRK, /* 비고 */
c.DSPS_BFHD_BGNG_YMD, /* 사전처분 시작일 */
c.DSPS_BFHD_END_YMD, /* 사전처분 종료일 */
c.CRC_CMD_BGNG_YMD, /* 시정명령 시작일 */
c.CRC_CMD_END_YMD, /* 시정명령 종료일 */
c.CRC_URG_BGNG_YMD, /* 시정촉구 시작일 */
c.CRC_URG_END_YMD, /* 시정촉구 종료일 */
c.LEVY_PRVNTC_BGNG_YMD, /* 부과예고 시작일 */
c.LEVY_PRVNTC_END_YMD, /* 부과예고 종료일 */
c.LEVY_BGNG_YMD, /* 부과 시작일 */
c.LEVY_END_YMD, /* 부과 종료일 */
c.PAY_URG_BGNG_YMD, /* 납부촉구 시작일 */
c.PAY_URG_END_YMD, /* 납부촉구 종료일 */
c.FRST_CRDN_YR, /* 최초 단속 연도 */
c.FRST_CRDN_NO, /* 최초 단속 번호 */
c.RELEVY_YN, /* 재과 여부 */
c.AGRVTN_LEVY_TRGT_YN, /* 가중 부과 대상 여부 */
c.CRDN_PRCS_STTS_CD, /* 단속 처리 상태 코드 */
stts.CD_NM AS CRDN_PRCS_STTS_CD_NM,
c.CRDN_PRCS_YMD,
c.CRDN_PRCS_YMD, /* 단속 처리 일자 */
c.REG_DT,
c.RGTR,
u.USER_ACNT AS RGTR_ACNT,
u.USER_NM AS RGTR_NM,
p.LOTNO_WHOL_ADDR,
p.LOTNO_WHOL_ADDR, /* 지번 전체 주소 */
p.ZIP,
(SELECT GROUP_CONCAT(DISTINCT o2.FLNM SEPARATOR ', ')
FROM tb_ownr_info oi2
@ -49,15 +49,18 @@
WHERE oi2.CRDN_YR = c.CRDN_YR
AND oi2.CRDN_NO = c.CRDN_NO
AND oi2.DEL_YN = 'N') AS OWNR_NAMS,
ii.IMPLT_BGNG_YMD, /* 사전처분 시작일 */
ii.IMPLT_END_YMD, /* 사전처분 종료일 */
ii.IMPLT_TASK_SE_CD, /* 사전처분 상태 */
ii2.IMPLT_BGNG_YMD as IMPLT_BGNG_YMD2, /* 시정명령 시작일 */
ii2.IMPLT_END_YMD as IMPLT_END_YMD2, /* 시정명령 종료일 */
ii2.IMPLT_TASK_SE_CD as IMPLT_TASK_SE_CD2, /* 시정명령 상태 */
ii3.IMPLT_BGNG_YMD as IMPLT_BGNG_YMD3, /* 시정촉구 시작일 */
ii3.IMPLT_END_YMD as IMPLT_END_YMD3, /* 시정촉구 종료일 */
ii3.IMPLT_TASK_SE_CD as IMPLT_TASK_SE_CD3 /* 시정촉구 상태 */
(SELECT GROUP_CONCAT(DISTINCT o2.FLNM SEPARATOR ', ')
FROM tb_actr_info ai
LEFT JOIN tb_ownr o2 ON o2.OWNR_ID = ai.OWNR_ID AND o2.DEL_YN = 'N'
WHERE ai.CRDN_YR = c.CRDN_YR
AND ai.CRDN_NO = c.CRDN_NO
AND ai.DEL_YN = 'N') AS ACTR_NAMS,
a.ACT_TYPE_CD, /* 행위 유형 코드 */
CASE WHEN (SELECT COUNT(1) FROM tb_act_info a2 WHERE a2.CRDN_YR = a.CRDN_YR AND a2.CRDN_NO = a.CRDN_NO AND a2.DEL_YN='N') > 1 THEN
CONCAT(act.VLTN_BDST, ' 등 ', (SELECT COUNT(1) FROM tb_act_info a2 WHERE a2.CRDN_YR = a.CRDN_YR AND a2.CRDN_NO = a.CRDN_NO AND a2.DEL_YN='N'), '건')
ELSE act.VLTN_BDST END ACT_TYPE_CD_NM,
a.USG_IDX_CD, /* 용도 지수 코드 */
usg.USG_NM AS USG_IDX_CD_NM
FROM tb_crdn c
LEFT JOIN tb_cd_detail sgg ON sgg.CD_GROUP_ID = 'ORG_CD' AND sgg.CD_ID = c.SGG_CD
LEFT JOIN tb_cd_detail rgn ON rgn.CD_GROUP_ID = 'RGN_SE_CD' AND rgn.CD_ID = c.RGN_SE_CD
@ -65,9 +68,9 @@
LEFT JOIN tb_cd_detail stts ON stts.CD_GROUP_ID = 'CRDN_PRCS_STTS_CD' AND stts.CD_ID = c.CRDN_PRCS_STTS_CD
LEFT JOIN tb_user u ON u.USER_ID = c.RGTR AND u.USE_YN = 'Y'
LEFT JOIN tb_pstn_info p ON p.CRDN_YR = c.CRDN_YR AND p.CRDN_NO = c.CRDN_NO AND p.DEL_YN = 'N'
LEFT JOIN TB_IMPLT_INFO ii ON ii.CRDN_YR = c.CRDN_YR AND ii.CRDN_NO = c.CRDN_NO AND ii.IMPLT_TASK_SE_CD = '1' AND ii.DEL_YN = 'N' /* 사전처분 */
LEFT JOIN TB_IMPLT_INFO ii2 ON ii2.CRDN_YR = c.CRDN_YR AND ii2.CRDN_NO = c.CRDN_NO AND ii2.IMPLT_TASK_SE_CD = '2' AND ii2.DEL_YN = 'N' /* 시정명령 */
LEFT JOIN TB_IMPLT_INFO ii3 ON ii3.CRDN_YR = c.CRDN_YR AND ii3.CRDN_NO = c.CRDN_NO AND ii3.IMPLT_TASK_SE_CD = '3' AND ii3.DEL_YN = 'N' /* 시정촉구 */
LEFT Join tb_act_info a ON a.CRDN_YR = c.CRDN_YR and a.CRDN_NO = c.CRDN_NO AND a.DEL_YN = 'N' AND a.ACT_NO = (SELECT MIN(a1.ACT_NO) FROM tb_act_info a1 WHERE a1.CRDN_YR = a.CRDN_YR AND a1.CRDN_NO = a.CRDN_NO AND a1.DEL_YN='N')
LEFT JOIN tb_act_type act ON act.ACT_TYPE_CD = a.ACT_TYPE_CD
LEFT JOIN tb_usg_idx usg ON usg.USG_IDX_CD = a.USG_IDX_CD AND usg.DEL_YN = 'N'
WHERE c.DEL_YN = 'N'
<if test='schCrdnYr != null and schCrdnYr != ""'>
AND c.CRDN_YR = #{schCrdnYr}

@ -19,9 +19,26 @@
</h2>
<a href="#" class="pop-x-btn modalclose" id="btnCloseTop"></a>
</div>
<!-- 탭 메뉴 -->
<div class="tab-container">
<ul class="tab-menu">
<li id="tab-act" class="tab-item active" data-tab="act">
<a href="#" class="tab-link">1. 불법행위정보</a>
</li>
<c:if test="${mode eq 'U'}">
<li id="tab-actn" class="tab-item" data-tab="actn">
<a href="#" class="tab-link">2. 조치정보</a>
</li>
</c:if>
</ul>
</div>
<div class="popup_con">
<!-- 중요로직: 단속연도/번호는 부모창에서 전달받아 그대로 저장에 사용 -->
<form id="actInfoForm" name="actInfoForm">
<!-- 불법행위정보 탭 컨텐츠 -->
<div id="tab-content-act" class="tab-content active">
<!-- 중요로직: 단속연도/번호는 부모창에서 전달받아 그대로 저장에 사용 -->
<form id="actInfoForm" name="actInfoForm">
<input type="hidden" id="mode" name="mode" value="${mode}" />
<input type="hidden" id="actInfoId" name="actInfoId" value="${data.actInfoId}" />
<input type="hidden" id="crdnYr" name="crdnYr" value="${crdnYr}" />
@ -107,7 +124,7 @@
<span>단속 사진 선택</span>
<input type="file" id="crdnPhotoFiles" name="crdnPhotoFiles" accept="image/*" multiple style="display: none;">
</label>
<span class="file-info">최대 10개, 각 20MB 이하의 이미지 파일만 업로드 가능합니다.</span>
<span class="file-info">최대 10개, 각 10MB 이하의 이미지 파일만 업로드 가능합니다.</span>
</div>
<div class="photo-preview-container" id="crdnPhotoPreviewContainer">
<!-- 중요한 로직 주석: 수정 모드에서 기존 등록된 단속 사진들을 표시한다 -->
@ -134,72 +151,10 @@
</tr>
</table>
</div>
<div class="forms_table_non mt-20">
<table>
<colgroup>
<col style="width: 18%;" />
<col style="width: 32%;" />
<col style="width: 18%;" />
<col style="width: 32%;" />
</colgroup>
<tr>
<th class="th">조치일자</th>
<td>
<input type="text" id="actnYmd" name="actnYmd" class="input calender datepicker" maxlength="10" style="width: 120px;" autocomplete="off" value="${dateUtil:formatDateString(data.actnYmd)}"/>
</td>
<th class="th">조치 면적(㎡)</th>
<td>
<input type="text" id="actnArea" name="actnArea" class="input decimalMask" style="width: 120px;" maxlength="12" placeholder="예) 45.94" value="${data.actnArea}"/>
</td>
</tr>
<tr>
<th class="th">비고</th>
<td colspan="3">
<textarea id="actnRmrk" name="actnRmrk" class="textarea" rows="3" maxlength="1000" style="height: 80px;">${data.rmrk}</textarea>
</td>
</tr>
<tr>
<th class="th">조치 사진</th>
<td colspan="3">
<!-- 중요한 로직 주석: 조치 사진 업로드와 미리보기 기능 (CRDN_PHOTO_SE_CD = 2) -->
<div class="file-upload-section" data-photo-type="2">
<div class="file-upload-controls">
<label for="actnPhotoFiles" class="file-upload-btn smallb-2">
<span>조치 사진 선택</span>
<input type="file" id="actnPhotoFiles" name="actnPhotoFiles" accept="image/*" multiple style="display: none;">
</label>
<span class="file-info">최대 10개, 각 20MB 이하의 이미지 파일만 업로드 가능합니다.</span>
</div>
<div class="photo-preview-container" id="actnPhotoPreviewContainer">
<!-- 중요한 로직 주석: 수정 모드에서 기존 등록된 조치 사진들을 표시한다 -->
<c:if test="${mode eq 'U' && not empty crdnPhotoList}">
<c:forEach var="photo" items="${crdnPhotoList}">
<c:if test="${photo.crdnPhotoSeCd eq '2'}">
<div class="photo-preview-item existing-photo" data-act-info-id="${photo.actInfoId}" data-photo-sn="${photo.crdnPhotoSn}" data-photo-type="2">
<div class="photo-thumbnail">
<img src="<c:url value='/crdn/crndRegistAndView/crdnActInfo/downloadPhoto.do'/>?actInfoId=${photo.actInfoId}&crdnPhotoSn=${photo.crdnPhotoSn}"
alt="${photo.orgnlPhotoNm}" onclick="viewOriginalPhoto('${photo.actInfoId}', '${photo.crdnPhotoSn}', '${photo.crdnPhotoSeCd}')">
</div>
<div class="photo-info">
<div class="photo-name" title="${photo.orgnlPhotoNm}">${photo.orgnlPhotoNm}</div>
<div class="photo-type">[조치]</div>
<button type="button" class="delete-photo-btn" onclick="deleteExistingPhoto('${photo.actInfoId}', '${photo.crdnPhotoSn}', '2')">삭제</button>
</div>
</div>
</c:if>
</c:forEach>
</c:if>
</div>
</div>
</td>
</tr>
</table>
</div>
</form>
</form>
</div>
</div>
<div class="popup_foot">
<button type="button" id="btnSave" class="newbtns bg1">저장</button>
<button type="button" id="btnClose" class="newbtns bg2 modalclose">닫기</button>
@ -222,6 +177,13 @@
language: "kr"
});
// 탭 전환 이벤트
$('.tab-link').on('click', function(e) {
e.preventDefault();
var tabType = $(this).parent().data('tab');
switchTab(tabType);
});
// 저장 버튼 클릭 이벤트
$('#btnSave').on('click', function() {
saveActInfo();
@ -251,6 +213,46 @@
console.log('불법행위정보 팝업이 초기화되었습니다. 모드:', mode);
});
/**
* 탭 전환 함수
* 중요한 로직 주석: levyPrvntcPopup.jsp와 동일한 방식으로 별도 팝업을 열어 탭을 전환합니다.
*/
function switchTab(tabType) {
if (tabType === 'actn') {
// 조치정보 탭으로 전환 - 새 팝업 열기
var actInfoId = $('#actInfoId').val();
var crdnYr = $('#crdnYr').val();
var crdnNo = $('#crdnNo').val();
var pstnInfoId = $('#pstnInfoId').val();
if (!actInfoId) {
alert('행위정보가 등록되지 않았습니다. 먼저 불법행위정보를 저장해주세요.');
return;
}
var url = '<c:url value="/crdn/crndRegistAndView/crdnActInfo/crdnActnInfoRegistPopup.do"/>?mode=U' +
'&actInfoId=' + encodeURIComponent(actInfoId) +
'&crdnYr=' + encodeURIComponent(crdnYr) +
'&pstnInfoId=' + encodeURIComponent(pstnInfoId) +
'&crdnNo=' + encodeURIComponent(crdnNo);
var newWindow = openPopup(url, 1400, 800, 'actnInfoManagePopup');
if (newWindow) {
// 메인페이지 opener 참조를 새 팝업에 전달
newWindow.mainOpener = window.opener;
window.close();
// 현재 창은 닫지 않고 유지 (사용자가 다시 돌아올 수 있도록)
} else {
alert('팝업이 차단되었습니다. 팝업 차단을 해제해주세요.');
}
} else {
// 불법행위정보 탭은 현재 페이지이므로 별도 처리 없음
$('.tab-item').removeClass('active');
$('.tab-item[data-tab="' + tabType + '"]').addClass('active');
}
}
// 드롭다운 인스턴스들
var actTypeCdDropdown;
var strctIdxDropdown;
@ -560,13 +562,6 @@
formData.append('crdnPhotoFiles', crdnSelectedFiles[i]);
}
}
// 중요한 로직 주석: 선택된 조치 사진들을 FormData에 추가 (있는 경우에만)
if (actnSelectedFiles && actnSelectedFiles.length > 0) {
for (var i = 0; i < actnSelectedFiles.length; i++) {
formData.append('actnPhotoFiles', actnSelectedFiles[i]);
}
}
var url = (mode === 'U') ?
'<c:url value="/crdn/crndRegistAndView/crdnActInfo/update.ajax"/>' :
@ -605,7 +600,6 @@
// 전역 변수: 선택된 파일들을 사진 종류별로 보관
var crdnSelectedFiles = []; // 단속 사진 파일들
var actnSelectedFiles = []; // 조치 사진 파일들
/**
* 파일 업로드 초기화
@ -621,11 +615,6 @@
handleFileSelect(e.target.files, '1');
});
// 조치 사진 파일 선택 이벤트
$('#actnPhotoFiles').on('change', function(e) {
handleFileSelect(e.target.files, '2');
});
// 중요로직: 단속 사진 드래그앤드롭 처리
crdnDropZone.on('dragover', function(e) {
e.preventDefault();
@ -649,30 +638,6 @@
handleFileSelect(files, '1'); // 단속 사진으로 처리
}
});
// 중요로직: 조치 사진 드래그앤드롭 처리
actnDropZone.on('dragover', function(e) {
e.preventDefault();
e.stopPropagation();
actnDropZone.addClass('drag-over');
});
actnDropZone.on('dragleave', function(e) {
e.preventDefault();
e.stopPropagation();
actnDropZone.removeClass('drag-over');
});
actnDropZone.on('drop', function(e) {
e.preventDefault();
e.stopPropagation();
actnDropZone.removeClass('drag-over');
var files = e.originalEvent.dataTransfer.files;
if (files.length > 0) {
handleFileSelect(files, '2'); // 조치 사진으로 처리
}
});
}
/**
@ -724,7 +689,7 @@
// 파일 크기 체크 (10MB)
var maxSize = 10 * 1024 * 1024;
if (file.size > maxSize) {
alert(file.name + ' 파일이 20MB를 초과합니다.');
alert(file.name + ' 파일이 10MB를 초과합니다.');
return false;
}
@ -841,5 +806,4 @@
openPopup(url, 1000, 700, '_blank');
}
</script>

@ -0,0 +1,728 @@
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="dateUtil" uri="http://egovframework.go.kr/functions/date-util" %>
<!-- 파일 업로드 관련 CSS -->
<link rel="stylesheet" type="text/css" href="<c:url value='/resources/xit/xit-fileupload.css'/>" />
<!-- 조치정보 관리 팝업 -->
<div class="popup_wrap">
<div class="popup_inner">
<div class="popup_tit">
<h2 class="tit" id="popupTitle">조치정보 관리</h2>
<a href="#" class="pop-x-btn modalclose" id="btnCloseTop"></a>
</div>
<!-- 탭 메뉴 -->
<div class="tab-container">
<ul class="tab-menu">
<li id="tab-act" class="tab-item" data-tab="act">
<a href="#" class="tab-link">1. 불법행위정보</a>
</li>
<li id="tab-actn" class="tab-item active" data-tab="actn">
<a href="#" class="tab-link">2. 조치정보</a>
</li>
</ul>
</div>
<div class="popup_con">
<!-- 조치정보 탭 컨텐츠 -->
<div id="tab-content-actn" class="tab-content active">
<!-- 조치정보 그리드 영역 -->
<div class="box_column">
<ul class="box_title">
<li class="tit">조치 정보</li>
</ul>
<div class="containers">
<div id="actnInfoGrid"></div>
</div>
</div>
<!-- 조치정보 입력 폼 -->
<div class="box_column mt-20">
<ul class="box_title">
<li class="tit">조치정보 상세</li>
</ul>
<div class="containers">
<form id="actnInfoForm" name="actnInfoForm">
<input type="hidden" id="mode" name="mode" value="${mode}" />
<input type="hidden" id="pstnInfoId" name="pstnInfoId" value="${pstnInfoId}" />
<input type="hidden" id="actInfoId" name="actInfoId" value="${actInfoId}" />
<input type="hidden" id="actnInfoId" name="actnInfoId" value="" />
<input type="hidden" id="crdnYr" name="crdnYr" value="${crdnYr}" />
<input type="hidden" id="crdnNo" name="crdnNo" value="${crdnNo}" />
<input type="hidden" id="actnMode" name="actnMode" value="C" />
<div class="forms_table_non">
<table>
<colgroup>
<col style="width: 18%;" />
<col style="width: 32%;" />
<col style="width: 18%;" />
<col style="width: 32%;" />
</colgroup>
<tr>
<th class="th"><span class="required">*</span> 조치일자</th>
<td>
<input type="text" id="actnYmd" name="actnYmd" class="input calender datepicker" maxlength="10" style="width: 120px;" autocomplete="off" validation-check="required date" />
</td>
<th class="th"><span class="required">*</span> 조치면적(㎡)</th>
<td>
<input type="text" id="actnArea" name="actnArea" class="input decimalMask" style="width: 120px;" maxlength="12" placeholder="예) 45.94" validation-check="required" />
</td>
</tr>
<tr>
<th class="th">조치비고</th>
<td colspan="3">
<textarea id="actnRmrk" name="actnRmrk" class="textarea" rows="3" maxlength="1000" style="height: 80px;"></textarea>
</td>
</tr>
<tr>
<th class="th">조치사진</th>
<td colspan="3">
<!-- 조치사진 업로드 영역 -->
<div class="file-upload-section" data-photo-type="2">
<div class="file-upload-controls">
<label for="actnPhotoFiles" class="file-upload-btn smallb-2">
<span>조치사진 선택</span>
<input type="file" id="actnPhotoFiles" name="actnPhotoFiles" accept="image/*" multiple style="display: none;">
</label>
<span class="file-info">최대 10개, 각 10MB 이하의 이미지 파일만 업로드 가능합니다.</span>
</div>
<div class="photo-preview-container" id="actnPhotoPreviewContainer">
<!-- 기존 조치사진들이 여기에 표시됩니다 -->
</div>
</div>
</td>
</tr>
</table>
</div>
</form>
<div class="forms_foot">
<div class="btn_group">
</div>
</div>
</div>
</div>
</div>
</div>
<div class="popup_foot">
<button type="button" id="btnSaveActn" class="newbtns bg1">조치정보 저장</button>
<button type="button" id="btnDeleteActn" class="newbtns bg6">조치정보 삭제</button>
<button type="button" id="btnClearActn" class="newbtns bg2-1">신규</button>
<button type="button" id="btnClose" class="newbtns bg2 modalclose">닫기</button>
</div>
</div>
</div>
<script type="text/javascript">
(function(window, $) {
'use strict';
var CrdnActnInfoRegistPopup = {
actInfoId: '${actInfoId}',
crdnYr: '${crdnYr}',
crdnNo: '${crdnNo}',
pstnInfoId: '${pstnInfoId}',
actnInfoIdSelect: null,
grid: {
instance: null,
initConfig: function() {
var dataSource = this.createDataSource();
var gridConfig = new XitTuiGridConfig();
gridConfig.setOptDataSource(dataSource);
gridConfig.setOptGridId('actnInfoGrid');
gridConfig.setOptGridHeight(150);
gridConfig.setOptRowHeight(30);
gridConfig.setOptUseClientSort(true);
gridConfig.setOptRowHeaderType('');
gridConfig.setOptColumns(this.getGridColumns());
return gridConfig;
},
getGridColumns: function() {
return [
{
header: '조치정보ID',
name: 'actnInfoId',
hidden: true
},
{
header: '순번',
name: 'rownum',
width: 60,
align: 'center'
},
{
header: '조치일자',
name: 'actnYmd',
width: 100,
align: 'center',
formatter: function(e) {
return e.value ? moment(e.value).format('YYYY-MM-DD') : '';
}
},
{
header: '조치면적(㎡)',
name: 'actnArea',
width: 100,
align: 'right',
formatter: function(e) {
return e.value ? parseFloat(e.value).toLocaleString() : '-';
}
},
{
header: '조치비고',
name: 'actnRmrk',
minWidth: 200,
},
{
header: '수정일시',
name: 'mdfcnDt',
width: 130,
align: 'center',
}
];
},
createDataSource: function() {
return {
api: {
readData: {
url: '<c:url value="/crdn/crndRegistAndView/crdnActInfo/actnInfoList.ajax"/>',
method: 'POST',
contentType: 'application/x-www-form-urlencoded',
processData: true
}
},
initialRequest: false,
serializer: function(params) {
var defaultParams = $.param(params);
var extra = $.param({ actInfoId: CrdnActnInfoRegistPopup.actInfoId });
return defaultParams + '&' + extra;
}
};
},
create: function() {
var gridConfig = this.initConfig();
var Grid = tui.Grid;
this.instance = gridConfig.instance(Grid);
Grid.applyTheme('striped');
this.gridBindEvents();
},
gridBindEvents: function() {
var self = CrdnActnInfoRegistPopup;
this.instance.on('successResponse', function(ev) {
var responseObj = JSON.parse(ev.xhr.response);
var totalCount = 0;
if (responseObj && responseObj.data && responseObj.data.contents) {
totalCount = responseObj.data.contents.length;
$('#actnTotalCount').text('총 ' + totalCount + '건');
}
});
this.instance.on('onGridUpdated', function() {
const firstVisibleColumn = self.grid.instance.getColumns().find(column => !column.hidden);
var allRows = self.grid.instance.getData();
var rowKey = null;
if (allRows && allRows.length > 0) {
allRows.forEach(function(row) {
if (self.actnInfoIdSelect === row.actnInfoId) {
rowKey = row.rowKey
console.log(self.actnInfoIdSelect, rowKey, row.rowKey, row.actnInfoId)
}
});
if(rowKey != null){
self.grid.instance.focus(rowKey, firstVisibleColumn.name, true);
}
}
});
this.instance.on('focusChange', function(ev) {
console.log(ev);
if (ev.rowKey !== null) {
var rowData = self.grid.instance.getRow(ev.rowKey);
self.actnInfoIdSelect = rowData.actnInfoId;
self.loadActnInfoToForm(rowData);
}
// validation 에러 스타일과 메시지 제거
$('#actnInfoForm').find('.is-invalid').removeClass('is-invalid');
$('#actnInfoForm').find('.is-invalid-custom').removeClass('is-invalid-custom');
$('#actnInfoForm').find('.error-message').remove();
});
this.instance.on('click', function(ev) {
if (ev.rowKey !== null) {
var rowData = self.grid.instance.getRow(ev.rowKey);
//self.actnInfoIdSelect = rowData.actnInfoId;
//self.loadActnInfoToForm(rowData);
}
// validation 에러 스타일과 메시지 제거
$('#actnInfoForm').find('.is-invalid').removeClass('is-invalid');
$('#actnInfoForm').find('.is-invalid-custom').removeClass('is-invalid-custom');
$('#actnInfoForm').find('.error-message').remove();
});
}
},
actnSelectedFiles: [],
init: function() {
this.grid.create();
this.eventBindEvents();
this.loadActnInfoList();
},
eventBindEvents: function() {
var self = this;
$('#actnYmd').datepicker({
container: '.popup_inner',
language: "kr"
});
$('.tab-link').on('click', function(e) {
e.preventDefault();
self.switchTab($(this).parent().data('tab'));
});
$('#btnSaveActn').on('click', function() {
self.saveActnInfo();
});
$('#btnDeleteActn').on('click', function() {
self.deleteActnInfo();
});
$('#btnClearActn').on('click', function() {
self.actnInfoIdSelect = null;
self.grid.instance.readData();
self.clearActnForm();
});
$('#actnPhotoFiles').on('change', function(e) {
self.handleFileSelect(e.target.files, '2');
});
$('#btnClose, #btnCloseTop').on('click', function() {
self.closePopup();
});
},
switchTab: function(tabType) {
var self = this;
if (tabType === 'act') {
var url = '<c:url value="/crdn/crndRegistAndView/crdnActInfo/crdnActInfoRegistPopup.do"/>?mode=U' +
'&actInfoId=' + encodeURIComponent(self.actInfoId) +
'&pstnInfoId=' + encodeURIComponent(self.pstnInfoId) +
'&crdnYr=' + encodeURIComponent(self.crdnYr) +
'&crdnNo=' + encodeURIComponent(self.crdnNo);
var newWindow = openPopup(url, 1200, 700, 'actInfoPopup');
if (newWindow) {
window.close();
} else {
alert('팝업이 차단되었습니다. 팝업 차단을 해제해주세요.');
}
} else if (tabType === 'actn') {
$('.tab-item').removeClass('active');
$('.tab-item[data-tab="' + tabType + '"]').addClass('active');
}
},
loadActnInfoList: function() {
if (this.grid.instance) {
this.grid.instance.readData();
}
},
loadActnInfoToForm: function(rowData) {
if (!rowData) return;
$('#actnInfoId').val(rowData.actnInfoId || '');
$('#actnYmd').val(moment(rowData.actnYmd).format('YYYY-MM-DD') || '');
$('#actnArea').val(rowData.actnArea || '');
$('#actnRmrk').val(rowData.actnRmrk || '');
$('#actnMode').val('U');
this.loadActnPhotos(rowData.actnInfoId);
console.log('조치정보 폼 로드 완료:', rowData);
},
loadActnPhotos: function(actnInfoId) {
var self = this;
if (!actnInfoId) {
$('#actnPhotoPreviewContainer').empty();
return;
}
$.ajax({
url: '<c:url value="/crdn/crndRegistAndView/crdnActInfo/selectActnPhotos.ajax"/>',
type: 'GET',
data: { actnInfoId: actnInfoId },
success: function(response) {
$('#actnPhotoPreviewContainer').empty();
if (response && response.data && response.data.length > 0) {
$.each(response.data, function(index, photo) {
var previewHtml =
'<div class="photo-preview-item existing-photo" data-act-info-id="' + photo.actInfoId + '" data-photo-sn="' + photo.crdnPhotoSn + '" data-photo-type="2">' +
' <div class="photo-thumbnail">' +
' <img src="<c:url value='/crdn/crndRegistAndView/crdnActInfo/downloadPhoto.do'/>?actInfoId=' + photo.actInfoId + '&crdnPhotoSn=' + photo.crdnPhotoSn + '"' +
' alt="' + photo.orgnlPhotoNm + '" onclick="viewOriginalPhoto(\'' + photo.actnInfoId + '\', \'' + photo.actInfoId + '\', \'' + photo.crdnPhotoSn + '\', \'' + photo.crdnPhotoSeCd + '\')">' +
' </div>' +
' <div class="photo-info">' +
' <div class="photo-name" title="' + photo.orgnlPhotoNm + '">' + photo.orgnlPhotoNm + '</div>' +
' <div class="photo-type">[조치]</div>' +
' <button type="button" class="delete-photo-btn" onclick="CrdnActnInfoRegistPopup.deleteExistingPhoto(\'' + photo.actInfoId + '\', \'' + photo.crdnPhotoSn + '\', \'2\')">삭제</button>' +
' </div>' +
'</div>';
$('#actnPhotoPreviewContainer').append(previewHtml);
});
}
},
error: function() {
alert('조치사진 조회 중 오류가 발생했습니다.');
}
});
},
saveActnInfo: function() {
var self = this;
if (!validateFormByAttributes('actnInfoForm')) {
return;
}
var actnInfoId = $('#actnInfoId').val().trim();
var actInfoId = this.actInfoId;
var mode = actnInfoId ? 'U' : 'C';
var formData = new FormData();
formData.append('mode', mode);
$('#actnInfoForm').find('input, select, textarea').each(function() {
var $this = $(this);
var name = $this.attr('name');
var value = $this.val();
if (name && value && $this.attr('type') !== 'file') {
formData.append(name, value);
}
});
if (this.actnSelectedFiles && this.actnSelectedFiles.length > 0) {
for (var i = 0; i < this.actnSelectedFiles.length; i++) {
formData.append('actnPhotoFiles', this.actnSelectedFiles[i]);
}
}
$.ajax({
url: '<c:url value="/crdn/crndRegistAndView/crdnActInfo/saveActnInfo.ajax"/>',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
if (response && response.success) {
alert(response.message);
if( mode === "C" ){
self.actnInfoIdSelect = null;
self.grid.instance.readData();
self.clearActnForm();
}else{
self.grid.instance.readData();
self.loadActnInfoToForm(response.data);
}
} else {
alert(response.message || '조치정보 저장에 실패했습니다.');
}
},
error: function() {
alert('조치정보 저장 중 오류가 발생했습니다.');
}
});
},
deleteActnInfo: function() {
var actnInfoId = $('#actnInfoId').val().trim();
if (!actnInfoId) {
alert('삭제할 조치정보를 선택해주세요.');
return;
}
if (!confirm('선택한 조치정보를 삭제하시겠습니까?\n관련된 조치사진도 함께 삭제됩니다.')) {
return;
}
var self = this;
$.ajax({
url: '<c:url value="/crdn/crndRegistAndView/crdnActInfo/deleteActnInfo.ajax"/>',
type: 'POST',
data: {
actnInfoIds: actnInfoId,
mode: 'D'
},
success: function(response) {
if (response && response.success) {
alert('조치정보가 삭제되었습니다.');
self.actnInfoIdSelect = null;
self.grid.instance.readData();
self.clearActnForm();
} else {
alert(response.message || '조치정보 삭제에 실패했습니다.');
}
},
error: function() {
alert('조치정보 삭제 중 오류가 발생했습니다.');
}
});
},
clearActnForm: function() {
var self = this;
$('#actnInfoId').val('');
$('#actnYmd').val('');
$('#actnArea').val('');
$('#actnRmrk').val('');
$('#actnMode').val('C');
// validation 에러 스타일과 메시지 제거
$('#actnInfoForm').find('.is-invalid').removeClass('is-invalid');
$('#actnInfoForm').find('.is-invalid-custom').removeClass('is-invalid-custom');
$('#actnInfoForm').find('.error-message').remove();
$('#actnPhotoPreviewContainer').empty();
$('#actnPhotoFiles').val('');
this.actnSelectedFiles = [];
console.log('조치정보 폼 초기화 완료');
},
handleFileSelect: function(files, photoType) {
if (!files || files.length === 0) {
return;
}
var containerSelector = '#actnPhotoPreviewContainer';
var selectedFilesArray = this.actnSelectedFiles;
var photoTypeName = '조치';
var existingPhotos = $(containerSelector + ' .photo-preview-item.existing-photo').length;
var newFiles = selectedFilesArray.length + files.length;
if (existingPhotos + newFiles > 10) {
alert('최대 10개의 ' + photoTypeName + ' 사진만 업로드할 수 있습니다.');
return;
}
for (var i = 0; i < files.length; i++) {
var file = files[i];
if (!this.validateFile(file)) {
continue;
}
selectedFilesArray.push(file);
this.createPhotoPreview(file, photoType);
}
},
validateFile: function(file) {
var maxSize = 10 * 1024 * 1024;
if (file.size > maxSize) {
alert(file.name + ' 파일이 10MB를 초과합니다.');
return false;
}
if (!file.type.startsWith('image/')) {
alert(file.name + ' 은(는) 이미지 파일이 아닙니다.');
return false;
}
var allowedExtensions = ['jpg', 'jpeg', 'png', 'gif'];
var fileName = file.name.toLowerCase();
var fileExtension = fileName.split('.').pop();
if (allowedExtensions.indexOf(fileExtension) === -1) {
alert('지원되지 않는 파일 형식입니다. (jpg, jpeg, png, gif만 가능)');
return false;
}
return true;
},
createPhotoPreview: function(file, photoType) {
var reader = new FileReader();
var containerSelector = '#actnPhotoPreviewContainer';
var photoTypeName = '조치';
reader.onload = function(e) {
var previewHtml =
'<div class="photo-preview-item new-photo" data-file-name="' + file.name + '" data-photo-type="' + photoType + '">' +
' <div class="photo-thumbnail">' +
' <img src="' + e.target.result + '" alt="' + file.name + '">' +
' </div>' +
' <div class="photo-info">' +
' <div class="photo-name" title="' + file.name + '">' + file.name + '</div>' +
' <div class="photo-type">[' + photoTypeName + ']</div>' +
' <button type="button" class="delete-photo-btn" onclick="CrdnActnInfoRegistPopup.deleteNewPhoto(this, \'' + photoType + '\')">삭제</button>' +
' </div>' +
'</div>';
$(containerSelector).append(previewHtml);
};
reader.readAsDataURL(file);
},
deleteNewPhoto: function(button, photoType) {
var $item = $(button).closest('.photo-preview-item');
var fileName = $item.data('file-name');
var selectedFilesArray = this.actnSelectedFiles;
for (var i = 0; i < selectedFilesArray.length; i++) {
if (selectedFilesArray[i].name === fileName) {
selectedFilesArray.splice(i, 1);
break;
}
}
$item.remove();
},
deleteExistingPhoto: function(actInfoId, crdnPhotoSn, photoType) {
var photoTypeName = photoType === '1' ? '단속' : '조치';
if (!confirm('선택한 ' + photoTypeName + ' 사진을 삭제하시겠습니까?')) {
return;
}
$.ajax({
url: '<c:url value="/crdn/crndRegistAndView/crdnActInfo/deletePhoto.ajax"/>',
type: 'POST',
data: {
actInfoId: actInfoId,
crdnPhotoSn: crdnPhotoSn
},
success: function(response) {
if (response && response.success) {
// 화면에서 해당 사진 제거
$('[data-act-info-id="' + actInfoId + '"][data-photo-sn="' + crdnPhotoSn + '"][data-photo-type="' + photoType + '"]').remove();
} else {
alert(response.message || '사진 삭제에 실패했습니다.');
}
}
});
},
closePopup: function() {
if (window.opener && !window.opener.closed) {
if (typeof window.opener.CrdnDetailViewActInfo.search === 'function') {
window.opener.CrdnDetailViewActInfo.search();
}
window.opener.focus();
}
window.close();
}
};
function viewOriginalPhoto(actnInfoId, actInfoId, crdnPhotoSn, crdnPhotoSeCd) {
var url = '<c:url value="/crdn/crndRegistAndView/crdnActInfo/photoView.do"/>?actnInfoId=' + actnInfoId + '&actInfoId=' + actInfoId + '&crdnPhotoSn=' + crdnPhotoSn + '&crdnPhotoSeCd=' + crdnPhotoSeCd ;
openPopup(url, 1000, 700, '_blank');
}
$(document).ready(function() {
CrdnActnInfoRegistPopup.init();
});
window.CrdnActnInfoRegistPopup = CrdnActnInfoRegistPopup;
})(window, jQuery);
</script>

@ -256,7 +256,7 @@
},
{ header: '진행단계', name: 'crdnPrcsSttsCdNm', align: 'center', width: 100 },
{
header: '지번주소',
header: '위치',
name: 'lotnoWholAddr',
align: 'left',
minWidth: 300,
@ -265,12 +265,21 @@
}
},
{
header: '우편번호',
name: 'zip',
align: 'center',
width: 80,
header: '행위유형',
name: 'actTypeCdNm',
align: 'left',
width: 130,
formatter: function(e) {
return e.value;
}
},
{
header: '용도',
name: 'usgIdxCdNm',
align: 'left',
width: 130,
formatter: function(e) {
return e.value || '-';
return e.value;
}
},
{
@ -282,12 +291,41 @@
return e.value;
}
},
{ header: '사전처분 시작일', name: 'impltBgngYmd', align: 'center', width: 150,
{
header: '행위자',
name: 'actrNams',
align: 'left',
width: 130,
formatter: function(e) {
return e.value;
}
},
{ header: '처분사전 일자', name: 'dspsBfhdBgngYmd', align: 'center', width: 120,
formatter: function (e) {
return e.value ? moment(e.value).format('YYYY-MM-DD') : '';
}
},
{ header: '시정명령 일자', name: 'crcCmdBgngYmd', align: 'center', width: 120,
formatter: function (e) {
return e.value ? moment(e.value).format('YYYY-MM-DD') : '';
}
},
{ header: '시정촉구 일자', name: 'crcUrgBgngYmd', align: 'center', width: 120,
formatter: function (e) {
return e.value ? moment(e.value).format('YYYY-MM-DD') : '';
}
},
{ header: '부과예고 일자', name: 'levyPrvntcBgngYmd', align: 'center', width: 120,
formatter: function (e) {
return e.value ? moment(e.value).format('YYYY-MM-DD') : '';
}
},
{ header: '부과 일자', name: 'levyBgngYmd', align: 'center', width: 120,
formatter: function (e) {
return e.value ? moment(e.value).format('YYYY-MM-DD') : '';
}
},
{ header: '사전처분 종료일', name: 'impltEndYmd', align: 'center', width: 150,
{ header: '납부촉구 일자', name: 'payUrgBgngYmd', align: 'center', width: 120,
formatter: function (e) {
return e.value ? moment(e.value).format('YYYY-MM-DD') : '';
}

Loading…
Cancel
Save