견적문의 추가

This commit is contained in:
2026-04-27 13:05:00 +09:00
parent 218b9f0fb4
commit 1888333fc2
8 changed files with 491 additions and 61 deletions

View File

@@ -0,0 +1,54 @@
package com.owrawww.controller;
import com.owrawww.domain.Quote;
import com.owrawww.service.QuoteService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Controller
@RequestMapping("/bbs/insertQuote")
@RequiredArgsConstructor
public class QuoteController {
private final QuoteService quoteService;
@GetMapping
public String quoteForm(@ModelAttribute Quote quote) {
return "insertQuote";
}
@PostMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> quoteSubmit(@Valid @ModelAttribute Quote quote,
BindingResult bindingResult) {
Map<String, Object> result = new HashMap<>();
if (bindingResult.hasErrors()) {
List<String> errors = bindingResult.getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.toList());
result.put("success", false);
result.put("message", "입력값을 확인해주세요.");
result.put("errors", errors);
return ResponseEntity.badRequest().body(result);
}
boolean success = quoteService.submit(quote);
result.put("success", success);
if (!success) {
result.put("message", "처리 중 오류가 발생했습니다. 다시 시도해주세요.");
}
return ResponseEntity.ok(result);
}
}

View File

@@ -0,0 +1,47 @@
package com.owrawww.domain;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
@NoArgsConstructor
public class Quote {
private Long id;
@NotBlank(message = "성명을 입력해주세요.")
private String name;
@NotBlank(message = "이메일을 입력해주세요.")
@Email(message = "올바른 이메일 형식이 아닙니다.")
private String email;
@NotBlank(message = "연락처를 입력해주세요.")
@Pattern(regexp = "^[0-9\\-+\\s]{7,20}$", message = "연락처 형식이 올바르지 않습니다.")
private String tel;
@NotBlank(message = "제목을 입력해주세요.")
private String title;
@NotBlank(message = "내용을 입력해주세요.")
private String content;
private LocalDateTime createdAt;
private String code;
private String comment;
private Integer topCode;
private Integer leftCode;
private Integer subGubun;
private Integer depth;
private String telHash;
private String emailHash;
private String SolutionGubun;
}

View File

@@ -0,0 +1,9 @@
package com.owrawww.domain.mapper;
import com.owrawww.domain.Quote;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface QuoteMapper {
int insert(Quote quote);
}

View File

@@ -0,0 +1,34 @@
package com.owrawww.service;
import com.owrawww.domain.Quote;
import com.owrawww.domain.mapper.QuoteMapper;
import com.owrawww.util.AesUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class QuoteService {
private final QuoteMapper quoteMapper;
private final AesUtil aesUtil;
public boolean submit(Quote quote) {
Quote saved = new Quote();
saved.setCode("3");
saved.setTitle(quote.getTitle());
saved.setComment(quote.getContent());
saved.setName(quote.getName());
saved.setTel(aesUtil.encrypt(quote.getTel()));
saved.setTelHash(aesUtil.hash(quote.getTel()));
saved.setEmail(aesUtil.encrypt(quote.getEmail()));
saved.setEmailHash(aesUtil.hash(quote.getEmail()));
saved.setTopCode(1);
saved.setLeftCode(5);
saved.setSubGubun(1);
saved.setDepth(1);
saved.setSolutionGubun("SOL001");
return quoteMapper.insert(saved) > 0;
}
}

View File

