diff --git a/docs/작업내역_20260225.md b/docs/작업내역_20260225.md new file mode 100644 index 0000000..58415eb --- /dev/null +++ b/docs/작업내역_20260225.md @@ -0,0 +1,42 @@ +# 메이드유 CRM 작업내역 — 2026년 2월 25일 + +### 1. 코드 관리 기능 추가 +- CRM에서 사용하는 **공통코드**(단위, 분류 등)를 조회·등록·수정·삭제할 수 있는 **코드 관리 화면**이 새로 추가되었습니다. + +### 2. 진료유형 설정 화면 수정 +- 진료유형 설정의 각 단계(1~5뎁스) 팝업 화면이 수정되었습니다. +- 진료유형 목록 화면의 기능이 개선되었습니다. + +--- + +## 직접 작업한 내역 + +### 1. 사용약품 검색 및 등록 기능 +- 용량/출력(4뎁스) 카테고리에서 **사용 약품을 검색하여 등록**할 수 있는 기능을 새로 만들었습니다. +- "약품 추가(검색)" 버튼을 누르면 **검색 팝업**이 열리고, 제품명으로 검색할 수 있습니다. +- 검색 결과에서 **제품명을 클릭**하면 해당 약품이 바로 등록됩니다. + +### 2. 사용약품 관리 표(그리드) 구성 +- 등록된 약품들이 아래 항목으로 구성된 표에 표시됩니다. + +| 항목 | 설명 | +|------|------| +| 거래처 | 약품을 공급하는 거래처 이름 | +| 약품명 | 등록된 약품 이름 | +| 재고 | 현재 남아있는 재고 수량 | +| 재고단위 | 재고의 단위 (ml, cc 등) | +| **사용량** | 1회 시술 시 사용하는 양 (클릭하여 수정 가능) | +| **사용단위** | 사용량의 단위 (클릭하여 선택 가능) | +| 삭제 | 약품을 목록에서 제거 | + +- **사용량**과 **사용단위**는 표에서 바로 클릭하여 수정할 수 있으며, 수정 즉시 자동 저장됩니다. +- 사용단위는 드롭다운 목록에서 선택하는 방식이며, 기본값은 "선택"입니다. +- 수정 가능한 칸은 연한 파란색 배경으로 표시되어 있습니다. + +### 3. 신규 등록 화면에서도 약품관리 표시 +- 기존에는 수정할 때만 약품관리가 보였으나, 이제 **새로 등록하는 화면에서도** 약품관리 영역이 보입니다. +- 단, 카테고리를 먼저 저장한 뒤에 약품을 추가할 수 있으며, 저장 전에는 안내 문구가 표시됩니다. + +### 4. 약품 데이터 저장 구조 신규 생성 +- 시술별 사용 약품 정보를 저장하기 위한 **데이터베이스 테이블을 새로 생성**하였습니다. +- 약품명, 거래처, 사용량, 단위, 단가 등의 정보가 저장됩니다. diff --git a/rules.md b/rules.md new file mode 100644 index 0000000..f9b3f97 --- /dev/null +++ b/rules.md @@ -0,0 +1,36 @@ +# 프로젝트 코딩 가이드라인 (Java Backend) + +AI 에디터(Agent)는 다음 규칙을 항상 준수하여 코드를 작성하고 수정해야 합니다. + +## 0. 기본 소통 규칙 (Communication) +- **언어**: 사용자에 대한 모든 답변과 코드 설명은 항상 **한글(Korean)**로만 작성해야 합니다. + +## 1. 패키지 구성 (Package Structure) +- **컨트롤러 (Controller)**: `ctrl` +- **서비스 (Service)**: `svc` +- **DTO (Data Transfer Object)**: `dto` +- **매퍼 (Mapper)**: `mapper` + +## 2. 파일 명명 규칙 및 구성 (File Naming Conventions) +- **컨트롤러 (Controller)**: `[도메인명]Controller.java` (예: `ABCDController.java`) +- **서비스 (Service)**: `[도메인명]Service.java` (인터페이스와 구현체(impl)를 분리하지 않고 Service 클래스 파일 하나로만 구현, 예: `ABCDService.java`) +- **DTO**: `[도메인명]DTO.java` (예: `ABCDDTO.java`) +- **매퍼 (Mapper)**: `[도메인명]Mapper.java` (예: `ABCDMapper.java`) + +## 3. URL 및 메소드 명명 규칙 (RequestMapping & Method Naming) + +### 1) RequestMapping (URL) 및 컨트롤러 메소드명 +- 페이지 이동하는 url : `moveXXXX.do` +- 팝업 오픈하는 url : `openXXXX.do` +- 조회 url : `getXXXX.do` +- 저장 url : `putXXXX.do` +- 수정 url : `modXXXX.do` +- 삭제 url : `delXXXX.do` +- **단, 컨트롤러 메소드명은 위 url에서 `.do`를 제외한 이름과 동일하게 명명합니다.** + +### 2) 서비스 메소드명 +- 단일조회 : `selectXXXX` +- 리스트조회 : `selectListXXXX` +- insert : `insertXXXX` +- update : `updateXXXX` +- delete : `deleteXXXX` diff --git a/sql/create_mu_procedure_review.sql b/sql/create_mu_procedure_review.sql new file mode 100644 index 0000000..92f73e8 --- /dev/null +++ b/sql/create_mu_procedure_review.sql @@ -0,0 +1,38 @@ +-- ===================================================== +-- MU_PROCEDURE_REVIEW : 시술후기 전용 테이블 +-- 기존 HP_BEFORE_AFTER_PHOTO_BBS 대신 사용 +-- ===================================================== + +CREATE TABLE `MU_PROCEDURE_REVIEW` ( + `MU_PROCEDURE_REVIEW_ID` varchar(25) NOT NULL COMMENT '시술후기 식별자', + `MU_MEMBER_ID` varchar(25) NOT NULL COMMENT '작성자 식별자', + `CATEGORY_NO` int(11) DEFAULT NULL COMMENT '카테고리 번호', + `TITLE` varchar(200) NOT NULL COMMENT '제목', + `CONTENT` text NOT NULL COMMENT '내용 (HTML - Quill 에디터)', + `HASHTAG` varchar(500) DEFAULT NULL COMMENT '해시태그', + `BEFORE_PHOTO_ATTACHFILE_ID` varchar(30) DEFAULT NULL COMMENT 'Before 사진 첨부파일 식별자', + `AFTER_PHOTO_ATTACHFILE_ID` varchar(30) DEFAULT NULL COMMENT 'After 사진 첨부파일 식별자', + `VIEW_COUNT` int(11) NOT NULL DEFAULT 0 COMMENT '조회수', + `USE_YN` char(1) NOT NULL DEFAULT 'Y' COMMENT '사용여부 (Y:사용, N:삭제)', + `REG_ID` varchar(25) NOT NULL COMMENT '등록자', + `REG_DATE` timestamp NOT NULL DEFAULT current_timestamp() COMMENT '등록일자', + `MOD_ID` varchar(25) NOT NULL COMMENT '수정자', + `MOD_DATE` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '수정일자', + PRIMARY KEY (`MU_PROCEDURE_REVIEW_ID`), + KEY `idx_mu_procedure_review_category` (`CATEGORY_NO`), + KEY `idx_mu_procedure_review_reg_date` (`REG_DATE`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='시술후기 정보'; + +-- 시퀀스 테이블 +CREATE TABLE `MU_PROCEDURE_REVIEW_SEQ` ( + `next_not_cached_value` bigint(21) NOT NULL, + `minimum_value` bigint(21) NOT NULL, + `maximum_value` bigint(21) NOT NULL, + `start_value` bigint(21) NOT NULL COMMENT 'start value when sequences is created or value if RESTART is used', + `increment` bigint(21) NOT NULL COMMENT 'increment value', + `cache_size` bigint(21) unsigned NOT NULL, + `cycle_option` tinyint(1) unsigned NOT NULL COMMENT '0 if no cycles are allowed, 1 if the sequence should begin a new cycle when maximum_value is passed', + `cycle_count` bigint(21) NOT NULL COMMENT 'How many cycles have been done' +) ENGINE=InnoDB SEQUENCE=1; + +INSERT INTO `MU_PROCEDURE_REVIEW_SEQ` VALUES (1,1,99999999999,1,1,0,1,0); diff --git a/src/main/java/com/madeu/crm/procedureReview/ctrl/ProcedureReviewController.java b/src/main/java/com/madeu/crm/procedureReview/ctrl/ProcedureReviewController.java new file mode 100644 index 0000000..c44ed90 --- /dev/null +++ b/src/main/java/com/madeu/crm/procedureReview/ctrl/ProcedureReviewController.java @@ -0,0 +1,473 @@ +package com.madeu.crm.procedureReview.ctrl; + +import com.madeu.constants.Constants; +import com.madeu.init.ManagerDraftAction; +import com.madeu.crm.procedureReview.svc.ProcedureReviewService; +import com.madeu.service.web.webloghistory.WebLogHistoryService; +import com.madeu.util.HttpUtil; +import com.madeu.util.RequestLogUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.ModelAndView; + +import java.util.HashMap; + +@Slf4j +@Controller +public class ProcedureReviewController extends ManagerDraftAction { + + @Autowired + private ProcedureReviewService procedureReviewService; + + @Autowired + private WebLogHistoryService webLogHistoryService; + + @RequestMapping(value = "/procedureReview/moveProcedureReviewList.do") + public String moveProcedureReviewList(HttpSession session, HttpServletRequest request, + HttpServletResponse response, Model model) { + log.debug("ProcedureReviewController moveProcedureReviewList START"); + + HashMap paramMap = HttpUtil.getParameterMap(request); + HashMap map = new HashMap(); + + try { + if (!webCheckLogin(session)) { + return "/web/login/logout"; + } else { + paramMap.put("loginMemberId", String.valueOf(session.getAttribute("loginMemberId"))); + map = procedureReviewService.selectProcedureReviewListIntro(paramMap); + + model.addAttribute("selectUseYn", map.get("selectUseYn")); + model.addAttribute("insertUseYn", map.get("insertUseYn")); + model.addAttribute("updateUseYn", map.get("updateUseYn")); + model.addAttribute("deleteUseYn", map.get("deleteUseYn")); + model.addAttribute("downloadUseYn", map.get("downloadUseYn")); + } + } catch (Exception e) { + e.printStackTrace(); + return "/web/login/logout"; + } + log.debug("ProcedureReviewController moveProcedureReviewList END"); + return "/crm/procedureReview/procedureReviewSelectList"; + } + + @RequestMapping(value = "/procedureReview/getProcedureReviewList.do") + public ModelAndView getProcedureReviewList(HttpSession session, HttpServletRequest request, + HttpServletResponse response) { + log.debug("ProcedureReviewController getProcedureReviewList START"); + + HashMap paramMap = HttpUtil.getParameterMap(request); + HashMap map = new HashMap(); + + try { + if (!webCheckLogin(session)) { + return null; + } else { + paramMap.put("loginMemberId", String.valueOf(session.getAttribute("loginMemberId"))); + map = procedureReviewService.selectListProcedureReview(paramMap); + } + } catch (Exception e) { + e.printStackTrace(); + return null; + } finally { + if (Constants.OK == map.get("msgCode")) { + + } else { + map.put("msgCode", Constants.FAIL); + map.put("success", false); + if (null == map.get("msgDesc") || ("").equals(map.get("msgDesc"))) { + map.put("msgDesc", "정상적으로 수행되지 않았습니다. 관리자에게 문의하시기 바랍니다."); + } + } + } + log.debug("ProcedureReviewController getProcedureReviewList END"); + return HttpUtil.makeHashToJsonModelAndView(map); + } + + @RequestMapping(value = "/procedureReview/getProcedureReview.do") + public ModelAndView getProcedureReview(HttpSession session, HttpServletRequest request, + HttpServletResponse response) { + log.debug("ProcedureReviewController getProcedureReview START"); + + HashMap paramMap = HttpUtil.getParameterMap(request); + HashMap map = new HashMap(); + + try { + if (!webCheckLogin(session)) { + return null; + } else { + paramMap.put("loginMemberId", String.valueOf(session.getAttribute("loginMemberId"))); + map = procedureReviewService.selectProcedureReview(paramMap); + } + } catch (Exception e) { + e.printStackTrace(); + return null; + } finally { + if (Constants.OK == map.get("msgCode")) { + + } else { + map.put("msgCode", Constants.FAIL); + map.put("success", false); + if (null == map.get("msgDesc") || ("").equals(map.get("msgDesc"))) { + map.put("msgDesc", "정상적으로 수행되지 않았습니다. 관리자에게 문의하시기 바랍니다."); + } + } + } + log.debug("ProcedureReviewController getProcedureReview END"); + return HttpUtil.makeHashToJsonModelAndView(map); + } + + @RequestMapping(value = "/procedureReview/getCategoryList.do") + public ModelAndView getCategoryList(HttpSession session, HttpServletRequest request, + HttpServletResponse response) { + + HashMap paramMap = HttpUtil.getParameterMap(request); + HashMap map = new HashMap(); + + try { + if (!webCheckLogin(session)) { + return null; + } else { + map = procedureReviewService.selectListCategory(paramMap); + } + } catch (Exception e) { + e.printStackTrace(); + return null; + } finally { + if (Constants.OK == map.get("msgCode")) { + + } else { + map.put("msgCode", Constants.FAIL); + map.put("success", false); + if (null == map.get("msgDesc") || ("").equals(map.get("msgDesc"))) { + map.put("msgDesc", "정상적으로 수행되지 않았습니다. 관리자에게 문의하시기 바랍니다."); + } + } + } + log.debug("ProcedureReviewController getCategoryList END"); + return HttpUtil.makeHashToJsonModelAndView(map); + } + + @RequestMapping(value = "/procedureReview/putProcedureReviewFile.do") + public ModelAndView putProcedureReviewFile(HttpSession session, HttpServletRequest request, + HttpServletResponse response, + @RequestParam(value = "file", required = false) MultipartFile file) { + log.debug("ProcedureReviewController putProcedureReviewFile START"); + + HashMap paramMap = HttpUtil.getParameterMap(request); + HashMap map = new HashMap(); + + StringBuffer errorMsg = new StringBuffer(); + + try { + if (!webCheckLogin(session)) { + return null; + } else { + paramMap.put("loginMemberId", String.valueOf(session.getAttribute("loginMemberId"))); + paramMap.put("regId", String.valueOf(session.getAttribute("loginMemberId"))); + paramMap.put("modId", String.valueOf(session.getAttribute("loginMemberId"))); + map = procedureReviewService.insertProcedureReviewFile(paramMap, file); + } + } catch (Exception e) { + e.printStackTrace(); + errorMsg.append(e); + } finally { + if (Constants.OK == map.get("msgCode")) { + + } else { + map.put("msgCode", Constants.FAIL); + map.put("success", false); + if (null == map.get("msgDesc") || ("").equals(map.get("msgDesc"))) { + map.put("msgDesc", "정상적으로 수행되지 않았습니다. 관리자에게 문의하시기 바랍니다."); + } + } + + try { + HashMap visitLogParamMap = RequestLogUtil.getVisitLogParameterMap(request); + HashMap insertMap = new HashMap(); + + insertMap.put("url", "/procedureReview/putProcedureReviewFile.do"); + insertMap.put("func", "putProcedureReviewFile"); + insertMap.put("funcName", "홈페이지 시술후기 상세 이미지파일 저장"); + insertMap.put("service", "procedureReviewService"); + insertMap.put("serviceName", "홈페이지 시술후기 등록"); + insertMap.put("requestValue", String.valueOf(paramMap)); + insertMap.put("responseValue", String.valueOf(map)); + insertMap.put("tId", map.get("tId")); + if ((String.valueOf(errorMsg)).equals("") || (String.valueOf(errorMsg) == null) + || String.valueOf(errorMsg).length() == 0) { + insertMap.put("resultCode", "SUCCESS"); + } else { + insertMap.put("resultCode", "ERROR"); + } + insertMap.put("resultMsg", String.valueOf(errorMsg)); + insertMap.put("muMemberId", paramMap.get("loginMemberId")); + + webLogHistoryService.insertLogHistory(insertMap, visitLogParamMap); + } catch (Exception e) { + e.printStackTrace(); + } + } + log.debug("ProcedureReviewController putProcedureReviewFile END"); + return HttpUtil.makeHashToJsonModelAndView(map); + } + + @RequestMapping(value = "/procedureReview/moveProcedureReviewInsert.do") + public String moveProcedureReviewInsert(HttpSession session, HttpServletRequest request, + HttpServletResponse response, + Model model) { + log.debug("ProcedureReviewController moveProcedureReviewInsert START"); + + HashMap paramMap = HttpUtil.getParameterMap(request); + HashMap map = new HashMap(); + + try { + if (!webCheckLogin(session)) { + return "/web/login/logout"; + } else { + paramMap.put("loginMemberId", String.valueOf(session.getAttribute("loginMemberId"))); + map = procedureReviewService.selectProcedureReviewInsertIntro(paramMap); + + model.addAttribute("selectUseYn", map.get("selectUseYn")); + model.addAttribute("insertUseYn", map.get("insertUseYn")); + model.addAttribute("updateUseYn", map.get("updateUseYn")); + model.addAttribute("deleteUseYn", map.get("deleteUseYn")); + model.addAttribute("downloadUseYn", map.get("downloadUseYn")); + model.addAttribute("categorylist", map.get("categorylist")); + } + } catch (Exception e) { + e.printStackTrace(); + return "/web/login/logout"; + } + log.debug("ProcedureReviewController moveProcedureReviewInsert END"); + return "/crm/procedureReview/procedureReviewInsert"; + } + + @RequestMapping(value = "/procedureReview/putProcedureReview.do") + public ModelAndView putProcedureReview(HttpSession session, HttpServletRequest request, + HttpServletResponse response) { + log.debug("ProcedureReviewController putProcedureReview START"); + + HashMap paramMap = HttpUtil.getParameterMap(request); + HashMap map = new HashMap(); + + StringBuffer errorMsg = new StringBuffer(); + + try { + if (!webCheckLogin(session)) { + return null; + } else { + paramMap.put("loginMemberId", String.valueOf(session.getAttribute("loginMemberId"))); + paramMap.put("regId", String.valueOf(session.getAttribute("loginMemberId"))); + paramMap.put("modId", String.valueOf(session.getAttribute("loginMemberId"))); + map = procedureReviewService.insertProcedureReview(paramMap); + } + } catch (Exception e) { + e.printStackTrace(); + errorMsg.append(e); + } finally { + if (Constants.OK == map.get("msgCode")) { + + } else { + map.put("msgCode", Constants.FAIL); + map.put("success", false); + if (null == map.get("msgDesc") || ("").equals(map.get("msgDesc"))) { + map.put("msgDesc", "정상적으로 수행되지 않았습니다. 관리자에게 문의하시기 바랍니다."); + } + } + + try { + HashMap visitLogParamMap = RequestLogUtil.getVisitLogParameterMap(request); + HashMap insertMap = new HashMap(); + + insertMap.put("url", "/procedureReview/putProcedureReview.do"); + insertMap.put("func", "putProcedureReview"); + insertMap.put("funcName", "홈페이지 시술후기 등록"); + insertMap.put("service", "procedureReviewService"); + insertMap.put("serviceName", "홈페이지 시술후기 등록"); + insertMap.put("requestValue", String.valueOf(paramMap)); + insertMap.put("responseValue", String.valueOf(map)); + insertMap.put("tId", map.get("tId")); + if ((String.valueOf(errorMsg)).equals("") || (String.valueOf(errorMsg) == null) + || String.valueOf(errorMsg).length() == 0) { + insertMap.put("resultCode", "SUCCESS"); + } else { + insertMap.put("resultCode", "ERROR"); + } + insertMap.put("resultMsg", String.valueOf(errorMsg)); + insertMap.put("muMemberId", paramMap.get("loginMemberId")); + + webLogHistoryService.insertLogHistory(insertMap, visitLogParamMap); + } catch (Exception e) { + e.printStackTrace(); + } + } + log.debug("ProcedureReviewController putProcedureReview END"); + return HttpUtil.makeHashToJsonModelAndView(map); + } + + @RequestMapping(value = "/procedureReview/moveProcedureReviewUpdate.do") + public String moveProcedureReviewUpdate(HttpSession session, HttpServletRequest request, + HttpServletResponse response, + Model model) { + log.debug("ProcedureReviewController moveProcedureReviewUpdate START"); + + HashMap paramMap = HttpUtil.getParameterMap(request); + HashMap map = new HashMap(); + + try { + if (!webCheckLogin(session)) { + return "/web/login/logout"; + } else { + paramMap.put("loginMemberId", String.valueOf(session.getAttribute("loginMemberId"))); + map = procedureReviewService.selectProcedureReviewUpdateIntro(paramMap); + + model.addAttribute("selectUseYn", map.get("selectUseYn")); + model.addAttribute("insertUseYn", map.get("insertUseYn")); + model.addAttribute("updateUseYn", map.get("updateUseYn")); + model.addAttribute("deleteUseYn", map.get("deleteUseYn")); + model.addAttribute("downloadUseYn", map.get("downloadUseYn")); + model.addAttribute("categorylist", map.get("categorylist")); + } + } catch (Exception e) { + e.printStackTrace(); + return "/web/login/logout"; + } + log.debug("ProcedureReviewController moveProcedureReviewUpdate END"); + return "/crm/procedureReview/procedureReviewUpdate"; + } + + @RequestMapping(value = "/procedureReview/modProcedureReview.do") + public ModelAndView modProcedureReview(HttpSession session, HttpServletRequest request, + HttpServletResponse response) { + log.debug("ProcedureReviewController modProcedureReview START"); + + HashMap paramMap = HttpUtil.getParameterMap(request); + HashMap map = new HashMap(); + + StringBuffer errorMsg = new StringBuffer(); + + try { + if (!webCheckLogin(session)) { + return null; + } else { + paramMap.put("loginMemberId", String.valueOf(session.getAttribute("loginMemberId"))); + paramMap.put("regId", String.valueOf(session.getAttribute("loginMemberId"))); + paramMap.put("modId", String.valueOf(session.getAttribute("loginMemberId"))); + map = procedureReviewService.updateProcedureReview(paramMap); + } + } catch (Exception e) { + e.printStackTrace(); + errorMsg.append(e); + } finally { + if (Constants.OK == map.get("msgCode")) { + + } else { + map.put("msgCode", Constants.FAIL); + map.put("success", false); + if (null == map.get("msgDesc") || ("").equals(map.get("msgDesc"))) { + map.put("msgDesc", "정상적으로 수행되지 않았습니다. 관리자에게 문의하시기 바랍니다."); + } + } + + try { + HashMap visitLogParamMap = RequestLogUtil.getVisitLogParameterMap(request); + HashMap insertMap = new HashMap(); + + insertMap.put("url", "/procedureReview/modProcedureReview.do"); + insertMap.put("func", "modProcedureReview"); + insertMap.put("funcName", "홈페이지 시술후기 수정"); + insertMap.put("service", "procedureReviewService"); + insertMap.put("serviceName", "홈페이지 시술후기 수정"); + insertMap.put("requestValue", String.valueOf(paramMap)); + insertMap.put("responseValue", String.valueOf(map)); + insertMap.put("tId", map.get("tId")); + if ((String.valueOf(errorMsg)).equals("") || (String.valueOf(errorMsg) == null) + || String.valueOf(errorMsg).length() == 0) { + insertMap.put("resultCode", "SUCCESS"); + } else { + insertMap.put("resultCode", "ERROR"); + } + insertMap.put("resultMsg", String.valueOf(errorMsg)); + insertMap.put("muMemberId", paramMap.get("loginMemberId")); + + webLogHistoryService.insertLogHistory(insertMap, visitLogParamMap); + } catch (Exception e) { + e.printStackTrace(); + } + } + log.debug("ProcedureReviewController modProcedureReview END"); + return HttpUtil.makeHashToJsonModelAndView(map); + } + + @RequestMapping(value = "/procedureReview/delProcedureReview.do") + public ModelAndView delProcedureReview(HttpSession session, HttpServletRequest request, + HttpServletResponse response) { + log.debug("ProcedureReviewController delProcedureReview START"); + + HashMap paramMap = HttpUtil.getParameterMap(request); + HashMap map = new HashMap(); + + StringBuffer errorMsg = new StringBuffer(); + + try { + if (!webCheckLogin(session)) { + return null; + } else { + paramMap.put("loginMemberId", String.valueOf(session.getAttribute("loginMemberId"))); + paramMap.put("regId", String.valueOf(session.getAttribute("loginMemberId"))); + paramMap.put("modId", String.valueOf(session.getAttribute("loginMemberId"))); + map = procedureReviewService.deleteProcedureReview(paramMap); + } + } catch (Exception e) { + e.printStackTrace(); + errorMsg.append(e); + } finally { + if (Constants.OK == map.get("msgCode")) { + + } else { + map.put("msgCode", Constants.FAIL); + map.put("success", false); + if (null == map.get("msgDesc") || ("").equals(map.get("msgDesc"))) { + map.put("msgDesc", "정상적으로 수행되지 않았습니다. 관리자에게 문의하시기 바랍니다."); + } + } + + try { + HashMap visitLogParamMap = RequestLogUtil.getVisitLogParameterMap(request); + HashMap insertMap = new HashMap(); + + insertMap.put("url", "/procedureReview/delProcedureReview.do"); + insertMap.put("func", "delProcedureReview"); + insertMap.put("funcName", "홈페이지 시술후기 삭제"); + insertMap.put("service", "procedureReviewService"); + insertMap.put("serviceName", "홈페이지 시술후기 삭제"); + insertMap.put("requestValue", String.valueOf(paramMap)); + insertMap.put("responseValue", String.valueOf(map)); + insertMap.put("tId", map.get("tId")); + if ((String.valueOf(errorMsg)).equals("") || (String.valueOf(errorMsg) == null) + || String.valueOf(errorMsg).length() == 0) { + insertMap.put("resultCode", "SUCCESS"); + } else { + insertMap.put("resultCode", "ERROR"); + } + insertMap.put("resultMsg", String.valueOf(errorMsg)); + insertMap.put("muMemberId", paramMap.get("loginMemberId")); + + webLogHistoryService.insertLogHistory(insertMap, visitLogParamMap); + } catch (Exception e) { + e.printStackTrace(); + } + } + log.debug("ProcedureReviewController delProcedureReview END"); + return HttpUtil.makeHashToJsonModelAndView(map); + } +} diff --git a/src/main/java/com/madeu/crm/procedureReview/dto/ProcedureReviewDTO.java b/src/main/java/com/madeu/crm/procedureReview/dto/ProcedureReviewDTO.java new file mode 100644 index 0000000..5a23e4b --- /dev/null +++ b/src/main/java/com/madeu/crm/procedureReview/dto/ProcedureReviewDTO.java @@ -0,0 +1,9 @@ +package com.madeu.crm.procedureReview.dto; + +import lombok.Data; + +@Data +public class ProcedureReviewDTO { + // WebPhotoDiet uses HashMap for data transfer, creating this DTO as placeholder + // to follow project conventions. +} diff --git a/src/main/java/com/madeu/crm/procedureReview/mapper/ProcedureReviewMapper.java b/src/main/java/com/madeu/crm/procedureReview/mapper/ProcedureReviewMapper.java new file mode 100644 index 0000000..5be132c --- /dev/null +++ b/src/main/java/com/madeu/crm/procedureReview/mapper/ProcedureReviewMapper.java @@ -0,0 +1,97 @@ +package com.madeu.crm.procedureReview.mapper; + +import jakarta.annotation.PostConstruct; +import org.mybatis.spring.SqlSessionTemplate; +import org.mybatis.spring.support.SqlSessionDaoSupport; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +import org.springframework.stereotype.Repository; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Repository +public class ProcedureReviewMapper extends SqlSessionDaoSupport { + + @Autowired + private SqlSessionTemplate sqlSessionTemplate; + + @PostConstruct + void init() { + setSqlSessionTemplate(sqlSessionTemplate); + } + + public List> selectTotalProcedureReviewCount(HashMap paramMap) + throws DataAccessException { + logger.debug("ProcedureReviewMapper selectTotalProcedureReviewCount START"); + String sqlId = "ProcedureReview.selectTotalProcedureReviewCount"; + logger.debug("ProcedureReviewMapper selectTotalProcedureReviewCount END"); + return getSqlSession().selectList(sqlId, paramMap); + } + + public List> selectListProcedureReview(HashMap paramMap) + throws DataAccessException { + logger.debug("ProcedureReviewMapper selectListProcedureReview START"); + String sqlId = "ProcedureReview.selectListProcedureReview"; + logger.debug("ProcedureReviewMapper selectListProcedureReview END"); + return getSqlSession().selectList(sqlId, paramMap); + } + + public List> selectProcedureReview(HashMap paramMap) + throws DataAccessException { + logger.debug("ProcedureReviewMapper selectProcedureReview START"); + String sqlId = "ProcedureReview.selectProcedureReview"; + logger.debug("ProcedureReviewMapper selectProcedureReview END"); + return getSqlSession().selectList(sqlId, paramMap); + } + + public int insertProcedureReview(HashMap paramMap) + throws DataAccessException { + logger.debug("ProcedureReviewMapper insertProcedureReview START"); + String sqlId = "ProcedureReview.insertProcedureReview"; + logger.debug("ProcedureReviewMapper insertProcedureReview END"); + return getSqlSession().insert(sqlId, paramMap); + } + + public int updateProcedureReview(HashMap paramMap) + throws DataAccessException { + logger.debug("ProcedureReviewMapper updateProcedureReview START"); + String sqlId = "ProcedureReview.updateProcedureReview"; + logger.debug("ProcedureReviewMapper updateProcedureReview END"); + return getSqlSession().update(sqlId, paramMap); + } + + public int deleteProcedureReview(HashMap paramMap) + throws DataAccessException { + logger.debug("ProcedureReviewMapper deleteProcedureReview START"); + String sqlId = "ProcedureReview.deleteProcedureReview"; + logger.debug("ProcedureReviewMapper deleteProcedureReview END"); + return getSqlSession().update(sqlId, paramMap); + } + + public List> selectListPhotoCategory(HashMap paramMap) + throws DataAccessException { + logger.debug("ProcedureReviewMapper selectListPhotoCategory START"); + String sqlId = "ProcedureReview.selectListPhotoCategory"; + logger.debug("ProcedureReviewMapper selectListPhotoCategory END"); + return getSqlSession().selectList(sqlId, paramMap); + } + + public int insertProcedureReviewBeforeFile(HashMap paramMap) + throws DataAccessException { + logger.debug("ProcedureReviewMapper insertProcedureReviewBeforeFile START"); + String sqlId = "ProcedureReview.insertProcedureReviewBeforeFile"; + logger.debug("ProcedureReviewMapper insertProcedureReviewBeforeFile END"); + return getSqlSession().insert(sqlId, paramMap); + } + + public int insertProcedureReviewAfterFile(HashMap paramMap) + throws DataAccessException { + logger.debug("ProcedureReviewMapper insertProcedureReviewAfterFile START"); + String sqlId = "ProcedureReview.insertProcedureReviewAfterFile"; + logger.debug("ProcedureReviewMapper insertProcedureReviewAfterFile END"); + return getSqlSession().insert(sqlId, paramMap); + } + +} diff --git a/src/main/java/com/madeu/crm/procedureReview/svc/ProcedureReviewService.java b/src/main/java/com/madeu/crm/procedureReview/svc/ProcedureReviewService.java new file mode 100644 index 0000000..ae60ecf --- /dev/null +++ b/src/main/java/com/madeu/crm/procedureReview/svc/ProcedureReviewService.java @@ -0,0 +1,728 @@ +package com.madeu.crm.procedureReview.svc; + +import com.madeu.constants.Constants; +import com.madeu.crm.procedureReview.mapper.ProcedureReviewMapper; +import com.madeu.dao.web.webauthmenurelation.WebAuthMenuRelationSqlMapDAO; +import com.madeu.dao.web.webmember.WebMemberSqlMapDAO; +import com.madeu.common.service.AttachFileService; +import com.madeu.util.ValidationCheckUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.text.SimpleDateFormat; +import java.util.*; + +@Slf4j +@Service("ProcedureReviewService") +public class ProcedureReviewService { + + @Autowired + private ProcedureReviewMapper procedureReviewMapper; + + @Autowired + private WebMemberSqlMapDAO webMemberSqlMapDAO; + + @Autowired + private WebAuthMenuRelationSqlMapDAO webAuthMenuRelationSqlMapDAO; + + @Autowired + private AttachFileService afs; + + @Value("${url.cdn}") + String CDN_URL; + + public HashMap selectProcedureReviewListIntro( + HashMap paramMap) throws Exception { + log.debug("ProcedureReviewService selectProcedureReviewListIntro START"); + + HashMap map = new HashMap(); + + try { + boolean check = true; + + String menuClass = String.valueOf(paramMap.get("menuClass")); + + if (true != ValidationCheckUtil.emptyCheck(menuClass)) { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "메뉴 정보가 없습니다."); + } + + if (true == check) { + List> userListMap = webMemberSqlMapDAO.checkMember(paramMap); + int userListMapSize = userListMap.size(); + + if (1 == userListMapSize) { + paramMap.put("menuClassAuthId", userListMap.get(0).get("muAuthId")); + } else { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "사용자 정보가 올바르지 않습니다."); + } + } + + if (true == check) { + HashMap authCheckParamMap = new HashMap(); + authCheckParamMap.put("menuClass", paramMap.get("menuClass")); + authCheckParamMap.put("muAuthId", paramMap.get("menuClassAuthId")); + List> authMenuRelationlistMap = webAuthMenuRelationSqlMapDAO + .selectAuthMenuRelation(authCheckParamMap); + int authMenuRelationlistMapSize = authMenuRelationlistMap.size(); + + if (1 == authMenuRelationlistMapSize) { + map.put("msgCode", Constants.OK); + map.put("success", "true"); + + map.put("selectUseYn", authMenuRelationlistMap.get(0).get("selectUseYn")); + map.put("insertUseYn", authMenuRelationlistMap.get(0).get("insertUseYn")); + map.put("updateUseYn", authMenuRelationlistMap.get(0).get("updateUseYn")); + map.put("deleteUseYn", authMenuRelationlistMap.get(0).get("deleteUseYn")); + map.put("downloadUseYn", authMenuRelationlistMap.get(0).get("downloadUseYn")); + } else { + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "권한 정보가 없습니다."); + } + } + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + log.debug("ProcedureReviewService selectProcedureReviewListIntro END"); + return map; + } + + public HashMap selectListProcedureReview( + HashMap paramMap) throws Exception { + log.debug("ProcedureReviewService selectListProcedureReview START"); + + HashMap map = new HashMap(); + + List> listMap = new ArrayList>(); + + try { + boolean check = true; + + if (true == check) { + List> userListMap = webMemberSqlMapDAO.checkMember(paramMap); + int userListMapSize = userListMap.size(); + + if (1 == userListMapSize) { + paramMap.put("menuClassAuthId", userListMap.get(0).get("muAuthId")); + } else { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "사용자 정보가 올바르지 않습니다."); + } + } + + if (true == check) { + HashMap authCheckParamMap = new HashMap(); + authCheckParamMap.put("menuClass", paramMap.get("menuClass")); + authCheckParamMap.put("muAuthId", paramMap.get("menuClassAuthId")); + List> authMenuRelationlistMap = webAuthMenuRelationSqlMapDAO + .selectAuthMenuRelation(authCheckParamMap); + + if (1 == authMenuRelationlistMap.size()) { + + if (("Y").equals(authMenuRelationlistMap.get(0).get("selectUseYn"))) { + + } else { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "조회 권한 정보가 없습니다."); + } + } else { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "권한 정보가 없습니다."); + } + } + + paramMap.put("categoryDivCd", "07"); + + if (true == check) { + if (null == paramMap.get("procedureReviewDir") || ("").equals(paramMap.get("procedureReviewDir"))) { + + } else { + String dir = String.valueOf(paramMap.get("procedureReviewDir")); + if (("A").equals(dir)) { + paramMap.put("procedureReviewDir", "DESC"); + } else if (("B").equals(dir)) { + paramMap.put("procedureReviewDir", "ASC"); + } else { + paramMap.put("procedureReviewDir", "DESC"); + } + } + + paramMap.put("useYn", "Y"); + + List> totalCountListMap = procedureReviewMapper + .selectTotalProcedureReviewCount(paramMap); + int totalCount = Integer.parseInt(String.valueOf(totalCountListMap.get(0).get("totalCount"))); + + if (0 < totalCount) { + listMap = procedureReviewMapper.selectListProcedureReview(paramMap); + } + + map.put("msgCode", Constants.OK); + map.put("success", "true"); + map.put("totalCount", totalCount); + map.put("rows", listMap); + } + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + log.debug("ProcedureReviewService selectListProcedureReview END"); + return map; + } + + public HashMap selectProcedureReview( + HashMap paramMap) throws Exception { + log.debug("ProcedureReviewService selectProcedureReview START"); + + HashMap map = new HashMap(); + + try { + boolean check = true; + String muProcedureReviewId = String.valueOf(paramMap.get("muProcedureReviewId")); + + if (true != ValidationCheckUtil.emptyCheck(muProcedureReviewId)) { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "시술후기 정보가 없습니다."); + } + + if (true == check) { + List> userListMap = webMemberSqlMapDAO.checkMember(paramMap); + int userListMapSize = userListMap.size(); + + if (1 == userListMapSize) { + paramMap.put("menuClassAuthId", userListMap.get(0).get("muAuthId")); + } else { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "사용자 정보가 올바르지 않습니다."); + } + } + + if (true == check) { + HashMap authCheckParamMap = new HashMap(); + authCheckParamMap.put("menuClass", paramMap.get("menuClass")); + authCheckParamMap.put("muAuthId", paramMap.get("menuClassAuthId")); + List> authMenuRelationlistMap = webAuthMenuRelationSqlMapDAO + .selectAuthMenuRelation(authCheckParamMap); + + if (1 == authMenuRelationlistMap.size()) { + + if (("Y").equals(authMenuRelationlistMap.get(0).get("selectUseYn"))) { + + } else { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "조회 권한 정보가 없습니다."); + } + } else { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "권한 정보가 없습니다."); + } + } + + if (true == check) { + List> listMap = procedureReviewMapper.selectProcedureReview(paramMap); + + map.put("msgCode", Constants.OK); + map.put("success", "true"); + map.put("rows", listMap); + } + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + log.debug("ProcedureReviewService selectProcedureReview END"); + return map; + } + + @Transactional(rollbackFor = { Exception.class }, propagation = Propagation.REQUIRES_NEW) + public HashMap insertProcedureReviewFile( + HashMap paramMap, MultipartFile file) throws Exception { + log.debug("ProcedureReviewService insertProcedureReviewFile START"); + + HashMap map = new HashMap(); + + try { + boolean check = true; + String tId = String.valueOf(System.currentTimeMillis()); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Calendar c1 = Calendar.getInstance(); + String tDate = sdf.format(c1.getTime()); + + if (null == file || file.isEmpty()) { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "이미지 정보가 없습니다."); + } + + if (true == check) { + List> userListMap = webMemberSqlMapDAO.checkMember(paramMap); + int userListMapSize = userListMap.size(); + + if (1 == userListMapSize) { + paramMap.put("menuClassAuthId", userListMap.get(0).get("muAuthId")); + } else { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "사용자 정보가 올바르지 않습니다."); + } + } + + if (true == check) { + HashMap authCheckParamMap = new HashMap(); + authCheckParamMap.put("menuClass", paramMap.get("menuClass")); + authCheckParamMap.put("muAuthId", paramMap.get("menuClassAuthId")); + List> authMenuRelationlistMap = webAuthMenuRelationSqlMapDAO + .selectAuthMenuRelation(authCheckParamMap); + int authMenuRelationlistMapSize = authMenuRelationlistMap.size(); + + if (1 == authMenuRelationlistMapSize) { + + if (("Y").equals(authMenuRelationlistMap.get(0).get("insertUseYn"))) { + + } else { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "등록 권한 정보가 없습니다."); + } + } else { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "권한 정보가 없습니다."); + } + } + + if (true == check) { + if (null != file && !file.isEmpty()) { + Map fileMap = afs.saveAttachFile("img", file); + String filePath = String.valueOf(fileMap.get("filePath")); + String fileName = String.valueOf(fileMap.get("attachfileNm")); + String chgFileName = String.valueOf(fileMap.get("orgAttachfileNm")); + + String formattedCdnUrl = CDN_URL.endsWith("/") ? CDN_URL : CDN_URL + "/"; + paramMap.put("filePath", formattedCdnUrl + filePath); + paramMap.put("fileName", fileName); + paramMap.put("originalFileName", chgFileName); + paramMap.put("attachfileId", fileMap.get("attachfileId")); + } + paramMap.put("tDate", tDate); + paramMap.put("tId", tId); + + map.put("rows", paramMap); + map.put("msgCode", Constants.OK); + map.put("msgDesc", "등록되었습니다."); + } + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + log.debug("ProcedureReviewService insertProcedureReviewFile END"); + return map; + } + + public HashMap selectListCategory(HashMap paramMap) throws Exception { + HashMap map = new HashMap(); + paramMap.put("categoryDivCd", "07"); + List> listMap = procedureReviewMapper.selectListPhotoCategory(paramMap); + map.put("msgCode", Constants.OK); + map.put("rows", listMap); + return map; + } + + public HashMap selectProcedureReviewInsertIntro( + HashMap paramMap) throws Exception { + log.debug("ProcedureReviewService selectProcedureReviewInsertIntro START"); + + HashMap map = new HashMap(); + + try { + boolean check = true; + + String menuClass = String.valueOf(paramMap.get("menuClass")); + + if (true != ValidationCheckUtil.emptyCheck(menuClass)) { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "메뉴 정보가 없습니다."); + } + + if (true == check) { + List> userListMap = webMemberSqlMapDAO.checkMember(paramMap); + int userListMapSize = userListMap.size(); + + if (1 == userListMapSize) { + paramMap.put("menuClassAuthId", userListMap.get(0).get("muAuthId")); + } else { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "사용자 정보가 올바르지 않습니다."); + } + } + + if (true == check) { + HashMap authCheckParamMap = new HashMap(); + authCheckParamMap.put("menuClass", paramMap.get("menuClass")); + authCheckParamMap.put("muAuthId", paramMap.get("menuClassAuthId")); + List> authMenuRelationlistMap = webAuthMenuRelationSqlMapDAO + .selectAuthMenuRelation(authCheckParamMap); + int authMenuRelationlistMapSize = authMenuRelationlistMap.size(); + + if (1 == authMenuRelationlistMapSize) { + map.put("msgCode", Constants.OK); + map.put("success", "true"); + + map.put("selectUseYn", authMenuRelationlistMap.get(0).get("selectUseYn")); + map.put("insertUseYn", authMenuRelationlistMap.get(0).get("insertUseYn")); + map.put("updateUseYn", authMenuRelationlistMap.get(0).get("updateUseYn")); + map.put("deleteUseYn", authMenuRelationlistMap.get(0).get("deleteUseYn")); + map.put("downloadUseYn", authMenuRelationlistMap.get(0).get("downloadUseYn")); + } else { + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "권한 정보가 없습니다."); + } + } + paramMap.put("categoryDivCd", "07"); + List> listMap = procedureReviewMapper.selectListPhotoCategory(paramMap); + map.put("categorylist", listMap); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + log.debug("ProcedureReviewService selectProcedureReviewInsertIntro END"); + return map; + } + + @Transactional(rollbackFor = { Exception.class }, propagation = Propagation.REQUIRES_NEW) + public HashMap insertProcedureReview( + HashMap paramMap) throws Exception { + log.debug("ProcedureReviewService insertProcedureReview START"); + + HashMap map = new HashMap(); + + try { + boolean check = true; + String tId = String.valueOf(System.currentTimeMillis()); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Calendar c1 = Calendar.getInstance(); + String tDate = sdf.format(c1.getTime()); + + String title = String.valueOf(paramMap.get("title")); + String content = String.valueOf(paramMap.get("content")); + String isEncoded = String.valueOf(paramMap.get("isEncoded")); + if ("Y".equals(isEncoded) && content != null) { + try { + byte[] decodedBytes = java.util.Base64.getDecoder().decode(content); + content = java.net.URLDecoder.decode(new String(decodedBytes, "UTF-8"), "UTF-8"); + paramMap.put("content", content); + } catch (Exception e) { + e.printStackTrace(); + } + } + + if (true != ValidationCheckUtil.emptyCheck(title)) { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "제목 정보가 없습니다."); + } + + if (true != ValidationCheckUtil.emptyCheck(content)) { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "시술후기 상세 정보가 없습니다."); + } + + if (true == check) { + List> userListMap = webMemberSqlMapDAO.checkMember(paramMap); + int userListMapSize = userListMap.size(); + + if (1 == userListMapSize) { + paramMap.put("menuClassAuthId", userListMap.get(0).get("muAuthId")); + } else { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "사용자 정보가 올바르지 않습니다."); + } + } + + if (true == check) { + HashMap authCheckParamMap = new HashMap(); + authCheckParamMap.put("menuClass", paramMap.get("menuClass")); + authCheckParamMap.put("muAuthId", paramMap.get("menuClassAuthId")); + List> authMenuRelationlistMap = webAuthMenuRelationSqlMapDAO + .selectAuthMenuRelation(authCheckParamMap); + int authMenuRelationlistMapSize = authMenuRelationlistMap.size(); + + if (1 == authMenuRelationlistMapSize) { + if (("Y").equals(authMenuRelationlistMap.get(0).get("insertUseYn"))) { + } else { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "등록 권한 정보가 없습니다."); + } + } else { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "권한 정보가 없습니다."); + } + } + + if (true == check) { + paramMap.put("tDate", tDate); + paramMap.put("tId", tId); + paramMap.put("muMemberId", paramMap.get("loginMemberId")); + procedureReviewMapper.insertProcedureReview(paramMap); + paramMap.put("muProcedureReviewId", paramMap.get("id")); + map.put("muProcedureReviewId", paramMap.get("muProcedureReviewId")); + map.put("msgCode", Constants.OK); + map.put("msgDesc", "등록되었습니다."); + } + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + log.debug("ProcedureReviewService insertProcedureReview END"); + return map; + } + + public HashMap selectProcedureReviewUpdateIntro( + HashMap paramMap) throws Exception { + log.debug("ProcedureReviewService selectProcedureReviewUpdateIntro START"); + + HashMap map = new HashMap(); + + try { + boolean check = true; + + String menuClass = String.valueOf(paramMap.get("menuClass")); + + if (true != ValidationCheckUtil.emptyCheck(menuClass)) { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "메뉴 정보가 없습니다."); + } + + if (true == check) { + List> userListMap = webMemberSqlMapDAO.checkMember(paramMap); + int userListMapSize = userListMap.size(); + + if (1 == userListMapSize) { + paramMap.put("menuClassAuthId", userListMap.get(0).get("muAuthId")); + } else { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "사용자 정보가 올바르지 않습니다."); + } + } + + if (true == check) { + HashMap authCheckParamMap = new HashMap(); + authCheckParamMap.put("menuClass", paramMap.get("menuClass")); + authCheckParamMap.put("muAuthId", paramMap.get("menuClassAuthId")); + List> authMenuRelationlistMap = webAuthMenuRelationSqlMapDAO + .selectAuthMenuRelation(authCheckParamMap); + int authMenuRelationlistMapSize = authMenuRelationlistMap.size(); + + if (1 == authMenuRelationlistMapSize) { + map.put("msgCode", Constants.OK); + map.put("success", "true"); + + map.put("selectUseYn", authMenuRelationlistMap.get(0).get("selectUseYn")); + map.put("insertUseYn", authMenuRelationlistMap.get(0).get("insertUseYn")); + map.put("updateUseYn", authMenuRelationlistMap.get(0).get("updateUseYn")); + map.put("deleteUseYn", authMenuRelationlistMap.get(0).get("deleteUseYn")); + map.put("downloadUseYn", authMenuRelationlistMap.get(0).get("downloadUseYn")); + } else { + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "권한 정보가 없습니다."); + } + } + paramMap.put("categoryDivCd", "07"); + List> listMap = procedureReviewMapper.selectListPhotoCategory(paramMap); + map.put("categorylist", listMap); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + log.debug("ProcedureReviewService selectProcedureReviewUpdateIntro END"); + return map; + } + + @Transactional(rollbackFor = { Exception.class }, propagation = Propagation.REQUIRES_NEW) + public HashMap updateProcedureReview( + HashMap paramMap) throws Exception { + log.debug("ProcedureReviewService updateProcedureReview START"); + + HashMap map = new HashMap(); + + try { + boolean check = true; + + String muProcedureReviewId = String.valueOf(paramMap.get("muProcedureReviewId")); + String title = String.valueOf(paramMap.get("title")); + String content = String.valueOf(paramMap.get("content")); + String isEncoded = String.valueOf(paramMap.get("isEncoded")); + if ("Y".equals(isEncoded) && content != null) { + try { + byte[] decodedBytes = java.util.Base64.getDecoder().decode(content); + content = java.net.URLDecoder.decode(new String(decodedBytes, "UTF-8"), "UTF-8"); + paramMap.put("content", content); + } catch (Exception e) { + e.printStackTrace(); + } + } + + if (true != ValidationCheckUtil.emptyCheck(muProcedureReviewId)) { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "시술후기 식별자 정보가 없습니다."); + } + + if (true != ValidationCheckUtil.emptyCheck(title)) { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "제목 정보가 없습니다."); + } + + if (true != ValidationCheckUtil.emptyCheck(content)) { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "시술후기 상세 정보가 없습니다."); + } + + if (true == check) { + List> userListMap = webMemberSqlMapDAO.checkMember(paramMap); + int userListMapSize = userListMap.size(); + + if (1 == userListMapSize) { + paramMap.put("menuClassAuthId", userListMap.get(0).get("muAuthId")); + } else { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "사용자 정보가 올바르지 않습니다."); + } + } + + if (true == check) { + HashMap authCheckParamMap = new HashMap(); + authCheckParamMap.put("menuClass", paramMap.get("menuClass")); + authCheckParamMap.put("muAuthId", paramMap.get("menuClassAuthId")); + List> authMenuRelationlistMap = webAuthMenuRelationSqlMapDAO + .selectAuthMenuRelation(authCheckParamMap); + int authMenuRelationlistMapSize = authMenuRelationlistMap.size(); + + if (1 == authMenuRelationlistMapSize) { + + if (("Y").equals(authMenuRelationlistMap.get(0).get("updateUseYn"))) { + } else { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "수정 권한 정보가 없습니다."); + } + } else { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "권한 정보가 없습니다."); + } + } + + if (true == check) { + procedureReviewMapper.updateProcedureReview(paramMap); + map.put("msgCode", Constants.OK); + map.put("msgDesc", "수정되었습니다."); + } + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + log.debug("ProcedureReviewService updateProcedureReview END"); + return map; + } + + public HashMap deleteProcedureReview( + HashMap paramMap) throws Exception { + log.debug("ProcedureReviewService deleteProcedureReview START"); + + HashMap map = new HashMap(); + + try { + boolean check = true; + + String muProcedureReviewId = String.valueOf(paramMap.get("muProcedureReviewId")); + + if (true != ValidationCheckUtil.emptyCheck(muProcedureReviewId)) { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "시술후기 식별자 정보가 없습니다."); + } + + if (true == check) { + List> userListMap = webMemberSqlMapDAO.checkMember(paramMap); + int userListMapSize = userListMap.size(); + + if (1 == userListMapSize) { + paramMap.put("menuClassAuthId", userListMap.get(0).get("muAuthId")); + } else { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "사용자 정보가 올바르지 않습니다."); + } + } + + if (true == check) { + HashMap authCheckParamMap = new HashMap(); + authCheckParamMap.put("menuClass", paramMap.get("menuClass")); + authCheckParamMap.put("muAuthId", paramMap.get("menuClassAuthId")); + List> authMenuRelationlistMap = webAuthMenuRelationSqlMapDAO + .selectAuthMenuRelation(authCheckParamMap); + int authMenuRelationlistMapSize = authMenuRelationlistMap.size(); + + if (1 == authMenuRelationlistMapSize) { + + if (("Y").equals(authMenuRelationlistMap.get(0).get("deleteUseYn"))) { + + } else { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "삭제 권한 정보가 없습니다."); + } + } else { + check = false; + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "권한 정보가 없습니다."); + } + } + + String muProcedureReviewIdStr = String.valueOf(paramMap.get("muProcedureReviewId")); + if (true == check) { + String[] idArray = muProcedureReviewIdStr.split(","); + for (String id : idArray) { + paramMap.put("muProcedureReviewId", id.trim()); + procedureReviewMapper.deleteProcedureReview(paramMap); + } + + map.put("msgCode", Constants.OK); + map.put("msgDesc", "삭제되었습니다."); + } + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + log.debug("ProcedureReviewService deleteProcedureReview END"); + return map; + } + +} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index bb064f4..bf297ba 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -1,10 +1,17 @@ server: + tomcat: + max-swallow-size: -1 + max-http-form-post-size: -1 port: 8080 servlet: session: timeout: -1 spring: + servlet: + multipart: + max-file-size: 500MB + max-request-size: 500MB datasource: hikari: driver-class-name: org.mariadb.jdbc.Driver diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 40d2b62..aa25770 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,8 +10,8 @@ spring: servlet: multipart: - maxFileSize: 500MB - maxRequestSize: 500MB + max-file-size: 500MB + max-request-size: 500MB aop: proxy-target-class: false @@ -30,6 +30,8 @@ spring: server: + tomcat: + max-swallow-size: -1 encoding: charset: UTF-8 enabled: true @@ -56,6 +58,7 @@ logging: org.hibernate.SQL: DEBUG org.hibernate.type.descriptor.sql.BasicBinder: TRACE p6spy: debug + org.springframework.aop.framework.CglibAopProxy: ERROR decorator: datasource: diff --git a/src/main/resources/mappers/ProcedureReviewSqlMap.xml b/src/main/resources/mappers/ProcedureReviewSqlMap.xml new file mode 100644 index 0000000..0f7d717 --- /dev/null +++ b/src/main/resources/mappers/ProcedureReviewSqlMap.xml @@ -0,0 +1,209 @@ + + + + + + + + + + + + + SELECT CONCAT('PR-', LPAD(NEXTVAL(MU_PROCEDURE_REVIEW_SEQ), 21, '0')) + + INSERT INTO MU_PROCEDURE_REVIEW ( + MU_PROCEDURE_REVIEW_ID + ,MU_MEMBER_ID + ,CATEGORY_NO + ,TITLE + ,CONTENT + ,HASHTAG + ,BEFORE_PHOTO_ATTACHFILE_ID + ,AFTER_PHOTO_ATTACHFILE_ID + ,VIEW_COUNT + ,USE_YN + ,REG_ID + ,REG_DATE + ,MOD_ID + ,MOD_DATE + ) VALUES ( + #{id} + ,#{muMemberId} + ,#{categoryno, jdbcType=VARCHAR} + ,#{title} + ,#{content} + ,#{hashtag} + ,#{beforeId, jdbcType=VARCHAR} + ,#{afterId, jdbcType=VARCHAR} + ,0 + ,'Y' + ,#{regId} + ,NOW() + ,#{modId} + ,NOW() + ) + + + + + SELECT CONCAT('ATF-', LPAD(NEXTVAL(seq_attachfile_id), 21, '0')) + + INSERT INTO HP_ATTACH_FILE ( + ATTACHFILE_ID + ,ATTACHFILE_NM + ,FILE_PATH + ,FILE_SIZE + ,FILE_TYPE + ,REG_ID + ,REG_DATE + ,MOD_ID + ,MOD_DATE + ) VALUES ( + #{beforeId} + ,#{beforeFileName} + ,#{beforeFilePath} + ,'0' + ,'img' + ,#{regId} + ,NOW() + ,#{modId} + ,NOW() + ) + + + + + SELECT CONCAT('ATF-', LPAD(NEXTVAL(seq_attachfile_id), 21, '0')) + + INSERT INTO HP_ATTACH_FILE ( + ATTACHFILE_ID + ,ATTACHFILE_NM + ,FILE_PATH + ,FILE_SIZE + ,FILE_TYPE + ,REG_ID + ,REG_DATE + ,MOD_ID + ,MOD_DATE + ) VALUES ( + #{afterId} + ,#{afterFileName} + ,#{afterFilePath} + ,'0' + ,'img' + ,#{regId} + ,NOW() + ,#{modId} + ,NOW() + ) + + + + UPDATE MU_PROCEDURE_REVIEW + SET TITLE = #{title} + ,CONTENT = #{content} + ,HASHTAG = #{hashtag} + + ,BEFORE_PHOTO_ATTACHFILE_ID = #{beforeId} + + + ,AFTER_PHOTO_ATTACHFILE_ID = #{afterId} + + ,MOD_ID = #{modId} + ,MOD_DATE = NOW() + WHERE USE_YN = 'Y' + AND MU_PROCEDURE_REVIEW_ID = #{muProcedureReviewId} + + + + UPDATE MU_PROCEDURE_REVIEW + SET MOD_ID = #{modId} + ,MOD_DATE = NOW() + ,USE_YN = 'N' + WHERE USE_YN = 'Y' + AND MU_PROCEDURE_REVIEW_ID = #{muProcedureReviewId} + + + + diff --git a/src/main/resources/static/css/web/procedureReview.css b/src/main/resources/static/css/web/procedureReview.css new file mode 100644 index 0000000..59dcb28 --- /dev/null +++ b/src/main/resources/static/css/web/procedureReview.css @@ -0,0 +1,321 @@ +/* ================================================ + 시술후기 등록/수정 전용 스타일 + ================================================ */ + +/* ── 전체 컨테이너 ── */ +.procedure-review-form { + display: flex; + gap: 24px; + width: 100%; + height: calc(100% - 10px); +} + +/* ── 좌측 패널 (폼 영역) ── */ +.procedure-review-form .form-panel { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 16px; + overflow-y: auto; + padding-right: 8px; +} + +/* ── 우측 패널 (이미지 영역) ── */ +.procedure-review-form .image-panel { + flex: 0 0 400px; + display: flex; + flex-direction: column; + gap: 16px; + overflow-y: auto; +} + +/* ── 폼 그룹 ── */ +.procedure-review-form .pr-form-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.procedure-review-form .pr-form-row { + display: flex; + gap: 16px; + align-items: flex-start; +} + +.procedure-review-form .pr-form-row .pr-form-group { + flex: 1; +} + +.procedure-review-form .pr-form-row .pr-form-group.pr-category { + flex: 0 0 180px; +} + +/* ── 라벨 ── */ +.procedure-review-form .pr-label { + font-size: 13px; + font-weight: 600; + color: #374151; + margin-bottom: 2px; + display: flex; + align-items: center; + gap: 4px; +} + +.procedure-review-form .pr-label .required { + color: #EF4444; + font-size: 12px; +} + +/* ── Input / Select 공통 ── */ +.procedure-review-form input[type="text"], +.procedure-review-form select { + width: 100%; + height: 38px; + padding: 0 12px; + border: 1px solid #D1D5DB; + border-radius: 6px; + font-size: 13px; + color: #1F2937; + background: #fff; + outline: none; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.procedure-review-form input[type="text"]:focus, +.procedure-review-form select:focus { + border-color: #3985EA; + box-shadow: 0 0 0 3px rgba(57, 133, 234, 0.12); +} + +.procedure-review-form input[type="text"]::placeholder { + color: #9CA3AF; +} + +/* ── Quill 에디터 ── */ +.procedure-review-form .editor-container { + flex: 1; + display: flex; + flex-direction: column; + min-height: 300px; +} + +.procedure-review-form .editor-container .ql-toolbar.ql-snow { + border: 1px solid #D1D5DB; + border-radius: 6px 6px 0 0; + background: #F9FAFB; + padding: 8px; +} + +.procedure-review-form .editor-container .ql-container.ql-snow { + border: 1px solid #D1D5DB; + border-top: none; + border-radius: 0 0 6px 6px; + flex: 1; + font-size: 14px; + font-family: 'Pretendard', 'Noto Sans KR', sans-serif; + min-height: 250px; +} + +.procedure-review-form .editor-container .ql-editor { + min-height: 240px; + line-height: 1.7; + padding: 16px; +} + +.procedure-review-form .editor-container .ql-editor.ql-blank::before { + color: #9CA3AF; + font-style: normal; +} + +/* ── 이미지 카드 ── */ +.procedure-review-form .image-card { + border: 1px solid #E5E7EB; + border-radius: 8px; + overflow: hidden; + background: #fff; +} + +.procedure-review-form .image-card .image-card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 14px; + background: #F3F4F6; + border-bottom: 1px solid #E5E7EB; +} + +.procedure-review-form .image-card .image-card-header span { + font-size: 13px; + font-weight: 600; + color: #374151; +} + +.procedure-review-form .image-card .image-card-header .image-card-actions { + display: flex; + align-items: center; + gap: 6px; +} + +.procedure-review-form .image-card .image-card-header .image-card-actions label { + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 4px; + transition: background 0.15s; +} + +.procedure-review-form .image-card .image-card-header .image-card-actions label:hover { + background: #E5E7EB; +} + +.procedure-review-form .image-card .image-card-header .image-card-actions label img { + width: 16px; + height: 16px; +} + +.procedure-review-form .image-card .image-card-header .image-card-actions .img-delete-btn { + border: none; + background: none; + cursor: pointer; + font-size: 12px; + color: #9CA3AF; + padding: 4px 8px; + border-radius: 4px; + transition: all 0.15s; +} + +.procedure-review-form .image-card .image-card-header .image-card-actions .img-delete-btn:hover { + background: #FEE2E2; + color: #EF4444; +} + +.procedure-review-form .image-card .image-card-body { + width: 100%; + aspect-ratio: 1 / 1; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + background: #FAFAFA; + position: relative; +} + +.procedure-review-form .image-card .image-card-body .placeholder-text { + color: #D1D5DB; + font-size: 13px; + text-align: center; + user-select: none; +} + +.procedure-review-form .image-card .image-card-body img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +/* ── 가이드 박스 ── */ +.procedure-review-form .photo-guide { + padding: 14px 16px; + background: #F0F5FF; + border: 1px solid #DBEAFE; + border-radius: 8px; + font-size: 12px; + color: #4B5563; +} + +.procedure-review-form .photo-guide p { + font-weight: 700; + margin-bottom: 6px; + color: #3985EA; + font-size: 12px; +} + +.procedure-review-form .photo-guide ul { + list-style: none; + padding: 0; + margin: 0; +} + +.procedure-review-form .photo-guide ul li { + position: relative; + padding-left: 14px; + line-height: 1.6; +} + +.procedure-review-form .photo-guide ul li::before { + content: '•'; + position: absolute; + left: 2px; + color: #3985EA; +} + +/* ── 액션 버튼 영역 ── */ +.procedure-review-form .pr-actions { + display: flex; + gap: 10px; + padding-top: 8px; +} + +.procedure-review-form .pr-actions .pr-btn { + width: 90px; + height: 38px; + border-radius: 6px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + border: none; + outline: none; + transition: all 0.2s; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.procedure-review-form .pr-actions .pr-btn-primary { + background: #3985EA; + color: #fff; +} + +.procedure-review-form .pr-actions .pr-btn-primary:hover { + background: #2563EB; + box-shadow: 0 2px 8px rgba(37, 99, 235, 0.25); +} + +.procedure-review-form .pr-actions .pr-btn-cancel { + background: #F3F4F6; + color: #6B7280; + border: 1px solid #D1D5DB; +} + +.procedure-review-form .pr-actions .pr-btn-cancel:hover { + background: #E5E7EB; + color: #374151; +} + +/* ── 반응형 ── */ +@media only screen and (max-width: 1400px) { + .procedure-review-form .image-panel { + flex: 0 0 340px; + } +} + +@media only screen and (max-width: 1200px) { + .procedure-review-form { + flex-direction: column; + } + .procedure-review-form .image-panel { + flex: none; + } + .procedure-review-form .image-panel .images-row { + display: flex; + gap: 16px; + } + .procedure-review-form .image-panel .images-row .image-card { + flex: 1; + } +} diff --git a/src/main/resources/static/js/crm/procedureReview/procedureReviewInsert.js b/src/main/resources/static/js/crm/procedureReview/procedureReviewInsert.js new file mode 100644 index 0000000..3f4908f --- /dev/null +++ b/src/main/resources/static/js/crm/procedureReview/procedureReviewInsert.js @@ -0,0 +1,380 @@ +/**************************************************************************** + * Quill 에디터 인스턴스 + ****************************************************************************/ +let quill; + +/**************************************************************************** + * Quill Image blot - data: URL (base64) 허용 + ****************************************************************************/ +const QuillImage = Quill.import('formats/image'); +const originalSanitize = QuillImage.sanitize; +QuillImage.sanitize = function (url) { + if (url && url.startsWith('data:')) { + return ''; + } + return originalSanitize.call(this, url); +}; + +/**************************************************************************** + * Quill 에디터 초기화 + ****************************************************************************/ +function fn_initQuillEditor() { + quill = new Quill('#quillEditor', { + theme: 'snow', + placeholder: '내용을 입력해주세요.', + modules: { + toolbar: { + container: [ + [{ 'header': [1, 2, 3, 4, 5, 6, false] }], + [{ 'font': [] }], + [{ 'size': ['small', false, 'large', 'huge'] }], + ['bold', 'italic', 'underline', 'strike'], + [{ 'color': [] }, { 'background': [] }], + [{ 'script': 'sub' }, { 'script': 'super' }], + [{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'list': 'check' }], + [{ 'indent': '-1' }, { 'indent': '+1' }], + [{ 'direction': 'rtl' }], + [{ 'align': [] }], + ['blockquote', 'code-block'], + ['link', 'image', 'video', 'formula'], + ['clean'] + ], + handlers: { + image: imageHandler + } + } + } + }); + + quill.root.addEventListener('paste', function (e) { + try { + console.log("[Paste Event] Triggered"); + let clipboardData = e.clipboardData || window.clipboardData || (e.originalEvent && e.originalEvent.clipboardData); + console.log("[Paste Event] clipboardData:", clipboardData); + if (!clipboardData || !clipboardData.items) { + console.warn("[Paste Event] No clipboardData or items found"); + return; + } + + let items = clipboardData.items; + console.log("[Paste Event] items length:", items.length); + for (let i = 0; i < items.length; i++) { + let item = items[i]; + console.log(`[Paste Event] item[${i}] kind: ${item.kind}, type: ${item.type}`); + if (item.kind === 'file' && item.type.match('^image/')) { + console.log(`[Paste Event] Image file detected. Prevent default.`); + e.preventDefault(); + e.stopPropagation(); + + let file = item.getAsFile(); + console.log(`[Paste Event] getAsFile() result:`, file); + if (file) { + if (!file.name || !file.name.includes('.')) { + let ext = 'png'; + if (file.type === 'image/jpeg') ext = 'jpg'; + else if (file.type === 'image/gif') ext = 'gif'; + file = new File([file], "clipboard_image." + ext, { type: file.type }); + console.log(`[Paste Event] Renamed missing file info:`, file.name); + } + console.log(`[Paste Event] Call fn_uploadImageAndInsertToQuill() for:`, file.name); + fn_uploadImageAndInsertToQuill(file); + } else { + console.error("[Paste Event] getAsFile() returned null."); + } + } + } + } catch (err) { + console.error("[Paste Event] Error parsing paste logic:", err); + } + }, true); + + quill.root.addEventListener('drop', function (e) { + try { + console.log("[Drop Event] Triggered"); + if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length) { + let hasImage = false; + console.log("[Drop Event] files length:", e.dataTransfer.files.length); + for (let i = 0; i < e.dataTransfer.files.length; i++) { + let file = e.dataTransfer.files[i]; + console.log(`[Drop Event] file[${i}] type: ${file.type}`); + if (file.type.match('^image/')) { + console.log(`[Drop Event] Image file detected. Prevent default.`); + hasImage = true; + e.preventDefault(); + e.stopPropagation(); + fn_uploadImageAndInsertToQuill(file); + } + } + } else { + console.log("[Drop Event] No files found in drop event"); + } + } catch (err) { + console.error("[Drop Event] Error parsing drop logic:", err); + } + }, true); +} +function imageHandler() { + const input = document.createElement('input'); + input.setAttribute('type', 'file'); + input.setAttribute('accept', 'image/*'); + input.click(); + + input.onchange = async () => { + const file = input.files[0]; + fn_uploadImageAndInsertToQuill(file); + }; +} + +function fn_uploadImageAndInsertToQuill(file) { + if (!file) { + console.error("[Upload] file is undefined or null"); + return; + } + console.log("[Upload] fn_uploadImageAndInsertToQuill Start. File name:", file.name, "size:", file.size); + + const formData = new FormData(); + formData.append('file', file); + formData.append("menuClass", menuClass); + + $.ajax({ + url: '/procedureReview/putProcedureReviewFile.do', + type: 'POST', + data: formData, + contentType: false, + processData: false, + dataType: 'json', + success: function (res) { + if (typeof res === 'string') { + try { res = JSON.parse(res); } catch (e) { } + } + console.log("[Upload] API Success response:", res); + if (res.msgCode == 0 || res.msgCode === '0') { + let range = quill.getSelection(true); + let index = range ? range.index : quill.getLength(); + console.log(`[Upload] Inserting image at index = ${index}, path = ${res.rows.filePath}`); + quill.insertEmbed(index, 'image', res.rows.filePath); + } else { + console.error("[Upload] API logic error msgDesc:", res.msgDesc); + modalEvent.danger("업로드 오류", res.msgDesc); + } + }, + error: function (xhr, status, error) { + console.error("[Upload] AJAX HTTP error. status:", status, "error:", error, "responseText:", xhr.responseText); + modalEvent.danger("업로드 오류", "이미지 업로드 중 오류가 발생했습니다."); + } + }); +} + +/**************************************************************************** + * Quill 콘텐츠를 Delta JSON 문자열로 반환 (저장용) + ****************************************************************************/ +function fn_getQuillContentForSave() { + return JSON.stringify(quill.getContents()); +} + +/**************************************************************************** + * 저장된 콘텐츠를 Quill에 로드 (Delta JSON / 레거시 HTML 호환) + ****************************************************************************/ +function fn_loadQuillContent(content) { + if (!content) return; + + if (typeof content === 'object') { + if (Array.isArray(content.ops)) quill.setContents(content.ops); + else if (Array.isArray(content)) quill.setContents(content); + return; + } + + try { + if (typeof content === 'string') { + // base64인지 판별해 디코딩 시도 (기존 데이터와 새 데이터 호환을 위해) + const isBase64 = /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$/.test(content); + if (isBase64 && content.length > 0 && !content.trim().startsWith('{') && !content.trim().startsWith('[')) { + try { + content = decodeURIComponent(escape(atob(content))); + } catch (e) { + // base64 디코딩 실패면 그냥 넘어감 + } + } + + if (content.trim().startsWith('{') || content.trim().startsWith('[')) { + let parsed = JSON.parse(content); + if (typeof parsed === 'string') { + parsed = JSON.parse(parsed); + } + + if (typeof parsed === 'object' && parsed !== null && Array.isArray(parsed.ops)) { + quill.setContents(parsed.ops); + return; + } else if (Array.isArray(parsed)) { + quill.setContents(parsed); + return; + } + } + } + } catch (e) { + console.warn("Quill Delta Parsing Failed - Fallback to HTML : ", e); + } + + quill.clipboard.dangerouslyPasteHTML(content); +} + +/**************************************************************************** + * 시술후기 등록 + ****************************************************************************/ +function fn_insertProcedureReview() { + if ("Y" != insertUseYn) { + modalEvent.warning("", "등록 권한이 없습니다."); + return false; + } + + let title = $("#title").val(); + let content = fn_getQuillContentForSave(); + let hashtag = $("#hashtag").val(); + + // Quill 에디터 빈 값 체크 + let quillText = quill.getText().trim(); + + if (true != fn_emptyCheck(title)) { + modalEvent.warning("등록", "제목을 입력하세요."); + return; + } + if (!quillText || quillText.length === 0) { + modalEvent.warning("등록", "내용을 입력하세요."); + return; + } + if (true != fn_emptyCheck(hashtag)) { + modalEvent.warning("등록", "해시태그를 입력하세요."); + return; + } + + modalEvent.info("등록", "시술후기 정보를 등록하시겠습니까?", function () { + let formData = new FormData(); + formData.append("menuClass", menuClass); + formData.append("title", title); + + let encodedContent = btoa(unescape(encodeURIComponent(content))); + formData.append("content", encodedContent); + formData.append("isEncoded", "Y"); // base64 인코딩 플래그 추가 + formData.append("hashtag", hashtag); + + $.ajax({ + url: encodeURI('/procedureReview/putProcedureReview.do'), + data: formData, + dataType: "json", + processData: false, + contentType: false, + type: 'POST', + async: true, + success: function (data) { + if ('0' == data.msgCode) { + modalEvent.success("등록 성공", data.msgDesc, function () { + fn_selectListProcedureReviewIntro(); + }); + } + else { + modalEvent.danger("등록 오류", data.msgDesc); + } + }, + error: function (xhr, status, error) { + modalEvent.danger("등록 오류", "등록 중 오류가 발생하였습니다. 잠시후 다시시도하십시오."); + } + }); + }); +} + +/**************************************************************************** + * 리스트 화면으로 이동. + ****************************************************************************/ +function fn_selectListProcedureReviewIntro() { + if ("Y" == selectUseYn) { + let pagingParam = "?menuClass=" + menuClass; + fn_leftFormAction("/procedureReview/moveProcedureReviewList.do" + pagingParam); + } else { + modalEvent.warning("", "조회 권한이 없습니다."); + return false; + } +} + +/**************************************************************************** + * 도움말 모달 + ****************************************************************************/ +function fn_initHelpModal() { + $('#btnHelp').on('click', function () { + $('#helpModal').addClass('active'); + }); + $('#helpModalClose').on('click', function () { + $('#helpModal').removeClass('active'); + }); + $('#helpModal').on('click', function (e) { + if (e.target === this) { + $(this).removeClass('active'); + } + }); +} + +/**************************************************************************** + * 미리보기 모달 + ****************************************************************************/ +function fn_initPreviewModal() { + $('#previewModalClose').on('click', function () { + $('#previewModal').removeClass('active'); + }); + $('#previewModal').on('click', function (e) { + if (e.target === this) { + $(this).removeClass('active'); + } + }); +} + +function fn_showPreview() { + let title = $("#title").val(); + let content = quill.root.innerHTML; // 미리보기는 렌더링된 HTML 사용 + let hashtag = $("#hashtag").val(); + + // 미리보기 데이터 설정 + $('#previewTitle').text(title || '(제목 없음)'); + $('#previewContent').html(content); + + if (hashtag) { + let tags = hashtag.split(/\s+/).map(function (tag) { + tag = tag.trim(); + if (tag && !tag.startsWith('#')) tag = '#' + tag; + return tag; + }).filter(function (tag) { return tag; }); + $('#previewHashtag').html(tags.join('  ')); + $('#previewHashtagArea').show(); + } else { + $('#previewHashtagArea').hide(); + } + + $('#previewModal').addClass('active'); +} + +/**************************************************************************** + * 페이지 init + ****************************************************************************/ +function fn_pageInit() { + fn_initQuillEditor(); + fn_initHelpModal(); + fn_initPreviewModal(); +} + +/**************************************************************************** + * 페이지 event + ****************************************************************************/ +function fn_pageProcedureReview() { + $('.btnCancle').on("click", function () { + fn_selectListProcedureReviewIntro(); + }); + $('.btnSave').on("click", function () { + fn_insertProcedureReview(); + }); + $('.btnPreview').on("click", function () { + fn_showPreview(); + }); +} + +$(function () { + fn_pageInit(); + fn_pageProcedureReview(); +}); diff --git a/src/main/resources/static/js/crm/procedureReview/procedureReviewSelectList.js b/src/main/resources/static/js/crm/procedureReview/procedureReviewSelectList.js new file mode 100644 index 0000000..290c77e --- /dev/null +++ b/src/main/resources/static/js/crm/procedureReview/procedureReviewSelectList.js @@ -0,0 +1,413 @@ +/* 페이징 관련 변수 */ +let procedureReviewTotalCount = 0; +let procedureReviewTotalPages = 0; + +/*aggird*/ +let procedureReviewAgGridData = []; + +let procedureReviewSelectId = ""; +let procedureReviewSelectCategoryNo = ""; + +/**************************************************************************** + * 시술후기 정보 리스트 조회 + ****************************************************************************/ +function fn_selectListProcedureReviewJson() { + let formData = new FormData(); + formData.append("menuClass", menuClass); + formData.append("procedureReviewSearchKeywordParam0", procedureReviewSearchKeywordParam0); + formData.append("procedureReviewSearchKeywordParam1", procedureReviewSearchKeywordParam1); + formData.append("procedureReviewSearchKeywordParam2", procedureReviewSearchKeywordParam2); + formData.append("procedureReviewSearchKeywordParam3", procedureReviewSearchKeywordParam3); + formData.append("procedureReviewSort", procedureReviewSort); + formData.append("procedureReviewDir", procedureReviewDir); + formData.append("procedureReviewStart", procedureReviewStart); + formData.append("procedureReviewLimit", procedureReviewLimit); + formData.append("procedureReviewSearchStartDate", procedureReviewSearchStartDate); + formData.append("procedureReviewSearchEndDate", procedureReviewSearchEndDate); + formData.append("procedureReviewSearchDateType", procedureReviewSearchDateType); + + $.ajax({ + url: encodeURI('/procedureReview/getProcedureReviewList.do'), + data: formData, + dataType: "json", + processData: false, + contentType: false, + type: 'POST', + async: true, + success: function (data) { + if ('0' == data.msgCode) { + // 페이징 처리 + procedureReviewTotalCount = data.totalCount; + + procedureReviewTotalPages = Math.ceil(procedureReviewTotalCount / procedureReviewLimit); + + // 리스트 조회 + procedureReviewAgGridData = data.rows; + procedureReviewGridOptions.api.setRowData(procedureReviewAgGridData); + + if (0 < data.rows.length) { + //페이징 처리 + window.pagObj = $('#procedureReviewPagination').twbsPagination({ + startPage: ((procedureReviewStart / procedureReviewLimit) + 1), + totalPages: (procedureReviewTotalPages == 0) ? 1 : procedureReviewTotalPages, + visiblePages: 10, + initiateStartPageClick: false, + prev: 'prev', + next: 'next', + first: '', + last: '', + onPageClick: function (event, page) { + fn_procedureReviewPagination(page); + } + }).on('page', function (event, page) { + }); + } + } + else { + modalEvent.danger("조회 오류", data.msgDesc); + } + }, + error: function (xhr, status, error) { + modalEvent.danger("조회 오류", "조회 중 오류가 발생하였습니다. 잠시후 다시시도하십시오."); + }, + beforeSend: function () { + // 로딩열기 + procedureReviewGridOptions.api.showLoadingOverlay(); + }, + complete: function () { + + } + }); +} + +/**************************************************************************** + * 검색하기 + ****************************************************************************/ +function fn_procedureReviewSearch(param) { + if ("A" != param && "Y" != selectUseYn) { + modalEvent.warning("", "조회 권한이 없습니다."); + return false; + } + + fn_procedureReviewPaginReset(); + + procedureReviewSearchKeywordParam0 = $("#procedureReviewSearchKeyword0").val(); + procedureReviewSearchKeywordParam1 = $("#procedureReviewSearchKeyword1").val(); + procedureReviewSearchKeywordParam2 = ""; // 작성자 검색 제거 + procedureReviewSearchKeywordParam3 = $("#procedureReviewSearchKeywordParam3").val(); + + procedureReviewSearchDateType = $("#procedureReviewSearchDateType").val(); + procedureReviewSearchStartDate = $("#procedureReviewSearchStartDate").val(); + procedureReviewSearchEndDate = $("#procedureReviewSearchEndDate").val(); + + fn_selectListProcedureReviewJson(); +} + + +/**************************************************************************** + * 초기화하기 + ****************************************************************************/ +function fn_procedureReviewReset() { + $("#procedureReviewSearchKeyword0").val(""); + $("#procedureReviewSearchKeyword1").val(""); + $("#procedureReviewSearchKeyword2").val(""); + $("#procedureReviewSearchKeyword3").val(""); + + fn_procedureReviewSearch(); +} + +/**************************************************************************** + * 페이징 처리 + ****************************************************************************/ +function fn_procedureReviewPagination(param) { + procedureReviewStart = (parseInt(param) - 1) * procedureReviewLimit; + + fn_selectListProcedureReviewJson(); +} + +/**************************************************************************** + * 페이징 리셋 + ****************************************************************************/ +function fn_procedureReviewPaginReset() { + procedureReviewSearchKeywordParam0 = ''; + procedureReviewSearchKeywordParam1 = ''; + procedureReviewSearchKeywordParam2 = ''; + procedureReviewSearchKeywordParam3 = ''; + + procedureReviewStart = 0; + procedureReviewLimit = 100; + procedureReviewTotalCount = 0; + procedureReviewTotalPages = 0; + + //페이징 초기화 + if ($("#procedureReviewPagination").data("twbs-pagination")) { + $("#procedureReviewPagination").twbsPagination("destroy"); + } +} + +/**************************************************************************** + * 시술후기 삭제 + ****************************************************************************/ +function fn_deleteProcedureReview() { + if ("Y" != deleteUseYn) { + modalEvent.warning("", "삭제 권한이 없습니다."); + return false; + } + + if (!procedureReviewSelectId) { + modalEvent.warning("", "삭제할 대상을 선택하세요."); + return false; + } + + modalEvent.info("삭제", "선택한 시술후기 정보를 삭제하시겠습니까?", function () { + let formData = new FormData(); + formData.append("menuClass", menuClass); + formData.append("muProcedureReviewId", procedureReviewSelectId); + + $.ajax({ + url: encodeURI('/procedureReview/delProcedureReview.do'), + data: formData, + dataType: "json", + processData: false, + contentType: false, + type: 'POST', + async: true, + success: function (data) { + if ('0' == data.msgCode) { + modalEvent.success("삭제 성공", data.msgDesc, function () { + fn_procedureReviewOk(); + }); + } + else { + modalEvent.danger("삭제 오류", data.msgDesc); + } + }, + error: function (xhr, status, error) { + modalEvent.danger("삭제 오류", "삭제 중 오류가 발생하였습니다. 잠시후 다시시도하십시오."); + }, + beforeSend: function () { + + }, + complete: function () { + + } + }); + }); +} + +/**************************************************************************** + * 검색 엔터 시술후기 + ****************************************************************************/ +function fn_procedureReviewEnter(e) { + if (e.which) { + if (13 == e.which) { + fn_procedureReviewSearch(); + } + } + else { + if (13 == e.keyCode) { + fn_procedureReviewSearch(); + } + } +} + +/**************************************************************************** + * 완료 + ****************************************************************************/ +function fn_procedureReviewOk() { + fn_procedureReviewReset(); +} + +/**************************************************************************** + * 등록 화면으로 이동. + ****************************************************************************/ +function fn_insertProcedureReviewIntro() { + if ("Y" == insertUseYn) { + let pagingParam = "?menuClass=" + menuClass; + fn_leftFormAction("/procedureReview/moveProcedureReviewInsert.do" + pagingParam); + } else { + modalEvent.warning("", "등록 권한이 없습니다."); + return false; + } + +} + +/**************************************************************************** + * 수정 화면으로 이동. + ****************************************************************************/ +function fn_updateProcedureReviewIntro(muProcedureReviewId) { + if ("Y" == updateUseYn) { + let pagingParam = "?menuClass=" + menuClass; + pagingParam += "&muProcedureReviewId=" + muProcedureReviewId; + fn_leftFormAction("/procedureReview/moveProcedureReviewUpdate.do" + pagingParam); + } else { + modalEvent.warning("", "수정 권한이 없습니다."); + return false; + } +} + + +let procedureReviewColumnDefs = [ + { field: "checkbox", headerName: "", minWidth: 55, maxWidth: 55, headerCheckboxSelection: true, checkboxSelection: true }, + { field: "rowNum", headerName: "번호", minWidth: 60, maxWidth: 60, sortable: false, cellStyle: { textAlign: 'center' } }, + { field: "title", headerName: "제목", minWidth: 150, cellStyle: { cursor: 'pointer', color: '#3985EA' } }, + { + field: "content", + headerName: "내용요약", + minWidth: 150, + valueFormatter: function (params) { + let val = params.value; + if (!val) return ""; + + try { + let delta = JSON.parse(val); + if (typeof delta === 'string') { + delta = JSON.parse(delta); + } + + if (delta && delta.ops) { + let text = ""; + delta.ops.forEach(function (op) { + if (op.insert) { + if (typeof op.insert === 'string') text += op.insert; + else if (op.insert.image) text += "[이미지] "; + } + }); + return text.replace(/\n/g, ' ').trim(); + } + } catch (e) { + // Fallback to strip HTML + } + return String(val).replace(/<[^>]*>?/gm, '').replace(/\n/g, ' ').trim(); + } + }, + { field: "hashtag", headerName: "해시태그", minWidth: 150 }, + { field: "writeDate", headerName: "등록일", minWidth: 100, maxWidth: 150 }, + { field: "writeName", headerName: "작성자", minWidth: 100, maxWidth: 150 }, +]; + +let procedureReviewGridOptions = { + suppressRowTransform: true, + columnDefs: procedureReviewColumnDefs, + defaultColDef: { + flex: 1, + sortable: true, + resizable: true, + editable: true, + cellStyle: { textAlign: 'left', fontSize: '14px', padding: '0' }, + enablePivot: true, + enableValue: true + }, + headerHeight: 41, + rowHeight: 41, + rowData: procedureReviewAgGridData, + suppressRowClickSelection: true, + localeText: { + noRowsToShow: '조회 결과가 없습니다.' + }, + rowSelection: 'multiple', + debug: false, + onCellClicked: function (event) { + if ('title' == event.column.colId) { + fn_updateProcedureReviewIntro(event.data.muProcedureReviewId); + } + }, + onSelectionChanged: function (event) { + let selectRows = []; + selectRows = event.api.getSelectedRows(); + + procedureReviewSelectId = ''; + for (let i = 0; i < selectRows.length; i++) { + procedureReviewSelectId += selectRows[i].muProcedureReviewId + ","; + } + procedureReviewSelectId = procedureReviewSelectId.substring(0, procedureReviewSelectId.lastIndexOf(',')); + }, + onSortChanged: function (event) { + procedureReviewSort = ''; + let columnArr = event.columnApi.getColumnState(); + if (0 < columnArr.length) { + columnArr.sort(function (a, b) { + return a.sortIndex - b.sortIndex; + }); + + let nullCnt = 0; + for (let i = 0; i < columnArr.length; i++) { + let gridSortModel = columnArr[i].colId; + let gridSort = columnArr[i].sort; + + if (gridSort != null) { + procedureReviewStart = 0; + procedureReviewSort += gridSortModel + ' ' + gridSort + ','; + } + else { + nullCnt++; + if (nullCnt == columnArr.length) { + procedureReviewSort = ''; + procedureReviewDir = ''; + procedureReviewStart = 0; + } + } + } + } + procedureReviewSort = procedureReviewSort.substring(0, procedureReviewSort.lastIndexOf(",")); + + fn_procedureReviewSearch(); + } +}; + +let procedureReviewGridDiv = document.querySelector('#procedureReviewGrid'); +new agGrid.Grid(procedureReviewGridDiv, procedureReviewGridOptions); + + +/**************************************************************************** + * 페이지 init + ****************************************************************************/ +function fn_pageInit() { + if (!procedureReviewSearchStartDate && !procedureReviewSearchEndDate) { + } else { + $("#procedureReviewSearchStartDate").val(procedureReviewSearchStartDate).trigger("change"); + $("#procedureReviewSearchEndDate").val(procedureReviewSearchEndDate).trigger("change"); + } + + $("#procedureReviewSearchKeyword0").val(procedureReviewSearchKeywordParam0); + $("#procedureReviewSearchKeyword1").val(procedureReviewSearchKeywordParam1); + $("#procedureReviewSearchKeyword2").val(procedureReviewSearchKeywordParam2); + $("#procedureReviewSearchKeyword3").val(procedureReviewSearchKeywordParam3); + + fn_procedureReviewSearch("A"); +} + + + +/**************************************************************************** + * 페이지 event + ****************************************************************************/ +function fn_pageProcedureReview() { + + $(document).on('keypress', '#procedureReviewSearchKeyword1', function (e) { + fn_procedureReviewEnter(e); + }); + + $(document).on('keypress', '#procedureReviewSearchKeyword3', function (e) { + fn_procedureReviewEnter(e); + }); + + $("#btnSearchProcedureReview").click(function () { + fn_procedureReviewSearch(); + }); + + $("#btnInsertProcedureReview").click(function () { + fn_insertProcedureReviewIntro(); + }); + + $("#btnDeleteProcedureReview").click(function () { + fn_deleteProcedureReview(); + }); + + +} + +$(function () { + fn_pageInit(); + fn_pageProcedureReview(); +}); diff --git a/src/main/resources/static/js/crm/procedureReview/procedureReviewUpdate.js b/src/main/resources/static/js/crm/procedureReview/procedureReviewUpdate.js new file mode 100644 index 0000000..2277d4d --- /dev/null +++ b/src/main/resources/static/js/crm/procedureReview/procedureReviewUpdate.js @@ -0,0 +1,421 @@ +/**************************************************************************** + * Quill 에디터 인스턴스 + ****************************************************************************/ +let quill; + +/**************************************************************************** + * Quill Image blot - data: URL (base64) 허용 + ****************************************************************************/ +const QuillImage = Quill.import('formats/image'); +const originalSanitize = QuillImage.sanitize; +QuillImage.sanitize = function (url) { + if (url && url.startsWith('data:')) { + return ''; + } + return originalSanitize.call(this, url); +}; + +/**************************************************************************** + * Quill 에디터 초기화 + ****************************************************************************/ +function fn_initQuillEditor() { + quill = new Quill('#quillEditor', { + theme: 'snow', + placeholder: '내용을 입력해주세요.', + modules: { + toolbar: { + container: [ + [{ 'header': [1, 2, 3, 4, 5, 6, false] }], + [{ 'font': [] }], + [{ 'size': ['small', false, 'large', 'huge'] }], + ['bold', 'italic', 'underline', 'strike'], + [{ 'color': [] }, { 'background': [] }], + [{ 'script': 'sub' }, { 'script': 'super' }], + [{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'list': 'check' }], + [{ 'indent': '-1' }, { 'indent': '+1' }], + [{ 'direction': 'rtl' }], + [{ 'align': [] }], + ['blockquote', 'code-block'], + ['link', 'image', 'video', 'formula'], + ['clean'] + ], + handlers: { + image: imageHandler + } + } + } + }); + + quill.root.addEventListener('paste', function (e) { + try { + console.log("[Paste Event] Triggered"); + let clipboardData = e.clipboardData || window.clipboardData || (e.originalEvent && e.originalEvent.clipboardData); + console.log("[Paste Event] clipboardData:", clipboardData); + if (!clipboardData || !clipboardData.items) { + console.warn("[Paste Event] No clipboardData or items found"); + return; + } + + let items = clipboardData.items; + console.log("[Paste Event] items length:", items.length); + for (let i = 0; i < items.length; i++) { + let item = items[i]; + console.log(`[Paste Event] item[${i}] kind: ${item.kind}, type: ${item.type}`); + if (item.kind === 'file' && item.type.match('^image/')) { + console.log(`[Paste Event] Image file detected. Prevent default.`); + e.preventDefault(); + e.stopPropagation(); + + let file = item.getAsFile(); + console.log(`[Paste Event] getAsFile() result:`, file); + if (file) { + if (!file.name || !file.name.includes('.')) { + let ext = 'png'; + if (file.type === 'image/jpeg') ext = 'jpg'; + else if (file.type === 'image/gif') ext = 'gif'; + file = new File([file], "clipboard_image." + ext, { type: file.type }); + console.log(`[Paste Event] Renamed missing file info:`, file.name); + } + console.log(`[Paste Event] Call fn_uploadImageAndInsertToQuill() for:`, file.name); + fn_uploadImageAndInsertToQuill(file); + } else { + console.error("[Paste Event] getAsFile() returned null."); + } + } + } + } catch (err) { + console.error("[Paste Event] Error parsing paste logic:", err); + } + }, true); + + quill.root.addEventListener('drop', function (e) { + try { + console.log("[Drop Event] Triggered"); + if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length) { + let hasImage = false; + console.log("[Drop Event] files length:", e.dataTransfer.files.length); + for (let i = 0; i < e.dataTransfer.files.length; i++) { + let file = e.dataTransfer.files[i]; + console.log(`[Drop Event] file[${i}] type: ${file.type}`); + if (file.type.match('^image/')) { + console.log(`[Drop Event] Image file detected. Prevent default.`); + hasImage = true; + e.preventDefault(); + e.stopPropagation(); + fn_uploadImageAndInsertToQuill(file); + } + } + } else { + console.log("[Drop Event] No files found in drop event"); + } + } catch (err) { + console.error("[Drop Event] Error parsing drop logic:", err); + } + }, true); +} +function imageHandler() { + const input = document.createElement('input'); + input.setAttribute('type', 'file'); + input.setAttribute('accept', 'image/*'); + input.click(); + + input.onchange = async () => { + const file = input.files[0]; + fn_uploadImageAndInsertToQuill(file); + }; +} + +function fn_uploadImageAndInsertToQuill(file) { + if (!file) { + console.error("[Upload] file is undefined or null"); + return; + } + console.log("[Upload] fn_uploadImageAndInsertToQuill Start. File name:", file.name, "size:", file.size); + + const formData = new FormData(); + formData.append('file', file); + formData.append("menuClass", menuClass); + + $.ajax({ + url: '/procedureReview/putProcedureReviewFile.do', + type: 'POST', + data: formData, + contentType: false, + processData: false, + dataType: 'json', + success: function (res) { + if (typeof res === 'string') { + try { res = JSON.parse(res); } catch (e) { } + } + console.log("[Upload] API Success response:", res); + if (res.msgCode == 0 || res.msgCode === '0') { + let range = quill.getSelection(true); + let index = range ? range.index : quill.getLength(); + console.log(`[Upload] Inserting image at index = ${index}, path = ${res.rows.filePath}`); + quill.insertEmbed(index, 'image', res.rows.filePath); + } else { + console.error("[Upload] API logic error msgDesc:", res.msgDesc); + modalEvent.danger("업로드 오류", res.msgDesc); + } + }, + error: function (xhr, status, error) { + console.error("[Upload] AJAX HTTP error. status:", status, "error:", error, "responseText:", xhr.responseText); + modalEvent.danger("업로드 오류", "이미지 업로드 중 오류가 발생했습니다."); + } + }); +} + +/**************************************************************************** + * Quill 콘텐츠를 Delta JSON 문자열로 반환 (저장용) + ****************************************************************************/ +function fn_getQuillContentForSave() { + return JSON.stringify(quill.getContents()); +} + +/**************************************************************************** + * 저장된 콘텐츠를 Quill에 로드 (Delta JSON / 레거시 HTML 호환) + ****************************************************************************/ +function fn_loadQuillContent(content) { + if (!content) return; + + if (typeof content === 'object') { + if (Array.isArray(content.ops)) quill.setContents(content.ops); + else if (Array.isArray(content)) quill.setContents(content); + return; + } + + try { + if (typeof content === 'string') { + // base64인지 판별해 디코딩 시도 (기존 데이터와 새 데이터 호환을 위해) + const isBase64 = /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$/.test(content); + if (isBase64 && content.length > 0 && !content.trim().startsWith('{') && !content.trim().startsWith('[')) { + try { + content = decodeURIComponent(escape(atob(content))); + } catch (e) { + // base64 디코딩 실패면 그냥 넘어감 + } + } + + if (content.trim().startsWith('{') || content.trim().startsWith('[')) { + let parsed = JSON.parse(content); + if (typeof parsed === 'string') { + parsed = JSON.parse(parsed); + } + + if (typeof parsed === 'object' && parsed !== null && Array.isArray(parsed.ops)) { + quill.setContents(parsed.ops); + return; + } else if (Array.isArray(parsed)) { + quill.setContents(parsed); + return; + } + } + } + } catch (e) { + console.warn("Quill Delta Parsing Failed - Fallback to HTML : ", e); + } + + quill.clipboard.dangerouslyPasteHTML(content); +} + +/**************************************************************************** + * 시술후기 상세 조회 + ****************************************************************************/ +function fn_selectProcedureReview() { + if (true != fn_emptyCheck(muProcedureReviewId)) { + modalEvent.warning("수정", "시술후기 정보가 없습니다."); + return; + } + let formData = new FormData(); + formData.append("menuClass", menuClass); + formData.append("muProcedureReviewId", muProcedureReviewId); + + $.ajax({ + url: encodeURI('/procedureReview/getProcedureReview.do'), + data: formData, + dataType: "json", + processData: false, + contentType: false, + type: 'POST', + async: true, + success: function (data) { + if ('0' == data.msgCode) { + if (data.rows && data.rows.length > 0) { + let photoData = data.rows[0]; + $("#title").val(photoData.title); + fn_loadQuillContent(photoData.content); + $("#hashtag").val(photoData.hashtag); + } else { + modalEvent.warning("수정", "조회된 시술후기 데이터가 없습니다."); + } + } else { + alert("수정 오류: " + data.msgDesc); + } + }, + error: function (xhr, status, error) { + alert("수정 오류: 수정 중 오류가 발생하였습니다. 잠시후 다시시도하십시오."); + } + }); +} + +/**************************************************************************** + * 시술후기 수정 + ****************************************************************************/ +function fn_updateProcedureReview() { + if ("Y" != updateUseYn) { + modalEvent.warning("", "수정 권한이 없습니다."); + return false; + } + let title = $("#title").val(); + let content = fn_getQuillContentForSave(); + let hashtag = $("#hashtag").val(); + + // Quill 에디터 빈 값 체크 + let quillText = quill.getText().trim(); + + if (true != fn_emptyCheck(title)) { + modalEvent.warning("수정", "제목을 입력하세요."); + return; + } + if (!quillText || quillText.length === 0) { + modalEvent.warning("수정", "내용을 입력하세요."); + return; + } + if (true != fn_emptyCheck(hashtag)) { + modalEvent.warning("수정", "해시태그를 입력하세요."); + return; + } + + modalEvent.info("수정", "시술후기 정보를 수정하시겠습니까?", function () { + let formData = new FormData(); + formData.append("menuClass", menuClass); + formData.append("title", title); + + let encodedContent = btoa(unescape(encodeURIComponent(content))); + formData.append("content", encodedContent); + formData.append("isEncoded", "Y"); // base64 인코딩 플래그 추가 + formData.append("hashtag", hashtag); + formData.append("muProcedureReviewId", muProcedureReviewId); + + $.ajax({ + url: encodeURI('/procedureReview/modProcedureReview.do'), + data: formData, + dataType: "json", + processData: false, + contentType: false, + type: 'POST', + async: true, + success: function (data) { + if ('0' == data.msgCode) { + modalEvent.success("수정 성공", data.msgDesc, function () { + fn_selectListProcedureReviewIntro(); + }); + } + else { + modalEvent.danger("수정 오류", data.msgDesc); + } + }, + error: function (xhr, status, error) { + modalEvent.danger("수정 오류", "수정 중 오류가 발생하였습니다. 잠시후 다시시도하십시오."); + } + }); + }); +} + +/**************************************************************************** + * 리스트 화면으로 이동 + ****************************************************************************/ +function fn_selectListProcedureReviewIntro() { + if ("Y" == selectUseYn) { + let pagingParam = "?menuClass=" + menuClass; + fn_leftFormAction("/procedureReview/moveProcedureReviewList.do" + pagingParam); + } else { + modalEvent.warning("", "조회 권한이 없습니다."); + return false; + } +} + +/**************************************************************************** + * 도움말 모달 + ****************************************************************************/ +function fn_initHelpModal() { + $('#btnHelp').on('click', function () { + $('#helpModal').addClass('active'); + }); + $('#helpModalClose').on('click', function () { + $('#helpModal').removeClass('active'); + }); + $('#helpModal').on('click', function (e) { + if (e.target === this) { + $(this).removeClass('active'); + } + }); +} + +/**************************************************************************** + * 미리보기 모달 + ****************************************************************************/ +function fn_initPreviewModal() { + $('#previewModalClose').on('click', function () { + $('#previewModal').removeClass('active'); + }); + $('#previewModal').on('click', function (e) { + if (e.target === this) { + $(this).removeClass('active'); + } + }); +} + +function fn_showPreview() { + let title = $("#title").val(); + let content = quill.root.innerHTML; + let hashtag = $("#hashtag").val(); + + // 미리보기 데이터 설정 + $('#previewTitle').text(title || '(제목 없음)'); + $('#previewContent').html(content); + + if (hashtag) { + let tags = hashtag.split(/\s+/).map(function (tag) { + tag = tag.trim(); + if (tag && !tag.startsWith('#')) tag = '#' + tag; + return tag; + }).filter(function (tag) { return tag; }); + $('#previewHashtag').html(tags.join('  ')); + $('#previewHashtagArea').show(); + } else { + $('#previewHashtagArea').hide(); + } + + $('#previewModal').addClass('active'); +} + +/**************************************************************************** + * 페이지 init + ****************************************************************************/ +function fn_pageInit() { + fn_initQuillEditor(); + fn_initHelpModal(); + fn_initPreviewModal(); + fn_selectProcedureReview(); +} + +/**************************************************************************** + * 페이지 event + ****************************************************************************/ +function fn_pageEvent() { + $('.btnCancle').on("click", function () { + fn_selectListProcedureReviewIntro(); + }); + $('.btnSave').on("click", function () { + fn_updateProcedureReview(); + }); + $('.btnPreview').on("click", function () { + fn_showPreview(); + }); +} + +$(function () { + fn_pageInit(); + fn_pageEvent(); +}); diff --git a/src/main/resources/templates/crm/procedureReview/procedureReviewInsert.html b/src/main/resources/templates/crm/procedureReview/procedureReviewInsert.html new file mode 100644 index 0000000..d4b8ddd --- /dev/null +++ b/src/main/resources/templates/crm/procedureReview/procedureReviewInsert.html @@ -0,0 +1,477 @@ + + + + + + + + + + + + +
+

