단속 등록&열람: 단속 사진 등록 기능 개발 진행 중
parent
c024796a44
commit
d4a3865d90
@ -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; // 신규 파일 여부 (등록/수정 구분용)
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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…
Reference in New Issue