불법행위 정보 - 조치에 대한 로직 재구성 진행

dev
박성영 3 months ago
parent 2abaa33f4e
commit 6cdb478d3a

@ -111,14 +111,14 @@ public class CrdnActInfoController {
// 수정 모드인 경우 기존 데이터 및 사진 목록 조회
if ("U".equals(mode) && actInfoId != null) {
data = service.selectActInfoByPk(actInfoId);
// 중요한 로직 주석: 수정 모드에서 기존 등록된 사진 목록을 조회하여 화면에 표시한다.
// 중요한 로직 주석: 수정 모드에서 기존 등록된 모든 사진 목록(단속+조치)을 조회하여 화면에 표시한다.
photoList = photoService.selectPhotoListByActInfoIdAndPhotoSeCd(
CrdnPhotoVO.builder().actInfoId(actInfoId).crdnPhotoSeCd("1").build());
CrdnPhotoVO.builder().actInfoId(actInfoId).build()); // 사진구분코드 없이 전체 조회
}
ModelAndView mav = new ModelAndView("crdn/crndRegistAndView/crdnActInfo/crdnActInfoRegistPopup" + TilesConstants.POPUP);
mav.addObject("data", data);
mav.addObject("photoList", photoList);
mav.addObject("crdnPhotoList", photoList); // 변수명을 JSP에서 사용하는 crdnPhotoList로 변경
mav.addObject("mode", mode);
mav.addObject("crdnYr", crdnYr);
mav.addObject("crdnNo", crdnNo);
@ -213,13 +213,13 @@ public class CrdnActInfoController {
/**
* ( , AJAX)
* : . service .
* : / . service .
* @param vo
* @param photoFiles ()
* @param crdnPhotoSeCd
* @param crdnPhotoFiles ()
* @param actnPhotoFiles ()
* @return
*/
@Operation(summary = "불법위반행위정보 등록", description = "불법위반행위정보를 등록합니다. 사진 파일도 함께 처리 가능합니다.")
@Operation(summary = "불법위반행위정보 등록", description = "불법위반행위정보를 등록합니다. 단속/조치 사진 파일도 함께 처리 가능합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "등록 성공"),
@ApiResponse(responseCode = "500", description = "서버 오류")
@ -227,11 +227,12 @@ public class CrdnActInfoController {
@PostMapping("/insert.ajax")
public ResponseEntity<?> insertAjax(
@ModelAttribute CrdnActInfoVO vo,
@RequestParam(value = "photoFiles", required = false) List<MultipartFile> photoFiles,
@RequestParam(value = "crdnPhotoSeCd", required = false) String crdnPhotoSeCd) {
log.debug("불법위반행위정보 등록 요청: {}, 파일 개수: {}", vo,
photoFiles != null ? photoFiles.size() : 0);
@RequestParam(value = "crdnPhotoFiles", required = false) List<MultipartFile> crdnPhotoFiles,
@RequestParam(value = "actnPhotoFiles", required = false) List<MultipartFile> actnPhotoFiles) {
log.debug("불법위반행위정보 등록 요청: {}, 단속 사진 개수: {}, 조치 사진 개수: {}",
vo, crdnPhotoFiles != null ? crdnPhotoFiles.size() : 0,
actnPhotoFiles != null ? actnPhotoFiles.size() : 0);
CrdnRegistAndViewVO crdnVO = new CrdnRegistAndViewVO();
@ -243,8 +244,8 @@ public class CrdnActInfoController {
vo.setRgtr(SessionUtil.getUserId());
vo.setMdfr(SessionUtil.getUserId());
// 중요한 로직 주석: service 단에서 행위정보 등록과 사진 파일 처리를 한 트랜잭션으로 처리
int result = service.insertActInfoWithFiles(vo, photoFiles, crdnPhotoSeCd);
// 중요한 로직 주석: service 단에서 행위정보 등록과 단속/조치 사진 파일 처리를 한 트랜잭션으로 처리
int result = service.insertActInfoWithFiles(vo, crdnPhotoFiles, actnPhotoFiles);
if (result > 0) {
return ApiResponseUtil.success(MessageConstants.Common.SAVE_SUCCESS);
@ -256,13 +257,13 @@ public class CrdnActInfoController {
/**
* ( , AJAX)
* : . service .
* : / . service .
* @param vo
* @param photoFiles ()
* @param crdnPhotoSeCd
* @param crdnPhotoFiles ()
* @param actnPhotoFiles ()
* @return
*/
@Operation(summary = "불법위반행위정보 수정", description = "불법위반행위정보를 수정합니다. 사진 파일도 함께 처리 가능합니다.")
@Operation(summary = "불법위반행위정보 수정", description = "불법위반행위정보를 수정합니다. 단속/조치 사진 파일도 함께 처리 가능합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "수정 성공"),
@ApiResponse(responseCode = "500", description = "서버 오류")
@ -270,17 +271,18 @@ public class CrdnActInfoController {
@PostMapping("/update.ajax")
public ResponseEntity<?> updateAjax(
@ModelAttribute CrdnActInfoVO vo,
@RequestParam(value = "photoFiles", required = false) List<MultipartFile> photoFiles,
@RequestParam(value = "crdnPhotoSeCd", required = false) String crdnPhotoSeCd) {
log.debug("불법위반행위정보 수정 요청: {}, 파일 개수: {}", vo,
photoFiles != null ? photoFiles.size() : 0);
@RequestParam(value = "crdnPhotoFiles", required = false) List<MultipartFile> crdnPhotoFiles,
@RequestParam(value = "actnPhotoFiles", required = false) List<MultipartFile> actnPhotoFiles) {
log.debug("불법위반행위정보 수정 요청: {}, 단속 사진 개수: {}, 조치 사진 개수: {}",
vo, crdnPhotoFiles != null ? crdnPhotoFiles.size() : 0,
actnPhotoFiles != null ? actnPhotoFiles.size() : 0);
vo.setMdfr(SessionUtil.getUserId());
vo.setSggCd(SessionUtil.getSessionVO().getUser().getOrgCd()); // 업데이트 화면에서 신규파일 등록 시 필요
// 중요한 로직 주석: service 단에서 행위정보 수정과 사진 파일 처리를 한 트랜잭션으로 처리
int result = service.updateActInfoWithFiles(vo, photoFiles, crdnPhotoSeCd);
// 중요한 로직 주석: service 단에서 행위정보 수정과 단속/조치 사진 파일 처리를 한 트랜잭션으로 처리
int result = service.updateActInfoWithFiles(vo, crdnPhotoFiles, actnPhotoFiles);
if (result > 0) {
return ApiResponseUtil.success(MessageConstants.Common.SAVE_SUCCESS);
@ -324,14 +326,15 @@ public class CrdnActInfoController {
public String photoView(
@Parameter(description = "행위정보ID") @RequestParam String actInfoId,
@Parameter(description = "사진순번") @RequestParam String crdnPhotoSn,
@Parameter(description = "단속조치사진구분") @RequestParam String crdnPhotoSeCd,
Model model) {
log.debug("단속 사진 보기 요청 - actInfoId: {}, crdnPhotoSn: {}", actInfoId, crdnPhotoSn);
log.debug("단속 사진 보기 요청 - actInfoId: {}, crdnPhotoSn: {}, crdnPhotoSeCd: {}", actInfoId, crdnPhotoSn, crdnPhotoSeCd);
// 중요한 로직 주석: 해당 행위정보의 모든 사진 목록 조회
CrdnPhotoVO searchVO = CrdnPhotoVO.builder()
.actInfoId(actInfoId)
.crdnPhotoSeCd("1") // 단속 사진
.crdnPhotoSeCd(crdnPhotoSeCd) // 단속 사진
.build();
List<CrdnPhotoVO> photoList = photoService.selectPhotoListByActInfoIdAndPhotoSeCd(searchVO);

@ -27,13 +27,13 @@ public interface CrdnActInfoService {
/**
* ( )
* : . .
* : / . .
* @param vo
* @param photoFiles ()
* @param crdnPhotoSeCd
* @param crdnPhotoFiles ()
* @param actnPhotoFiles ()
* @return
*/
int insertActInfoWithFiles(CrdnActInfoVO vo, List<MultipartFile> photoFiles, String crdnPhotoSeCd);
int insertActInfoWithFiles(CrdnActInfoVO vo, List<MultipartFile> crdnPhotoFiles, List<MultipartFile> actnPhotoFiles);
/**
* (PK )
@ -44,13 +44,13 @@ public interface CrdnActInfoService {
/**
* ( )
* : . .
* : / . .
* @param vo
* @param photoFiles ()
* @param crdnPhotoSeCd
* @param crdnPhotoFiles ()
* @param actnPhotoFiles ()
* @return
*/
int updateActInfoWithFiles(CrdnActInfoVO vo, List<MultipartFile> photoFiles, String crdnPhotoSeCd);
int updateActInfoWithFiles(CrdnActInfoVO vo, List<MultipartFile> crdnPhotoFiles, List<MultipartFile> actnPhotoFiles);
/**
* ()

@ -54,9 +54,10 @@ public class CrdnActInfoServiceImpl extends EgovAbstractServiceImpl implements C
@Override
@Transactional
public int insertActInfoWithFiles(CrdnActInfoVO vo, List<MultipartFile> photoFiles, String crdnPhotoSeCd) {
log.debug("불법위반행위정보 등록 (파일 포함): {}, 파일 개수: {}", vo,
photoFiles != null ? photoFiles.size() : 0);
public int insertActInfoWithFiles(CrdnActInfoVO vo, List<MultipartFile> crdnPhotoFiles, List<MultipartFile> actnPhotoFiles) {
log.debug("불법위반행위정보 등록 (파일 포함): {}, 단속 사진 개수: {}, 조치 사진 개수: {}",
vo, crdnPhotoFiles != null ? crdnPhotoFiles.size() : 0,
actnPhotoFiles != null ? actnPhotoFiles.size() : 0);
try {
// 중요한 로직 주석: 먼저 행위정보를 등록한다 (insertActInfo 메서드에서 actInfoId가 자동 설정됨)
@ -65,10 +66,16 @@ public class CrdnActInfoServiceImpl extends EgovAbstractServiceImpl implements C
if (result > 0) {
log.debug("행위정보 등록 완료: actInfoId={}", vo.getActInfoId());
// 중요한 로직 주석: 파일이 있는 경우에만 사진 처리를 진행한다
if (photoFiles != null && !photoFiles.isEmpty()) {
int photoResult = photoService.insertPhotosWithFiles(photoFiles, vo.getActInfoId(), crdnPhotoSeCd, vo);
log.debug("사진 파일 처리 완료: actInfoId={}, 등록된 사진 수={}", vo.getActInfoId(), photoResult);
// 중요한 로직 주석: 단속 사진이 있는 경우 처리 (CRDN_PHOTO_SE_CD = '1')
if (crdnPhotoFiles != null && !crdnPhotoFiles.isEmpty()) {
int crdnPhotoResult = photoService.insertPhotosWithFiles(crdnPhotoFiles, vo.getActInfoId(), "1", vo);
log.debug("단속 사진 파일 처리 완료: actInfoId={}, 등록된 단속 사진 수={}", vo.getActInfoId(), crdnPhotoResult);
}
// 중요한 로직 주석: 조치 사진이 있는 경우 처리 (CRDN_PHOTO_SE_CD = '2')
if (actnPhotoFiles != null && !actnPhotoFiles.isEmpty()) {
int actnPhotoResult = photoService.insertPhotosWithFiles(actnPhotoFiles, vo.getActInfoId(), "2", vo);
log.debug("조치 사진 파일 처리 완료: actInfoId={}, 등록된 조치 사진 수={}", vo.getActInfoId(), actnPhotoResult);
}
} else {
log.error("행위정보 등록 실패: {}", vo);
@ -91,9 +98,10 @@ public class CrdnActInfoServiceImpl extends EgovAbstractServiceImpl implements C
@Override
@Transactional
public int updateActInfoWithFiles(CrdnActInfoVO vo, List<MultipartFile> photoFiles, String crdnPhotoSeCd) {
log.debug("불법위반행위정보 수정 (파일 포함): {}, 파일 개수: {}", vo,
photoFiles != null ? photoFiles.size() : 0);
public int updateActInfoWithFiles(CrdnActInfoVO vo, List<MultipartFile> crdnPhotoFiles, List<MultipartFile> actnPhotoFiles) {
log.debug("불법위반행위정보 수정 (파일 포함): {}, 단속 사진 개수: {}, 조치 사진 개수: {}",
vo, crdnPhotoFiles != null ? crdnPhotoFiles.size() : 0,
actnPhotoFiles != null ? actnPhotoFiles.size() : 0);
try {
// 중요한 로직 주석: 먼저 행위정보를 수정한다
@ -102,11 +110,17 @@ public class CrdnActInfoServiceImpl extends EgovAbstractServiceImpl implements C
if (result > 0) {
log.debug("행위정보 수정 완료: actInfoId={}", vo.getActInfoId());
// 중요한 로직 주석: 새로운 파일이 있는 경우에만 사진 추가 처리를 진행한다
// 중요한 로직 주석: 새로운 단속 사진이 있는 경우 추가 처리 (CRDN_PHOTO_SE_CD = '1')
// 기존 사진의 개별 삭제는 별도의 Ajax 호출로 처리되므로 여기서는 신규 추가만 한다
if (photoFiles != null && !photoFiles.isEmpty()) {
int photoResult = photoService.insertPhotosWithFiles(photoFiles, vo.getActInfoId(), crdnPhotoSeCd, vo);
log.debug("사진 파일 추가 완료: actInfoId={}, 추가된 사진 수={}", vo.getActInfoId(), photoResult);
if (crdnPhotoFiles != null && !crdnPhotoFiles.isEmpty()) {
int crdnPhotoResult = photoService.insertPhotosWithFiles(crdnPhotoFiles, vo.getActInfoId(), "1", vo);
log.debug("단속 사진 파일 추가 완료: actInfoId={}, 추가된 단속 사진 수={}", vo.getActInfoId(), crdnPhotoResult);
}
// 중요한 로직 주석: 새로운 조치 사진이 있는 경우 추가 처리 (CRDN_PHOTO_SE_CD = '2')
if (actnPhotoFiles != null && !actnPhotoFiles.isEmpty()) {
int actnPhotoResult = photoService.insertPhotosWithFiles(actnPhotoFiles, vo.getActInfoId(), "2", vo);
log.debug("조치 사진 파일 추가 완료: actInfoId={}, 추가된 조치 사진 수={}", vo.getActInfoId(), actnPhotoResult);
}
} else {
log.error("행위정보 수정 실패: {}", vo);
@ -130,7 +144,10 @@ public class CrdnActInfoServiceImpl extends EgovAbstractServiceImpl implements C
if (vo.getActInfoId() != null) {
try {
photoService.deletePhotosByActInfoIdAndCrdnPhotoSecd(vo.getActInfoId(), "1", vo.getDltr());
log.debug("행위정보 관련 사진 삭제 완료: actInfoId={}", vo.getActInfoId());
log.debug("행위정보 단속 사진 삭제 완료: actInfoId={}, crdnPhotoSeCd={}", vo.getActInfoId(), "1");
photoService.deletePhotosByActInfoIdAndCrdnPhotoSecd(vo.getActInfoId(), "2", vo.getDltr());
log.debug("행위정보 조치 사진 삭제 완료: actInfoId={}, crdnPhotoSeCd={}", vo.getActInfoId(), "2");
} catch (Exception e) {
log.warn("행위정보 관련 사진 삭제 중 오류 발생: actInfoId={}", vo.getActInfoId(), e);
// 사진 삭제 실패해도 행위정보 삭제는 진행

@ -1,5 +1,9 @@
# Local profile
spring:
servlet:
multipart:
max-file-size: 15MB # 개별 파일 최대 크기를 15MB로 증가, multipart 를 이용시 header, type 등 정보가 붙어서 over head 가 발생할 수 있다고 함.
max-request-size: 150MB # 전체 요청 최대 크기를 150MB로 증가, multipart 를 이용시 header, type 등 정보가 붙어서 over head 가 발생할 수 있다고 함.
config:
activate:
on-profile: dev

@ -1,5 +1,9 @@
# Local profile
spring:
servlet:
multipart:
max-file-size: 15MB # 개별 파일 최대 크기를 15MB로 증가, multipart 를 이용시 header, type 등 정보가 붙어서 over head 가 발생할 수 있다고 함.
max-request-size: 150MB # 전체 요청 최대 크기를 150MB로 증가, multipart 를 이용시 header, type 등 정보가 붙어서 over head 가 발생할 수 있다고 함.
config:
activate:
on-profile: local
@ -94,7 +98,7 @@ file:
path: d:/data/@projectName@/file
max-size: 10 # 단일 파일 최대 크기 (MB)
max-total-size: 100 # 총 파일 최대 크기 (MB)
max-files: 10 # 최대 파일 개수
max-files: 20 # 최대 파일 개수
allowed-extensions: hwp,jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx,ppt,pptx,txt,zip
real-file-delete: true # 실제 파일 삭제 여부
sub-dirs:

@ -1,5 +1,9 @@
# Local profile
spring:
servlet:
multipart:
max-file-size: 15MB # 개별 파일 최대 크기를 15MB로 증가, multipart 를 이용시 header, type 등 정보가 붙어서 over head 가 발생할 수 있다고 함.
max-request-size: 150MB # 전체 요청 최대 크기를 150MB로 증가, multipart 를 이용시 header, type 등 정보가 붙어서 over head 가 발생할 수 있다고 함.
config:
activate:
on-profile: prd

@ -7,9 +7,7 @@ server:
charset: UTF-8
enabled: true
force: true # 서버가 지정한 인코딩(UTF-8)을 항상 강제로 적용, 클라이언트의 요청과 상관없이 서버 인코딩이 우선
multipart:
max-file-size: 10MB
max-request-size: 50MB
Globals:
DbType: maria

@ -209,7 +209,7 @@
VRTC = #{vrtc},
AREA = #{area},
RMRK = #{rmrk},
ACTN_YMD = #{actnYmd},
ACTN_YMD = replace(#{actnYmd},'-',''),
ACTN_AREA = #{actnArea},
ACTN_RMRK = #{actnRmrk},
MDFCN_DT = NOW(),

@ -25,7 +25,9 @@
FROM tb_crdn_photo p
LEFT JOIN tb_cd_detail cd ON cd.CD_GROUP_ID = 'CRDN_PHOTO_SE_CD' AND cd.CD_ID = p.CRDN_PHOTO_SE_CD
WHERE p.ACT_INFO_ID = #{actInfoId}
<if test='crdnPhotoSeCd != null and crdnPhotoSeCd != ""'>
AND p.CRDN_PHOTO_SE_CD = #{crdnPhotoSeCd}
</if>
AND p.DEL_YN = 'N'
ORDER BY p.CRDN_PHOTO_SN ASC
</select>

@ -20,15 +20,15 @@
<a href="#" class="pop-x-btn modalclose" id="btnCloseTop"></a>
</div>
<div class="popup_con">
<div class="forms_table_non">
<!-- 중요로직: 단속연도/번호는 부모창에서 전달받아 그대로 저장에 사용 -->
<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}" />
<input type="hidden" id="crdnNo" name="crdnNo" value="${crdnNo}" />
<input type="hidden" id="pstnInfoId" name="pstnInfoId" value="${pstnInfoId}" />
<!-- 중요로직: 단속연도/번호는 부모창에서 전달받아 그대로 저장에 사용 -->
<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}" />
<input type="hidden" id="crdnNo" name="crdnNo" value="${crdnNo}" />
<input type="hidden" id="pstnInfoId" name="pstnInfoId" value="${pstnInfoId}" />
<div class="forms_table_non">
<table>
<colgroup>
<col style="width: 18%;" />
@ -74,21 +74,21 @@
<tr>
<th class="th"><span class="required">*</span> 가로(m)</th>
<td>
<input type="text" id="wdth" name="wdth" class="input decimalMask" style="width: 120px;" maxlength="10" placeholder="예) 5.25" validation-check="required number" value="${data.wdth}"/>
<input type="text" id="wdth" name="wdth" class="input decimalMask" style="width: 120px;" maxlength="10" placeholder="예) 5.25" validation-check="required" value="${data.wdth}"/>
</td>
<th class="th"><span class="required">*</span> 세로(m)</th>
<td>
<input type="text" id="vrtc" name="vrtc" class="input decimalMask" style="width: 120px;" maxlength="10" placeholder="예) 8.75" validation-check="required number" value="${data.vrtc}"/>
<input type="text" id="vrtc" name="vrtc" class="input decimalMask" style="width: 120px;" maxlength="10" placeholder="예) 8.75" validation-check="required" value="${data.vrtc}"/>
</td>
</tr>
<tr>
<th class="th"><span class="required">*</span> 면적(㎡)</th>
<td>
<input type="text" id="area" name="area" class="input decimalMask" style="width: 120px;" maxlength="12" placeholder="예) 45.94" validation-check="required number" value="${data.area}"/>
<input type="text" id="area" name="area" class="input decimalMask" style="width: 120px;" maxlength="12" placeholder="예) 45.94" validation-check="required" value="${data.area}"/>
</td>
<th class="th">높이(m)</th>
<td>
<input type="text" id="hgt" name="hgt" class="input decimalMask" style="width: 120px;" maxlength="10" placeholder="예) 10.50" validation-check="number" value="${data.hgt}"/>
<input type="text" id="hgt" name="hgt" class="input decimalMask" style="width: 120px;" maxlength="10" placeholder="예) 10.50" value="${data.hgt}"/>
</td>
</tr>
<tr>
@ -98,33 +98,97 @@
</td>
</tr>
<tr>
<th class="th">행위 사진</th>
<th class="th">단속 사진</th>
<td colspan="3">
<!-- 중요한 로직 주석: 파일 업로드와 미리보기 기능을 제공한다 -->
<div class="file-upload-section">
<!-- 중요한 로직 주석: 단속 사진 업로드와 미리보기 기능 (CRDN_PHOTO_SE_CD = 1) -->
<div class="file-upload-section" data-photo-type="1">
<div class="file-upload-controls">
<label for="photoFiles" class="file-upload-btn smallb-2">
<span>사진 선택</span>
<input type="file" id="photoFiles" name="photoFiles" accept="image/*" multiple style="display: none;">
<label for="crdnPhotoFiles" class="file-upload-btn smallb-2">
<span>단속 사진 선택</span>
<input type="file" id="crdnPhotoFiles" name="crdnPhotoFiles" accept="image/*" multiple style="display: none;">
</label>
<input type="hidden" id="crdnPhotoSeCd" name="crdnPhotoSeCd" value="1"/>
<span class="file-info">최대 10개, 각 10MB 이하의 이미지 파일만 업로드 가능합니다.</span>
<span class="file-info">최대 10개, 각 20MB 이하의 이미지 파일만 업로드 가능합니다.</span>
</div>
<div class="photo-preview-container" id="photoPreviewContainer">
<!-- 중요한 로직 주석: 수정 모드에서 기존 등록된 사진들을 표시한다 -->
<c:if test="${mode eq 'U' && not empty photoList}">
<c:forEach var="photo" items="${photoList}">
<div class="photo-preview-item existing-photo" data-act-info-id="${photo.actInfoId}" data-photo-sn="${photo.crdnPhotoSn}">
<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}')">
<div class="photo-preview-container" id="crdnPhotoPreviewContainer">
<!-- 중요한 로직 주석: 수정 모드에서 기존 등록된 단속 사진들을 표시한다 -->
<c:if test="${mode eq 'U' && not empty crdnPhotoList}">
<c:forEach var="photo" items="${crdnPhotoList}">
<c:if test="${photo.crdnPhotoSeCd eq '1'}">
<div class="photo-preview-item existing-photo" data-act-info-id="${photo.actInfoId}" data-photo-sn="${photo.crdnPhotoSn}" data-photo-type="1">
<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}', '1')">삭제</button>
</div>
</div>
<div class="photo-info">
<div class="photo-name" title="${photo.orgnlPhotoNm}">${photo.orgnlPhotoNm}</div>
<div class="photo-type">[${photo.crdnPhotoSeCdNm}]</div>
<button type="button" class="delete-photo-btn" onclick="deleteExistingPhoto('${photo.actInfoId}', '${photo.crdnPhotoSn}')">삭제</button>
</c:if>
</c:forEach>
</c:if>
</div>
</div>
</td>
</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>
</div>
</c:if>
</c:forEach>
</c:if>
</div>
@ -132,8 +196,9 @@
</td>
</tr>
</table>
</form>
</div>
</div>
</form>
</div>
<div class="popup_foot">
<button type="button" id="btnSave" class="newbtns bg1">저장</button>
@ -489,10 +554,17 @@
}
});
// 중요한 로직 주석: 선택된 파일들을 FormData에 추가 (있는 경우에만)
if (selectedFiles && selectedFiles.length > 0) {
for (var i = 0; i < selectedFiles.length; i++) {
formData.append('photoFiles', selectedFiles[i]);
// 중요한 로직 주석: 선택된 단속 사진들을 FormData에 추가 (있는 경우에만)
if (crdnSelectedFiles && crdnSelectedFiles.length > 0) {
for (var i = 0; i < crdnSelectedFiles.length; i++) {
formData.append('crdnPhotoFiles', crdnSelectedFiles[i]);
}
}
// 중요한 로직 주석: 선택된 조치 사진들을 FormData에 추가 (있는 경우에만)
if (actnSelectedFiles && actnSelectedFiles.length > 0) {
for (var i = 0; i < actnSelectedFiles.length; i++) {
formData.append('actnPhotoFiles', actnSelectedFiles[i]);
}
}
@ -531,79 +603,116 @@
}
}
// 전역 변수: 선택된 파일들을 보관
var selectedFiles = [];
// 전역 변수: 선택된 파일들을 사진 종류별로 보관
var crdnSelectedFiles = []; // 단속 사진 파일들
var actnSelectedFiles = []; // 조치 사진 파일들
/**
* 파일 업로드 초기화
* 중요한 로직 주석: 파일 선택 이벤트와 드래그앤드롭 이벤트를 추가하고 파일 유효성 검증을 초기화한다.
* 중요한 로직 주석: 단속/조치 사진 각각의 파일 선택 이벤트와 드래그앤드롭 이벤트를 추가한다.
*/
function initFileUpload() {
var dropZone = $('#photoPreviewContainer');
// 단속 사진 영역
var crdnDropZone = $('#crdnPhotoPreviewContainer');
var actnDropZone = $('#actnPhotoPreviewContainer');
// 단속 사진 파일 선택 이벤트
$('#crdnPhotoFiles').on('change', function(e) {
handleFileSelect(e.target.files, '1');
});
// 파일 선택 이벤트
$('#photoFiles').on('change', function(e) {
handleFileSelect(e.target.files);
// 조치 사진 파일 선택 이벤트
$('#actnPhotoFiles').on('change', function(e) {
handleFileSelect(e.target.files, '2');
});
// 중요로직: 드래그앤드롭으로 파일 업로드 처리
dropZone.on('dragover', function(e) {
// 중요로직: 단속 사진 드래그앤드롭 처리
crdnDropZone.on('dragover', function(e) {
e.preventDefault();
e.stopPropagation();
dropZone.addClass('drag-over');
crdnDropZone.addClass('drag-over');
});
dropZone.on('dragleave', function(e) {
crdnDropZone.on('dragleave', function(e) {
e.preventDefault();
e.stopPropagation();
dropZone.removeClass('drag-over');
crdnDropZone.removeClass('drag-over');
});
dropZone.on('drop', function(e) {
crdnDropZone.on('drop', function(e) {
e.preventDefault();
e.stopPropagation();
dropZone.removeClass('drag-over');
crdnDropZone.removeClass('drag-over');
var files = e.originalEvent.dataTransfer.files;
if (files.length > 0) {
// 기존 파일 선택 로직 재사용
handleFileSelect(files);
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'); // 조치 사진으로 처리
}
});
}
/**
* 파일 선택 처리
* 중요한 로직 주석: 선택된 파일들을 검증하고 파일명만 표시한다. 미리보기는 하지 않는다.
* 중요한 로직 주석: 사진 종류별로 선택된 파일들을 검증하고 각자의 영역에 미리보기를 표시한다.
* @param {FileList} files - 선택된 파일 목록
* @param {string} photoType - 사진 종류 ('1': 단속, '2': 조치)
*/
function handleFileSelect(files) {
function handleFileSelect(files, photoType) {
if (!files || files.length === 0) {
return;
}
// 파일 개수 제한 체크
var existingPhotos = $('.photo-preview-item.existing-photo').length;
var newFiles = selectedFiles.length + files.length;
var containerSelector = photoType === '1' ? '#crdnPhotoPreviewContainer' : '#actnPhotoPreviewContainer';
var selectedFilesArray = photoType === '1' ? crdnSelectedFiles : actnSelectedFiles;
var photoTypeName = photoType === '1' ? '단속' : '조치';
// 파일 개수 제한 체크 (각 종류별로 10개까지)
var existingPhotos = $(containerSelector + ' .photo-preview-item.existing-photo').length;
var newFiles = selectedFilesArray.length + files.length;
if (existingPhotos + newFiles > 10) {
alert('최대 10개의 사진만 업로드할 수 있습니다.');
alert('최대 10개의 ' + photoTypeName + ' 사진만 업로드할 수 있습니다.');
return;
}
// 각 파일에 대해 검증 및 파일명만 표시
// 각 파일에 대해 검증 및 미리보기 생성
for (var i = 0; i < files.length; i++) {
var file = files[i];
// 파일 유효성 검증
if (!validateFile(file)) {
continue;
}
// 선택된 파일 목록에 추가
selectedFiles.push(file);
// 미리보기 생성 (클릭 이벤트 없음)
createPhotoPreview(file);
// 해당 종류의 선택된 파일 목록에 추가
selectedFilesArray.push(file);
// 미리보기 생성
createPhotoPreview(file, photoType);
}
}
@ -615,7 +724,7 @@
// 파일 크기 체크 (10MB)
var maxSize = 10 * 1024 * 1024;
if (file.size > maxSize) {
alert(file.name + ' 파일이 10MB를 초과합니다.');
alert(file.name + ' 파일이 20MB를 초과합니다.');
return false;
}
@ -639,40 +748,46 @@
}
/**
* 사진 미리보기 생성 (클릭 이벤트 없음)
* 중요한 로직 주석: 선택된 파일을 읽어서 미리보기 이미지를 생성한다. 클릭 시 원본 이미지 팝업은 제공하지 않는다.
* 사진 미리보기 생성
* 중요한 로직 주석: 선택된 파일을 읽어서 해당 종류의 미리보기 영역에 이미지를 생성한다.
* @param {File} file - 미리보기를 생성할 파일
* @param {string} photoType - 사진 종류 ('1': 단속, '2': 조치)
*/
function createPhotoPreview(file) {
function createPhotoPreview(file, photoType) {
var reader = new FileReader();
var containerSelector = photoType === '1' ? '#crdnPhotoPreviewContainer' : '#actnPhotoPreviewContainer';
var photoTypeName = photoType === '1' ? '단속' : '조치';
reader.onload = function(e) {
var photoSeCd = $('#crdnPhotoSeCd').val();
var photoSeCdNm = photoSeCd === '1' ? '단속' : '조치';
var previewHtml =
'<div class="photo-preview-item new-photo" data-file-name="' + file.name + '">' +
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">[' + photoSeCdNm + ']</div>' +
' <button type="button" class="delete-photo-btn" onclick="deleteNewPhoto(this)">삭제</button>' +
' <div class="photo-type">[' + photoTypeName + ']</div>' +
' <button type="button" class="delete-photo-btn" onclick="deleteNewPhoto(this, \'' + photoType + '\')">삭제</button>' +
' </div>' +
'</div>';
$('#photoPreviewContainer').append(previewHtml);
$(containerSelector).append(previewHtml);
};
reader.readAsDataURL(file);
}
/**
* 기존 사진 삭제
* 중요한 로직 주석: DB에 등록된 사진을 논리삭제하고 화면에서 제거한다.
* @param {string} actInfoId - 행위정보 ID
* @param {string} crdnPhotoSn - 단속사진 순번
* @param {string} photoType - 사진 종류 ('1': 단속, '2': 조치)
*/
function deleteExistingPhoto(actInfoId, crdnPhotoSn) {
if (!confirm('선택한 사진을 삭제하시겠습니까?')) {
function deleteExistingPhoto(actInfoId, crdnPhotoSn, photoType) {
var photoTypeName = photoType === '1' ? '단속' : '조치';
if (!confirm('선택한 ' + photoTypeName + ' 사진을 삭제하시겠습니까?')) {
return;
}
@ -686,7 +801,7 @@
success: function(response) {
if (response && response.success) {
// 화면에서 해당 사진 제거
$('[data-act-info-id="' + actInfoId + '"][data-photo-sn="' + crdnPhotoSn + '"]').remove();
$('[data-act-info-id="' + actInfoId + '"][data-photo-sn="' + crdnPhotoSn + '"][data-photo-type="' + photoType + '"]').remove();
} else {
alert(response.message || '사진 삭제에 실패했습니다.');
}
@ -696,20 +811,23 @@
/**
* 새로 선택한 파일 삭제
* 중요한 로직 주석: 아직 저장되지 않은 파일을 목록에서 제거하고 selectedFiles 배열에서도 제거한다.
* 중요한 로직 주석: 아직 저장되지 않은 파일을 해당 종류의 목록에서 제거하고 selectedFiles 배열에서도 제거한다.
* @param {Element} button - 삭제 버튼 요소
* @param {string} photoType - 사진 종류 ('1': 단속, '2': 조치)
*/
function deleteNewPhoto(button) {
function deleteNewPhoto(button, photoType) {
var $item = $(button).closest('.photo-preview-item');
var fileName = $item.data('file-name');
// selectedFiles 배열에서 해당 파일 제거
for (var i = 0; i < selectedFiles.length; i++) {
if (selectedFiles[i].name === fileName) {
selectedFiles.splice(i, 1);
var selectedFilesArray = photoType === '1' ? crdnSelectedFiles : actnSelectedFiles;
// 해당 종류의 selectedFiles 배열에서 파일 제거
for (var i = 0; i < selectedFilesArray.length; i++) {
if (selectedFilesArray[i].name === fileName) {
selectedFilesArray.splice(i, 1);
break;
}
}
// 화면에서 제거
$item.remove();
}
@ -718,8 +836,8 @@
* 원본 사진 보기 (기존 DB 사진)
* 중요한 로직 주석: 등록된 사진을 photoView 페이지에서 원본 크기로 표시하고 다운로드 버튼을 제공한다.
*/
function viewOriginalPhoto(actInfoId, crdnPhotoSn) {
var url = '<c:url value="/crdn/crndRegistAndView/crdnActInfo/photoView.do"/>?actInfoId=' + actInfoId + '&crdnPhotoSn=' + crdnPhotoSn;
function viewOriginalPhoto(actInfoId, crdnPhotoSn, crdnPhotoSeCd) {
var url = '<c:url value="/crdn/crndRegistAndView/crdnActInfo/photoView.do"/>?actInfoId=' + actInfoId + '&crdnPhotoSn=' + crdnPhotoSn + '&crdnPhotoSeCd=' + crdnPhotoSeCd ;
openPopup(url, 1000, 700, '_blank');
}

@ -49,19 +49,27 @@ function validateFormByAttributes(formId) {
// validation-check 속성을 가진 모든 요소 선택
const elements = form.querySelectorAll('[validation-check]');
let isValid = true;
let focusTarget = null;
// 각 요소에 대해 검증 수행
elements.forEach(element => {
const elementValid = validateByAttribute(element);
isValid = isValid && elementValid;
if( !elementValid ){
focusTarget = element;
}
});
// 별도 함수로 진행된 validation 중에 is-invalid class 가 포함되어 있으면 취소
elements.forEach(element => {
if( element.classList.contains('is-invalid-custom') ){
isValid = false;
focusTarget = element;
}
});
if( focusTarget ){
focusTarget.focus();
}
return isValid;
}
@ -130,7 +138,6 @@ function validateByAttribute(element) {
switch (type) {
case 'required':
if (isEmpty(value)) {
console.log(value, isEmpty(value));
isValid = false;
errorMessage = '필수 입력 항목입니다.';
}

Loading…
Cancel
Save