단속 등록&열람: 단속 사진 등록 기능 개발 진행 중

dev
박성영 3 months ago
parent c024796a44
commit d4a3865d90

@ -3,6 +3,7 @@ package go.kr.project.crdn.crndRegistAndView.crdnActInfo.controller;
import egovframework.constant.MessageConstants;
import egovframework.constant.TilesConstants;
import egovframework.util.ApiResponseUtil;
import egovframework.util.FileUtil;
import egovframework.util.SessionUtil;
import go.kr.project.common.model.CmmnCodeSearchVO;
import go.kr.project.common.service.CommonCodeService;
@ -10,8 +11,10 @@ import go.kr.project.crdn.crndRegistAndView.crdnActInfo.model.CrdnActInfoVO;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.model.CrdnStrctIdxVO;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.model.CrdnVltnLwrgVO;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.model.CrdnUsgIdxVO;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.model.CrdnPhotoVO;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.model.CrdnPstnIdxVO;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.service.CrdnActInfoService;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.service.CrdnPhotoService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@ -23,7 +26,12 @@ import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;
import go.kr.project.common.model.FileVO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@ -51,6 +59,8 @@ public class CrdnActInfoController {
private final CrdnActInfoService service;
private final CommonCodeService commonCodeService;
private final CrdnPhotoService photoService;
private final FileUtil fileUtil;
/**
* (AJAX)
@ -99,14 +109,19 @@ public class CrdnActInfoController {
log.debug("불법위반행위정보 팝업 요청 - crdnYr: {}, crdnNo: {}, mode: {}, actInfoId: {}", crdnYr, crdnNo, mode, actInfoId);
CrdnActInfoVO data = null;
List<CrdnPhotoVO> photoList = null;
// 수정 모드인 경우 기존 데이터 조회
// 수정 모드인 경우 기존 데이터 및 사진 목록 조회
if ("U".equals(mode) && actInfoId != null) {
data = service.selectActInfoByPk(actInfoId);
// 중요한 로직 주석: 수정 모드에서 기존 등록된 사진 목록을 조회하여 화면에 표시한다.
photoList = photoService.selectPhotoListByActInfoIdAndPhotoSeCd(
CrdnPhotoVO.builder().actInfoId(actInfoId).crdnPhotoSeCd("1").build());
}
ModelAndView mav = new ModelAndView("crdn/crndRegistAndView/crdnActInfo/crdnActInfoRegistPopup" + TilesConstants.POPUP);
mav.addObject("data", data);
mav.addObject("photoList", photoList);
mav.addObject("mode", mode);
mav.addObject("crdnYr", crdnYr);
mav.addObject("crdnNo", crdnNo);
@ -114,6 +129,7 @@ public class CrdnActInfoController {
// 필요한 공통코드 조회
model.addAttribute("actTypeCdList", commonCodeService.selectCodeDetailList(CmmnCodeSearchVO.builder().searchCdGroupId("ACT_TYPE_CD").build()));
model.addAttribute("crdnPhotoCdList", commonCodeService.selectCodeDetailList(CmmnCodeSearchVO.builder().searchCdGroupId("CRDN_PHOTO_CD").build()));
return mav;
}
@ -183,23 +199,33 @@ public class CrdnActInfoController {
}
/**
* (AJAX)
* ( , AJAX)
* : . service .
* @param vo
* @param photoFiles ()
* @param crdnPhotoSeCd
* @return
*/
@Operation(summary = "불법위반행위정보 등록", description = "불법위반행위정보를 등록합니다.")
@Operation(summary = "불법위반행위정보 등록", description = "불법위반행위정보를 등록합니다. 사진 파일도 함께 처리 가능합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "등록 성공"),
@ApiResponse(responseCode = "500", description = "서버 오류")
})
@PostMapping("/insert.ajax")
public ResponseEntity<?> insertAjax(@ModelAttribute CrdnActInfoVO vo) {
log.debug("불법위반행위정보 등록 요청: {}", vo);
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);
vo.setSggCd(SessionUtil.getSessionVO().getUser().getOrgCd());
vo.setRgtr(SessionUtil.getUserId());
vo.setMdfr(SessionUtil.getUserId());
int result = service.insertActInfo(vo);
// 중요한 로직 주석: service 단에서 행위정보 등록과 사진 파일 처리를 한 트랜잭션으로 처리
int result = service.insertActInfoWithFiles(vo, photoFiles, crdnPhotoSeCd);
if (result > 0) {
return ApiResponseUtil.success(MessageConstants.Common.SAVE_SUCCESS);
@ -208,21 +234,34 @@ public class CrdnActInfoController {
}
}
/**
* (AJAX)
* ( , AJAX)
* : . service .
* @param vo
* @param photoFiles ()
* @param crdnPhotoSeCd
* @return
*/
@Operation(summary = "불법위반행위정보 수정", description = "불법위반행위정보를 수정합니다.")
@Operation(summary = "불법위반행위정보 수정", description = "불법위반행위정보를 수정합니다. 사진 파일도 함께 처리 가능합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "수정 성공"),
@ApiResponse(responseCode = "500", description = "서버 오류")
})
@PostMapping("/update.ajax")
public ResponseEntity<?> updateAjax(@ModelAttribute CrdnActInfoVO vo) {
log.debug("불법위반행위정보 수정 요청: {}", vo);
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);
int result = service.updateActInfo(vo);
vo.setMdfr(SessionUtil.getUserId());
vo.setSggCd(SessionUtil.getSessionVO().getUser().getOrgCd()); // 업데이트 화면에서 신규파일 등록 시 필요
// 중요한 로직 주석: service 단에서 행위정보 수정과 사진 파일 처리를 한 트랜잭션으로 처리
int result = service.updateActInfoWithFiles(vo, photoFiles, crdnPhotoSeCd);
if (result > 0) {
return ApiResponseUtil.success(MessageConstants.Common.SAVE_SUCCESS);
@ -231,6 +270,7 @@ public class CrdnActInfoController {
}
}
/**
* (AJAX)
* @param actInfoIds ID
@ -252,4 +292,103 @@ public class CrdnActInfoController {
return ApiResponseUtil.error("삭제에 실패했습니다.");
}
}
/**
*
* : . photoView.jsp .
* @param actInfoId ID
* @param crdnPhotoSn
* @return photoView
*/
@Operation(summary = "단속 사진 보기", description = "단속 사진을 화면에 표시합니다.")
@GetMapping("/photoView.do")
public String photoView(
@Parameter(description = "행위정보ID") @RequestParam String actInfoId,
@Parameter(description = "사진순번") @RequestParam String crdnPhotoSn,
Model model) {
log.debug("단속 사진 보기 요청 - actInfoId: {}, crdnPhotoSn: {}", actInfoId, crdnPhotoSn);
// 중요한 로직 주석: URL 파라미터를 모델에 추가하여 JSP에서 사용할 수 있도록 한다.
model.addAttribute("actInfoId", actInfoId);
model.addAttribute("crdnPhotoSn", crdnPhotoSn);
return "crdn/crndRegistAndView/crdnActInfo/photoView";
}
/**
* ( )
* :
* @param actInfoId ID
* @param crdnPhotoSn
* @param request HTTP
* @param response HTTP
*/
@Operation(summary = "단속 사진 다운로드", description = "단속 사진을 다운로드합니다.")
@GetMapping("/downloadPhoto.do")
public void downloadPhoto(
@Parameter(description = "행위정보ID") @RequestParam String actInfoId,
@Parameter(description = "사진순번") @RequestParam String crdnPhotoSn,
HttpServletRequest request, HttpServletResponse response) {
log.debug("단속 사진 다운로드 요청 - actInfoId: {}, crdnPhotoSn: {}", actInfoId, crdnPhotoSn);
try {
// 중요한 로직 주석: 사진 정보 생성 및 조회
CrdnPhotoVO photoVO = CrdnPhotoVO.builder()
.actInfoId(actInfoId)
.crdnPhotoSn(crdnPhotoSn)
.build();
FileVO fileVO = photoService.getPhotoFileForDownload(photoVO);
if (fileVO == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 중요한 로직 주석: FileUtil을 통한 파일 다운로드 처리
fileUtil.downloadFile(fileVO, request, response);
} catch (Exception e) {
log.error("사진 다운로드 중 오류 발생: actInfoId={}, crdnPhotoSn={}", actInfoId, crdnPhotoSn, e);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
/**
* (AJAX)
* : Ajax , DB .
* @param actInfoId ID
* @param crdnPhotoSn
* @return
*/
@Operation(summary = "단속 사진 개별 삭제", description = "단속 사진을 개별적으로 삭제합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "삭제 성공"),
@ApiResponse(responseCode = "500", description = "서버 오류")
})
@PostMapping("/deletePhoto.ajax")
public ResponseEntity<?> deletePhotoAjax(
@Parameter(description = "행위정보ID") @RequestParam String actInfoId,
@Parameter(description = "사진순번") @RequestParam String crdnPhotoSn) {
log.debug("단속 사진 개별 삭제 요청 - actInfoId: {}, crdnPhotoSn: {}", actInfoId, crdnPhotoSn);
// 중요한 로직 주석: 삭제할 사진 정보 생성
CrdnPhotoVO photoVO = CrdnPhotoVO.builder()
.actInfoId(actInfoId)
.crdnPhotoSn(crdnPhotoSn)
.dltr(SessionUtil.getUserId())
.build();
// 중요한 로직 주석: 사진 삭제 (논리삭제 + 파일삭제)
int result = photoService.deletePhotoWithFile(photoVO);
if (result > 0) {
return ApiResponseUtil.success("사진이 성공적으로 삭제되었습니다.");
} else {
return ApiResponseUtil.error("사진 삭제에 실패했습니다.");
}
}
}

@ -0,0 +1,80 @@
package go.kr.project.crdn.crndRegistAndView.crdnActInfo.mapper;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.model.CrdnPhotoVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
*
* : tb_crdn_photo CRUD .
* ID , / .
*/
@Mapper
public interface CrdnPhotoMapper {
/**
*
* @param photoVO ID
* @return ( )
*/
List<CrdnPhotoVO> selectPhotoListByActInfoIdAndPhotoSeCd(CrdnPhotoVO photoVO);
/**
* (PK )
* @param photoVO ID +
* @return
*/
CrdnPhotoVO selectPhotoByPk(CrdnPhotoVO photoVO);
/**
*
* : .
* @param actInfoId ID
* @return (01, 02, 03 )
*/
String selectNextPhotoSn(String actInfoId);
/**
*
* @param photoVO
* @return
*/
int insertPhoto(CrdnPhotoVO photoVO);
/**
* (, )
* @param photoVO
* @return
*/
int updatePhoto(CrdnPhotoVO photoVO);
/**
* ()
* @param photoVO (delYn, delDt, dltr )
* @return
*/
int deletePhoto(CrdnPhotoVO photoVO);
/**
* ()
* : .
* @param photoVO (actInfoId, delYn, delDt, dltr )
* @return
*/
int deletePhotosByActInfoId(CrdnPhotoVO photoVO);
/**
*
* @param actInfoId ID
* @return ( )
*/
int selectPhotoCountByActInfoId(String actInfoId);
/**
*
* @param photoVO (actInfoId + crdnPhotoSn)
* @return (1: , 0: )
*/
int existsPhoto(CrdnPhotoVO photoVO);
}

@ -0,0 +1,51 @@
package go.kr.project.crdn.crndRegistAndView.crdnActInfo.model;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
/**
* VO
* : tb_crdn_photo , // .
* 01, 02, 03 .
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CrdnPhotoVO {
// ============ 테이블 컬럼 ============
private String actInfoId; // 행위 정보 ID (FK)
private String crdnPhotoSn; // 단속 사진 순번 (PK)
private String sggCd; // 시군구 코드
private String crdnYr; // 단속 연도
private String crdnNo; // 단속 번호
private String crdnPhotoPath; // 단속 사진 경로
private String crdnPhotoNm; // 단속 사진 명
private String crdnPhotoSeCd; // 단속 사진 구분 코드 (01:단속, 02:조치)
private String orgnlPhotoNm; // 원본 사진 명
@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; // 등록자
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 String crdnPhotoSeCdNm; // 단속 사진 구분 코드명
// ============ 파일 처리용 추가 필드 ============
private String fileSize; // 파일 크기 (표시용)
private String fileExt; // 파일 확장자
private String fullFilePath; // 전체 파일 경로 (서버 내부용)
private boolean isNewFile; // 신규 파일 여부 (등록/수정 구분용)
}

@ -5,6 +5,7 @@ import go.kr.project.crdn.crndRegistAndView.crdnActInfo.model.CrdnStrctIdxVO;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.model.CrdnVltnLwrgVO;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.model.CrdnUsgIdxVO;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.model.CrdnPstnIdxVO;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@ -27,13 +28,16 @@ public interface CrdnActInfoService {
* @return
*/
int selectActInfoListTotalCount(CrdnActInfoVO vo);
/**
*
* ( )
* : . .
* @param vo
* @param photoFiles ()
* @param crdnPhotoSeCd
* @return
*/
int insertActInfo(CrdnActInfoVO vo);
int insertActInfoWithFiles(CrdnActInfoVO vo, List<MultipartFile> photoFiles, String crdnPhotoSeCd);
/**
* (PK )
@ -43,11 +47,14 @@ public interface CrdnActInfoService {
CrdnActInfoVO selectActInfoByPk(String actInfoId);
/**
*
* ( )
* : . .
* @param vo
* @param photoFiles ()
* @param crdnPhotoSeCd
* @return
*/
int updateActInfo(CrdnActInfoVO vo);
int updateActInfoWithFiles(CrdnActInfoVO vo, List<MultipartFile> photoFiles, String crdnPhotoSeCd);
/**
* ()

@ -0,0 +1,96 @@
package go.kr.project.crdn.crndRegistAndView.crdnActInfo.service;
import go.kr.project.common.model.FileVO;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.model.CrdnPhotoVO;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
*
* : tb_crdn_photo CRUD / .
* FileUtil .
*/
public interface CrdnPhotoService {
/**
*
* @param photoVO ID
* @return ( )
*/
List<CrdnPhotoVO> selectPhotoListByActInfoIdAndPhotoSeCd(CrdnPhotoVO photoVO);
/**
* (PK )
* @param photoVO ID +
* @return
*/
CrdnPhotoVO selectPhotoByPk(CrdnPhotoVO photoVO);
/**
*
* : MultipartFile FileUtil DB .
* , DB .
* @param files
* @param actInfoId ID
* @param crdnPhotoSeCd (01:, 02:)
* @param actInfoVO (, , )
* @return
*/
int insertPhotosWithFiles(List<MultipartFile> files, String actInfoId, String crdnPhotoSeCd, Object actInfoVO);
/**
* ( )
* : , .
* @param file (null )
* @param photoVO
* @return
*/
int updatePhotoWithFile(MultipartFile file, CrdnPhotoVO photoVO);
/**
* ( + )
* : DB .
* @param photoVO
* @return
*/
int deletePhotoWithFile(CrdnPhotoVO photoVO);
/**
* ( + )
* : .
* @param actInfoId ID
* @param crdnPhotoSeCd [, ]
* @param dltr ID
* @return
*/
int deletePhotosByActInfoIdAndCrdnPhotoSecd(String actInfoId, String crdnPhotoSeCd, String dltr);
/**
*
* @param photoVO
* @return FileVO
*/
FileVO getPhotoFileForDownload(CrdnPhotoVO photoVO);
/**
*
* @param actInfoId ID
* @return ( )
*/
int selectPhotoCountByActInfoId(String actInfoId);
/**
*
* @param photoVO
* @return
*/
boolean existsPhoto(CrdnPhotoVO photoVO);
/**
*
* @param actInfoId ID
* @return (01, 02, 03 )
*/
String getNextPhotoSn(String actInfoId);
}

@ -9,12 +9,14 @@ import go.kr.project.crdn.crndRegistAndView.crdnActInfo.model.CrdnVltnLwrgVO;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.model.CrdnUsgIdxVO;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.model.CrdnPstnIdxVO;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.service.CrdnActInfoService;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.service.CrdnPhotoService;
import go.kr.project.crdn.crndRegistAndView.crdnActrInfo.mapper.CrdnActrInfoMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@ -40,6 +42,7 @@ public class CrdnActInfoServiceImpl extends EgovAbstractServiceImpl implements C
private final CrdnActInfoMapper mapper;
private final CrdnActrInfoMapper actrInfoMapper;
private final CrdnPhotoService photoService;
@Override
public List<CrdnActInfoVO> selectActInfoList(CrdnActInfoVO vo) {
@ -54,10 +57,34 @@ public class CrdnActInfoServiceImpl extends EgovAbstractServiceImpl implements C
}
@Override
public int insertActInfo(CrdnActInfoVO vo) {
log.debug("불법위반행위정보 등록: {}", vo);
return mapper.insertActInfo(vo);
@Transactional
public int insertActInfoWithFiles(CrdnActInfoVO vo, List<MultipartFile> photoFiles, String crdnPhotoSeCd) {
log.debug("불법위반행위정보 등록 (파일 포함): {}, 파일 개수: {}", vo,
photoFiles != null ? photoFiles.size() : 0);
try {
// 중요한 로직 주석: 먼저 행위정보를 등록한다 (insertActInfo 메서드에서 actInfoId가 자동 설정됨)
int result = mapper.insertActInfo(vo);
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);
}
} else {
log.error("행위정보 등록 실패: {}", vo);
throw new MessageException("행위정보 등록에 실패했습니다.");
}
return result;
} catch (Exception e) {
log.error("행위정보 등록 (파일 포함) 중 오류 발생: {}", vo, e);
throw new MessageException("행위정보 등록 중 오류가 발생했습니다: " + e.getMessage());
}
}
@Override
@ -67,14 +94,53 @@ public class CrdnActInfoServiceImpl extends EgovAbstractServiceImpl implements C
}
@Override
public int updateActInfo(CrdnActInfoVO vo) {
log.debug("불법위반행위정보 수정: {}", vo);
return mapper.updateActInfo(vo);
@Transactional
public int updateActInfoWithFiles(CrdnActInfoVO vo, List<MultipartFile> photoFiles, String crdnPhotoSeCd) {
log.debug("불법위반행위정보 수정 (파일 포함): {}, 파일 개수: {}", vo,
photoFiles != null ? photoFiles.size() : 0);
try {
// 중요한 로직 주석: 먼저 행위정보를 수정한다
int result = mapper.updateActInfo(vo);
if (result > 0) {
log.debug("행위정보 수정 완료: actInfoId={}", vo.getActInfoId());
// 중요한 로직 주석: 새로운 파일이 있는 경우에만 사진 추가 처리를 진행한다
// 기존 사진의 개별 삭제는 별도의 Ajax 호출로 처리되므로 여기서는 신규 추가만 한다
if (photoFiles != null && !photoFiles.isEmpty()) {
int photoResult = photoService.insertPhotosWithFiles(photoFiles, vo.getActInfoId(), crdnPhotoSeCd, vo);
log.debug("사진 파일 추가 완료: actInfoId={}, 추가된 사진 수={}", vo.getActInfoId(), photoResult);
}
} 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 deleteActInfo(CrdnActInfoVO vo) {
log.debug("불법위반행위정보 삭제: {}", vo);
// 중요한 로직 주석: 행위정보 삭제 시 관련된 모든 사진도 함께 삭제한다.
if (vo.getActInfoId() != null) {
try {
photoService.deletePhotosByActInfoIdAndCrdnPhotoSecd(vo.getActInfoId(), "1", vo.getDltr());
log.debug("행위정보 관련 사진 삭제 완료: actInfoId={}", vo.getActInfoId());
} catch (Exception e) {
log.warn("행위정보 관련 사진 삭제 중 오류 발생: actInfoId={}", vo.getActInfoId(), e);
// 사진 삭제 실패해도 행위정보 삭제는 진행
}
}
return mapper.deleteActInfo(vo);
}
@ -143,6 +209,15 @@ public class CrdnActInfoServiceImpl extends EgovAbstractServiceImpl implements C
continue;
}
// 중요한 로직 주석: 해당 행위정보의 관련 사진들을 먼저 삭제한다.
try {
photoService.deletePhotosByActInfoIdAndCrdnPhotoSecd(actInfoId, "1", userId);
log.debug("행위정보 관련 사진 삭제 완료: actInfoId={}", actInfoId);
} catch (Exception e) {
log.warn("행위정보 관련 사진 삭제 중 오류 발생: actInfoId={}", actInfoId, e);
// 사진 삭제 실패해도 행위정보 삭제는 진행
}
// 중요한 로직 주석: 삭제 VO 생성 및 삭제자 정보 설정
CrdnActInfoVO deleteVO = CrdnActInfoVO.builder()
.actInfoId(actInfoId)

@ -0,0 +1,294 @@
package go.kr.project.crdn.crndRegistAndView.crdnActInfo.service.impl;
import egovframework.exception.MessageException;
import egovframework.util.FileUtil;
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.CrdnPhotoVO;
import go.kr.project.crdn.crndRegistAndView.crdnActInfo.service.CrdnPhotoService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
/**
*
* <pre>
* packageName : go.kr.project.crdn.crndRegistAndView.crdnActInfo.service.impl
* fileName : CrdnPhotoServiceImpl
* author :
* date : 2025-09-03
* description :
* : tb_crdn_photo CRUD FileUtil .
* .
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-09-03
* </pre>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CrdnPhotoServiceImpl extends EgovAbstractServiceImpl implements CrdnPhotoService {
private final CrdnPhotoMapper mapper;
private final FileUtil fileUtil;
public List<CrdnPhotoVO> selectPhotoListByActInfoIdAndPhotoSeCd(CrdnPhotoVO photoVO) {
log.debug("단속 사진 목록 조회: actInfoId={}, CrdnPhotoSeCd={}", photoVO.getActInfoId(), photoVO.getCrdnPhotoSeCd());
return mapper.selectPhotoListByActInfoIdAndPhotoSeCd(photoVO);
}
@Override
public CrdnPhotoVO selectPhotoByPk(CrdnPhotoVO photoVO) {
log.debug("단속 사진 상세 조회: {}", photoVO);
return mapper.selectPhotoByPk(photoVO);
}
/**
*
* : MultipartFile DB .
* DB , .
*/
@Override
@Transactional
public int insertPhotosWithFiles(List<MultipartFile> files, String actInfoId, String crdnPhotoSeCd, Object actInfoVO) {
log.debug("사진 파일 업로드 및 등록: actInfoId={}, files.size={}", actInfoId, files != null ? files.size() : 0);
// 중요한 로직 주석: 파일 목록 유효성 검증
if (files == null || files.isEmpty()) {
log.warn("업로드할 파일이 없습니다.");
return 0;
}
// 중요한 로직 주석: 현재 사용자 정보 가져오기
String userId = SessionUtil.getUserId();
if (userId == null || userId.trim().isEmpty()) {
log.error("등록자 정보를 가져올 수 없습니다.");
throw new MessageException("사용자 정보를 확인할 수 없습니다.");
}
// 중요한 로직 주석: 행위정보에서 필요한 정보 추출
CrdnActInfoVO actInfo = (CrdnActInfoVO) actInfoVO;
int insertedCount = 0;
try {
// 중요한 로직 주석: FileUtil을 통해 파일 업로드 처리
List<FileVO> uploadedFiles = fileUtil.uploadFiles(files, "crdn-act-photo");
for (FileVO fileVO : uploadedFiles) {
// 중요한 로직 주석: 다음 사진 순번 생성
String nextPhotoSn = mapper.selectNextPhotoSn(actInfoId);
// 중요한 로직 주석: 사진 정보 VO 생성
CrdnPhotoVO photoVO = CrdnPhotoVO.builder()
.actInfoId(actInfoId)
.crdnPhotoSn(nextPhotoSn)
.sggCd(actInfo.getSggCd())
.crdnYr(actInfo.getCrdnYr())
.crdnNo(actInfo.getCrdnNo())
.crdnPhotoPath(fileVO.getFilePath())
.crdnPhotoNm(fileVO.getStoredFileNm())
.crdnPhotoSeCd(crdnPhotoSeCd)
.orgnlPhotoNm(fileVO.getOriginalFileNm())
.regDt(LocalDateTime.now())
.rgtr(userId)
.delYn("N")
.build();
// 중요한 로직 주석: DB에 사진 정보 등록
int result = mapper.insertPhoto(photoVO);
if (result > 0) {
insertedCount++;
log.debug("사진 등록 완료: actInfoId={}, photoSn={}, fileName={}",
actInfoId, nextPhotoSn, fileVO.getOriginalFileNm());
} else {
log.error("사진 DB 등록 실패: actInfoId={}, photoSn={}", actInfoId, nextPhotoSn);
throw new MessageException("사진 정보 등록에 실패했습니다.");
}
}
} catch (Exception e) {
log.error("사진 업로드 중 오류 발생: actInfoId={}", actInfoId, e);
throw new MessageException("사진 업로드에 실패했습니다: " + e.getMessage());
}
log.info("사진 파일 업로드 완료: actInfoId={}, 등록된 사진 수={}", actInfoId, insertedCount);
return insertedCount;
}
/**
* ( )
* : .
*/
@Override
@Transactional
public int updatePhotoWithFile(MultipartFile file, CrdnPhotoVO photoVO) {
log.debug("사진 정보 수정: {}", photoVO);
try {
// 중요한 로직 주석: 새로운 파일이 있는 경우 파일 교체 처리
if (file != null && !file.isEmpty()) {
// 중요한 로직 주석: 기존 사진 정보 조회
CrdnPhotoVO existingPhoto = mapper.selectPhotoByPk(photoVO);
if (existingPhoto != null) {
// 중요한 로직 주석: 기존 파일 삭제
FileVO existingFileVO = new FileVO();
existingFileVO.setFilePath(existingPhoto.getCrdnPhotoPath());
existingFileVO.setStoredFileNm(existingPhoto.getCrdnPhotoNm());
existingFileVO.setOriginalFileNm(existingPhoto.getOrgnlPhotoNm());
fileUtil.deleteFile(existingFileVO);
}
// 중요한 로직 주석: 새 파일 업로드
List<MultipartFile> fileList = Arrays.asList(file);
List<FileVO> uploadedFiles = fileUtil.uploadFiles(fileList, "crdn-act-photo");
if (!uploadedFiles.isEmpty()) {
FileVO newFileVO = uploadedFiles.get(0);
photoVO.setCrdnPhotoPath(newFileVO.getFilePath());
photoVO.setCrdnPhotoNm(newFileVO.getStoredFileNm());
photoVO.setOrgnlPhotoNm(newFileVO.getOriginalFileNm());
}
}
// 중요한 로직 주석: DB 정보 수정
int result = mapper.updatePhoto(photoVO);
log.debug("사진 정보 수정 완료: {}", photoVO);
return result;
} catch (Exception e) {
log.error("사진 수정 중 오류 발생: {}", photoVO, e);
throw new MessageException("사진 수정에 실패했습니다: " + e.getMessage());
}
}
/**
* ( + )
* : DB .
*/
@Override
@Transactional
public int deletePhotoWithFile(CrdnPhotoVO photoVO) {
log.debug("사진 삭제: {}", photoVO);
try {
// 중요한 로직 주석: 삭제할 사진 정보 조회
CrdnPhotoVO existingPhoto = mapper.selectPhotoByPk(photoVO);
if (existingPhoto == null) {
log.warn("삭제할 사진을 찾을 수 없습니다: {}", photoVO);
return 0;
}
// 중요한 로직 주석: 삭제자 정보 설정
String userId = SessionUtil.getUserId();
photoVO.setDltr(userId);
photoVO.setDelDt(LocalDateTime.now());
// 중요한 로직 주석: DB에서 논리삭제
int result = mapper.deletePhoto(photoVO);
if (result > 0) {
// 중요한 로직 주석: 실제 파일 삭제
FileVO fileVO = new FileVO();
fileVO.setFilePath(existingPhoto.getCrdnPhotoPath());
fileVO.setStoredFileNm(existingPhoto.getCrdnPhotoNm());
fileVO.setOriginalFileNm(existingPhoto.getOrgnlPhotoNm());
fileUtil.deleteFile(fileVO);
log.debug("사진 삭제 완료: {}", photoVO);
}
return result;
} catch (Exception e) {
log.error("사진 삭제 중 오류 발생: {}", photoVO, e);
throw new MessageException("사진 삭제에 실패했습니다: " + e.getMessage());
}
}
@Transactional
public int deletePhotosByActInfoIdAndCrdnPhotoSecd(String actInfoId, String crdnPhotoSeCd, String dltr) {
log.debug("행위정보 관련 모든 사진 삭제: actInfoId={}", actInfoId);
try {
// 중요한 로직 주석: 삭제할 사진 목록 조회
List<CrdnPhotoVO> photoList = mapper.selectPhotoListByActInfoIdAndPhotoSeCd(
CrdnPhotoVO.builder().actInfoId(actInfoId).crdnPhotoSeCd(crdnPhotoSeCd).build());
if (photoList.isEmpty()) {
log.debug("삭제할 사진이 없습니다: actInfoId={}", actInfoId);
return 0;
}
// 중요한 로직 주석: DB에서 모든 사진 논리삭제
CrdnPhotoVO deleteVO = CrdnPhotoVO.builder()
.actInfoId(actInfoId)
.dltr(dltr)
.delDt(LocalDateTime.now())
.build();
int deletedCount = mapper.deletePhotosByActInfoId(deleteVO);
// 중요한 로직 주석: 실제 파일들 삭제
for (CrdnPhotoVO photo : photoList) {
FileVO fileVO = new FileVO();
fileVO.setFilePath(photo.getCrdnPhotoPath());
fileVO.setStoredFileNm(photo.getCrdnPhotoNm());
fileVO.setOriginalFileNm(photo.getOrgnlPhotoNm());
fileUtil.deleteFile(fileVO);
}
log.info("행위정보 관련 모든 사진 삭제 완료: actInfoId={}, 삭제된 사진 수={}", actInfoId, deletedCount);
return deletedCount;
} catch (Exception e) {
log.error("사진 일괄 삭제 중 오류 발생: actInfoId={}", actInfoId, e);
throw new MessageException("사진 삭제에 실패했습니다: " + e.getMessage());
}
}
@Override
public FileVO getPhotoFileForDownload(CrdnPhotoVO photoVO) {
log.debug("사진 파일 다운로드 정보 조회: {}", photoVO);
CrdnPhotoVO photo = mapper.selectPhotoByPk(photoVO);
if (photo == null) {
log.warn("다운로드할 사진을 찾을 수 없습니다: {}", photoVO);
return null;
}
FileVO fileVO = new FileVO();
fileVO.setFilePath(photo.getCrdnPhotoPath());
fileVO.setStoredFileNm(photo.getCrdnPhotoNm());
fileVO.setOriginalFileNm(photo.getOrgnlPhotoNm());
return fileVO;
}
@Override
public int selectPhotoCountByActInfoId(String actInfoId) {
log.debug("사진 개수 조회: actInfoId={}", actInfoId);
return mapper.selectPhotoCountByActInfoId(actInfoId);
}
@Override
public boolean existsPhoto(CrdnPhotoVO photoVO) {
log.debug("사진 존재 여부 확인: {}", photoVO);
return mapper.existsPhoto(photoVO) > 0;
}
@Override
public String getNextPhotoSn(String actInfoId) {
log.debug("다음 사진 순번 조회: actInfoId={}", actInfoId);
return mapper.selectNextPhotoSn(actInfoId);
}
}

@ -74,6 +74,9 @@
<!-- 불법위반행위정보 등록 (시컨스 사용) -->
<insert id="insertActInfo" parameterType="CrdnActInfoVO">
/* ActInfoMapper.insertActInfo : 불법위반행위정보 등록 */
<selectKey keyProperty="actInfoId" resultType="String" order="BEFORE">
SELECT LPAD(NEXTVAL(seq_act_info_id), 10, '0') AS actInfoId
</selectKey>
INSERT INTO tb_act_info (
ACT_INFO_ID,
SGG_CD,
@ -97,7 +100,7 @@
MDFR,
DEL_YN
) VALUES (
LPAD(NEXTVAL(seq_act_info_id), 10, '0'),
#{actInfoId},
#{sggCd},
#{crdnYr},
#{crdnNo},

@ -0,0 +1,158 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="go.kr.project.crdn.crndRegistAndView.crdnActInfo.mapper.CrdnPhotoMapper">
<!-- 특정 행위정보의 사진 목록 조회 -->
<select id="selectPhotoListByActInfoIdAndPhotoSeCd" parameterType="CrdnPhotoVO" resultType="CrdnPhotoVO">
/* CrdnPhotoMapper.selectPhotoListByActInfoId : 특정 행위정보의 사진 목록 조회 */
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,
cd.CD_NM AS CRDN_PHOTO_SE_CD_NM,
p.ORGNL_PHOTO_NM,
p.REG_DT,
p.RGTR,
p.DEL_YN,
p.DEL_DT,
p.DLTR
FROM tb_crdn_photo p
LEFT JOIN tb_cd_detail cd ON cd.CD_GROUP_ID = 'CRDN_PHOTO_CD' AND cd.CD_ID = p.CRDN_PHOTO_SE_CD
WHERE p.ACT_INFO_ID = #{actInfoId}
AND p.CRDN_PHOTO_SE_CD = #{crdnPhotoSeCd}
AND p.DEL_YN = 'N'
ORDER BY p.CRDN_PHOTO_SN ASC
</select>
<!-- 사진 상세 조회 (PK 기준) -->
<select id="selectPhotoByPk" parameterType="CrdnPhotoVO" resultType="CrdnPhotoVO">
/* CrdnPhotoMapper.selectPhotoByPk : 사진 상세 조회 */
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,
cd.CD_NM AS CRDN_PHOTO_SE_CD_NM,
p.ORGNL_PHOTO_NM,
p.REG_DT,
p.RGTR,
p.DEL_YN,
p.DEL_DT,
p.DLTR
FROM tb_crdn_photo p
LEFT JOIN tb_cd_detail cd ON cd.CD_GROUP_ID = 'CRDN_PHOTO_CD' AND cd.CD_ID = p.CRDN_PHOTO_SE_CD
WHERE p.ACT_INFO_ID = #{actInfoId}
AND p.CRDN_PHOTO_SN = #{crdnPhotoSn}
</select>
<!-- 다음 사진 순번 조회 -->
<select id="selectNextPhotoSn" parameterType="string" resultType="string">
/* CrdnPhotoMapper.selectNextPhotoSn : 다음 사진 순번 조회 */
SELECT IFNULL(
LPAD(
CAST(MAX(CAST(CRDN_PHOTO_SN AS UNSIGNED)) + 1 AS CHAR),
2,
'0'
),
'01'
) AS NEXT_SN
FROM tb_crdn_photo
WHERE ACT_INFO_ID = #{actInfoId}
</select>
<!-- 사진 등록 -->
<insert id="insertPhoto" parameterType="CrdnPhotoVO">
/* CrdnPhotoMapper.insertPhoto : 사진 등록 */
INSERT INTO tb_crdn_photo (
ACT_INFO_ID,
CRDN_PHOTO_SN,
SGG_CD,
CRDN_YR,
CRDN_NO,
CRDN_PHOTO_PATH,
CRDN_PHOTO_NM,
CRDN_PHOTO_SE_CD,
ORGNL_PHOTO_NM,
REG_DT,
RGTR,
DEL_YN
) VALUES (
#{actInfoId},
#{crdnPhotoSn},
#{sggCd},
#{crdnYr},
#{crdnNo},
#{crdnPhotoPath},
#{crdnPhotoNm},
#{crdnPhotoSeCd},
#{orgnlPhotoNm},
NOW(),
#{rgtr},
'N'
)
</insert>
<!-- 사진 정보 수정 -->
<update id="updatePhoto" parameterType="CrdnPhotoVO">
/* CrdnPhotoMapper.updatePhoto : 사진 정보 수정 */
UPDATE tb_crdn_photo SET
CRDN_PHOTO_PATH = #{crdnPhotoPath},
CRDN_PHOTO_NM = #{crdnPhotoNm},
ORGNL_PHOTO_NM = #{orgnlPhotoNm}
WHERE ACT_INFO_ID = #{actInfoId}
AND CRDN_PHOTO_SN = #{crdnPhotoSn}
</update>
<!-- 사진 삭제 (논리삭제) -->
<update id="deletePhoto" parameterType="CrdnPhotoVO">
/* CrdnPhotoMapper.deletePhoto : 사진 삭제 */
UPDATE tb_crdn_photo SET
DEL_YN = 'Y',
DEL_DT = NOW(),
DLTR = #{dltr}
WHERE ACT_INFO_ID = #{actInfoId}
AND CRDN_PHOTO_SN = #{crdnPhotoSn}
AND DEL_YN = 'N'
</update>
<!-- 특정 행위정보의 모든 사진 삭제 (논리삭제) -->
<update id="deletePhotosByActInfoId" parameterType="CrdnPhotoVO">
/* CrdnPhotoMapper.deletePhotosByActInfoId : 특정 행위정보의 모든 사진 삭제 */
UPDATE tb_crdn_photo SET
DEL_YN = 'Y',
DEL_DT = NOW(),
DLTR = #{dltr}
WHERE ACT_INFO_ID = #{actInfoId}
AND DEL_YN = 'N'
</update>
<!-- 특정 행위정보의 사진 개수 조회 -->
<select id="selectPhotoCountByActInfoId" parameterType="string" resultType="int">
/* CrdnPhotoMapper.selectPhotoCountByActInfoId : 특정 행위정보의 사진 개수 조회 */
SELECT COUNT(*)
FROM tb_crdn_photo
WHERE ACT_INFO_ID = #{actInfoId}
AND DEL_YN = 'N'
</select>
<!-- 사진 존재 여부 확인 -->
<select id="existsPhoto" parameterType="CrdnPhotoVO" resultType="int">
/* CrdnPhotoMapper.existsPhoto : 사진 존재 여부 확인 */
SELECT COUNT(*)
FROM tb_crdn_photo
WHERE ACT_INFO_ID = #{actInfoId}
AND CRDN_PHOTO_SN = #{crdnPhotoSn}
AND DEL_YN = 'N'
</select>
</mapper>

@ -4,6 +4,9 @@
<%@ 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">
@ -49,7 +52,7 @@
</td>
</tr>
<tr>
<th class="th"><span class="required">*</span> 위반법규1</th>
<th class="th"><span class="required">*</span> 위반법규</th>
<td colspan="3">
<input type="text" id="vltnLaw1" name="vltnLaw1" class="input" style="width: 250px;" placeholder="위반법규를 입력하거나 선택하세요" autocomplete="off" value="${data.vltnLaw1}" validation-check="required"/>
<input type="hidden" id="vltnLwrgCd1" name="vltnLwrgCd1" value="${data.vltnLwrgCd1}"/>
@ -98,6 +101,40 @@
<textarea id="rmrk" name="rmrk" class="textarea" rows="3" maxlength="1000" style="height: 80px;">${data.rmrk}</textarea>
</td>
</tr>
<tr>
<th class="th">행위 사진</th>
<td colspan="3">
<!-- 중요한 로직 주석: 파일 업로드와 미리보기 기능을 제공한다 -->
<div class="file-upload-section">
<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>
<input type="hidden" id="crdnPhotoSeCd" name="crdnPhotoSeCd" value="1"/>
<span class="file-info">최대 10개, 각 10MB 이하의 이미지 파일만 업로드 가능합니다.</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>
<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>
</div>
</div>
</c:forEach>
</c:if>
</div>
</div>
</td>
</tr>
</table>
</form>
</div>
@ -146,6 +183,9 @@
initVltnLwrgDropdowns();
initUsgIdxDropdown();
// 파일 업로드 이벤트 초기화
initFileUpload();
console.log('불법행위정보 팝업이 초기화되었습니다. 모드:', mode);
});
@ -373,6 +413,7 @@
/**
* 불법행위정보 저장 함수
* 중요한 로직 주석: validation 체크 후 mode에 따라 등록/수정 API를 호출한다.
* 새로 선택한 사진 파일이 있으면 FormData를 사용하여 파일과 함께 전송한다.
*/
function saveActInfo() {
// 중요로직: validateFormByAttributes를 사용하여 모든 validation-check 속성 검증
@ -391,33 +432,229 @@
}
var mode = $('#mode').val();
var formData = $('#actInfoForm').serialize();
var actionText = (mode === 'U') ? '수정' : '등록';
// 중요한 로직 주석: FormData를 사용하여 모든 데이터 전송 (파일 유무에 상관없이)
var formData = new FormData();
// 폼의 모든 입력값을 FormData에 추가
$('#actInfoForm').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);
}
});
// 중요한 로직 주석: 선택된 파일들을 FormData에 추가 (있는 경우에만)
if (selectedFiles && selectedFiles.length > 0) {
for (var i = 0; i < selectedFiles.length; i++) {
formData.append('photoFiles', selectedFiles[i]);
}
}
var url = (mode === 'U') ?
'<c:url value="/crdn/crndRegistAndView/crdnActInfo/update.ajax"/>' :
'<c:url value="/crdn/crndRegistAndView/crdnActInfo/insert.ajax"/>';
var actionText = (mode === 'U') ? '수정' : '등록';
$.ajax({
url: url,
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
handleSaveResponse(response, actionText);
}
});
}
/**
* 저장 응답 처리
* 중요한 로직 주석: 성공/실패에 따른 공통 처리 로직
*/
function handleSaveResponse(response, actionText) {
if (response && response.success) {
alert('불법행위정보가 성공적으로 ' + actionText + '되었습니다.');
// 부모창의 그리드 새로고침
if (window.opener && window.opener.CrdnDetailView && window.opener.CrdnDetailView.grids.actInfo) {
window.opener.CrdnDetailView.grids.actInfo.search();
}
window.close();
} else {
alert(response.message || actionText + ' 중 오류가 발생했습니다.');
}
}
// 전역 변수: 선택된 파일들을 보관
var selectedFiles = [];
/**
* 파일 업로드 초기화
* 중요한 로직 주석: 파일 선택 이벤트와 파일 유효성 검증을 초기화한다.
*/
function initFileUpload() {
// 파일 선택 이벤트
$('#photoFiles').on('change', function(e) {
handleFileSelect(e.target.files);
});
}
/**
* 파일 선택 처리
* 중요한 로직 주석: 선택된 파일들을 검증하고 파일명만 표시한다. 미리보기는 하지 않는다.
*/
function handleFileSelect(files) {
if (!files || files.length === 0) {
return;
}
// 파일 개수 제한 체크
var existingPhotos = $('.photo-preview-item.existing-photo').length;
var newFiles = selectedFiles.length + files.length;
if (existingPhotos + newFiles > 10) {
alert('최대 10개의 사진만 업로드할 수 있습니다.');
return;
}
// 각 파일에 대해 검증 및 파일명만 표시
for (var i = 0; i < files.length; i++) {
var file = files[i];
// 파일 유효성 검증
if (!validateFile(file)) {
continue;
}
// 선택된 파일 목록에 추가
selectedFiles.push(file);
// 미리보기 생성 (클릭 이벤트 없음)
createPhotoPreview(file);
}
}
/**
* 파일 유효성 검증
* 중요한 로직 주석: 파일 크기, 확장자, 타입을 검증한다.
*/
function validateFile(file) {
// 파일 크기 체크 (10MB)
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;
}
/**
* 사진 미리보기 생성 (클릭 이벤트 없음)
* 중요한 로직 주석: 선택된 파일을 읽어서 미리보기 이미지를 생성한다. 클릭 시 원본 이미지 팝업은 제공하지 않는다.
*/
function createPhotoPreview(file) {
var reader = new FileReader();
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 + '">' +
' <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>' +
'</div>';
$('#photoPreviewContainer').append(previewHtml);
};
reader.readAsDataURL(file);
}
/**
* 기존 사진 삭제
* 중요한 로직 주석: DB에 등록된 사진을 논리삭제하고 화면에서 제거한다.
*/
function deleteExistingPhoto(actInfoId, crdnPhotoSn) {
if (!confirm('선택한 사진을 삭제하시겠습니까?')) {
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) {
alert('불법행위정보가 성공적으로 ' + actionText + '되었습니다.');
// 부모창의 그리드 새로고침
if (window.opener && window.opener.CrdnDetailView && window.opener.CrdnDetailView.grids.actInfo) {
window.opener.CrdnDetailView.grids.actInfo.search();
}
window.close();
// 화면에서 해당 사진 제거
$('[data-act-info-id="' + actInfoId + '"][data-photo-sn="' + crdnPhotoSn + '"]').remove();
alert('사진이 삭제되었습니다.');
} else {
alert(response.message || actionText + ' 중 오류가 발생했습니다.');
alert(response.message || '사진 삭제에 실패했습니다.');
}
}
});
}
/**
* 새로 선택한 파일 삭제
* 중요한 로직 주석: 아직 저장되지 않은 파일을 목록에서 제거하고 selectedFiles 배열에서도 제거한다.
*/
function deleteNewPhoto(button) {
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);
break;
}
}
// 화면에서 제거
$item.remove();
}
/**
* 원본 사진 보기 (기존 DB 사진)
* 중요한 로직 주석: 등록된 사진을 photoView 페이지에서 원본 크기로 표시하고 다운로드 버튼을 제공한다.
*/
function viewOriginalPhoto(actInfoId, crdnPhotoSn) {
var url = '<c:url value="/crdn/crndRegistAndView/crdnActInfo/photoView.do"/>?actInfoId=' + actInfoId + '&crdnPhotoSn=' + crdnPhotoSn;
window.open(url, '_blank', 'width=1000,height=700,scrollbars=yes,resizable=yes');
}
</script>

