diff --git a/rules.md b/rules.md new file mode 100644 index 0000000..98b2bff --- /dev/null +++ b/rules.md @@ -0,0 +1,94 @@ +# 프로젝트 코딩 가이드라인 (Java Backend) + +AI 에디터(Agent)는 다음 규칙을 항상 준수하여 코드를 작성하고 수정해야 합니다. + +## 0. 기본 소통 규칙 (Communication) +- **언어**: 사용자에 대한 모든 답변과 코드 설명은 항상 **한글(Korean)**로만 작성해야 합니다. + +## 1. 패키지 구성 (Package Structure) +- **베이스 패키지**: `com.madeuhome` +- **컨트롤러 (Controller)**: `ctrl` +- **서비스 (Service)**: `svc` +- **DTO (Data Transfer Object)**: `dto` +- **매퍼 (Mapper)**: `mapper` +- **공통 서비스**: `com.madeuhome.common.service` (LogHistoryService 등 시스템 공통 모듈) + +## 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`) +- **XML Mapper**: `[도메인명]SqlMap.xml` (namespace는 Mapper 인터페이스의 **FQCN**과 반드시 일치) + +## 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) 서비스 메소드명 +- 서비스 메소드명은 **컨트롤러 메소드명과 동일**하게 명명합니다. + - 조회: `getXXXX` / 목록 조회: `getXXXXList` + - 저장: `putXXXX` + - 수정: `modXXXX` + - 삭제: `delXXXX` + +### 3) Mapper 인터페이스 메소드명 +- 단일조회 : `selectXXXX` +- 리스트조회 : `selectListXXXX` +- insert : `insertXXXX` +- update : `updateXXXX` +- delete : `deleteXXXX` + +## 4. 데이터베이스 연동 정보 (DB Connection) +- `application-local.yml`의 설정을 기반으로 한 공통 접속 정보입니다. +- **Host**: 183.98.184.84 +- **Port**: 3306 +- **Database**: madeu +- **User**: madeu +- **Password**: apdlemdb12#$ + +## 5. 아키텍처 및 코딩 원칙 (Architecture & Coding Principles) + +### 5-1. 컨트롤러 원칙 (Skinny Controller) +- 컨트롤러에는 비즈니스 로직이나 예외처리 로직을 넣지 않고, **서비스 메서드를 호출하는 1줄로만 작성**합니다. +- 컨트롤러 클래스는 `@Controller` 대신 **`@RestController`**를 사용하며, `@ResponseBody`는 생략합니다. +- 데이터 입출력 메소드의 파라미터는 **단일 DTO 하나만 `@RequestBody`**로 받습니다. +- 파일 업로드가 포함된 경우에만 `@ModelAttribute` + `@RequestParam MultipartFile`을 허용합니다. +- 화면 이동(`move~`) 메소드는 **`ModelAndView`를 리턴**합니다. (`@RestController`에서 String 리턴 시 뷰 이름이 아닌 응답 바디로 해석되므로) +- 화면 이동 메소드의 뷰 경로는 **컨트롤러에서 직접 명시**합니다. (서비스에 위임 금지) + +### 5-2. 서비스 원칙 (Service Layer) +- 에러 처리(try-catch), 응답 메시지(msgCode, msgDesc) 설정은 **서비스 계층에서 전담**합니다. +- `HttpServletRequest`, `HttpSession`은 `@Autowired`로 직접 주입받아 사용합니다. (Spring이 Request-scope 프록시로 제공) +- `session.getAttribute("loginMemberId")`를 통해 로그인 ID를 가져오고, DTO에 설정합니다. + +### 5-3. DTO 원칙 (DTO Communication) +- **HashMap 사용 금지**. 컨트롤러 ↔ 서비스 ↔ 매퍼 간 모든 데이터는 **DTO 객체만 사용**합니다. +- DTO에는 `@Data` (Lombok)을 사용합니다. +- DTO 필드 구성: + - **DB 컬럼 매핑 필드**: `muProcedureReviewId`, `title`, `content` 등 + - **조회 결과 전용 필드**: `rowNum`, `writeDate`, `writeName` 등 + - **검색/UI 변수**: `startDate`, `endDate`, `start`, `limit`, `sort`, `dir` 등 + - **응답 매핑 변수**: `msgCode`, `msgDesc`, `success`, `totalCount`, `rows`(Object 타입), `tId` + +### 5-4. Mapper 원칙 (MyBatis Mapper) +- Mapper는 **`@Mapper` 어노테이션을 사용한 인터페이스**로 작성합니다. (`SqlSessionDaoSupport` 상속 금지) +- XML Mapper의 `namespace`는 Mapper 인터페이스의 **FQCN(Fully Qualified Class Name)**과 일치시킵니다. +- XML의 `resultType`은 `hashmap` 대신 **DTO FQCN**을 사용합니다. + - 예외: 도메인 외부 테이블 조회(카테고리 등)는 `hashmap` 허용 +- XML alias는 **DTO 필드명(camelCase)**과 정확히 일치시킵니다. +- 단건 조회는 `List` 대신 **DTO 단일 객체를 리턴**합니다. + +## 6. 파일 업로드 규칙 (File Upload) +- `MultipartFile.transferTo()` 사용 시 반드시 **절대경로를 명시**합니다: + ```java + File dest = new File(outDir, savedName); + file.transferTo(dest.toPath().toAbsolutePath()); + ``` +- 상대경로 사용 시 Tomcat 임시 디렉토리 기준으로 해석되어 오류가 발생합니다. diff --git a/src/main/java/com/madeuhome/controller/web/webreview/WebReviewController.java b/src/main/java/com/madeuhome/controller/web/webreview/WebReviewController.java new file mode 100644 index 0000000..7a966d8 --- /dev/null +++ b/src/main/java/com/madeuhome/controller/web/webreview/WebReviewController.java @@ -0,0 +1,95 @@ +package com.madeuhome.controller.web.webreview; + +import com.madeuhome.constants.Constants; +import com.madeuhome.init.ManagerDraftAction; +import com.madeuhome.service.web.webreview.WebReviewService; +import com.madeuhome.util.HttpUtil; +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.servlet.ModelAndView; + +import java.util.HashMap; + +@Slf4j +@Controller +public class WebReviewController extends ManagerDraftAction { + + @Autowired + private WebReviewService webReviewService; + + @RequestMapping(value = "/webreview/selectListProcedureReviewIntro.do") + public String selectListProcedureReviewIntro(HttpSession session, HttpServletRequest request) { + log.debug("WebReviewController selectListProcedureReviewIntro START"); + log.debug("WebReviewController selectListProcedureReviewIntro END"); + return "/web/webreview/procedureReviewSelectList"; + } + + @RequestMapping(value = "/webreview/selectListProcedureReview.do") + public ModelAndView selectListProcedureReview(HttpSession session, HttpServletRequest request, + HttpServletResponse response) { + log.debug("WebReviewController selectListProcedureReview START"); + HashMap paramMap = HttpUtil.getParameterMap(request); + HashMap map = new HashMap(); + try { + map = webReviewService.selectListProcedureReview(paramMap); + } catch (Exception e) { + e.printStackTrace(); + return null; + } finally { + if (Constants.OK == map.get("msgCode")) { + } else { + if (null == map.get("msgCode") || ("").equals(map.get("msgCode"))) { + map.put("msgCode", Constants.FAIL); + } + map.put("success", false); + if (null == map.get("msgDesc") || ("").equals(map.get("msgDesc"))) { + map.put("msgDesc", "정상적으로 수행되지 않았습니다."); + } + } + } + log.debug("WebReviewController selectListProcedureReview END"); + return HttpUtil.makeHashToJsonModelAndView(map); + } + + @RequestMapping(value = "/webreview/selectProcedureReviewIntro.do") + public String selectProcedureReviewIntro(HttpSession session, HttpServletRequest request, Model model) { + log.debug("WebReviewController selectProcedureReviewIntro START"); + HashMap paramMap = HttpUtil.getParameterMap(request); + model.addAttribute("muProcedureReviewId", paramMap.get("muProcedureReviewId")); + log.debug("WebReviewController selectProcedureReviewIntro END"); + return "/web/webreview/procedureReviewSelect"; + } + + @RequestMapping(value = "/webreview/selectProcedureReview.do") + public ModelAndView selectProcedureReview(HttpSession session, HttpServletRequest request, + HttpServletResponse response) { + log.debug("WebReviewController selectProcedureReview START"); + HashMap paramMap = HttpUtil.getParameterMap(request); + HashMap map = new HashMap(); + try { + map = webReviewService.selectProcedureReview(paramMap); + } catch (Exception e) { + e.printStackTrace(); + return null; + } finally { + if (Constants.OK == map.get("msgCode")) { + } else { + if (null == map.get("msgCode") || ("").equals(map.get("msgCode"))) { + map.put("msgCode", Constants.FAIL); + } + map.put("success", false); + if (null == map.get("msgDesc") || ("").equals(map.get("msgDesc"))) { + map.put("msgDesc", "정상적으로 수행되지 않았습니다."); + } + } + } + log.debug("WebReviewController selectProcedureReview END"); + return HttpUtil.makeHashToJsonModelAndView(map); + } +} diff --git a/src/main/java/com/madeuhome/dao/web/webreview/WebReviewSqlMapDAO.java b/src/main/java/com/madeuhome/dao/web/webreview/WebReviewSqlMapDAO.java new file mode 100644 index 0000000..ee88989 --- /dev/null +++ b/src/main/java/com/madeuhome/dao/web/webreview/WebReviewSqlMapDAO.java @@ -0,0 +1,42 @@ +package com.madeuhome.dao.web.webreview; + +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 WebReviewSqlMapDAO extends SqlSessionDaoSupport { + + @Autowired + private SqlSessionTemplate sqlSessionTemplate; + + @PostConstruct + void init() { + setSqlSessionTemplate(sqlSessionTemplate); + } + + public Map selectTotalProcedureReviewCount(HashMap paramMap) + throws DataAccessException { + return getSqlSession().selectOne("WebReview.selectTotalProcedureReviewCount", paramMap); + } + + public List> selectListProcedureReview(HashMap paramMap) + throws DataAccessException { + return getSqlSession().selectList("WebReview.selectListProcedureReview", paramMap); + } + + public Map selectProcedureReview(HashMap paramMap) throws DataAccessException { + return getSqlSession().selectOne("WebReview.selectProcedureReview", paramMap); + } + + public void updateViewCount(HashMap paramMap) throws DataAccessException { + getSqlSession().update("WebReview.updateViewCount", paramMap); + } +} diff --git a/src/main/java/com/madeuhome/service/web/webreview/WebReviewService.java b/src/main/java/com/madeuhome/service/web/webreview/WebReviewService.java new file mode 100644 index 0000000..160cc9c --- /dev/null +++ b/src/main/java/com/madeuhome/service/web/webreview/WebReviewService.java @@ -0,0 +1,9 @@ +package com.madeuhome.service.web.webreview; + +import java.util.HashMap; + +public interface WebReviewService { + public HashMap selectListProcedureReview(HashMap paramMap) throws Exception; + + public HashMap selectProcedureReview(HashMap paramMap) throws Exception; +} diff --git a/src/main/java/com/madeuhome/service/web/webreview/impl/WebReviewServiceImpl.java b/src/main/java/com/madeuhome/service/web/webreview/impl/WebReviewServiceImpl.java new file mode 100644 index 0000000..a6d4640 --- /dev/null +++ b/src/main/java/com/madeuhome/service/web/webreview/impl/WebReviewServiceImpl.java @@ -0,0 +1,59 @@ +package com.madeuhome.service.web.webreview.impl; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.madeuhome.constants.Constants; +import com.madeuhome.dao.web.webreview.WebReviewSqlMapDAO; +import com.madeuhome.service.web.webreview.WebReviewService; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service("WebReviewService") +public class WebReviewServiceImpl implements WebReviewService { + + @Autowired + private WebReviewSqlMapDAO webReviewSqlMapDAO; + + @Override + public HashMap selectListProcedureReview(HashMap paramMap) throws Exception { + log.debug("WebReviewServiceImpl selectListProcedureReview START"); + HashMap map = new HashMap(); + try { + Map totalMap = webReviewSqlMapDAO.selectTotalProcedureReviewCount(paramMap); + map.put("totalCount", totalMap.get("totalCount")); + List> listMap = webReviewSqlMapDAO.selectListProcedureReview(paramMap); + map.put("rows", listMap); + map.put("msgCode", Constants.OK); + map.put("success", "true"); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + log.debug("WebReviewServiceImpl selectListProcedureReview END"); + return map; + } + + @Override + public HashMap selectProcedureReview(HashMap paramMap) throws Exception { + log.debug("WebReviewServiceImpl selectProcedureReview START"); + HashMap map = new HashMap(); + try { + webReviewSqlMapDAO.updateViewCount(paramMap); + Map detailMap = webReviewSqlMapDAO.selectProcedureReview(paramMap); + map.put("rows", detailMap); + map.put("msgCode", Constants.OK); + map.put("success", "true"); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + log.debug("WebReviewServiceImpl selectProcedureReview END"); + return map; + } +} diff --git a/src/main/resources/mappers/WebReviewSqlMap.xml b/src/main/resources/mappers/WebReviewSqlMap.xml new file mode 100644 index 0000000..e1b5804 --- /dev/null +++ b/src/main/resources/mappers/WebReviewSqlMap.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + UPDATE MU_PROCEDURE_REVIEW + SET VIEW_COUNT = IFNULL(VIEW_COUNT, 0) + 1 + WHERE USE_YN = 'Y' + AND MU_PROCEDURE_REVIEW_ID = #{muProcedureReviewId} + + + diff --git a/src/main/resources/static/css/web/webreview/procedureReviewSelect.css b/src/main/resources/static/css/web/webreview/procedureReviewSelect.css new file mode 100644 index 0000000..0f3d7d5 --- /dev/null +++ b/src/main/resources/static/css/web/webreview/procedureReviewSelect.css @@ -0,0 +1,248 @@ +* { + box-sizing: border-box; +} + +html, +body { + height: 100vh; + margin: 0; + padding: 0; + font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + background: #f8f9fa; + color: #1a1a1a; + overflow-x: hidden; + font-size: 16px; + line-height: 1.6; +} + +.container { + max-width: 900px; + width: 100%; + margin: 0 auto; + min-height: calc(100vh - 300px); + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.breadcrumb { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; + color: #9ca3af; + padding: 0.5rem 0; +} + +.breadcrumb a { + color: #6b7280; + text-decoration: none; + transition: color 0.2s; +} + +.breadcrumb a:hover { + color: #C60B24; +} + +.review-article { + background: white; + border-radius: 16px; + box-shadow: 0 2px 16px rgba(0, 0, 0, 0.06); + overflow: hidden; +} + +.review-header { + padding: 2rem 2rem 1.5rem; + border-bottom: 1px solid #f1f5f9; +} + +.review-title { + font-size: clamp(1.375rem, 3vw, 1.75rem); + font-weight: 700; + color: #1a1a1a; + margin: 0 0 1rem; + letter-spacing: -0.025em; + line-height: 1.4; +} + +.review-meta { + display: flex; + gap: 1.5rem; + font-size: 0.875rem; + color: #9ca3af; + margin-bottom: 0.75rem; +} + +.review-tags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.review-tag { + display: inline-block; + padding: 0.25rem 0.75rem; + background: rgba(198, 11, 36, 0.08); + color: #C60B24; + border-radius: 20px; + font-size: 0.8125rem; + font-weight: 500; +} + +.review-content { + padding: 2rem; + min-height: 300px; +} + +.review-content img { + max-width: 100%; + height: auto; + border-radius: 8px; + margin: 0.5rem 0; +} + +.review-content p { + margin: 0 0 0.75rem; + line-height: 1.8; +} + +.review-content h1, +.review-content h2, +.review-content h3 { + margin: 1.5rem 0 0.75rem; + font-weight: 700; +} + +.review-content blockquote { + border-left: 4px solid #C60B24; + padding: 0.75rem 1rem; + margin: 1rem 0; + background: #fafafa; + border-radius: 0 8px 8px 0; +} + +.review-content ul, +.review-content ol { + padding-left: 1.5rem; + margin: 0.5rem 0; +} + +.review-content li { + margin: 0.25rem 0; + list-style: inherit; +} + +.ql-editor .ql-align-center { + text-align: center; +} + +.ql-editor .ql-align-right { + text-align: right; +} + +.ql-editor .ql-align-justify { + text-align: justify; +} + +.ql-editor .ql-indent-1 { + padding-left: 3em; +} + +.ql-editor .ql-indent-2 { + padding-left: 6em; +} + +.ql-editor .ql-indent-3 { + padding-left: 9em; +} + +.ql-editor .ql-size-small { + font-size: 0.75em; +} + +.ql-editor .ql-size-large { + font-size: 1.5em; +} + +.ql-editor .ql-size-huge { + font-size: 2.5em; +} + +.ql-editor .ql-font-serif { + font-family: Georgia, 'Times New Roman', serif; +} + +.ql-editor .ql-font-monospace { + font-family: 'Monaco', 'Courier New', monospace; +} + +.btn-area { + display: flex; + justify-content: center; + padding-bottom: 2rem; +} + +.btn-list { + display: inline-flex; + align-items: center; + padding: 0.75rem 2.5rem; + background: white; + color: #1a1a1a; + border: 1px solid #e5e7eb; + border-radius: 10px; + font-size: 0.9375rem; + font-weight: 600; + text-decoration: none; + transition: all 0.2s; +} + +.btn-list:hover { + border-color: #C60B24; + color: #C60B24; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); +} + +.loading { + text-align: center; + padding: 3rem; + color: #6b7280; +} + +@media (max-width: 768px) { + .container { + padding: 1rem; + gap: 1rem; + } + + .review-header { + padding: 1.5rem; + } + + .review-content { + padding: 1.5rem; + } + + .review-title { + font-size: 1.25rem; + } + + .review-meta { + flex-direction: column; + gap: 0.25rem; + } +} + +@media (max-width: 480px) { + .container { + padding: 0.75rem; + } + + .review-header { + padding: 1rem; + } + + .review-content { + padding: 1rem; + } +} \ No newline at end of file diff --git a/src/main/resources/static/css/web/webreview/procedureReviewSelectList.css b/src/main/resources/static/css/web/webreview/procedureReviewSelectList.css new file mode 100644 index 0000000..8a464e3 --- /dev/null +++ b/src/main/resources/static/css/web/webreview/procedureReviewSelectList.css @@ -0,0 +1,399 @@ +* { + box-sizing: border-box; +} + +html, +body { + height: 100vh; + margin: 0; + padding: 0; + font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + background: #f8f9fa; + color: #1a1a1a; + overflow-x: hidden; + font-size: 16px; + line-height: 1.6; +} + +.container { + max-width: 1280px; + width: 100%; + margin: 0 auto; + min-height: calc(100vh - 300px); + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.header { + background: white; + border-radius: 16px; + box-shadow: 0 2px 16px rgba(0, 0, 0, 0.06); + padding: 1.5rem 2rem; + border-bottom: 3px solid #C60B24; +} + +.page-title { + font-size: clamp(1.5rem, 3vw, 1.875rem); + font-weight: 700; + color: #1a1a1a; + margin: 0; + letter-spacing: -0.025em; +} + +.page-subtitle { + font-size: 0.95rem; + color: #6b7280; + margin: 0.25rem 0 0; +} + +.search-area { + display: flex; + justify-content: flex-end; +} + +.search-box { + display: flex; + gap: 0.5rem; +} + +.search-box input { + width: 280px; + padding: 0.625rem 1rem; + border: 1px solid #e5e7eb; + border-radius: 10px; + font-size: 0.9375rem; + outline: none; + transition: border-color 0.2s; +} + +.search-box input:focus { + border-color: #C60B24; +} + +.search-btn { + padding: 0.625rem 1.25rem; + background: #C60B24; + color: white; + border: none; + border-radius: 10px; + font-size: 0.9375rem; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; +} + +.search-btn:hover { + background: #a5091e; +} + +.review-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); + gap: 1.5rem; +} + +.review-card { + background: white; + border-radius: 16px; + box-shadow: 0 2px 16px rgba(0, 0, 0, 0.06); + overflow: hidden; + transition: all 0.3s ease; + border: 1px solid #f1f5f9; + cursor: pointer; + display: flex; + flex-direction: column; +} + +.review-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); + border-color: #C60B24; +} + +/* ===== 이미지 슬라이더 ===== */ +.review-slider { + position: relative; + width: 100%; + aspect-ratio: 4 / 3; + overflow: hidden; + background: #f1f5f9; +} + +.review-slider-track { + display: flex; + width: 100%; + height: 100%; + transition: transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); +} + +.review-slider-track img { + min-width: 100%; + max-width: 100%; + height: 100%; + object-fit: cover; + flex-shrink: 0; +} + +.slider-arrow { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 32px; + height: 32px; + background: rgba(0, 0, 0, 0.45); + color: white; + border: none; + border-radius: 50%; + font-size: 14px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.25s; + z-index: 2; + line-height: 1; +} + +.review-slider:hover .slider-arrow { + opacity: 1; +} + +.slider-arrow.prev { + left: 8px; +} + +.slider-arrow.next { + right: 8px; +} + +.slider-arrow:hover { + background: rgba(0, 0, 0, 0.7); +} + +.slider-dots { + position: absolute; + bottom: 8px; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 5px; + z-index: 2; +} + +.slider-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.5); + border: none; + cursor: pointer; + padding: 0; + transition: all 0.25s; +} + +.slider-dot.active { + background: white; + transform: scale(1.3); +} + +.slider-count { + position: absolute; + top: 10px; + right: 10px; + background: rgba(0, 0, 0, 0.55); + color: white; + font-size: 0.75rem; + padding: 2px 8px; + border-radius: 12px; + z-index: 2; +} + +.review-no-image { + width: 100%; + aspect-ratio: 4 / 3; + background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%); + display: flex; + align-items: center; + justify-content: center; + font-size: 2.5rem; + color: #cbd5e1; +} + +.review-card-body { + padding: 1.25rem 1.5rem 1.5rem; + flex: 1; + display: flex; + flex-direction: column; +} + +.review-card-title { + font-size: 1.0625rem; + font-weight: 700; + color: #1a1a1a; + margin-bottom: 0.5rem; + letter-spacing: -0.025em; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.review-card-summary { + color: #6b7280; + font-size: 0.8125rem; + line-height: 1.6; + flex: 1; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + margin-bottom: 0.75rem; +} + +.review-card-tags { + display: flex; + flex-wrap: wrap; + gap: 0.375rem; + margin-bottom: 0.75rem; +} + +.review-tag { + display: inline-block; + padding: 0.2rem 0.5rem; + background: rgba(198, 11, 36, 0.08); + color: #C60B24; + border-radius: 20px; + font-size: 0.6875rem; + font-weight: 500; +} + +.review-card-footer { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.8125rem; + color: #9ca3af; +} + +.review-card-footer .views { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.pagination-area { + display: flex; + justify-content: center; + gap: 0.375rem; + padding: 1rem 0 2rem; +} + +.page-btn { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid #e5e7eb; + background: white; + border-radius: 8px; + font-size: 0.875rem; + color: #6b7280; + cursor: pointer; + transition: all 0.2s; +} + +.page-btn:hover { + border-color: #C60B24; + color: #C60B24; +} + +.page-btn.active { + background: #C60B24; + color: white; + border-color: #C60B24; +} + +.page-btn.disabled { + opacity: 0.4; + cursor: default; +} + +.loading { + text-align: center; + padding: 3rem; + color: #6b7280; + font-size: 0.9375rem; + grid-column: 1 / -1; +} + +.empty-state { + text-align: center; + padding: 4rem 2rem; + grid-column: 1 / -1; +} + +.empty-state .icon { + font-size: 3rem; + margin-bottom: 1rem; +} + +.empty-state .message { + font-size: 1.125rem; + color: #6b7280; + font-weight: 500; +} + +@media (max-width: 768px) { + .container { + padding: 1rem; + gap: 1rem; + } + + .header { + padding: 1rem 1.5rem; + border-radius: 12px; + } + + .page-title { + font-size: clamp(1.25rem, 2.5vw, 1.5rem); + } + + .review-grid { + grid-template-columns: 1fr; + gap: 1rem; + } + + .search-area { + justify-content: stretch; + } + + .search-box { + width: 100%; + } + + .search-box input { + flex: 1; + width: auto; + } + + .slider-arrow { + opacity: 0.7; + } +} + +@media (max-width: 480px) { + .container { + padding: 0.75rem; + } + + .header { + padding: 0.875rem 1rem; + } + + .review-card-body { + padding: 1rem; + } +} \ No newline at end of file diff --git a/src/main/resources/static/image/equip/온다리프팅.jpg b/src/main/resources/static/image/equip/온다리프팅.jpg new file mode 100644 index 0000000..74d41aa Binary files /dev/null and b/src/main/resources/static/image/equip/온다리프팅.jpg differ diff --git a/src/main/resources/static/image/quick_menu/review.png b/src/main/resources/static/image/quick_menu/review.png new file mode 100644 index 0000000..087b3b3 Binary files /dev/null and b/src/main/resources/static/image/quick_menu/review.png differ diff --git a/src/main/resources/static/js/web/webreview/procedureReviewSelect.js b/src/main/resources/static/js/web/webreview/procedureReviewSelect.js new file mode 100644 index 0000000..4790dea --- /dev/null +++ b/src/main/resources/static/js/web/webreview/procedureReviewSelect.js @@ -0,0 +1,89 @@ +class ReviewDetailManager { + constructor() { + this.quill = null; + this.init(); + } + + async init() { + if (!muProcedureReviewId) { this.showError('잘못된 접근입니다.'); return; } + await this.loadReview(); + } + + async apiRequest(url, data) { + return new Promise((resolve, reject) => { + $.ajax({ + url: encodeURI(url), data: data, dataType: 'json', + processData: false, contentType: false, type: 'POST', + success: resolve, error: reject + }); + }); + } + + async loadReview() { + try { + const formData = new FormData(); + formData.append('muProcedureReviewId', muProcedureReviewId); + const data = await this.apiRequest('/webreview/selectProcedureReview.do', formData); + if (data.msgCode === '0' && data.rows) { + this.renderReview(data.rows); + } else { + this.showError('게시글을 찾을 수 없습니다.'); + } + } catch (error) { + this.showError('게시글 조회 중 오류가 발생하였습니다.'); + } + } + + renderReview(review) { + document.getElementById('review-title').textContent = review.title || ''; + document.getElementById('breadcrumb-title').textContent = review.title || '상세보기'; + document.getElementById('review-date').textContent = review.writeDate || ''; + document.getElementById('review-views').textContent = review.viewCount || 0; + + const tagsContainer = document.getElementById('review-tags'); + if (review.hashtag) { + const tags = review.hashtag.split(',').map(t => t.trim()).filter(t => t); + tagsContainer.innerHTML = tags.map(tag => `#${tag}`).join(''); + } + + // Quill Delta JSON 본문 렌더링 + const contentDiv = document.getElementById('review-content'); + if (review.content) { + try { + const decoded = decodeURIComponent(escape(atob(review.content))); + const delta = JSON.parse(decoded); + + contentDiv.innerHTML = ''; + this.quill = new Quill(contentDiv, { + readOnly: true, + modules: { toolbar: false }, + theme: 'snow' + }); + this.quill.setContents(delta); + + const toolbar = contentDiv.parentElement.querySelector('.ql-toolbar'); + if (toolbar) toolbar.style.display = 'none'; + const container = contentDiv.parentElement.querySelector('.ql-container'); + if (container) { container.style.border = 'none'; container.style.fontSize = '16px'; } + } catch (e) { + try { + const html = decodeURIComponent(escape(atob(review.content))); + contentDiv.innerHTML = html; + } catch (e2) { + contentDiv.innerHTML = review.content; + } + } + } else { + contentDiv.innerHTML = '

