시술후기 구현

This commit is contained in:
pjs
2026-02-28 19:15:32 +09:00
parent 1b4e1d78de
commit 5009601633
17 changed files with 4220 additions and 2 deletions

View File

@@ -0,0 +1,42 @@
# 메이드유 CRM 작업내역 — 2026년 2월 25일
### 1. 코드 관리 기능 추가
- CRM에서 사용하는 **공통코드**(단위, 분류 등)를 조회·등록·수정·삭제할 수 있는 **코드 관리 화면**이 새로 추가되었습니다.
### 2. 진료유형 설정 화면 수정
- 진료유형 설정의 각 단계(1~5뎁스) 팝업 화면이 수정되었습니다.
- 진료유형 목록 화면의 기능이 개선되었습니다.
---
## 직접 작업한 내역
### 1. 사용약품 검색 및 등록 기능
- 용량/출력(4뎁스) 카테고리에서 **사용 약품을 검색하여 등록**할 수 있는 기능을 새로 만들었습니다.
- "약품 추가(검색)" 버튼을 누르면 **검색 팝업**이 열리고, 제품명으로 검색할 수 있습니다.
- 검색 결과에서 **제품명을 클릭**하면 해당 약품이 바로 등록됩니다.
### 2. 사용약품 관리 표(그리드) 구성
- 등록된 약품들이 아래 항목으로 구성된 표에 표시됩니다.
| 항목 | 설명 |
|------|------|
| 거래처 | 약품을 공급하는 거래처 이름 |
| 약품명 | 등록된 약품 이름 |
| 재고 | 현재 남아있는 재고 수량 |
| 재고단위 | 재고의 단위 (ml, cc 등) |
| **사용량** | 1회 시술 시 사용하는 양 (클릭하여 수정 가능) |
| **사용단위** | 사용량의 단위 (클릭하여 선택 가능) |
| 삭제 | 약품을 목록에서 제거 |
- **사용량**과 **사용단위**는 표에서 바로 클릭하여 수정할 수 있으며, 수정 즉시 자동 저장됩니다.
- 사용단위는 드롭다운 목록에서 선택하는 방식이며, 기본값은 "선택"입니다.
- 수정 가능한 칸은 연한 파란색 배경으로 표시되어 있습니다.
### 3. 신규 등록 화면에서도 약품관리 표시
- 기존에는 수정할 때만 약품관리가 보였으나, 이제 **새로 등록하는 화면에서도** 약품관리 영역이 보입니다.
- 단, 카테고리를 먼저 저장한 뒤에 약품을 추가할 수 있으며, 저장 전에는 안내 문구가 표시됩니다.
### 4. 약품 데이터 저장 구조 신규 생성
- 시술별 사용 약품 정보를 저장하기 위한 **데이터베이스 테이블을 새로 생성**하였습니다.
- 약품명, 거래처, 사용량, 단위, 단가 등의 정보가 저장됩니다.

36
rules.md Normal file
View File