@ -0,0 +1,346 @@
<%@ 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" %>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>사진 보기</title>
<link rel="stylesheet" type="text/css" href="<c:url value='/resources/xit/xit-common.css'/>" />
<style>
body {
margin: 0;
padding: 0;
background-color: #000;
color: #fff;
font-family: Arial, sans-serif;
overflow: hidden;
}
.photo-viewer {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.photo-header {
position: fixed;
top: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.8);
padding: 15px 20px;
z-index: 1000;
display: flex;
justify-content: space-between;
align-items: center;
backdrop-filter: blur(5px);
}
.photo-info {
display: flex;
flex-direction: column;
gap: 5px;
}
.photo-title {
font-size: 16px;
font-weight: bold;
color: #fff;
}
.photo-details {
font-size: 12px;
color: #ccc;
}
.photo-actions {
display: flex;
gap: 10px;
align-items: center;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 5px;
transition: all 0.3s ease;
}
.btn-download {
background-color: #007bff;
color: white;
}
.btn-download:hover {
background-color: #0056b3;
}
.btn-close {
background-color: #6c757d;
color: white;
}
.btn-close:hover {
background-color: #545b62;
}
.photo-container {
position: relative;
max-width: 90vw;
max-height: 80vh;
display: flex;
justify-content: center;
align-items: center;
margin-top: 80px;
}
.photo-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
cursor: zoom-in;
transition: transform 0.3s ease;
}
.photo-image.zoomed {
cursor: zoom-out;
transform: scale(1.5);
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-left: 4px solid #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
text-align: center;
padding: 40px;
}
.error-title {
font-size: 24px;
margin-bottom: 10px;
}
.error-text {
font-size: 16px;
color: #ccc;
}
/* 반응형 디자인 */
@media (max-width: 768px) {
.photo-header {
flex-direction: column;
gap: 10px;
padding: 10px;
}
.photo-actions {
justify-content: center;
width: 100%;
}
.photo-container {
margin-top: 120px;
max-height: 70vh;
}
.btn {
padding: 6px 12px;
font-size: 12px;
}
}
</style>
</head>
<body>
<div class="photo-viewer">
<!-- 헤더 -->
<div class="photo-header">
<div class="photo-info">
<div class="photo-title" id="photoTitle">사진을 불러오는 중...</div>
<div class="photo-details" id="photoDetails">-</div>
</div>
<div class="photo-actions">
<a href="#" class="btn btn-download" id="downloadBtn" onclick="downloadPhoto()">
<i class="fas fa-download"></i> 다운로드
</a>
<button type="button" class="btn btn-close" onclick="closeWindow()">
<i class="fas fa-times"></i> 닫기
</button>
</div>
</div>
<!-- 사진 컨테이너 -->
<div class="photo-container">
<div class="loading" id="loadingArea">
<div class="loading-spinner"></div>
<div>사진을 불러오는 중입니다...</div>
</div>
<img id="photoImage" class="photo-image" style="display: none;" alt="사진" onclick="toggleZoom(this)">
<div class="error-message" id="errorArea" style="display: none;">
<div class="error-title">사진을 불러올 수 없습니다</div>
<div class="error-text">파일이 존재하지 않거나 접근할 수 없습니다.</div>
</div>
</div>
</div>
<script>
// URL 파라미터 가져오기
const urlParams = new URLSearchParams(window.location.search);
const actInfoId = urlParams.get('actInfoId');
const crdnPhotoSn = urlParams.get('crdnPhotoSn');
// 페이지 로드 시 사진 정보 가져오기
document.addEventListener('DOMContentLoaded', function() {
if (!actInfoId || !crdnPhotoSn) {
showError('필수 파라미터가 누락되었습니다.');
return;
}
loadPhotoInfo();
});
/**
* 사진 정보 로드
* 중요한 로직 주석: 서버에서 사진 정보를 가져와서 화면에 표시한다.
*/
function loadPhotoInfo() {
// 사진 이미지 로드
const photoImage = document.getElementById('photoImage');
const downloadUrl = '<c:url value="/crdn/crndRegistAndView/crdnActInfo/downloadPhoto.do"/>?actInfoId=' + actInfoId + '&crdnPhotoSn=' + crdnPhotoSn;
photoImage.onload = function() {
document.getElementById('loadingArea').style.display = 'none';
photoImage.style.display = 'block';
// 사진 정보 업데이트
updatePhotoInfo();
};
photoImage.onerror = function() {
showError('사진을 불러올 수 없습니다.');
};
// 다운로드 버튼 준비 (href는 설정하지 않음 - onclick으로만 처리)
// 이미지 소스 설정
photoImage.src = downloadUrl;
}
/**
* 사진 정보 업데이트
* 중요한 로직 주석: URL 파라미터를 기반으로 사진 제목과 상세정보를 설정한다.
*/
function updatePhotoInfo() {
const photoTitle = document.getElementById('photoTitle');
const photoDetails = document.getElementById('photoDetails');
//photoTitle.textContent = '단속 사진 (순번: ' + crdnPhotoSn + ')';
photoTitle.textContent = '단속 사진';
photoDetails.textContent = '행위정보ID: ' + actInfoId + ' | 사진순번: ' + crdnPhotoSn;
}
/**
* 오류 표시
* 중요한 로직 주석: 사진 로드 실패 시 오류 메시지를 표시한다.
*/
function showError(message) {
document.getElementById('loadingArea').style.display = 'none';
document.getElementById('photoImage').style.display = 'none';
const errorArea = document.getElementById('errorArea');
const errorText = errorArea.querySelector('.error-text');
errorText.textContent = message;
errorArea.style.display = 'block';
// 헤더 정보 업데이트
document.getElementById('photoTitle').textContent = '오류 발생';
document.getElementById('photoDetails').textContent = message;
// 다운로드 버튼 비활성화
const downloadBtn = document.getElementById('downloadBtn');
downloadBtn.style.opacity = '0.5';
downloadBtn.style.pointerEvents = 'none';
}
/**
* 줌 토글
* 중요한 로직 주석: 이미지 클릭 시 확대/축소 기능을 제공한다.
*/
function toggleZoom(img) {
img.classList.toggle('zoomed');
}
/**
* 사진 다운로드
* 중요한 로직 주석: 다운로드 버튼 클릭 시 파일 다운로드를 시작한다.
*/
function downloadPhoto() {
if (!actInfoId || !crdnPhotoSn) {
alert('다운로드할 사진 정보가 없습니다.');
return;
}
const downloadUrl = '<c:url value="/crdn/crndRegistAndView/crdnActInfo/downloadPhoto.do"/>?actInfoId=' + actInfoId + '&crdnPhotoSn=' + crdnPhotoSn;
// 중요한 로직 주석: 현재 창에서 다운로드 URL로 이동하여 파일 다운로드 실행
window.location.href = downloadUrl;
}
/**
* 창 닫기
* 중요한 로직 주석: 닫기 버튼 클릭 시 현재 창을 닫는다.
*/
function closeWindow() {
if (window.opener) {
window.close();
} else {
// 팝업이 아닌 경우 이전 페이지로 이동
history.back();
}
}
// ESC 키로 창 닫기
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
closeWindow();
}
});
</script>
</body>
</html>