내용이 없습니다.

'; + } + } + + showError(msg) { + document.getElementById('review-content').innerHTML = ` +
+
😔
${msg}
+
`; + } +} + +const reviewDetailManager = new ReviewDetailManager(); diff --git a/src/main/resources/static/js/web/webreview/procedureReviewSelectList.js b/src/main/resources/static/js/web/webreview/procedureReviewSelectList.js new file mode 100644 index 0000000..31f4fce --- /dev/null +++ b/src/main/resources/static/js/web/webreview/procedureReviewSelectList.js @@ -0,0 +1,205 @@ +class ReviewListManager { + constructor() { + this.reviews = []; + this.currentPage = 1; + this.pageSize = 9; + this.totalCount = 0; + this.categoryDivCd = '08'; // 쁘띠센터 + this.sliders = {}; + this.init(); + } + + async init() { + this.bindEvents(); + await this.loadReviews(); + } + + bindEvents() { + document.getElementById('btnSearch').addEventListener('click', () => { + this.currentPage = 1; + this.loadReviews(); + }); + document.getElementById('searchTitle').addEventListener('keypress', (e) => { + if (e.key === 'Enter') { this.currentPage = 1; this.loadReviews(); } + }); + } + + async apiRequest(url, data) { + return new Promise((resolve, reject) => { + $.ajax({ + url: encodeURI(url), data: data, dataType: 'json', + processData: false, contentType: false, type: 'POST', + success: resolve, error: reject + }); + }); + } + + async loadReviews() { + try { + const formData = new FormData(); + formData.append('categoryDivCd', this.categoryDivCd); + formData.append('start', (this.currentPage - 1) * this.pageSize); + formData.append('limit', this.pageSize); + const searchTitle = document.getElementById('searchTitle').value.trim(); + if (searchTitle) formData.append('title', searchTitle); + + const data = await this.apiRequest('/webreview/selectListProcedureReview.do', formData); + if (data.msgCode === '0') { + this.reviews = data.rows || []; + this.totalCount = parseInt(data.totalCount) || 0; + this.renderReviews(); + this.renderPagination(); + } else { + this.showEmpty('조회 중 오류가 발생하였습니다.'); + } + } catch (error) { + this.showEmpty('조회 중 오류가 발생하였습니다.'); + } + } + + extractImages(content) { + if (!content) return []; + try { + const decoded = decodeURIComponent(escape(atob(content))); + const delta = JSON.parse(decoded); + if (delta.ops) { + return delta.ops + .filter(op => op.insert && typeof op.insert === 'object' && op.insert.image) + .map(op => op.insert.image); + } + return []; + } catch (e) { return []; } + } + + extractSummary(content) { + if (!content) return ''; + try { + const decoded = decodeURIComponent(escape(atob(content))); + const delta = JSON.parse(decoded); + if (delta.ops) { + const text = delta.ops + .filter(op => typeof op.insert === 'string') + .map(op => op.insert).join('').replace(/\n/g, ' ').trim(); + return text.length > 100 ? text.substring(0, 100) + '...' : text; + } + return ''; + } catch (e) { return ''; } + } + + buildSliderHtml(images, cardIdx) { + if (images.length === 0) return '
📷
'; + const imagesHtml = images.map(src => `고객후기`).join(''); + const dotsHtml = images.length > 1 + ? `
${images.map((_, i) => + ``).join('')}
` : ''; + const arrowsHtml = images.length > 1 + ? `` : ''; + const countHtml = images.length > 1 + ? `1 / ${images.length}` : ''; + return `
+
${imagesHtml}
${arrowsHtml}${dotsHtml}${countHtml}
`; + } + + renderReviews() { + const grid = document.getElementById('review-grid'); + if (this.reviews.length === 0) { + grid.innerHTML = `
📝
등록된 고객후기가 없습니다.
`; + return; + } + grid.innerHTML = this.reviews.map((review, idx) => { + const images = this.extractImages(review.summary || review.content); + const summaryText = this.extractSummary(review.summary || review.content); + const sliderHtml = this.buildSliderHtml(images, idx); + let tagsHtml = ''; + if (review.hashtag) { + const tags = review.hashtag.split(',').map(t => t.trim()).filter(t => t); + tagsHtml = `
${tags.slice(0, 3).map(tag => + `#${tag}`).join('')}
`; + } + return `
+ ${sliderHtml} +
+
${this.escapeHtml(review.title)}
+
${summaryText}
+ ${tagsHtml} + +
+
`; + }).join(''); + + this.bindSliderEvents(); + + grid.querySelectorAll('.review-card').forEach(card => { + card.addEventListener('click', (e) => { + if (e.target.closest('.slider-arrow') || e.target.closest('.slider-dot')) return; + location.href = `/webreview/selectProcedureReviewIntro.do?muProcedureReviewId=${card.dataset.id}`; + }); + }); + } + + bindSliderEvents() { + document.querySelectorAll('.review-slider').forEach(slider => { + const total = parseInt(slider.dataset.total); + if (total <= 1) return; + const track = slider.querySelector('.review-slider-track'); + const dots = slider.querySelectorAll('.slider-dot'); + const countEl = slider.querySelector('.slider-count'); + + const goTo = (idx) => { + const current = Math.max(0, Math.min(idx, total - 1)); + slider.dataset.current = current; + track.style.transform = `translateX(-${current * 100}%)`; + dots.forEach((d, i) => d.classList.toggle('active', i === current)); + if (countEl) countEl.textContent = `${current + 1} / ${total}`; + }; + slider.querySelectorAll('.slider-arrow').forEach(arrow => { + arrow.addEventListener('click', (e) => { + e.stopPropagation(); + let next = parseInt(slider.dataset.current) + parseInt(arrow.dataset.dir); + if (next < 0) next = total - 1; + if (next >= total) next = 0; + goTo(next); + }); + }); + dots.forEach(dot => { + dot.addEventListener('click', (e) => { e.stopPropagation(); goTo(parseInt(dot.dataset.idx)); }); + }); + }); + } + + renderPagination() { + const area = document.getElementById('pagination-area'); + const totalPages = Math.ceil(this.totalCount / this.pageSize); + if (totalPages <= 1) { area.innerHTML = ''; return; } + let html = ``; + const startPage = Math.max(1, this.currentPage - 2); + const endPage = Math.min(totalPages, startPage + 4); + for (let i = startPage; i <= endPage; i++) { + html += ``; + } + html += ``; + area.innerHTML = html; + area.querySelectorAll('.page-btn:not(.disabled)').forEach(btn => { + btn.addEventListener('click', () => { + this.currentPage = parseInt(btn.dataset.page); + this.loadReviews(); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }); + }); + } + + showEmpty(msg) { + document.getElementById('review-grid').innerHTML = `
⚠️
${msg}
`; + document.getElementById('pagination-area').innerHTML = ''; + } + + escapeHtml(str) { + if (!str) return ''; + return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); + } +} + +const reviewListManager = new ReviewListManager(); diff --git a/src/main/resources/templates/web/introduction/introductionHospitalSelect.html b/src/main/resources/templates/web/introduction/introductionHospitalSelect.html index bc143f5..361e596 100644 --- a/src/main/resources/templates/web/introduction/introductionHospitalSelect.html +++ b/src/main/resources/templates/web/introduction/introductionHospitalSelect.html @@ -1,8 +1,6 @@ - + @@ -10,16 +8,16 @@
- introduction + introduction

#맞춤진료 #만족스러운결과

- MADE U 강남본점
- Total Beauty
One Stop System + MADE U 강남본점
+ Total Beauty
One Stop System

- 한 공간에서 고객 한분 한분께 자연스러운
아름다움과 건강한 다이어트를 위해
- 항상 노력하는 함께하는
조언자가 되어 드릴 것을 약속드립니다. + 한 공간에서 고객 한분 한분께 자연스러운
아름다움과 건강한 다이어트를 위해
+ 항상 노력하는 함께하는
조언자가 되어 드릴 것을 약속드립니다.

@@ -28,47 +26,47 @@

#차별화된 맞춤 플랜

-

Looking Around
MADE U

+

Looking Around
MADE U

- 쾌적하고 안락한 공간을 제공하는
+ 쾌적하고 안락한 공간을 제공하는
메이드유 강남본점

깨끗한 공간, 친절한 상담을 제공하겠습니다.

- content1 + content1
-

쉬운다이어트 방법 없을까?
MADE U 시그니처 프로그램

+

쉬운다이어트 방법 없을까?
MADE U 시그니처 프로그램

  • - content2-1 + content2-1

    One-On-One Customized Counseling

    - MADE U 강남본점만의
    + MADE U 강남본점만의
    1:1 맞춤 상담

    - 고객님의 체형별 그리고 고민
    부위별 맞춤진료로,
    + 고객님의 체형별 그리고 고민
    부위별 맞춤진료로,
    숨겨진 아름다움을 찾아드립니다.

  • - content2-2 + content2-2

    Trademark Application

    - MADE U 강남본점만의
    + MADE U 강남본점만의
    특허상표 출원

    - 메이드유 강남본점에서 자체 개발한
    메쉬다 주사
    - 레시피의 뛰어난
    효과를 바탕으로 다수의
    + 메이드유 강남본점에서 자체 개발한
    메쉬다 주사
    + 레시피의 뛰어난
    효과를 바탕으로 다수의
    특허와 상표를 등록했습니다.

    @@ -77,151 +75,151 @@
-
-

국내 고가명품 최다보유
MADE U 프리미엄 장비 소개

- -
-
-
- 써마지 -
-
-

써마지

-

깊은 탄력과 리프팅을 동시에

-

-
-
- -
-
- 울쎄라 -
-
-

울쎄라

-

깊은 탄력과 리프팅을 동시에

-

-
-
- -
-
- 티타늄리프팅 -
-
-

티타늄리프팅

-

깊은 탄력과 리프팅을 동시에

-

-
-
- -
-
- 튠바디 -
-
-

튠바디

-

깊은 탄력과 리프팅을 동시에

-

-
-
- - -
-
- 튠페이스 -
-
-

튠페이스

-

깊은 탄력과 리프팅을 동시에

-

-
-
- -
-
- 울핏 -
-
-

울핏

-

깊은 탄력과 리프팅을 동시에

-

-
-
- -
-
- 포텐자 -
-
-

포텐자

-

깊은 탄력과 리프팅을 동시에

-

-
-
- -
-
- 인모드 -
-
-

인모드

-

깊은 탄력과 리프팅을 동시에

-

-
-
- - -
-
- 슈링크유니버스 -
-
-

슈링크유니버스

-

깊은 탄력과 리프팅을 동시에

-

-
-
- -
-
- 바디고주파테라피 -
-
-

바디고주파테라피

-

깊은 탄력과 리프팅을 동시에

-

-
-
- -
-
- 리포덤 -
-
-

리포덤

-

깊은 탄력과 리프팅을 동시에

-

-
-
- -
-
- 라비앙 -
-
-

라비앙

-

깊은 탄력과 리프팅을 동시에

-

-
-
-
-
+
+

국내 고가명품 최다보유
MADE U 프리미엄 장비 소개

+ +
+
+
+ 써마지 +
+
+

써마지

+ +

+
+
+ +
+
+ 울쎄라 +
+
+

울쎄라

+ +

+
+
+ +
+
+ 티타늄리프팅 +
+
+

티타늄리프팅

+ +

+
+
+
+
+ 온다리프팅 +
+
+

온다리프팅

+ + +
+
+
+
+ 튠바디 +
+
+

튠바디

+ +

+
+
+ + +
+
+ 튠페이스 +
+
+

튠페이스

+ +

+
+
+ +
+
+ 울핏 +
+
+

울핏

+ +

+
+
+ +
+
+ 포텐자 +
+
+

포텐자

+ +

+
+
+ +
+
+ 인모드 +
+
+

인모드

+ +

+
+
+ + +
+
+ 슈링크유니버스 +
+
+

슈링크유니버스

+ +

+
+
+ +
+
+ 바디고주파테라피 +
+
+

바디고주파테라피

+ +

+
+
+ +
+
+ 리포덤 +
+
+

리포덤

+ +

+
+
+ + +
+

- 한 공간에서 고객 한분 한분께 자연스러운 아름다움과
건강한 다이어트를 위해 항상 노력하는
+ 한 공간에서 고객 한분 한분께 자연스러운 아름다움과
건강한 다이어트를 위해 항상 노력하는
Total Beauty One Stop System

    @@ -229,56 +227,57 @@

    - 자연스러운 아름다움과
    + 자연스러운 아름다움과
    건강한 다이어트

    - 메이드유는 Total Beauty One Stop System으로
    - 한 공간에서 고객 한분
    한분께 자연스러운 아름다움과
    - 건강한 다이어트를 위해 항상 노력하는 함께하는 조언자가
    + 메이드유는 Total Beauty One Stop System으로
    + 한 공간에서 고객 한분
    한분께 자연스러운 아름다움과
    + 건강한 다이어트를 위해 항상 노력하는 함께하는 조언자가
    되어 드릴 것을 약속드립니다.

    - introduction_content4-1 - introduction_content4-1 + introduction_content4-1 + introduction_content4-1
  • - 고객을 가족으로 생각하는 마음으로
    정직한 시술,
    만족할 수 있는 결과,

    - 더 나은 감동을 선사하기 위해
    + 고객을 가족으로 생각하는 마음으로
    정직한 시술,
    만족할 수 있는 + 결과,

    + 더 나은 감동을 선사하기 위해
    끊임없이 노력하겠습니다.

    - 메이드유는 전국, 해외에서 찾아오는 비만센터와
    프리미멈 명품 - 장비 보유 및 장비 최다 보유
    와 엘란쎄,
    스컬트라 콜라겐 볼륨 - 전국 3대 병원인 쁘띠 센터로
    구분되어 고객에 맞춰 운영되고 + 메이드유는 전국, 해외에서 찾아오는 비만센터와
    프리미멈 명품 + 장비 보유 및 장비 최다 보유
    와 엘란쎄,
    스컬트라 콜라겐 볼륨 + 전국 3대 병원인 쁘띠 센터로
    구분되어 고객에 맞춰 운영되고 있습니다.

    - introduction_content4-2 - introduction_content4-2 + introduction_content4-2 + introduction_content4-2
  • - No Pain 통증 없이
    - No Bruise 멍 없이
    - No Swelling 붓기 없이
    + No Pain 통증 없이
    + No Bruise 멍 없이
    + No Swelling 붓기 없이
    3No 의료서비스를 지향

    - 모든 제품은 정품, 정량, 정품 장비 사용을 원칙으로 안전을
    - 최우선으로 하고 있으며, 모든 시술은 No Pain, No Bruise,
    - No Swelling라는 3No 의료서비스를 지향
    합니다.
    - 앞으로도 고품질의 관리와 서비스를 받을 수 있도록 노력하며
    - 사소한 불편까지 읽어주는 세심한 배려, 마음까지 읽는 서비스로
    + 모든 제품은 정품, 정량, 정품 장비 사용을 원칙으로 안전을
    + 최우선으로 하고 있으며, 모든 시술은 No Pain, No Bruise,
    + No Swelling라는 3No 의료서비스를 지향
    합니다.
    + 앞으로도 고품질의 관리와 서비스를 받을 수 있도록 노력하며
    + 사소한 불편까지 읽어주는 세심한 배려, 마음까지 읽는 서비스로
    무한 감동을 드릴 것을 약속합니다.

    - introduction_content4-3 - introduction_content4-3 + introduction_content4-3 + introduction_content4-3
@@ -287,4 +286,5 @@ + \ No newline at end of file diff --git a/src/main/resources/templates/web/layout/layoutHeader.html b/src/main/resources/templates/web/layout/layoutHeader.html index 2f42eae..9ee3325 100644 --- a/src/main/resources/templates/web/layout/layoutHeader.html +++ b/src/main/resources/templates/web/layout/layoutHeader.html @@ -1,109 +1,117 @@ - - - -
+ // 퀵메뉴 표시/숨김 함수 + function toggleQuickMenu() { + const quickMenu = document.querySelector('.quick-menu-simple'); + if (!quickMenu) return; // 요소가 없으면 종료 -
- - - + if (isMobile()) { + // 모바일: /index일 때만 표시 + quickMenu.style.display = isIndexPage() ? '' : 'none'; + } else { + // 데스크톱: 항상 표시 + quickMenu.style.display = ''; + } + } + function moveEvent() { + window.location.href = "https://petit.madeu.co.kr/webevent/selectListWebEventIntro.do"; + } + function moveReview() { + window.location.href = "https://petit.madeu.co.kr/webreview/selectListWebReviewIntro.do"; + } + + // 페이지 로드와 리사이즈 이벤트 연결 + window.addEventListener('load', toggleQuickMenu); + window.addEventListener('resize', toggleQuickMenu); + +
+ +
+ + + +
+
+ +
+
+ 고객후기 +
+
+ 이벤트 +
+ +
+ 다이어트센터 +
+ + +
+ 카카오톡 상담 +
+ + +
+
+ 전화 상담 +
+
+ 전화 상담 +
+
-
- -
-
- 이벤트 -
- -
- 다이어트센터 -
- - -
- 카카오톡 상담 -
- - -
-
- 전화 상담 -
-
- 전화 상담 -
-
-
- +
+ \ No newline at end of file diff --git a/src/main/resources/templates/web/webreview/procedureReviewSelect.html b/src/main/resources/templates/web/webreview/procedureReviewSelect.html new file mode 100644 index 0000000..00c8055 --- /dev/null +++ b/src/main/resources/templates/web/webreview/procedureReviewSelect.html @@ -0,0 +1,44 @@ + + + + + + + + + + +
+ +
+
+

+
+ + 조회 0 +
+
+
+
+
게시글을 불러오는 중...
+
+
+ +
+
+ + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/web/webreview/procedureReviewSelectList.html b/src/main/resources/templates/web/webreview/procedureReviewSelectList.html new file mode 100644 index 0000000..de1fd82 --- /dev/null +++ b/src/main/resources/templates/web/webreview/procedureReviewSelectList.html @@ -0,0 +1,34 @@ + + + + + + + + + +
+
+

고객후기

+

실제 고객님들의 생생한 후기를 확인하세요

+
+
+ +
+
+
고객후기를 불러오는 중...
+
+
+
+
+ + + + + \ No newline at end of file