@@ -0,0 +1,12 @@
<?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="com.owrawww.domain.mapper.QuoteMapper">
<insert id="insert" parameterType="com.owrawww.domain.Quote" useGeneratedKeys="true" keyProperty="id">
INSERT INTO application_table (code, title, name, phone, tel_hash, email, email_hash, comment, depth, in_date, top_code, left_code, sub_gubun,solution_gubun)
VALUES (#{code}, #{title}, #{name}, #{tel}, #{telHash}, #{email}, #{emailHash}, #{comment}, #{depth}, now(), #{topCode}, #{leftCode}, #{subGubun}, #{solutionGubun})
</insert>
</mapper>

View File

@@ -1,32 +1,15 @@
<!doctype html> <!doctype html>
<html lang="ko"> <html lang="ko"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/default}">
<head> <head>
<meta charset="utf-8"> <title>PG업 등록</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=yes"> <!-- 이 페이지 전용 CSS -->
<meta name="format-detection" content="telephone=no">
<title>회사소개 - 오라인포</title>
<script src="/js/jquery-3.7.1.min.js"></script>
<script src="/js/jquery-ui-1.13.3.min.js"></script>
<script src="/js/topmenu_script.js"></script>
<script src="/js/swiper.min.js"></script>
<script src="/js/aos.js"></script>
<script src="/js/gsap.min.js"></script>
<script src="/js/ScrollTrigger.min.js"></script>
<link rel="icon" href="/img/common/favicon.ico" type="image/x-icon">
<link rel="apple-touch-icon" sizes="180x180" href="/img/common/apple-touch-icon.png">
<link rel="stylesheet" href="/css/common.css">
<link rel="stylesheet" href="/css/sub.css">
<link rel="stylesheet" href="/css/aos.css">
<link rel="stylesheet" href="/css/swiper.min.css">
<script src="/js/feather.min.js"></script> <!-- feather 아이콘-->
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap" rel="stylesheet"> <!-- 영문폰트 -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" /> <!--구글머티리얼 아이콘-->
</head> </head>
<body> <body>
<div id="wrapper"> <div id="wrapper" layout:fragment="content">
<!-- ======================== HEADER --> <!-- ======================== HEADER -->
<header id="hd" class="sub"></header> <header id="hd" class="sub"></header>
@@ -279,7 +262,7 @@
<svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> <svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
PG시스템 상세보기 PG시스템 상세보기
</a> </a>
<a href="/bbs/partnership" class="evpg-btn-outline"> <a href="/bbs/insertQuote" class="evpg-btn-outline">
<svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg> <svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
견적 문의하기 견적 문의하기
</a> </a>
@@ -296,39 +279,7 @@
</div> </div>
</div> </div>
<footer id="ft"></footer> <th:block layout:fragment="scripts">
<!-- 퀵메뉴 -->
<div id="quick_wrap" class="quick_wrap">
<div class="quick-menu">
<ul class="quick_list">
<li>
<a href="https://www.owra.net/etc/solution" target="_blank">
<div class="icon_img"><img src="/img/common/pg_bn.png" alt="선 정산 서비스"></div>
</a>
</li>
<li>
<a href="https://holaedu.co.kr/" target="_blank">
<div class="icon_img"><img src="/img/common/hola_bn.png" alt="hola"></div>
</a>
</li>
<li>
<a href="delivera.co.kr" target="_blank">
<div class="icon_img"><img src="/img/common/del_bn.png" alt="배달시대"></div>
</a>
</li>
<li>
<a href="#">
<div class="icon_img"><img src="/img/common/ing_bg.png" alt="현재 진행 프로젝트"></div>
</a>
</li>
</ul>
<a href="#" class="top_btn pc"><span class="material-symbols-outlined">arrow_upward</span> TOP</a>
</div>
</div>
</div>
<script src="/js/sub.js"></script>
<script> <script>
/* 스크롤 페이드업 */ /* 스크롤 페이드업 */
const _obs = new IntersectionObserver(entries => { const _obs = new IntersectionObserver(entries => {
@@ -344,5 +295,4 @@
}); });
}); });
</script> </script>
</body> </th:block>
</html>

View File