시술후기 등록

+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+ + + +
+
+
+ + +
+
+
+

👁 미리보기

+ +
+
+ +
+

제목

+

+
+ +
+
+
+ +
+ +
+
+
+
+ + +
+
+
+

📝 에디터 사용 가이드

+ +
+
+

📌 텍스트 서식

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
기능설명단축키
B 굵게선택한 텍스트를 굵게 표시Ctrl+B
I 기울임선택한 텍스트를 기울임꼴로 표시Ctrl+I
U 밑줄선택한 텍스트에 밑줄 적용Ctrl+U
S 취소선선택한 텍스트에 취소선 적용-
+ +

🎨 글자 스타일

+ + + + + + + + + + + + + + + + + + + + + +
기능설명
헤더 (H1~H6)제목 크기를 설정합니다. 숫자가 작을수록 큰 제목
폰트글꼴을 변경합니다
글자 크기Small, Normal, Large, Huge 중 선택
글자색 / 배경색텍스트 색상 또는 배경 하이라이트 색상 선택
+ +

📋 문단 서식

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
기능설명
순서 목록1. 2. 3. 형태의 번호 목록
비순서 목록• 형태의 글머리 기호 목록
체크 목록☐ 형태의 체크박스 목록
들여쓰기텍스트를 안쪽/바깥쪽으로 이동
정렬좌측, 중앙, 우측, 양쪽 정렬
인용문인용 블록으로 표시
코드 블록코드 형태로 표시
+ +