@ -309,7 +309,7 @@
+ '&pstnInfoId=' + encodeURIComponent(pstnInfoId)
+ '&crdnNo=' + encodeURIComponent(crdnNo)
+ '&mode=C';
var w = 1200, h = 480;
var w = 1200, h = 700;
var left = Math.max(0, (screen.width - w) / 2);
var top = Math.max(0, (screen.height - h) / 2);
window.open(url, 'actInfoPopup', 'width=' + w + ',height=' + h + ',left=' + left + ',top=' + top + ',resizable=yes,scrollbars=yes');
@ -344,7 +344,7 @@
+ '&actInfoId=' + encodeURIComponent(actInfoId)
+ '&mode=U';
var w = 1200, h = 480;
var w = 1200, h = 700;
var left = Math.max(0, (screen.width - w) / 2);
var top = Math.max(0, (screen.height - h) / 2);

@ -0,0 +1,288 @@
/**
* CSS
* : UI
*/
/* 파일 업로드 섹션 전체 컨테이너 */
.file-upload-section {
margin-top: 10px;
}
/* 파일 업로드 컨트롤 영역 */
.file-upload-controls {
display: flex;
align-items: center;
margin-bottom: 15px;
gap: 10px;
flex-wrap: wrap;
}
/* 파일 선택 버튼 */
.file-upload-btn {
background-color: #007bff;
color: white;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
border: none;
font-size: 13px;
transition: background-color 0.3s ease;
}
.file-upload-btn:hover {
background-color: #0056b3;
}
.file-upload-btn span {
pointer-events: none;
}
/* 파일 정보 텍스트 */
.file-info {
font-size: 12px;
color: #6c757d;
margin-left: 10px;
}
/* 사진 미리보기 컨테이너 */
.photo-preview-container {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 15px;
min-height: 80px;
padding: 10px 10px 10px 50px;
border: 1px dashed #ddd;
border-radius: 4px;
background-color: #fafafa;
}
.photo-preview-container:empty {
display: flex;
align-items: center;
justify-content: center;
}
.photo-preview-container:empty::before {
content: "선택된 사진이 없습니다.";
color: #999;
font-size: 14px;
}
/* 개별 사진 미리보기 아이템 */
.photo-preview-item {
display: flex;
flex-direction: column;
width: 150px;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
padding: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: box-shadow 0.3s ease;
}
.photo-preview-item:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
/* 기존 사진 (DB에서 조회된) 스타일 */
.photo-preview-item.existing-photo {
border-color: #28a745;
}
.photo-preview-item.existing-photo::before {
content: "등록됨";
font-size: 10px;
color: #28a745;
text-align: center;
margin-bottom: 4px;
font-weight: bold;
}
/* 새로 선택한 사진 스타일 */
.photo-preview-item.new-photo {
border-color: #007bff;
}
.photo-preview-item.new-photo::before {
content: "신규";
font-size: 10px;
color: #007bff;
text-align: center;
margin-bottom: 4px;
font-weight: bold;
}
/* 사진 썸네일 영역 */
.photo-thumbnail {
width: 100%;
height: 80px;
overflow: hidden;
border-radius: 4px;
margin-bottom: 8px;
cursor: pointer;
position: relative;
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
}
.photo-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.photo-thumbnail:hover img {
transform: scale(1.1);
}
/* 사진 정보 영역 */
.photo-info {
display: flex;
flex-direction: column;
gap: 4px;
}
/* 사진 파일명 */
.photo-name {
font-size: 12px;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500;
line-height: 1.2;
}
/* 사진 구분 타입 */
.photo-type {
font-size: 11px;
color: #666;
font-style: italic;
}
/* 사진 삭제 버튼 */
.delete-photo-btn {
background-color: #dc3545;
color: white;
border: none;
border-radius: 3px;
padding: 4px 8px;
font-size: 11px;
cursor: pointer;
transition: background-color 0.3s ease;
margin-top: 4px;
}
.delete-photo-btn:hover {
background-color: #c82333;
}
/* 반응형 디자인 */
@media (max-width: 768px) {
.photo-preview-container {
gap: 10px;
}
.photo-preview-item {
width: 100px;
}
.photo-thumbnail {
height: 70px;
}
.file-upload-controls {
flex-direction: column;
align-items: flex-start;
}
.file-info {
margin-left: 0;
margin-top: 5px;
}
}
/* 로딩 상태 */
.photo-preview-item.loading {
opacity: 0.7;
}
.photo-preview-item.loading::after {
content: "처리 중...";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.8);
color: white;
padding: 4px 8px;
border-radius: 3px;
font-size: 11px;
}
/* 에러 상태 */
.photo-preview-item.error {
border-color: #dc3545;
background-color: #fff5f5;
}
.photo-preview-item.error .photo-name {
color: #dc3545;
}
/* 드래그 앤 드롭 효과 (향후 확장용) */
.photo-preview-container.drag-over {
border-color: #007bff;
background-color: #e7f3ff;
}
/* 애니메이션 */
.photo-preview-item {
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 팝업 창에서의 스타일 조정 */
.popup_wrap .file-upload-section {
max-width: 100%;
}
.popup_wrap .photo-preview-container {
max-height: 430px;
overflow-y: auto;
}
/* 스크롤바 스타일 (웹킷 기반 브라우저) */
.photo-preview-container::-webkit-scrollbar {
width: 6px;
}
.photo-preview-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.photo-preview-container::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.photo-preview-container::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
Loading…
Cancel
Save