@@ -0,0 +1,324 @@
<!doctype html>
<html lang="ko"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/default}">
<head>
<title>PG견적문의</title>
<body class="page-inquiry" >
<div id="wrapper" layout:fragment="content">
<!-- ======================== HEADER -->
<header id="hd" class="sub"></header>
<!-- ======================== CONTAINER -->
<div id="container">
<div id="container_wrapper">
<!-- ========= SUB CONTENT [s] ========= -->
<div class="sub_content sec-recreuit">
<div class="sub-section2">
<div class="sub-inner">
<div data-aos="fade-up">
<h2 class="sub-h2 text-center">
PG 견적 문의
</h2>
</div>
<div class="inquiry-wrap">
<!-- 좌측 타이틀 -->
<aside class="inquiry-aside" aria-label="폼 안내">
<h2 class="inquiry-aside__title" data-aos="fade-up">
견적문의
</h2>
</aside>
<!-- 우측 폼 -->
<div class="inquiry-form" role="main" data-aos="fade-up">
<form id="inquiryForm" th:action="@{/bbs/insertQuote}" method="post" novalidate>
<!-- Row 1: 성명 + 이메일 -->
<div class="form-row">
<div class="form-group">
<label class="form-label" for="inp_name">
성명
<span class="required" aria-hidden="true">*</span>
</label>
<input
type="text"
id="inp_name"
name="name"
class="form-input"
placeholder="성함을 입력해주세요."
autocomplete="name"
required
>
<span class="form-error-msg" role="alert" id="err_name">성명을 입력해주세요.</span>
</div>
<div class="form-group">
<label class="form-label" for="inp_email">
이메일
<span class="required" aria-hidden="true">*</span>
</label>
<input
type="email"
id="inp_email"
name="email"
class="form-input"
placeholder="이메일을 입력해주세요."
autocomplete="email"
required
>
<span class="form-error-msg" role="alert" id="err_email">올바른 이메일을 입력해주세요.</span>
</div>
</div>
<!-- Row 2: 연락처 + 지원분야 -->
<div class="form-row">
<div class="form-group">
<label class="form-label" for="inp_tel">
연락처
<span class="required" aria-hidden="true">*</span>
</label>
<input
type="tel"
id="inp_tel"
name="tel"
class="form-input"
placeholder="연락처를 입력해주세요."
autocomplete="tel"
required
>
<span class="form-error-msg" role="alert" id="err_tel">연락처를 입력해주세요.</span>
</div>
<div class="form-group">
</div>
</div>
<!-- Row 3: 제목 -->
<div class="form-row form-row--full">
<div class="form-group">
<label class="form-label" for="inp_title">
제목
<span class="required" aria-hidden="true">*</span>
</label>
<input
type="text"
id="inp_title"
name="title"
class="form-input"
placeholder="제목을 입력해주세요."
required
>
<span class="form-error-msg" role="alert" id="err_title">제목을 입력해주세요.</span>
</div>
</div>
<!-- Row 4: 내용 -->
<div class="form-row form-row--full">
<div class="form-group">
<label class="form-label" for="inp_content">
내용
<span class="required" aria-hidden="true">*</span>
</label>
<textarea
id="inp_content"
name="content"
class="form-textarea"
placeholder="문의내용을 입력해주세요."
required
></textarea>
<span class="form-error-msg" role="alert" id="err_content">내용을 입력해주세요.</span>
</div>
</div>
<!-- 개인정보 수집 및 이용 안내 -->
<div class="privacy-section">
<h3 class="privacy-section__title">개인정보 수집 및 이용 안내</h3>
<div class="privacy-box" tabindex="0" aria-label="개인정보 수집 및 이용 안내 내용">
<p>(주)오라인포는 아래의 목적으로 개인정보를 수집 및 이용하며, 방문자의 개인정보를 안전하게 취급하는데 최선을 다합니다.</p>
<p>
<strong>1. 수집목적</strong>
원활한 서비스를 위한 이용자 식별<br>
문의사항(광고제휴, 채용문의)의 처리 및 결과 통보
</p>
<p>
<strong>2. 수집항목</strong>
필수항목 : 성명, 이메일, 연락처, 제목, 문의내용<br>
</p>
<p>
<strong>3. 보유 및 이용기간</strong>
수집된 개인정보는 수집 및 이용 목적이 달성된 후 지체없이 파기합니다.<br>
단, 관련 법령에 의거하여 일정 기간 보존이 필요한 경우 해당 기간 동안 보관합니다.
</p>
<p>귀하는 개인정보 수집·이용에 대한 동의를 거부할 권리가 있으며, 동의 거부 시 채용문의 및 입사지원 서비스 이용이 제한됩니다.</p>
</div>
<!-- 동의 체크박스 -->
<label class="form-agree" id="agreeLabel">
<span class="form-agree__cb">
<input
type="checkbox"
id="inp_agree"
name="agree"
required
aria-required="true"
aria-describedby="err_agree"
>
<span class="cb-custom" aria-hidden="true"></span>
</span>
<span class="form-agree__label">
개인정보 수집·이용에 대해 동의합니다.
<span class="required" aria-hidden="true">*</span>
</span>
</label>
<span class="form-error-msg" role="alert" id="err_agree">개인정보 수집·이용에 동의해주세요.</span>
</div>
<!-- 접수하기 버튼 -->
<div class="form-submit-wrap">
<button type="submit" class="form-submit-btn" aria-label="견적문의 접수하기">
견적 문의하기
<span class="btn-arrow material-symbols-outlined" aria-hidden="true">arrow_forward</span>
</button>
</div>
</form>
</div>
<!-- // 우측 폼 -->
</div>
<!-- // 채용문의 & 입사지원 폼 -->
</div>
</div>
<!-- ========= SUB CONTENT [e] ========= -->
</div>
</div>
</div>
</div>
<th:block layout:fragment="scripts">
<script>
/* ========================== 제휴문의 폼 */
$(document).ready(function () {
const $form = $('#inquiryForm');
/* ---- 유효성 검사 규칙 ---- */
const validators = {
name: { el: '#inp_name', err: '#err_name', check: function (v) { return v.trim().length > 0; } },
email: { el: '#inp_email', err: '#err_email', check: function (v) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim()); } },
tel: { el: '#inp_tel', err: '#err_tel', check: function (v) { return /^[0-9\-+\s]{7,20}$/.test(v.trim()); } },
dept: { el: '#inp_dept', err: '#err_dept', check: function (v) { return v !== '' && v !== null; } },
title: { el: '#inp_title', err: '#err_title', check: function (v) { return v.trim().length > 0; } },
content: { el: '#inp_content', err: '#err_content', check: function (v) { return v.trim().length > 0; } },
agree: { el: '#inp_agree', err: '#err_agree', check: function () { return $('#inp_agree').is(':checked'); } }
};
/* ---- 단일 필드 유효성 표시 ---- */
function validateField(key) {
const rule = validators[key];
const $el = $(rule.el);
const $err = $(rule.err);
const val = key === 'agree' ? '' : $el.val();
const ok = rule.check(val);
$el.toggleClass('is-error', !ok);
$err.toggleClass('is-show', !ok);
return ok;
}
/* ---- 실시간 검사 ---- */
$.each(validators, function (key, rule) {
const $el = $(rule.el);
const evt = (key === 'dept' || key === 'agree') ? 'change' : 'input';
$el.on(evt, function () { validateField(key); });
});
/* ---- 연락처: 숫자·하이픈·공백·+만 입력 허용 ---- */
$('#inp_tel').on('input', function () {
this.value = this.value.replace(/[^0-9\-+\s]/g, '');
});
/* ---- 폼 제출 ---- */
$form.on('submit', function (e) {
e.preventDefault();
let allOk = true;
$.each(validators, function (key) {
if (!validateField(key)) allOk = false;
});
if (!allOk) {
const $firstErr = $form.find('.is-error').first();
if ($firstErr.length) {
$firstErr[0].focus();
$('html, body').animate({ scrollTop: $firstErr.offset().top - 120 }, 300);
}
return;
}
/* 유효성 통과 시 AJAX 제출 */
const $btn = $form.find('[type="submit"]');
$btn.prop('disabled', true);
$.ajax({
url: $form.attr('action'),
method: 'POST',
data: $form.serialize(),
success: function (res) {
if (res.success) {
showInquiryModal();
} else {
alert(res.message || '처리 중 오류가 발생했습니다. 다시 시도해주세요.');
}
},
error: function () {
alert('처리 중 오류가 발생했습니다. 다시 시도해주세요.');
},
complete: function () {
$btn.prop('disabled', false);
}
});
});
/* ---- 완료 모달 ---- */
function showInquiryModal() {
if ($('#inquiryModal').length === 0) {
$('body').append(`
<div id="inquiryModal" role="dialog" aria-modal="true" aria-labelledby="modalTitle" style="display:none;">
<div class="inquiry-modal__backdrop"></div>
<div class="inquiry-modal__box">
<span class="inquiry-modal__icon material-symbols-outlined">check_circle</span>
<h3 class="inquiry-modal__title" id="modalTitle">문의가 접수되었습니다</h3>
<p class="inquiry-modal__desc">빠른 시일 내에 담당자가<br>연락드리겠습니다.</p>
<button type="button" class="inquiry-modal__close" id="inquiryModalClose">확인</button>
</div>
</div>
`);
$(document).on('click', '#inquiryModalClose, .inquiry-modal__backdrop', function () {
closeInquiryModal();
});
$(document).on('keydown', function (e) {
if (e.key === 'Escape' && $('#inquiryModal').is(':visible')) closeInquiryModal();
});
}
$('#inquiryModal').css('display', 'flex').hide().fadeIn(200);
$('#inquiryModalClose').focus();
$('body').css('overflow', 'hidden');
}
function closeInquiryModal() {
$('#inquiryModal').fadeOut(200);
$('body').css('overflow', '');
$form[0].reset();
$form.find('.is-error').removeClass('is-error');
$form.find('.form-error-msg.is-show').removeClass('is-show');
}
});
</script>
</th:block>

View File

@@ -136,7 +136,7 @@
<li>2차 면접 : 1차 서류 전형 완료 후 합격자에 한해 개별 연락</li> <li>2차 면접 : 1차 서류 전형 완료 후 합격자에 한해 개별 연락</li>
</ul> </ul>
<div class="ri-apply-wrap"> <div class="ri-apply-wrap">
<a href="#" class="ri-apply-btn"> <a href="/careers/recruitment" class="ri-apply-btn">
지원하기 지원하기
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<line x1="5" y1="12" x2="19" y2="12"/> <line x1="5" y1="12" x2="19" y2="12"/>