🔗 멀티미디어

+ + + + + + + + + + + + + + + + + +
기능설명
링크선택한 텍스트에 URL 링크 삽입
이미지이미지 파일을 선택하여 삽입 (서버에 자동 업로드)
동영상YouTube 등 동영상 URL을 삽입
+ +

🧹 기타

+ + + + + + + + + + + + + +
기능설명
서식 지우기선택한 텍스트의 모든 서식을 초기화
위첨자 / 아래첨자X² 또는 H₂O 같은 수식 표현
+ +
+ 💡 TIP
+ • 텍스트를 드래그한 후 툴바 버튼을 클릭하면 선택 영역에만 서식이 적용됩니다.
+ • 이미지는 클립보드에서 Ctrl+V로 붙여넣기도 가능합니다.
+ • 실행 취소는 Ctrl+Z, 다시 실행은 Ctrl+Y 입니다. +
+
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/crm/procedureReview/procedureReviewSelectList.html b/src/main/resources/templates/crm/procedureReview/procedureReviewSelectList.html new file mode 100644 index 0000000..2b19b66 --- /dev/null +++ b/src/main/resources/templates/crm/procedureReview/procedureReviewSelectList.html @@ -0,0 +1,85 @@ + + + + + + + + + + + + +
+

시술후기

+ +
+
+ + +
+ + +
+ +
+ + +
+
+
+ +
+ + +
+ +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/crm/procedureReview/procedureReviewUpdate.html b/src/main/resources/templates/crm/procedureReview/procedureReviewUpdate.html new file mode 100644 index 0000000..a004bbe --- /dev/null +++ b/src/main/resources/templates/crm/procedureReview/procedureReviewUpdate.html @@ -0,0 +1,479 @@ + + + + + + + + + + + + +
+