@@ -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`

View File

@@ -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);

View File

@@ -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<String, Object> paramMap = HttpUtil.getParameterMap(request);
HashMap<String, Object> map = new HashMap<String, Object>();
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<String, Object> paramMap = HttpUtil.getParameterMap(request);
HashMap<String, Object> map = new HashMap<String, Object>();
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<String, Object> paramMap = HttpUtil.getParameterMap(request);
HashMap<String, Object> map = new HashMap<String, Object>();
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<String, Object> paramMap = HttpUtil.getParameterMap(request);
HashMap<String, Object> map = new HashMap<String, Object>();
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<String, Object> paramMap = HttpUtil.getParameterMap(request);
HashMap<String, Object> map = new HashMap<String, Object>();
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<String, Object> visitLogParamMap = RequestLogUtil.getVisitLogParameterMap(request);
HashMap<String, Object> insertMap = new HashMap<String, Object>();
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<String, Object> paramMap = HttpUtil.getParameterMap(request);
HashMap<String, Object> map = new HashMap<String, Object>();
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<String, Object> paramMap = HttpUtil.getParameterMap(request);
HashMap<String, Object> map = new HashMap<String, Object>();
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<String, Object> visitLogParamMap = RequestLogUtil.getVisitLogParameterMap(request);
HashMap<String, Object> insertMap = new HashMap<String, Object>();
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<String, Object> paramMap = HttpUtil.getParameterMap(request);
HashMap<String, Object> map = new HashMap<String, Object>();
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<String, Object> paramMap = HttpUtil.getParameterMap(request);
HashMap<String, Object> map = new HashMap<String, Object>();
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<String, Object> visitLogParamMap = RequestLogUtil.getVisitLogParameterMap(request);
HashMap<String, Object> insertMap = new HashMap<String, Object>();
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<String, Object> paramMap = HttpUtil.getParameterMap(request);
HashMap<String, Object> map = new HashMap<String, Object>();
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<String, Object> visitLogParamMap = RequestLogUtil.getVisitLogParameterMap(request);
HashMap<String, Object> insertMap = new HashMap<String, Object>();
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);
}
}

View File

@@ -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.
}

View File

@@ -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<Map<String, Object>> selectTotalProcedureReviewCount(HashMap<String, Object> paramMap)
throws DataAccessException {
logger.debug("ProcedureReviewMapper selectTotalProcedureReviewCount START");
String sqlId = "ProcedureReview.selectTotalProcedureReviewCount";
logger.debug("ProcedureReviewMapper selectTotalProcedureReviewCount END");
return getSqlSession().selectList(sqlId, paramMap);
}
public List<Map<String, Object>> selectListProcedureReview(HashMap<String, Object> paramMap)
throws DataAccessException {
logger.debug("ProcedureReviewMapper selectListProcedureReview START");
String sqlId = "ProcedureReview.selectListProcedureReview";
logger.debug("ProcedureReviewMapper selectListProcedureReview END");
return getSqlSession().selectList(sqlId, paramMap);
}
public List<Map<String, Object>> selectProcedureReview(HashMap<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> paramMap)
throws DataAccessException {
logger.debug("ProcedureReviewMapper deleteProcedureReview START");
String sqlId = "ProcedureReview.deleteProcedureReview";
logger.debug("ProcedureReviewMapper deleteProcedureReview END");
return getSqlSession().update(sqlId, paramMap);
}
public List<Map<String, Object>> selectListPhotoCategory(HashMap<String, Object> 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<String, Object> 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<String, Object> paramMap)
throws DataAccessException {
logger.debug("ProcedureReviewMapper insertProcedureReviewAfterFile START");
String sqlId = "ProcedureReview.insertProcedureReviewAfterFile";
logger.debug("ProcedureReviewMapper insertProcedureReviewAfterFile END");
return getSqlSession().insert(sqlId, paramMap);
}
}

View File

@@ -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<String, Object> selectProcedureReviewListIntro(
HashMap<String, Object> paramMap) throws Exception {
log.debug("ProcedureReviewService selectProcedureReviewListIntro START");
HashMap<String, Object> map = new HashMap<String, Object>();
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<Map<String, Object>> 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<String, Object> authCheckParamMap = new HashMap<String, Object>();
authCheckParamMap.put("menuClass", paramMap.get("menuClass"));
authCheckParamMap.put("muAuthId", paramMap.get("menuClassAuthId"));
List<Map<String, Object>> 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<String, Object> selectListProcedureReview(
HashMap<String, Object> paramMap) throws Exception {
log.debug("ProcedureReviewService selectListProcedureReview START");
HashMap<String, Object> map = new HashMap<String, Object>();
List<Map<String, Object>> listMap = new ArrayList<Map<String, Object>>();
try {
boolean check = true;
if (true == check) {
List<Map<String, Object>> 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<String, Object> authCheckParamMap = new HashMap<String, Object>();
authCheckParamMap.put("menuClass", paramMap.get("menuClass"));
authCheckParamMap.put("muAuthId", paramMap.get("menuClassAuthId"));
List<Map<String, Object>> 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<Map<String, Object>> 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<String, Object> selectProcedureReview(
HashMap<String, Object> paramMap) throws Exception {
log.debug("ProcedureReviewService selectProcedureReview START");
HashMap<String, Object> map = new HashMap<String, Object>();
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<Map<String, Object>> 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<String, Object> authCheckParamMap = new HashMap<String, Object>();
authCheckParamMap.put("menuClass", paramMap.get("menuClass"));
authCheckParamMap.put("muAuthId", paramMap.get("menuClassAuthId"));
List<Map<String, Object>> 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<Map<String, Object>> 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<String, Object> insertProcedureReviewFile(
HashMap<String, Object> paramMap, MultipartFile file) throws Exception {
log.debug("ProcedureReviewService insertProcedureReviewFile START");
HashMap<String, Object> map = new HashMap<String, Object>();
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<Map<String, Object>> 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<String, Object> authCheckParamMap = new HashMap<String, Object>();
authCheckParamMap.put("menuClass", paramMap.get("menuClass"));
authCheckParamMap.put("muAuthId", paramMap.get("menuClassAuthId"));
List<Map<String, Object>> 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<String, Object> 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<String, Object> selectListCategory(HashMap<String, Object> paramMap) throws Exception {
HashMap<String, Object> map = new HashMap<String, Object>();
paramMap.put("categoryDivCd", "07");
List<Map<String, Object>> listMap = procedureReviewMapper.selectListPhotoCategory(paramMap);
map.put("msgCode", Constants.OK);
map.put("rows", listMap);
return map;
}
public HashMap<String, Object> selectProcedureReviewInsertIntro(
HashMap<String, Object> paramMap) throws Exception {
log.debug("ProcedureReviewService selectProcedureReviewInsertIntro START");
HashMap<String, Object> map = new HashMap<String, Object>();
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<Map<String, Object>> 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<String, Object> authCheckParamMap = new HashMap<String, Object>();
authCheckParamMap.put("menuClass", paramMap.get("menuClass"));
authCheckParamMap.put("muAuthId", paramMap.get("menuClassAuthId"));
List<Map<String, Object>> 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<Map<String, Object>> 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<String, Object> insertProcedureReview(
HashMap<String, Object> paramMap) throws Exception {
log.debug("ProcedureReviewService insertProcedureReview START");
HashMap<String, Object> map = new HashMap<String, Object>();
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<Map<String, Object>> 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<String, Object> authCheckParamMap = new HashMap<String, Object>();
authCheckParamMap.put("menuClass", paramMap.get("menuClass"));
authCheckParamMap.put("muAuthId", paramMap.get("menuClassAuthId"));
List<Map<String, Object>> 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<String, Object> selectProcedureReviewUpdateIntro(
HashMap<String, Object> paramMap) throws Exception {
log.debug("ProcedureReviewService selectProcedureReviewUpdateIntro START");
HashMap<String, Object> map = new HashMap<String, Object>();
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<Map<String, Object>> 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<String, Object> authCheckParamMap = new HashMap<String, Object>();
authCheckParamMap.put("menuClass", paramMap.get("menuClass"));
authCheckParamMap.put("muAuthId", paramMap.get("menuClassAuthId"));
List<Map<String, Object>> 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<Map<String, Object>> 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<String, Object> updateProcedureReview(
HashMap<String, Object> paramMap) throws Exception {
log.debug("ProcedureReviewService updateProcedureReview START");
HashMap<String, Object> map = new HashMap<String, Object>();
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<Map<String, Object>> 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<String, Object> authCheckParamMap = new HashMap<String, Object>();
authCheckParamMap.put("menuClass", paramMap.get("menuClass"));
authCheckParamMap.put("muAuthId", paramMap.get("menuClassAuthId"));
List<Map<String, Object>> 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<String, Object> deleteProcedureReview(
HashMap<String, Object> paramMap) throws Exception {
log.debug("ProcedureReviewService deleteProcedureReview START");
HashMap<String, Object> map = new HashMap<String, Object>();
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<Map<String, Object>> 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<String, Object> authCheckParamMap = new HashMap<String, Object>();
authCheckParamMap.put("menuClass", paramMap.get("menuClass"));
authCheckParamMap.put("muAuthId", paramMap.get("menuClassAuthId"));
List<Map<String, Object>> 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;
}
}

View File

@@ -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

View File

@@ -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:

View File

@@ -0,0 +1,209 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="ProcedureReview">
<select id="selectTotalProcedureReviewCount" parameterType="hashmap" resultType="hashmap">
SELECT COUNT(*) AS "totalCount"
FROM MU_PROCEDURE_REVIEW MPR
WHERE MPR.USE_YN = 'Y'
<if test="procedureReviewSearchKeywordParam1 != null and procedureReviewSearchKeywordParam1 != ''">
AND MPR.TITLE LIKE CONCAT('%', TRIM(#{procedureReviewSearchKeywordParam1}), '%')
</if>
<if test="procedureReviewSearchKeywordParam2 != null and procedureReviewSearchKeywordParam2 != ''">
AND IFNULL((SELECT MM.NAME FROM MU_MEMBER AS MM
WHERE MM.USE_YN = 'Y'
AND MM.MU_MEMBER_ID = MPR.REG_ID
LIMIT 0, 1
),'') LIKE CONCAT('%', TRIM(#{procedureReviewSearchKeywordParam2}), '%')
</if>
</select>
<select id="selectListProcedureReview" parameterType="hashmap" resultType="hashmap">
SELECT MPR.*
FROM (
SELECT MPR.*
,CAST(@RNUM:=@RNUM + 1 AS CHAR) AS "rowNum"
FROM (
SELECT MPR.MU_PROCEDURE_REVIEW_ID AS "muProcedureReviewId"
,MPR.TITLE AS "title"
,MPR.CONTENT AS "content"
,MPR.HASHTAG AS "hashtag"
,MPR.VIEW_COUNT AS "viewCount"
,DATE_FORMAT(MPR.REG_DATE, '%Y-%m-%d') AS "writeDate"
,IFNULL((SELECT MM.NAME
FROM MU_MEMBER AS MM
WHERE MM.USE_YN = 'Y'
AND MM.MU_MEMBER_ID = MPR.REG_ID
LIMIT 0, 1
),'') AS "writeName"
FROM MU_PROCEDURE_REVIEW MPR
WHERE MPR.USE_YN = 'Y'
<if test="procedureReviewSearchKeywordParam1 != null and procedureReviewSearchKeywordParam1 != ''">
AND MPR.TITLE LIKE CONCAT('%', TRIM(#{procedureReviewSearchKeywordParam1}), '%')
</if>
<if test="procedureReviewSearchKeywordParam2 != null and procedureReviewSearchKeywordParam2 != ''">
AND IFNULL((SELECT MM.NAME FROM MU_MEMBER AS MM
WHERE MM.USE_YN = 'Y'
AND MM.MU_MEMBER_ID = MPR.REG_ID
LIMIT 0, 1
),'') LIKE CONCAT('%', TRIM(#{procedureReviewSearchKeywordParam2}), '%')
</if>
<choose>
<when test="procedureReviewSort != null and procedureReviewSort != ''">
ORDER BY ${procedureReviewSort}
</when>
<otherwise>
ORDER BY MPR.REG_DATE DESC
</otherwise>
</choose>
LIMIT 18446744073709551615
) MPR, (SELECT @RNUM:=0) R
WHERE 1 = 1
) MPR
WHERE 1 = 1
LIMIT ${procedureReviewStart}, ${procedureReviewLimit}
</select>
<select id="selectProcedureReview" parameterType="hashmap" resultType="hashmap">
SELECT MPR.MU_PROCEDURE_REVIEW_ID AS "muProcedureReviewId"
,MPR.CATEGORY_NO AS "categoryno"
,MPR.TITLE AS "title"
,MPR.CONTENT AS "content"
,MPR.HASHTAG AS "hashtag"
,HAF.FILE_PATH AS "beforefile"
,HAF2.FILE_PATH AS "afterfile"
FROM MU_PROCEDURE_REVIEW MPR
LEFT OUTER JOIN HP_ATTACH_FILE HAF ON HAF.ATTACHFILE_ID = MPR.BEFORE_PHOTO_ATTACHFILE_ID
LEFT OUTER JOIN HP_ATTACH_FILE HAF2 ON HAF2.ATTACHFILE_ID = MPR.AFTER_PHOTO_ATTACHFILE_ID
WHERE MPR.USE_YN = 'Y'
AND MPR.MU_PROCEDURE_REVIEW_ID = #{muProcedureReviewId}
LIMIT 0, 1
</select>
<insert id="insertProcedureReview" parameterType="hashmap">
<selectKey resultType="string" keyProperty="id" order="BEFORE">
SELECT CONCAT('PR-', LPAD(NEXTVAL(MU_PROCEDURE_REVIEW_SEQ), 21, '0'))
</selectKey>
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()
)
</insert>
<insert id="insertProcedureReviewBeforeFile" parameterType="hashmap">
<selectKey resultType="string" keyProperty="beforeId" order="BEFORE">
SELECT CONCAT('ATF-', LPAD(NEXTVAL(seq_attachfile_id), 21, '0'))
</selectKey>
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()
)
</insert>
<insert id="insertProcedureReviewAfterFile" parameterType="hashmap">
<selectKey resultType="string" keyProperty="afterId" order="BEFORE">
SELECT CONCAT('ATF-', LPAD(NEXTVAL(seq_attachfile_id), 21, '0'))
</selectKey>
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()
)
</insert>
<update id="updateProcedureReview" parameterType="hashmap">
UPDATE MU_PROCEDURE_REVIEW
SET TITLE = #{title}
,CONTENT = #{content}
,HASHTAG = #{hashtag}
<if test="beforeId != null and beforeId != ''">
,BEFORE_PHOTO_ATTACHFILE_ID = #{beforeId}
</if>
<if test="afterId != null and afterId != ''">
,AFTER_PHOTO_ATTACHFILE_ID = #{afterId}
</if>
,MOD_ID = #{modId}
,MOD_DATE = NOW()
WHERE USE_YN = 'Y'
AND MU_PROCEDURE_REVIEW_ID = #{muProcedureReviewId}
</update>
<update id="deleteProcedureReview" parameterType="hashmap">
UPDATE MU_PROCEDURE_REVIEW
SET MOD_ID = #{modId}
,MOD_DATE = NOW()
,USE_YN = 'N'
WHERE USE_YN = 'Y'
AND MU_PROCEDURE_REVIEW_ID = #{muProcedureReviewId}
</update>
<select id="selectListPhotoCategory" parameterType="hashmap" resultType="hashmap">
SELECT ROW_NUMBER() OVER (ORDER BY HC.REG_DATE DESC) AS "rowNum"
,HC.CATEGORY_NO AS "categoryNo"
,HC.CATEGORY_NM AS "categoryNm"
FROM HP_CATEGORY AS HC
WHERE HC.USE_YN = 'Y'
AND HC.CATEGORY_DIV_CD = #{categoryDivCd}
ORDER BY HC.CATEGORY_NO ASC
</select>
</mapper>

View File

@@ -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;
}
}

View File

@@ -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('&nbsp;&nbsp;'));
$('#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();
});

View File

@@ -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: '<img src="/image/web/page_navigation_arrow.svg" alt="prev"/>',
next: '<img src="/image/web/page_navigation_arrow.svg" alt="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();
});

View File

@@ -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('&nbsp;&nbsp;'));
$('#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();
});

View File

@@ -0,0 +1,477 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{/web/layout/homeLayout}">
<th:block layout:fragment="layout_css">
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.snow.css" rel="stylesheet">
<link rel="stylesheet" href="/css/web/ContentsBbsUpd.css">
<link rel="stylesheet" href="/css/web/grid.css?v1.1">
<style>
/* Quill 에디터 스타일 보정 */
.ql-toolbar.ql-snow {
border: 1px solid #E9ECF0;
border-radius: 5px 5px 0 0;
background: #F9FAFB;
}
.ql-container.ql-snow {
border: 1px solid #E9ECF0;
border-top: none;
border-radius: 0 0 5px 5px;
min-height: 350px;
max-height: 600px;
overflow-y: auto;
font-size: 14px;
}
.ql-editor {
min-height: 340px;
line-height: 1.7;
}
.ql-editor.ql-blank::before {
color: #B5BDC4;
font-style: normal;
}
/* 페이지 스크롤 활성화 오버라이드 */
.center_box {
overflow-y: auto !important;
}
.project_wrap .content_section .hospital_wrap .center_box .content_box {
background: none !important;
border: none !important;
}
/* 단일 폼 레이아웃 */
.pr-single-form {
width: 100%;
background: #fff;
border: 1px solid #E9ECF0;
border-radius: 5px;
padding: 24px;
}
.pr-single-form .form-grid-row {
display: flex;
gap: 16px;
margin-bottom: 16px;
}
.pr-single-form .form-group {
flex: 1;
display: flex;
flex-direction: column;
}
.pr-single-form .form-group label {
font-size: 13px;
font-weight: 600;
color: #374151;
margin-bottom: 6px;
}
.pr-single-form .form-group input[type="text"] {
height: 38px;
padding: 0 12px;
border: 1px solid #E9ECF0;
border-radius: 5px;
font-size: 14px;
outline: none;
}
.pr-single-form .form-group input[type="text"]:focus {
border-color: #3985EA;
box-shadow: 0 0 0 2px rgba(57, 133, 234, 0.1);
}
/* 내용 헤더 (라벨 + 도움말 버튼) */
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.content-header label {
font-size: 13px;
font-weight: 600;
color: #374151;
margin-bottom: 0 !important;
}
.btn-help {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
font-size: 12px;
font-weight: 600;
color: #3985EA;
background: #EFF6FF;
border: 1px solid #BFDBFE;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s;
}
.btn-help:hover {
background: #DBEAFE;
border-color: #93C5FD;
}
/* 액션 버튼 */
.pr-actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
/* Quill 에디터 및 미리보기 이미지 크기 제한 */
.ql-editor img,
#previewContent img {
max-width: 100%;
height: auto;
}
/* 도움말 모달 */
.help-modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
justify-content: center;
align-items: center;
}
.help-modal-overlay.active {
display: flex;
}
.help-modal {
background: #fff;
border-radius: 10px;
width: 640px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
}
.help-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #E5E7EB;
}
.help-modal-header h3 {
font-size: 16px;
font-weight: 700;
color: #1F2937;
margin: 0;
}
.help-modal-close {
width: 32px;
height: 32px;
border: none;
background: #F3F4F6;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
color: #6B7280;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.help-modal-close:hover {
background: #E5E7EB;
color: #374151;
}
.help-modal-body {
padding: 24px;
font-size: 13px;
color: #4B5563;
line-height: 1.8;
}
.help-modal-body h4 {
font-size: 14px;
font-weight: 700;
color: #1F2937;
margin: 20px 0 8px 0;
}
.help-modal-body h4:first-child {
margin-top: 0;
}
.help-modal-body table {
width: 100%;
border-collapse: collapse;
margin: 8px 0 16px 0;
}
.help-modal-body table th {
background: #F9FAFB;
padding: 8px 12px;
text-align: left;
font-weight: 600;
border: 1px solid #E5E7EB;
font-size: 12px;
}
.help-modal-body table td {
padding: 8px 12px;
border: 1px solid #E5E7EB;
font-size: 12px;
}
.help-modal-body .tip-box {
background: #F0F9FF;
border: 1px solid #BAE6FD;
border-radius: 6px;
padding: 12px 16px;
margin-top: 16px;
}
.help-modal-body .tip-box strong {
color: #0284C7;
}
</style>
</th:block>
<th:block layout:fragment="layout_top_script">
<script>
let menuClass = "[[${param.menuClass}]]" == "" ? "" : "[[${param.menuClass}]]";
let selectUseYn = "[[${selectUseYn}]]" == "" ? "N" : "[[${selectUseYn}]]";
let insertUseYn = "[[${insertUseYn}]]" == "" ? "N" : "[[${insertUseYn}]]";
</script>
</th:block>
<th:block layout:fragment="layout_content">
<div class="center_box">
<p class="page_title" style="float:none; display:block; width:100%;">시술후기 등록</p>
<div class="pr-single-form">
<!-- 제목 -->
<div class="form-grid-row">
<div class="form-group">
<label>제목</label>
<input type="text" id="title" placeholder="제목을 입력해주세요." />
</div>
</div>
<!-- 내용 (Quill 에디터) -->
<div class="form-group" style="margin-bottom: 16px;">
<div class="content-header">
<label>내용</label>
<button type="button" class="btn-help" id="btnHelp">? 도움말</button>
</div>
<div id="quillEditor"></div>
</div>
<!-- 해시태그 -->
<div class="form-grid-row">
<div class="form-group">
<label>해시태그</label>
<input type="text" id="hashtag" placeholder="#해시태그를 입력해주세요." />
</div>
</div>
<!-- 버튼 -->
<div class="pr-actions">
<button class="preview_btn btnPreview"
style="width: 90px; height: 36px; border-radius: 4px; background: #fff; color: #3985EA; border: 1px solid #3985EA; font-weight: 600; cursor: pointer;">미리보기</button>
<button class="registration_btn btnSave"
style="width: 80px; height: 36px; border-radius: 4px;">등록</button>
<button class="cancel_btn btnCancle"
style="width: 80px; height: 36px; border-radius: 4px; margin-left: 10px;">취소</button>
</div>
</div>
</div>
<!-- 미리보기 모달 -->
<div class="help-modal-overlay" id="previewModal">
<div class="help-modal" style="width: 800px; max-height: 90vh;">
<div class="help-modal-header">
<h3>👁 미리보기</h3>
<button class="help-modal-close" id="previewModalClose"></button>
</div>
<div class="help-modal-body" style="padding: 0;">
<!-- 제목 영역 -->
<div style="padding: 20px 24px; border-bottom: 1px solid #E5E7EB;">
<p style="font-size: 11px; color: #9CA3AF; margin-bottom: 4px;">제목</p>
<h2 id="previewTitle" style="font-size: 20px; font-weight: 700; color: #1F2937; margin: 0;"></h2>
</div>
<!-- 내용 영역 -->
<div style="padding: 24px;">
<div id="previewContent" class="ql-editor"
style="min-height: 200px; max-height: 500px; overflow-y: auto; padding: 0;"></div>
</div>
<!-- 해시태그 영역 -->
<div id="previewHashtagArea" style="padding: 12px 24px 20px; border-top: 1px solid #E5E7EB;">
<span id="previewHashtag"
style="display: inline-block; padding: 4px 12px; background: #EFF6FF; color: #3985EA; border-radius: 20px; font-size: 13px; font-weight: 500;"></span>
</div>
</div>
</div>
</div>
<!-- 도움말 모달 -->
<div class="help-modal-overlay" id="helpModal">
<div class="help-modal">
<div class="help-modal-header">
<h3>📝 에디터 사용 가이드</h3>
<button class="help-modal-close" id="helpModalClose"></button>
</div>
<div class="help-modal-body">
<h4>📌 텍스트 서식</h4>
<table>
<tr>
<th>기능</th>
<th>설명</th>
<th>단축키</th>
</tr>
<tr>
<td><strong>B</strong> 굵게</td>
<td>선택한 텍스트를 굵게 표시</td>
<td>Ctrl+B</td>
</tr>
<tr>
<td><em>I</em> 기울임</td>
<td>선택한 텍스트를 기울임꼴로 표시</td>
<td>Ctrl+I</td>
</tr>
<tr>
<td><u>U</u> 밑줄</td>
<td>선택한 텍스트에 밑줄 적용</td>
<td>Ctrl+U</td>
</tr>
<tr>
<td><s>S</s> 취소선</td>
<td>선택한 텍스트에 취소선 적용</td>
<td>-</td>
</tr>
</table>
<h4>🎨 글자 스타일</h4>
<table>
<tr>
<th>기능</th>
<th>설명</th>
</tr>
<tr>
<td>헤더 (H1~H6)</td>
<td>제목 크기를 설정합니다. 숫자가 작을수록 큰 제목</td>
</tr>
<tr>
<td>폰트</td>
<td>글꼴을 변경합니다</td>
</tr>
<tr>
<td>글자 크기</td>
<td>Small, Normal, Large, Huge 중 선택</td>
</tr>
<tr>
<td>글자색 / 배경색</td>
<td>텍스트 색상 또는 배경 하이라이트 색상 선택</td>
</tr>
</table>
<h4>📋 문단 서식</h4>
<table>
<tr>
<th>기능</th>
<th>설명</th>
</tr>
<tr>
<td>순서 목록</td>
<td>1. 2. 3. 형태의 번호 목록</td>
</tr>
<tr>
<td>비순서 목록</td>
<td>• 형태의 글머리 기호 목록</td>
</tr>
<tr>
<td>체크 목록</td>
<td>☐ 형태의 체크박스 목록</td>
</tr>
<tr>
<td>들여쓰기</td>
<td>텍스트를 안쪽/바깥쪽으로 이동</td>
</tr>
<tr>
<td>정렬</td>
<td>좌측, 중앙, 우측, 양쪽 정렬</td>
</tr>
<tr>
<td>인용문</td>
<td>인용 블록으로 표시</td>
</tr>
<tr>
<td>코드 블록</td>
<td>코드 형태로 표시</td>
</tr>
</table>
<h4>🔗 멀티미디어</h4>
<table>
<tr>
<th>기능</th>
<th>설명</th>
</tr>
<tr>
<td>링크</td>
<td>선택한 텍스트에 URL 링크 삽입</td>
</tr>
<tr>
<td>이미지</td>
<td>이미지 파일을 선택하여 삽입 (서버에 자동 업로드)</td>
</tr>
<tr>
<td>동영상</td>
<td>YouTube 등 동영상 URL을 삽입</td>
</tr>
</table>
<h4>🧹 기타</h4>
<table>
<tr>
<th>기능</th>
<th>설명</th>
</tr>
<tr>
<td>서식 지우기</td>
<td>선택한 텍스트의 모든 서식을 초기화</td>
</tr>
<tr>
<td>위첨자 / 아래첨자</td>
<td>X² 또는 H₂O 같은 수식 표현</td>
</tr>
</table>
<div class="tip-box">
<strong>💡 TIP</strong><br>
• 텍스트를 드래그한 후 툴바 버튼을 클릭하면 선택 영역에만 서식이 적용됩니다.<br>
• 이미지는 클립보드에서 Ctrl+V로 붙여넣기도 가능합니다.<br>
• 실행 취소는 Ctrl+Z, 다시 실행은 Ctrl+Y 입니다.
</div>
</div>
</div>
</div>
</th:block>
<th:block layout:fragment="layout_popup">
</th:block>
<th:block layout:fragment="layout_script">
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"></script>
<script src="/js/crm/procedureReview/procedureReviewInsert.js"></script>
</th:block>
</html>

View File

@@ -0,0 +1,85 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{/web/layout/homeLayout}">
<th:block layout:fragment="layout_css">
<link rel="stylesheet" href="/css/web/webPhotoDietSelectList.css">
<link rel="stylesheet" href="/css/web/grid.css?v1.1">
</th:block>
<th:block layout:fragment="layout_top_script">
<script src="/js/web/jquery.twbsPagination.js" type="text/javascript"></script>
<script>
let menuClass = "[[${param.menuClass}]]" == "" ? "" : "[[${param.menuClass}]]";
let categoryDivCd = "07";
let selectUseYn = "[[${selectUseYn}]]" == "" ? "N" : "[[${selectUseYn}]]";
let insertUseYn = "[[${insertUseYn}]]" == "" ? "N" : "[[${insertUseYn}]]";
let updateUseYn = "[[${updateUseYn}]]" == "" ? "N" : "[[${updateUseYn}]]";
let deleteUseYn = "[[${deleteUseYn}]]" == "" ? "N" : "[[${deleteUseYn}]]";
let downloadUseYn = "[[${downloadUseYn}]]" == "" ? "N" : "[[${downloadUseYn}]]";
/* 검색 관련 변수 */
let procedureReviewSearchKeywordParam0 = "[[${param.procedureReviewSearchKeywordParam0}]]";
let procedureReviewSearchKeywordParam1 = "[[${param.procedureReviewSearchKeywordParam1}]]";
let procedureReviewSearchKeywordParam2 = "";
let procedureReviewSearchKeywordParam3 = "";
let procedureReviewSearchDateType = "";
let procedureReviewSearchStartDate = "";
let procedureReviewSearchEndDate = "";
let procedureReviewSort = "[[${param.procedureReviewSort}]]";
let procedureReviewDir = "[[${param.procedureReviewDir}]]";
let procedureReviewStart = "[[${param.procedureReviewStart}]]" == "" ? 0 : "[[${param.procedureReviewStart}]]";
let procedureReviewLimit = "[[${param.procedureReviewLimit}]]" == "" ? 500 : "[[${param.procedureReviewLimit}]]";
</script>
</th:block>
<th:block layout:fragment="layout_content">
<!-- 센터쪽 -->
<div class="center_box">
<p class="page_title">시술후기</p>
<div class="filter_box">
<div class="form_box">
<!-- 제목 검색 input -->
<div class="search_list">
<div class="search_box">
<img src="/image/web/search_G.svg" alt="search" />
<input type="text" id="procedureReviewSearchKeyword1" required placeholder="제목">
<div class="search_list"></div><!-- 검색내역 나오는곳 -->
</div>
<button id="btnSearchProcedureReview" class="search_btn" data-toggle="modal"
data-target=".work_closed_modal" style="transition: all 0.2s ease-in-out 0s;">조회</button>
</div>
<div class="right_btn_box">
<button id="btnInsertProcedureReview" class="treatmentdiet_btn">
<img src="/image/web/notice_btn_icon.svg" alt="등록">등록
</button>
<button id="btnDeleteProcedureReview" class="delete_btn">
<img src="/image/web/delete_btn_icon.svg" alt="삭제">삭제
</button>
</div>
</div>
</div>
<div id="procedureReviewGrid" class="table_box ag-theme-balham"></div>
<!-- 페이지게이션 -->
<div class="page_box">
<nav aria-label="Page navigation" class="navigation">
<ul class="pagination" id="procedureReviewPagination"></ul>
</nav>
</div>
</div>
<form id="procedureReviewSelectListForm" method="POST" target="_blank"></form>
</th:block>
<th:block layout:fragment="layout_popup">
</th:block>
<th:block layout:fragment="layout_script">
<script src="/js/web/ag-grid-community-29.3.5.min.js"></script>
<script src="/js/crm/procedureReview/procedureReviewSelectList.js"></script>
</th:block>
</html>

View File

@@ -0,0 +1,479 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{/web/layout/homeLayout}">
<th:block layout:fragment="layout_css">
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.snow.css" rel="stylesheet">
<link rel="stylesheet" href="/css/web/ContentsBbsUpd.css">
<link rel="stylesheet" href="/css/web/grid.css?v1.1">
<style>
/* Quill 에디터 스타일 보정 */
.ql-toolbar.ql-snow {
border: 1px solid #E9ECF0;
border-radius: 5px 5px 0 0;
background: #F9FAFB;
}
.ql-container.ql-snow {
border: 1px solid #E9ECF0;
border-top: none;
border-radius: 0 0 5px 5px;
min-height: 350px;
max-height: 600px;
overflow-y: auto;
font-size: 14px;
}
.ql-editor {
min-height: 340px;
line-height: 1.7;
}
.ql-editor.ql-blank::before {
color: #B5BDC4;
font-style: normal;
}
/* 페이지 스크롤 활성화 오버라이드 */
.center_box {
overflow-y: auto !important;
}
.project_wrap .content_section .hospital_wrap .center_box .content_box {
background: none !important;
border: none !important;
}
/* 단일 폼 레이아웃 */
.pr-single-form {
width: 100%;
background: #fff;
border: 1px solid #E9ECF0;
border-radius: 5px;
padding: 24px;
}
.pr-single-form .form-grid-row {
display: flex;
gap: 16px;
margin-bottom: 16px;
}
.pr-single-form .form-group {
flex: 1;
display: flex;
flex-direction: column;
}
.pr-single-form .form-group label {
font-size: 13px;
font-weight: 600;
color: #374151;
margin-bottom: 6px;
}
.pr-single-form .form-group input[type="text"] {
height: 38px;
padding: 0 12px;
border: 1px solid #E9ECF0;
border-radius: 5px;
font-size: 14px;
outline: none;
}
.pr-single-form .form-group input[type="text"]:focus {
border-color: #3985EA;
box-shadow: 0 0 0 2px rgba(57, 133, 234, 0.1);
}
/* 내용 헤더 (라벨 + 도움말 버튼) */
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.content-header label {
font-size: 13px;
font-weight: 600;
color: #374151;
margin-bottom: 0 !important;
}
.btn-help {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
font-size: 12px;
font-weight: 600;
color: #3985EA;
background: #EFF6FF;
border: 1px solid #BFDBFE;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s;
}
.btn-help:hover {
background: #DBEAFE;
border-color: #93C5FD;
}
/* 액션 버튼 */
.pr-actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
/* Quill 에디터 및 미리보기 이미지 크기 제한 */
.ql-editor img,
#previewContent img {
max-width: 100%;
height: auto;
}
/* 도움말 모달 */
.help-modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
justify-content: center;
align-items: center;
}
.help-modal-overlay.active {
display: flex;
}
.help-modal {
background: #fff;
border-radius: 10px;
width: 640px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
}
.help-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #E5E7EB;
}
.help-modal-header h3 {
font-size: 16px;
font-weight: 700;
color: #1F2937;
margin: 0;
}
.help-modal-close {
width: 32px;
height: 32px;
border: none;
background: #F3F4F6;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
color: #6B7280;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.help-modal-close:hover {
background: #E5E7EB;
color: #374151;
}
.help-modal-body {
padding: 24px;
font-size: 13px;
color: #4B5563;
line-height: 1.8;
}
.help-modal-body h4 {
font-size: 14px;
font-weight: 700;
color: #1F2937;
margin: 20px 0 8px 0;
}
.help-modal-body h4:first-child {
margin-top: 0;
}
.help-modal-body table {
width: 100%;
border-collapse: collapse;
margin: 8px 0 16px 0;
}
.help-modal-body table th {
background: #F9FAFB;
padding: 8px 12px;
text-align: left;
font-weight: 600;
border: 1px solid #E5E7EB;
font-size: 12px;
}
.help-modal-body table td {
padding: 8px 12px;
border: 1px solid #E5E7EB;
font-size: 12px;
}
.help-modal-body .tip-box {
background: #F0F9FF;
border: 1px solid #BAE6FD;
border-radius: 6px;
padding: 12px 16px;
margin-top: 16px;
}
.help-modal-body .tip-box strong {
color: #0284C7;
}
</style>
</th:block>
<th:block layout:fragment="layout_top_script">
<script>
let menuClass = "[[${param.menuClass}]]" == "" ? "" : "[[${param.menuClass}]]";
let selectUseYn = "[[${selectUseYn}]]" == "" ? "N" : "[[${selectUseYn}]]";
let updateUseYn = "[[${updateUseYn}]]" == "" ? "N" : "[[${updateUseYn}]]";
let muProcedureReviewId = "[[${param.muProcedureReviewId}]]";
const CDN_URL = "[[${@environment.getProperty('url.cdn')}]]";
</script>
</th:block>
<th:block layout:fragment="layout_content">
<div class="center_box">
<p class="page_title" style="float:none; display:block; width:100%;">시술후기 수정</p>
<div class="pr-single-form">
<!-- 제목 -->
<div class="form-grid-row">
<div class="form-group">
<label>제목</label>
<input type="text" id="title" placeholder="제목을 입력해주세요." />
</div>
</div>
<!-- 내용 (Quill 에디터) -->
<div class="form-group" style="margin-bottom: 16px;">
<div class="content-header">
<label>내용</label>
<button type="button" class="btn-help" id="btnHelp">? 도움말</button>
</div>
<div id="quillEditor"></div>
</div>
<!-- 해시태그 -->
<div class="form-grid-row">
<div class="form-group">
<label>해시태그</label>
<input type="text" id="hashtag" placeholder="#해시태그를 입력해주세요." />
</div>
</div>
<!-- 버튼 -->
<div class="pr-actions">
<button class="preview_btn btnPreview"
style="width: 90px; height: 36px; border-radius: 4px; background: #fff; color: #3985EA; border: 1px solid #3985EA; font-weight: 600; cursor: pointer;">미리보기</button>
<button class="registration_btn btnSave"
style="width: 80px; height: 36px; border-radius: 4px;">수정</button>
<button class="cancel_btn btnCancle"
style="width: 80px; height: 36px; border-radius: 4px; margin-left: 10px;">취소</button>
</div>
</div>
</div>
<!-- 미리보기 모달 -->
<div class="help-modal-overlay" id="previewModal">
<div class="help-modal" style="width: 800px; max-height: 90vh;">
<div class="help-modal-header">
<h3>👁 미리보기</h3>
<button class="help-modal-close" id="previewModalClose"></button>
</div>
<div class="help-modal-body" style="padding: 0;">
<!-- 제목 영역 -->
<div style="padding: 20px 24px; border-bottom: 1px solid #E5E7EB;">
<p style="font-size: 11px; color: #9CA3AF; margin-bottom: 4px;">제목</p>
<h2 id="previewTitle" style="font-size: 20px; font-weight: 700; color: #1F2937; margin: 0;"></h2>
</div>
<!-- 내용 영역 -->
<div style="padding: 24px;">
<div id="previewContent" class="ql-editor"
style="min-height: 200px; max-height: 500px; overflow-y: auto; padding: 0;"></div>
</div>
<!-- 해시태그 영역 -->
<div id="previewHashtagArea" style="padding: 12px 24px 20px; border-top: 1px solid #E5E7EB;">
<span id="previewHashtag"
style="display: inline-block; padding: 4px 12px; background: #EFF6FF; color: #3985EA; border-radius: 20px; font-size: 13px; font-weight: 500;"></span>
</div>
</div>
</div>
</div>
<!-- 도움말 모달 -->
<div class="help-modal-overlay" id="helpModal">
<div class="help-modal">
<div class="help-modal-header">
<h3>📝 에디터 사용 가이드</h3>
<button class="help-modal-close" id="helpModalClose"></button>
</div>
<div class="help-modal-body">
<h4>📌 텍스트 서식</h4>
<table>
<tr>
<th>기능</th>
<th>설명</th>
<th>단축키</th>
</tr>
<tr>
<td><strong>B</strong> 굵게</td>
<td>선택한 텍스트를 굵게 표시</td>
<td>Ctrl+B</td>
</tr>
<tr>
<td><em>I</em> 기울임</td>
<td>선택한 텍스트를 기울임꼴로 표시</td>
<td>Ctrl+I</td>
</tr>
<tr>
<td><u>U</u> 밑줄</td>
<td>선택한 텍스트에 밑줄 적용</td>
<td>Ctrl+U</td>
</tr>
<tr>
<td><s>S</s> 취소선</td>
<td>선택한 텍스트에 취소선 적용</td>
<td>-</td>
</tr>
</table>
<h4>🎨 글자 스타일</h4>
<table>
<tr>
<th>기능</th>
<th>설명</th>
</tr>
<tr>
<td>헤더 (H1~H6)</td>
<td>제목 크기를 설정합니다. 숫자가 작을수록 큰 제목</td>
</tr>
<tr>
<td>폰트</td>
<td>글꼴을 변경합니다</td>
</tr>
<tr>
<td>글자 크기</td>
<td>Small, Normal, Large, Huge 중 선택</td>
</tr>
<tr>
<td>글자색 / 배경색</td>
<td>텍스트 색상 또는 배경 하이라이트 색상 선택</td>
</tr>
</table>
<h4>📋 문단 서식</h4>
<table>
<tr>
<th>기능</th>
<th>설명</th>
</tr>
<tr>
<td>순서 목록</td>
<td>1. 2. 3. 형태의 번호 목록</td>
</tr>
<tr>
<td>비순서 목록</td>
<td>• 형태의 글머리 기호 목록</td>
</tr>
<tr>
<td>체크 목록</td>
<td>☐ 형태의 체크박스 목록</td>
</tr>
<tr>
<td>들여쓰기</td>
<td>텍스트를 안쪽/바깥쪽으로 이동</td>
</tr>
<tr>
<td>정렬</td>
<td>좌측, 중앙, 우측, 양쪽 정렬</td>
</tr>
<tr>
<td>인용문</td>
<td>인용 블록으로 표시</td>
</tr>
<tr>
<td>코드 블록</td>
<td>코드 형태로 표시</td>
</tr>
</table>
<h4>🔗 멀티미디어</h4>
<table>
<tr>
<th>기능</th>
<th>설명</th>
</tr>
<tr>
<td>링크</td>
<td>선택한 텍스트에 URL 링크 삽입</td>
</tr>
<tr>
<td>이미지</td>
<td>이미지 파일을 선택하여 삽입 (서버에 자동 업로드)</td>
</tr>
<tr>
<td>동영상</td>
<td>YouTube 등 동영상 URL을 삽입</td>
</tr>
</table>
<h4>🧹 기타</h4>
<table>
<tr>
<th>기능</th>
<th>설명</th>
</tr>
<tr>
<td>서식 지우기</td>
<td>선택한 텍스트의 모든 서식을 초기화</td>
</tr>
<tr>
<td>위첨자 / 아래첨자</td>
<td>X² 또는 H₂O 같은 수식 표현</td>
</tr>
</table>
<div class="tip-box">
<strong>💡 TIP</strong><br>
• 텍스트를 드래그한 후 툴바 버튼을 클릭하면 선택 영역에만 서식이 적용됩니다.<br>
• 이미지는 클립보드에서 Ctrl+V로 붙여넣기도 가능합니다.<br>
• 실행 취소는 Ctrl+Z, 다시 실행은 Ctrl+Y 입니다.
</div>
</div>
</div>
</div>
</th:block>
<th:block layout:fragment="layout_popup">
</th:block>
<th:block layout:fragment="layout_script">
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"></script>
<script src="/js/crm/procedureReview/procedureReviewUpdate.js"></script>
</th:block>
</html>