시술후기 수정

+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+ + + +
+
+
+ + +
+
+
+

👁 미리보기

+ +
+
+ +
+

제목

+

+
+ +
+
+
+ +
+ +
+
+
+
+ + +
+
+
+

📝 에디터 사용 가이드

+ +
+
+

📌 텍스트 서식

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
기능설명단축키
B 굵게선택한 텍스트를 굵게 표시Ctrl+B
I 기울임선택한 텍스트를 기울임꼴로 표시Ctrl+I
U 밑줄선택한 텍스트에 밑줄 적용Ctrl+U
S 취소선선택한 텍스트에 취소선 적용-
+ +

🎨 글자 스타일

+ + + + + + + + + + + + + + + + + + + + + +
기능설명
헤더 (H1~H6)제목 크기를 설정합니다. 숫자가 작을수록 큰 제목
폰트글꼴을 변경합니다
글자 크기Small, Normal, Large, Huge 중 선택
글자색 / 배경색텍스트 색상 또는 배경 하이라이트 색상 선택
+ +

📋 문단 서식

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
기능설명
순서 목록1. 2. 3. 형태의 번호 목록
비순서 목록• 형태의 글머리 기호 목록
체크 목록☐ 형태의 체크박스 목록
들여쓰기텍스트를 안쪽/바깥쪽으로 이동
정렬좌측, 중앙, 우측, 양쪽 정렬
인용문인용 블록으로 표시
코드 블록코드 형태로 표시
+ +

🔗 멀티미디어

+ + + + + + + + + + + + + + + + + +
기능설명
링크선택한 텍스트에 URL 링크 삽입
이미지이미지 파일을 선택하여 삽입 (서버에 자동 업로드)
동영상YouTube 등 동영상 URL을 삽입
+ +

🧹 기타

+ + + + + + + + + + + + + +
기능설명
서식 지우기선택한 텍스트의 모든 서식을 초기화
위첨자 / 아래첨자X² 또는 H₂O 같은 수식 표현
+ +
+ 💡 TIP
+ • 텍스트를 드래그한 후 툴바 버튼을 클릭하면 선택 영역에만 서식이 적용됩니다.
+ • 이미지는 클립보드에서 Ctrl+V로 붙여넣기도 가능합니다.
+ • 실행 취소는 Ctrl+Z, 다시 실행은 Ctrl+Y 입니다. +
+
+
+
+
+ + + + + + + + \ No newline at end of file