최초 커밋
This commit is contained in:
commit
c8f5efb6b7
14652 files changed
+4812531
No files matched your search
@@ -0,0 +1,716 @@
|
||||
@charset "UTF-8";
|
||||
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@100;300;400;500;700;900&display=swap");
|
||||
/**
|
||||
* 반응형 폰트 크기 (responsive font)
|
||||
* @param {number} $min-px - 최소 폰트 크기 (px)
|
||||
* @param {number} $max-px - 최대 폰트 크기 (px)
|
||||
* @param {number} $min-vw - 최소 뷰포트 너비 (기본값: 375px)
|
||||
* @param {number} $max-vw - 최대 뷰포트 너비 (기본값: 1920px)
|
||||
*/
|
||||
/**
|
||||
* 박스 그림자 (box-shadow)
|
||||
* @param {number} $x - X 오프셋 (기본값: 0px)
|
||||
* @param {number} $y - Y 오프셋 (기본값: 3px)
|
||||
* @param {number} $blur - 블러 반경 (기본값: 6px)
|
||||
* @param {number} $spread - 확산 반경 (기본값: 0px)
|
||||
* @param {color} $color - 색상 (기본값: rgba(0, 0, 0, 0.25))
|
||||
* @param {boolean} $important - !important 사용 여부 (기본값: false)
|
||||
*/
|
||||
/**
|
||||
* 텍스트 그림자(text-shadow)
|
||||
* @param {number} $x - X 오프셋 (기본값: 0px)
|
||||
* @param {number} $y - Y 오프셋 (기본값: 1px)
|
||||
* @param {number} $blur - 블러 반경 (기본값: 0px)
|
||||
* @param {color} $color - 색상 (기본값: rgba(0, 0, 0, 0.25))
|
||||
* @param {boolean} $important - !important 사용 여부 (기본값: false)
|
||||
*/
|
||||
/**
|
||||
* 미디어 쿼리 mixin
|
||||
* @param {string|number} $width - 브레이크포인트 이름 또는 픽셀 값
|
||||
* @param {string} $type - 'min' 또는 'max'
|
||||
* @example
|
||||
* @include mq('desktop') { ... }
|
||||
* @include mq(1920px) { ... }
|
||||
* @include mq('tablet', 'max') { ... }
|
||||
*/
|
||||
html {
|
||||
font-size: 10px;
|
||||
}
|
||||
html[lang=ko-KR], html[lang=ko] {
|
||||
font-family: "Noto Sans KR", sans-serif;
|
||||
}
|
||||
html[lang=en],
|
||||
html *[lang=en] {
|
||||
font-family: "Noto Sans KR", Arial, sans-serif, serif;
|
||||
}
|
||||
html[lang=en] body,
|
||||
html *[lang=en] body {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
}
|
||||
html body {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
color: #000;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
/* ====================================================================== */
|
||||
/* [Web Font]
|
||||
/* ====================================================================== */
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none;
|
||||
text-decoration: underline;
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type=button]::-moz-focus-inner,
|
||||
[type=reset]::-moz-focus-inner,
|
||||
[type=submit]::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button:-moz-focusring,
|
||||
[type=button]:-moz-focusring,
|
||||
[type=reset]:-moz-focusring,
|
||||
[type=submit]:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
legend {
|
||||
box-sizing: border-box;
|
||||
color: inherit;
|
||||
display: table;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
[type=checkbox],
|
||||
[type=radio] {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
[type=number]::-webkit-inner-spin-button,
|
||||
[type=number]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
[type=search]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* reset */
|
||||
html * {
|
||||
box-sizing: border-box;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
body,
|
||||
div,
|
||||
dl,
|
||||
dt,
|
||||
dd,
|
||||
ul,
|
||||
ol,
|
||||
li,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
pre,
|
||||
code,
|
||||
form,
|
||||
figure,
|
||||
legend,
|
||||
input,
|
||||
textarea,
|
||||
button,
|
||||
p,
|
||||
blockquote,
|
||||
th,
|
||||
td,
|
||||
form,
|
||||
fieldset,
|
||||
blockquote,
|
||||
iframe {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-moz-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
article,
|
||||
aside,
|
||||
canvas,
|
||||
details,
|
||||
embed,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
menu,
|
||||
nav,
|
||||
section,
|
||||
summary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
command,
|
||||
datalist,
|
||||
keygen,
|
||||
mark,
|
||||
meter,
|
||||
progress,
|
||||
rp,
|
||||
rt,
|
||||
ruby,
|
||||
time,
|
||||
wbr {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
img {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
max-width: 100%;
|
||||
border: 0;
|
||||
image-rendering: -moz-crisp-edges; /* firefox */
|
||||
image-rendering: -o-crisp-edges; /* opera */
|
||||
image-rendering: -webkit-optimize-contrast; /* chrome(비표준) */
|
||||
image-rendering: crisp-edges;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol,
|
||||
li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
legend,
|
||||
caption {
|
||||
position: relative;
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
}
|
||||
a:link {
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover, a:focus, a:active, a:visited {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
em,
|
||||
i,
|
||||
address,
|
||||
cite {
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
button,
|
||||
table {
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
textarea,
|
||||
input,
|
||||
select {
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
outline-color: -moz-use-text-color;
|
||||
outline-width: medium;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
select::-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 스크롤바 스타일 */
|
||||
::-webkit-scrollbar {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #aaa;
|
||||
border-radius: 8px;
|
||||
background-clip: padding-box;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* clearfix */
|
||||
.clearfix {
|
||||
/* IE 지원 방식 : *zoom: 1;*/
|
||||
}
|
||||
.clearfix:before, .clearfix:after {
|
||||
content: "";
|
||||
display: block;
|
||||
}
|
||||
.clearfix:after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* hidden */
|
||||
.blind {
|
||||
position: absolute;
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.d-none {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
[class*=gra] {
|
||||
position: relative;
|
||||
}
|
||||
[class*=gra]::before, [class*=gra]::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
}
|
||||
[class*=gra]::after {
|
||||
inset: 0;
|
||||
padding: 1px;
|
||||
-webkit-mask: var(--mask-contents);
|
||||
mask: var(--mask-contents);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* toggle btn*/
|
||||
.toggle-area {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 6px;
|
||||
width: 172px;
|
||||
height: 48px;
|
||||
background: #e5dfd3;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 50px;
|
||||
box-shadow: 0 3px 8px 0 rgba(0, 0, 0, 0.2) inset;
|
||||
}
|
||||
.toggle-area input[type=radio] {
|
||||
display: none;
|
||||
}
|
||||
.toggle-area input[type=radio]:checked + label {
|
||||
color: white;
|
||||
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.5));
|
||||
text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.5), 1px -1px 0 rgba(0, 0, 0, 0.5), -1px 1px 0 rgba(0, 0, 0, 0.5), 1px 1px 0 rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.toggle-area input[type=radio]:checked:nth-of-type(2) ~ .toggle-slider {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
.toggle-area label {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 2;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
color: rgba(16, 16, 16, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
width: calc(50% - 4px);
|
||||
height: calc(100% - 8px);
|
||||
background: linear-gradient(179deg, #ff5c00 47.43%, #943600 99.56%);
|
||||
border-radius: 50px;
|
||||
transition: transform 0.3s ease;
|
||||
z-index: 1;
|
||||
box-shadow: 0 -1px 2px 0 rgba(0, 0, 0, 0.2), 0 1px 2px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.toggle-slider::after {
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #ffb68e 0%, #cd6f3b 24%, #ffdbc8 50%, #682500 83%, #a04411 100%);
|
||||
inset: 0;
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.modal.active {
|
||||
display: flex;
|
||||
}
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.modal-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
}
|
||||
.modal-header h2 {
|
||||
font-size: 24px;
|
||||
color: #1f2937;
|
||||
}
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 32px;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.modal-close:hover {
|
||||
background: #f3f4f6;
|
||||
color: #4b5563;
|
||||
}
|
||||
.modal-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 60px;
|
||||
}
|
||||
.modal .spinner {
|
||||
border: 3px solid #e5e7eb;
|
||||
border-top: 3px solid #2563eb;
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
/* media query */
|
||||
/*
|
||||
@media all and (min-width: 3841px) {
|
||||
html {
|
||||
//font-size: 14px;
|
||||
}
|
||||
}
|
||||
// 3200px 초과 ~ 3840px: 4K 디스플레이
|
||||
@media all and (min-width: 3201px) and (max-width: 3840px) {
|
||||
html {
|
||||
//font-size: 13px;
|
||||
}
|
||||
}
|
||||
// 2560px 초과 ~ 3200px: 매우 큰 데스크톱
|
||||
@media all and (min-width: 2561px) and (max-width: 3200px) {
|
||||
html {
|
||||
//font-size: 12px;
|
||||
}
|
||||
}
|
||||
// 1920px 초과 ~ 2560px: 큰 데스크톱
|
||||
@media all and (min-width: 1921px) and (max-width: 2560px) {
|
||||
html {
|
||||
//font-size: 11px;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
*/
|
||||
@media all and (min-width: 1024px) {
|
||||
html {
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
@media all and (min-width: 1024px) and (max-width: 1920px) {
|
||||
html {
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
/*
|
||||
// 1023px 이하: 태블릿
|
||||
@media all and (max-width: 1023px) {
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 767px 이하
|
||||
@media all and (max-width: 767px) {
|
||||
html {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// 639px 이하
|
||||
@media all and (max-width: 639px) {
|
||||
html {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
// 479px 이하
|
||||
@media all and (max-width: 479px) {
|
||||
html {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// 359px 이하
|
||||
@media all and (max-width: 359px) {
|
||||
html {
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
// 319px 이하
|
||||
@media all and (max-width: 319px) {
|
||||
html {
|
||||
font-size: 8px;
|
||||
}
|
||||
}
|
||||
*/
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1 @@
|
||||
<svg width="276" height="135" fill="none" xmlns="http://www.w3.org/2000/svg"><g opacity=".35" fill="#fff"><path d="M126.53 81.446c-8.872 3.213-13.546.174-15.62-2.074l-23.7 25.861c-20.014 21.67-44.027 30.023-62.261 22.833l94.655-102.422s9.409-11.67 20.237-19.243h-.009c.772-.56 1.067-1.096.814-2.12-.623-2.542-6.963-4.23-14.506-4.205h-.006l-18.839.046s-6.918-.28-11.502.009c-.95.06-13.033.576-17.116.917-4.17.347-14.935 2.055-17.951 2.804-19.212 4.766-37.584 13.223-51.85 28.224-18.766 19.727-28.538 43.07-27.516 65.73.751 16.687 7.433 31.356 18.812 41.299 10.673 9.328 24.061 14.213 38.528 14.21 3.708 0 7.49-.32 11.311-.969 18.899-3.21 37.371-14.093 53.448-31.503l34.266-37.384c-1.145-1.4-4.533-4.428-11.198-2.016l.003.003ZM3.648 96.758C2.914 80.456 10.38 63.053 24.874 47.96 42.026 30.1 56.046 24.79 69.238 24.018c6.21-.362 11.88.372 18.025 3.2L7.954 113.252c-2.545-4.766-4.032-10.375-4.306-16.493Z"/><path d="M275.378 54.144c-1.561-16.886-9.802-31.984-22.602-41.414-30.568-22.518-66.159-14.916-97.668 20.846l-35.901 38.842s.003.002.003.005l-3.006 3.261c.722.948 3.302 3.288 10.023.878 8.291-2.979 14.018.13 16.849 3.176l5.066-5.489 23.367-25.278.185-.206c23.991-27.283 46.853-33.238 67.942-17.701 7.607 5.603 12.522 14.788 13.486 25.196 1.443 15.58-5.413 33.39-20.661 48.817-21.345 21.591-41.852 28.048-62.603 21.388l30.783-33.94-16.451-15.34-45.999 50.718s-9.134 11.234-19.76 18.77c-1.155.693-1.627 1.22-1.326 2.437.606 2.449 6.593 4.102 13.845 4.177l.063.028 19.199-.009s16.314.06 19.76-.185c.656-.046 12.229-.96 14.51-1.148 2.037-.166 11.976-1.949 14.21-2.512 19.521-4.902 35.241-12.912 50.041-28.851 19.232-20.71 28.694-44.317 26.642-66.469l.003.003Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1 @@
|
||||
<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg" data-name="레이어_2"><g data-name="레이어_1" class="layer"><path d="M31.59 31.61c-1.81.72-2.76.04-3.19-.47l-4.83 5.85c-4.09 4.9-8.97 6.79-12.69 5.16l19.3-23.16s1.93-2.63 4.12-4.35c.16-.12.21-.25.18-.47-.14-.58-1.42-.97-2.96-.95l-3.83.02s-1.42-.07-2.35 0c-.19.02-2.67.12-3.48.21-.86.07-3.04.46-3.66.63-3.91 1.07-7.67 2.98-10.57 6.37-3.83 4.46-5.82 9.74-5.6 14.85.16 3.77 1.52 7.09 3.83 9.34 2.18 2.11 4.9 3.21 7.86 3.21.76 0 1.54-.07 2.32-.21 3.85-.72 7.61-3.18 10.9-7.13l6.98-8.44c-.23-.32-.91-1-2.28-.46h-.06zM6.55 35.07c-.16-3.69 1.36-7.62 4.32-11.03 3.5-4.04 6.34-5.23 9.05-5.41 1.26-.09 2.41.09 3.68.72L7.43 38.8c-.53-1.07-.82-2.35-.88-3.72v-.02z" style="fill:#ca2829"/><path d="M61.56 25.31c-.31-3.84-1.98-7.27-4.59-9.41-6.23-5.13-13.46-3.39-19.87 4.74l-7.3 8.83-.6.74c.14.21.66.75 2.04.19 1.69-.68 2.86.04 3.42.72l1.03-1.25 4.75-5.74.04-.05c4.88-6.2 9.53-7.57 13.83-4.02 1.56 1.28 2.55 3.37 2.74 5.72.29 3.55-1.11 7.58-4.2 11.1-4.34 4.92-8.52 6.37-12.72 4.86l6.27-7.72-3.35-3.49-9.36 11.53s-1.87 2.55-4.03 4.27c-.23.16-.33.28-.27.54.12.56 1.34.93 2.82.95h3.93s3.33.02 4.03-.04c.14-.02 2.49-.23 2.96-.26.41-.04 2.43-.44 2.9-.56 3.97-1.12 7.18-2.93 10.18-6.57 3.91-4.71 5.84-10.08 5.43-15.12l-.08.04z" style="fill:#263b7c"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" fill="#1f1f1f"><path d="m313-440 224 224-57 56-320-320 320-320 57 56-224 224h487v80H313Z"/></svg>
|
||||
|
After Width: | Height: | Size: 185 B |
@@ -0,0 +1 @@
|
||||
<svg width="100" height="100" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h100C80.15 4.572 65.782 29.834 52.14 53.82 38.652 77.534 25.874 100 9.21 100H0V0Z" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 192 B |
@@ -0,0 +1 @@
|
||||
"use strict";function safeGrad(t,a,e){var r=arguments.length>3&&void 0!==arguments[3]&&arguments[3];if(!a||void 0===a.left||void 0===a.right)return e[0].color;var i=a.left,n=a.right,o=a.top,l=a.bottom,s=r?t.createLinearGradient(0,o,0,l):t.createLinearGradient(i,0,n,0);return e.forEach(function(t){return s.addColorStop(t.stop,t.color)}),s}var hideNullPointsPlugin={id:"hideNullPoints",beforeDatasetsDraw:function(t){t.data.datasets.forEach(function(t){if("line"===t.type){var a=t.data;Array.isArray(a)&&(Array.isArray(t.pointRadius)||(t.pointRadius=a.map(function(){return t.pointRadius||6})),Array.isArray(t.pointHoverRadius)||(t.pointHoverRadius=a.map(function(){return t.pointHoverRadius||8})),t.pointRadius=a.map(function(a,e){return null===a?0:t.pointRadius[e]}),t.pointHoverRadius=a.map(function(a,e){return null===a?0:t.pointHoverRadius[e]}))}})}},highlightXFactory=function(t){var a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:cssVar("--bg-chart-highlight");return{id:"highlightX_".concat(t),beforeDatasetsDraw:function(e){if(e.chartArea){var r=e.ctx,i=e.chartArea,n=e.scales.x,o=e.data.labels.indexOf(t);if(-1!==o){var l=n.getPixelForValue(o+1)-n.getPixelForValue(o);isNaN(l)&&o>0&&(l=n.getPixelForValue(o)-n.getPixelForValue(o-1)),r.save(),r.fillStyle=a,r.fillRect(n.getPixelForValue(o)-l/2,i.top,l,i.bottom-i.top),r.restore()}}}}};function createMixedValuePlugin(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:10;return{id:"mixedValuePlugin",afterDatasetsDraw:function(a){var e=a.ctx,r=[],i=[],n=[];a.data.datasets.forEach(function(t,e){var r=a.getDatasetMeta(e);"line"===t.type&&r&&r.data&&r.data.forEach(function(a,e){var r,i,o=t.data[e];if(null!=o){var l=Array.isArray(t.pointRadius)?null!==(r=t.pointRadius[e])&&void 0!==r?r:3:null!==(i=t.pointRadius)&&void 0!==i?i:3;n.push({x:a.x,y:a.y,radius:l})}})}),a.data.datasets.forEach(function(n,o){var l=a.getDatasetMeta(o);l&&l.data&&l.data.forEach(function(a,l){var s=n.data[l];if(null!=s){var d=String(s);e.font="bold 1.2rem Noto Sans KR, sans-serif";var f,u=e.measureText(d).width;"bar"===n.type?f=a.y:"line"===n.type&&(f=a.y-t);var c={datasetIndex:o,elementIndex:l,element:a,value:s,type:n.type,x:a.x,y:f,originalY:f,textWidth:u,textHeight:20};"bar"===n.type?r.push(c):"line"===n.type&&i.push(c)}})});var o=function(t,a){var e=Math.abs(t.x-a.x)<10,r=Math.abs(t.y-a.y)<25;return e&&r};r.forEach(function(t){n.forEach(function(a){(function(t,a){if(!(Math.abs(t.x-a.x)<10))return!1;var e=t.textWidth/2,r=(t.y,t.y,a.radius+3),i=Math.abs(t.x-a.x),n=Math.abs(t.y-a.y);return i<e+r&&n<r+10})(t,a)&&(t.y=a.y-a.radius-5)})}),i.forEach(function(t){r.forEach(function(a){o(t,a)&&(t.y=a.y-20)})});for(var l=0;l<i.length;l++)for(var s=l+1;s<i.length;s++)o(i[l],i[s])&&(i[s].y=i[l].y-20);for(var d=0;d<r.length;d++)for(var f=d+1;f<r.length;f++)o(r[d],r[f])&&(r[f].y=r[d].y-20);[].concat(r,i).forEach(function(t){var r=a.data.datasets[t.datasetIndex];e.save(),e.font="bold 1.2rem Noto Sans KR, sans-serif",e.textAlign="center",e.textBaseline="bottom";var i=String(t.value);if("bar"===r.type)e.strokeStyle="white",e.lineWidth=3,e.fillStyle=cssVar("--chart-mixbar"),e.strokeText(i,t.x,t.y),e.fillText(i,t.x,t.y);else if("line"===r.type){var n=e.measureText(i).width,o=t.x-n/2-4,l=t.y-12-4,s=n+8;e.fillStyle="rgba(233, 192, 192, 0.5)",e.beginPath(),e.roundRect(o,l,s,20,4),e.fill(),e.strokeStyle="white",e.lineWidth=3,e.fillStyle=cssVar("--chart-mixline"),e.strokeText(i,t.x,t.y),e.fillText(i,t.x,t.y)}e.restore()})}}}function createMixedPointGradientPlugin(t){return{id:"mixedPointGradient",afterDatasetsDraw:function(a){var e=a.ctx;a.data.datasets.forEach(function(r,i){var n=a.getDatasetMeta(i);"line"===r.type&&n&&n.data&&n.data.forEach(function(a,i){var n,o,l=a.x,s=a.y,d=Array.isArray(r.pointRadius)?null!==(n=r.pointRadius[i])&&void 0!==n?n:3:null!==(o=r.pointRadius)&&void 0!==o?o:3;if(0!==d){var f=l-d+2*d*.3333,u=s-d+2*d*.3333,c=.7862*d,h=e.createRadialGradient(f,u,0,f,u,c);(t||[{stop:1,color:cssVar("--bg-chart-mixbullet")}]).forEach(function(t){return h.addColorStop(t.stop,t.color)}),e.save(),e.beginPath(),e.arc(l,s,d,0,2*Math.PI),e.fillStyle=h,e.fill(),e.shadowColor="rgba(0,0,0,0.4)",e.shadowBlur=4,e.shadowOffsetX=0,e.shadowOffsetY=0,e.strokeStyle="rgba(255,255,255)",e.lineWidth=2,e.stroke(),e.restore()}})})}}}function createPointGradientPluginWithBorder(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return{id:"pointGradientWithBorder",afterDatasetsDraw:function(a){var e=a.ctx,r=[];a.data.datasets.forEach(function(i,n){var o=a.getDatasetMeta(n);o&&"line"===o.type&&!o.hidden&&o.data.forEach(function(a,o){var l,s;if(void 0!==a&&null!==i.data[o]){var d=a.x,f=a.y,u=i.data[o],c=Array.isArray(i.pointRadius)?null!==(l=i.pointRadius[o])&&void 0!==l?l:3:null!==(s=i.pointRadius)&&void 0!==s?s:3;if(0!==c){var h=d-c+2*c*.3333,v=f-c+2*c*.3333,x=.7862*c,g=e.createRadialGradient(h,v,0,h,v,x),y=t[i.label];if(y&&Array.isArray(y)?y.forEach(function(t){return g.addColorStop(t.stop,t.color)}):Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
"use strict";function safeGrad(t,a,e){var r=arguments.length>3&&void 0!==arguments[3]&&arguments[3];if(!a||void 0===a.left||void 0===a.right)return e[0].color;var i=a.left,n=a.right,l=a.top,o=a.bottom,s=r?t.createLinearGradient(0,l,0,o):t.createLinearGradient(i,0,n,0);return e.forEach(function(t){return s.addColorStop(t.stop,t.color)}),s}var hideNullPointsPlugin={id:"hideNullPoints",beforeDatasetsDraw:function(t){t.data.datasets.forEach(function(t){if("line"===t.type){var a=t.data;Array.isArray(a)&&(Array.isArray(t.pointRadius)||(t.pointRadius=a.map(function(){return t.pointRadius||6})),Array.isArray(t.pointHoverRadius)||(t.pointHoverRadius=a.map(function(){return t.pointHoverRadius||8})),t.pointRadius=a.map(function(a,e){return null===a?0:t.pointRadius[e]}),t.pointHoverRadius=a.map(function(a,e){return null===a?0:t.pointHoverRadius[e]}))}})}},highlightXFactory=function(t){var a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:cssVar("--bg-chart-highlight");return{id:"highlightX_".concat(t),beforeDatasetsDraw:function(e){if(e.chartArea){var r=e.ctx,i=e.chartArea,n=e.scales.x,l=e.data.labels.indexOf(t);if(-1!==l){var o=n.getPixelForValue(l+1)-n.getPixelForValue(l);isNaN(o)&&l>0&&(o=n.getPixelForValue(l)-n.getPixelForValue(l-1)),r.save(),r.fillStyle=a,r.fillRect(n.getPixelForValue(l)-o/2,i.top,o,i.bottom-i.top),r.restore()}}}}};function createMixedValuePlugin(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:10;return{id:"mixedValuePlugin",afterDatasetsDraw:function(a){var e=a.ctx,r=[],i=[],n=[];a.data.datasets.forEach(function(t,e){var r=a.getDatasetMeta(e);"line"===t.type&&r&&r.data&&r.data.forEach(function(a,e){var r,i,l=t.data[e];if(null!=l){var o=Array.isArray(t.pointRadius)?null!==(r=t.pointRadius[e])&&void 0!==r?r:3:null!==(i=t.pointRadius)&&void 0!==i?i:3;n.push({x:a.x,y:a.y,radius:o})}})}),a.data.datasets.forEach(function(n,l){var o=a.getDatasetMeta(l);o&&o.data&&o.data.forEach(function(a,o){var s=n.data[o];if(null!=s){var d=String(s);e.font="bold 1.2rem Noto Sans KR, sans-serif";var f,u=e.measureText(d).width;"bar"===n.type?f=a.y:"line"===n.type&&(f=a.y-t);var c={datasetIndex:l,elementIndex:o,element:a,value:s,type:n.type,x:a.x,y:f,originalY:f,textWidth:u,textHeight:20};"bar"===n.type?r.push(c):"line"===n.type&&i.push(c)}})});var l=function(t,a){var e=Math.abs(t.x-a.x)<10,r=Math.abs(t.y-a.y)<25;return e&&r};r.forEach(function(t){n.forEach(function(a){(function(t,a){if(!(Math.abs(t.x-a.x)<10))return!1;var e=t.textWidth/2,r=(t.y,t.y,a.radius+3),i=Math.abs(t.x-a.x),n=Math.abs(t.y-a.y);return i<e+r&&n<r+10})(t,a)&&(t.y=a.y-a.radius-5)})}),i.forEach(function(t){r.forEach(function(a){l(t,a)&&(t.y=a.y-20)})});for(var o=0;o<i.length;o++)for(var s=o+1;s<i.length;s++)l(i[o],i[s])&&(i[s].y=i[o].y-20);for(var d=0;d<r.length;d++)for(var f=d+1;f<r.length;f++)l(r[d],r[f])&&(r[f].y=r[d].y-20);[].concat(r,i).forEach(function(t){var r=a.data.datasets[t.datasetIndex];e.save(),e.font="bold 1.2rem Noto Sans KR, sans-serif",e.textAlign="center",e.textBaseline="bottom";var i=String(t.value);if("bar"===r.type)e.strokeStyle="white",e.lineWidth=3,e.fillStyle=cssVar("--chart-mixbar"),e.strokeText(i,t.x,t.y),e.fillText(i,t.x,t.y);else if("line"===r.type){var n=e.measureText(i).width,l=t.x-n/2-4,o=t.y-12-4,s=n+8;e.fillStyle="rgba(233, 192, 192, 0.5)",e.beginPath(),e.roundRect(l,o,s,20,4),e.fill(),e.strokeStyle="white",e.lineWidth=3,e.fillStyle=cssVar("--chart-mixline"),e.strokeText(i,t.x,t.y),e.fillText(i,t.x,t.y)}e.restore()})}}}function createMixedPointGradientPlugin(t){return{id:"mixedPointGradient",afterDatasetsDraw:function(a){var e=a.ctx;a.data.datasets.forEach(function(r,i){var n=a.getDatasetMeta(i);"line"===r.type&&n&&n.data&&n.data.forEach(function(a,i){var n,l,o=a.x,s=a.y,d=Array.isArray(r.pointRadius)?null!==(n=r.pointRadius[i])&&void 0!==n?n:3:null!==(l=r.pointRadius)&&void 0!==l?l:3;if(0!==d){var f=o-d+2*d*.3333,u=s-d+2*d*.3333,c=.7862*d,h=e.createRadialGradient(f,u,0,f,u,c);(t||[{stop:1,color:cssVar("--bg-chart-mixbullet")}]).forEach(function(t){return h.addColorStop(t.stop,t.color)}),e.save(),e.beginPath(),e.arc(o,s,d,0,2*Math.PI),e.fillStyle=h,e.fill(),e.shadowColor="rgba(0,0,0,0.4)",e.shadowBlur=4,e.shadowOffsetX=0,e.shadowOffsetY=0,e.strokeStyle="rgba(255,255,255)",e.lineWidth=2,e.stroke(),e.restore()}})})}}}function createPointGradientPluginWithBorder(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return{id:"pointGradientWithBorder",afterDatasetsDraw:function(a){var e=a.ctx,r=[];a.data.datasets.forEach(function(i,n){var l=a.getDatasetMeta(n);l&&"line"===l.type&&!l.hidden&&l.data.forEach(function(a,l){var o,s;if(void 0!==a&&null!==i.data[l]){var d=a.x,f=a.y,u=i.data[l],c=Array.isArray(i.pointRadius)?null!==(o=i.pointRadius[l])&&void 0!==o?o:3:null!==(s=i.pointRadius)&&void 0!==s?s:3;if(0!==c){var h=d-c+2*c*.3333,v=f-c+2*c*.3333,g=.7862*c,x=e.createRadialGradient(h,v,0,h,v,g),y=t[i.label];if(y&&Array.isArray(y)?y.forEach(function(t){return x.addColorStop(t.stop,t.color)}):Line truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
"use strict";var scrollY,wrap,MOBILE_BREAKPOINT=1025;function syncHeight(){document.documentElement.style.setProperty("--window-inner-height","".concat(window.innerHeight,"px"))}function isMobile(){return window.innerWidth<MOBILE_BREAKPOINT}function bodyLock(){wrap?(scrollY=window.scrollY,document.documentElement.classList.add("is-locked"),document.documentElement.style.scrollBehavior="auto",wrap.style.top="-".concat(scrollY,"px")):console.warn("wrap element not found")}function bodyUnlock(){document.documentElement.classList.remove("is-locked"),void 0!==scrollY&&window.scrollTo(0,scrollY),wrap&&(wrap.style.top=""),document.documentElement.style.scrollBehavior=""}function popOpen(e){document.getElementById(e)?($("#"+e).fadeIn("fast"),bodyLock()):console.warn('Popup with id "'.concat(e,'" not found'))}function popClose(e){$(e).parents(".popup").fadeOut("fast"),bodyUnlock()}document.addEventListener("DOMContentLoaded",function(){(wrap=document.querySelector(".wrap"))?syncHeight():console.error("Wrap element not found")}),window.addEventListener("resize",function(){syncHeight()});var rootStyle=getComputedStyle(document.documentElement);function cssVar(e){return rootStyle.getPropertyValue(e).trim()}function fullScreen(){document.fullscreenElement?document.exitFullscreen().catch(function(e){console.log("전체화면 종료 실패:",e)}):document.documentElement.requestFullscreen().catch(function(e){console.log("전체화면 전환 실패:",e)})}
|
||||
@@ -0,0 +1 @@
|
||||
"use strict";var eventsData={"2025-10-31":[{time:"",title:"영업 활동",tags:["염"]}],"2025-11-03":[{time:"",title:"EG-BIM V1.2.0 업데이트 문의 대응",tags:["김"]},{time:"",title:"외부사용자 업데이트 안내 ( 개인설정 저장 )<br><em>※ 메일전송 후 유선안내</em>",tags:["김"]}],"2025-11-04":[{time:"",title:"더부엔지니어링",tags:["염"]},{time:"",title:"선인이엔지",tags:["염"]},{time:"",title:"제이알조경 ID&PW 생성(4)",tags:["김"]}],"2025-11-05":[{time:"12:00~14:00",title:"연세대 ( 한맥 구조부 부서장님 연계교 )",tags:["염"]},{time:"15:00~16:00",title:"대림대 ( 이진형교수 )",tags:["염"]},{time:"16:00~18:00",title:"한길아이티 ( 계약헙의 및 EG-BIM 소개 )",tags:["권"]},{time:"",title:"안양, 과천, 방문업체 선정",tags:["권"]},{time:"",title:"캐드제품군 가격정보 최신화",tags:["권"]},{time:"",title:"가족사 부서별 CAD 프로그램 사용현황 파악",tags:["김"]}],"2025-11-06":[{time:"09:00~10:00",title:"피컴스엔지니어링",tags:["염"]},{time:"11:00~12:00",title:"영업대시보드 회의",tags:["권","염","김"]},{time:"",title:"삼안 CM현장 방문협의 : 통복공공하수처리시설( KG, 갑인, 세일 : 9명 미팅가능)",tags:["권"]}],"2025-11-07":[{time:"14:00~15:00",title:"- 마이다스아이티 계약협의 (거여동)",tags:["권","염"]},{time:"",title:"삼안 & 한맥 CM현장 방문협의",tags:["권"]},{time:"",title:"구조기술사회 팝업사항 진도사항 점검",tags:["염"]},{time:"",title:"인하공전 교수님 연락처 확인",tags:["염"]}],"2025-11-10":[{time:"10:00~12:00",title:"큰길 개발자 최종리뷰",tags:["권","염"]},{time:"15:00~16:00",title:"사장님 영업보고",tags:["권","염"]},{time:"17:00~18:00",title:"영업대시보드 회의",tags:["염"]}],"2025-11-11":[{time:"10:30~11:30",title:"삼안 CM현장 : 통복공공하수처리시설, 평택 홍만기단장(공동도급관련 5개사)",tags:["권","염"]},{time:"13:00~14:00",title:"공단배수지 추가 증설공사 등 7개소 통합건설사업관리단 ( 동명기술공단 외 )김민관단장",tags:["권","염"]},{time:"15:00~16:00",title:"서림주택관리",tags:["권"]},{time:"",title:"영업대시보드 기획",tags:["김"]},{time:"",title:"DREAM PLUS",tags:["김"]}],"2025-11-12":[{time:"13:00~14:00",title:"대전대 유병재 교수",tags:["염"]},{time:"15:00~16:00",title:"우송대 안태봉교수",tags:["염"]},{time:"17:00~18:00",title:"CIVIL EXPO 준비참석",tags:["염"]},{time:"17:00~18:00",title:"한국종합엔지니어링 기획팀 방문",tags:["권"]},{time:"",title:"원건설(청주): 오전",tags:["권"]}],"2025-11-13":[{time:"14:00~15:00",title:"나모소프트 ( 연구소 팀장, 유선협의)",tags:["권"]},{time:"",title:"업무지원 ( 장헌산업, 당진공장 )",tags:["권"]},{time:"",title:"CIVIL EXPO 참석",tags:["염"]},{time:"",title:"영업 대시보드 기획 (9,10월 영업내역 전달)",tags:["김"]}],"2025-11-14":[{time:"10:00~11:00",title:"피컴스, 오토데스크 이슈 컨설팅 미팅",tags:["권"]},{time:"",title:"CIVIL EXPO 참석",tags:["염"]},{time:"",title:"각 부서 주사용 DREAM PLUS 명령어 취합",tags:["김"]}],"2025-11-17":[{time:"",title:"의왕도시공사 ( MOU 초안 작성 )",tags:["권","염"]},{time:"",title:" 한라대학교 ( 강의커리큐럼 초안 작성 )",tags:["권","염"]},{time:"",title:"인하공전 특강준비",tags:["염"]},{time:"",title:"영업대시보드 DB 정리",tags:["김"]}],"2025-11-18":[{time:"13:30",title:"인하대 김수전교수",tags:["염"]},{time:"14:00~15:00",title:"[ Smart Construction x OSC x SCCI ]( 오토데스크, 도로공사, 대한 경제, 정보수집 외)",tags:["권"]},{time:"17:00~18:00",title:"인하대 남진원교수",tags:["권"]},{time:"",title:"인천시청",tags:["염"]},{time:"",title:"회원관리시스템 flow작성(EG-BIM / KNGIL)",tags:["김"]}],"2025-11-19":[{time:"11:00~12:00",title:"해린엔지니어링 토목사업부 및 도시계획부 ( 청주 : 배창근 이사님,이동현 이사님) 청주시 청원구 1순환로 1896번길 20",tags:["권","염"]},{time:"13:00~14:00",title:"홍익기술단 안전진단부 방문협의(청주) 청주시 흥덕구 풍년로 150번길 28",tags:["권"]},{time:"13:00~14:00",title:"홍익기술단 총무팀 방문협의 (청주)",tags:["염"]},{time:"",title:"홍익기술단 감리부 방문협의",tags:["권"]},{time:"",title:"회원관리시스템 flow작성(EG-BIM / KNGIL)",tags:["김"]},{time:"",title:"대림대 계정 생성(100copy)",tags:["김"]},{time:"",title:"드림플러스 주사용 명령어 각부서 조사 시트 정리",tags:["김"]}],"2025-11-20":[{time:"11:00~12:00",title:"삼안 CM현장 : 대전산업단지완충저류시설...( 대전 / 박준성 전무님) 대전광역시 대덕구 대화로 150번길 147-10",tags:["권","염"]},{time:"15:00~16:00",title:"삼안 CM현장 : 대청댐광역Line truncated
|
||||
File diff suppressed because it is too large.
Load diff
+14
@@ -0,0 +1,14 @@
|
||||
"use strict";function ownKeys(t,e){var i=Object.keys(t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(t);e&&(n=n.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),i.push.apply(i,n)}return i}function _objectSpread(t){for(var e=1;e<arguments.length;e++){var i=null!=arguments[e]?arguments[e]:{};e%2?ownKeys(Object(i),!0).forEach(function(e){_defineProperty(t,e,i[e])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(i)):ownKeys(Object(i)).forEach(function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(i,e))})}return t}function _superPropGet(t,e,i,n){var r=_get2(_getPrototypeOf(1&n?t.prototype:t),e,i);return 2&n&&"function"==typeof r?function(t){return r.apply(i,t)}:r}function _get2(){return _get2="undefined"!=typeof Reflect&&Reflect.get?Reflect.get.bind():function(t,e,i){var n=_superPropBase(t,e);if(n){var r=Object.getOwnPropertyDescriptor(n,e);return r.get?r.get.call(arguments.length<3?t:i):r.value}},_get2.apply(null,arguments)}function _superPropBase(t,e){for(;!{}.hasOwnProperty.call(t,e)&&null!==(t=_getPrototypeOf(t)););return t}function _slicedToArray(t,e){return _arrayWithHoles(t)||_iterableToArrayLimit(t,e)||_unsupportedIterableToArray(t,e)||_nonIterableRest()}function _nonIterableRest(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function _iterableToArrayLimit(t,e){var i=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null!=i){var n,r,a,o,s=[],l=!0,h=!1;try{if(a=(i=i.call(t)).next,0===e){if(Object(i)!==i)return;l=!1}else for(;!(l=(n=a.call(i)).done)&&(s.push(n.value),s.length!==e);l=!0);}catch(t){h=!0,r=t}finally{try{if(!l&&null!=i.return&&(o=i.return(),Object(o)!==o))return}finally{if(h)throw r}}return s}}function _arrayWithHoles(t){if(Array.isArray(t))return t}function _callSuper(t,e,i){return e=_getPrototypeOf(e),_possibleConstructorReturn(t,_isNativeReflectConstruct()?Reflect.construct(e,i||[],_getPrototypeOf(t).constructor):e.apply(t,i))}function _possibleConstructorReturn(t,e){if(e&&("object"==_typeof(e)||"function"==typeof e))return e;if(void 0!==e)throw new TypeError("Derived constructors may only return object or undefined");return _assertThisInitialized(t)}function _assertThisInitialized(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function _isNativeReflectConstruct(){try{var t=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){}))}catch(t){}return(_isNativeReflectConstruct=function(){return!!t})()}function _getPrototypeOf(t){return _getPrototypeOf=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(t){return t.__proto__||Object.getPrototypeOf(t)},_getPrototypeOf(t)}function _inherits(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),Object.defineProperty(t,"prototype",{writable:!1}),e&&_setPrototypeOf(t,e)}function _setPrototypeOf(t,e){return _setPrototypeOf=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,e){return t.__proto__=e,t},_setPrototypeOf(t,e)}function _defineProperty(t,e,i){return(e=_toPropertyKey(e))in t?Object.defineProperty(t,e,{value:i,enumerable:!0,configurable:!0,writable:!0}):t[e]=i,t}function _toConsumableArray(t){return _arrayWithoutHoles(t)||_iterableToArray(t)||_unsupportedIterableToArray(t)||_nonIterableSpread()}function _nonIterableSpread(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function _iterableToArray(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}function _arrayWithoutHoles(t){if(Array.isArray(t))return _arrayLikeToArray(t)}function _classCallCheck(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function _defineProperties(t,e){for(var i=0;i<e.length;i++){var n=e[i];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,_toPropertyKey(n.key),n)}}function _createClass(t,e,i){return e&&_defineProperties(t.prototype,e),i&&_defineProperties(t,i),Object.defineProperty(t,"prototype",{writable:!1}),t}function _toPropertyKey(t){var e=_toPrimitive(t,"string");return"symbol"==_typeof(e)?e:e+""}function _toPrimitive(t,e){if("object"!=_typeof(t)||!t)return t;var i=t[Symbol.toPrimitive];if(void 0!==i){var n=i.call(t,e||"default");if("object"!=_typeof(n))return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===e?String:Number)(t)}function _createForOfIteratorHelper(t,e){var i="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!i){if(Array.isArray(t)||(i=_unsupportedILine truncated
|
||||
/*!
|
||||
* Chart.js v4.5.1
|
||||
* https://www.chartjs.org
|
||||
* (c) 2025 Chart.js Contributors
|
||||
* Released under the MIT License
|
||||
*/}!function(t,e){"object"==("undefined"==typeof exports?"undefined":_typeof(exports))&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(void 0,function(){var t,e,i,n,r,a,o,s,l,h,u,c=Object.freeze({__proto__:null,get Colors(){return Xa},get Decimation(){return qa},get Filler(){return po},get Legend(){return bo},get SubTitle(){return wo},get Title(){return xo},get Tooltip(){return Vo}});function f(){}var d=function(){var t=0;return function(){return t++}}();function v(t){return null==t}function p(t){if(Array.isArray&&Array.isArray(t))return!0;var e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function g(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function y(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function m(t,e){return y(t)?t:e}function b(t,e){return void 0===t?e:t}var _=function(t,e){return"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:+t/e},x=function(t,e){return"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t};function k(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function w(t,e,i,n){var r,a,o;if(p(t))if(a=t.length,n)for(r=a-1;r>=0;r--)e.call(i,t[r],r);else for(r=0;r<a;r++)e.call(i,t[r],r);else if(g(t))for(a=(o=Object.keys(t)).length,r=0;r<a;r++)e.call(i,t[o[r]],o[r])}function M(t,e){var i,n,r,a;if(!t||!e||t.length!==e.length)return!1;for(i=0,n=t.length;i<n;++i)if(r=t[i],a=e[i],r.datasetIndex!==a.datasetIndex||r.index!==a.index)return!1;return!0}function P(t){if(p(t))return t.map(P);if(g(t)){for(var e=Object.create(null),i=Object.keys(t),n=i.length,r=0;r<n;++r)e[i[r]]=P(t[i[r]]);return e}return t}function S(t){return-1===["__proto__","prototype","constructor"].indexOf(t)}function C(t,e,i,n){if(S(t)){var r=e[t],a=i[t];g(r)&&g(a)?O(r,a,n):e[t]=P(a)}}function O(t,e,i){var n=p(e)?e:[e],r=n.length;if(!g(t))return t;for(var a,o=(i=i||{}).merger||C,s=0;s<r;++s)if(g(a=n[s]))for(var l=Object.keys(a),h=0,u=l.length;h<u;++h)o(l[h],t,a,i);return t}function A(t,e){return O(t,e,{merger:D})}function D(t,e,i){if(S(t)){var n=e[t],r=i[t];g(n)&&g(r)?A(n,r):Object.prototype.hasOwnProperty.call(e,t)||(e[t]=P(r))}}var T={"":function(t){return t},x:function(t){return t.x},y:function(t){return t.y}};function I(t){var e,i=[],n="",r=_createForOfIteratorHelper(t.split("."));try{for(r.s();!(e=r.n()).done;){(n+=e.value).endsWith("\\")?n=n.slice(0,-1)+".":(i.push(n),n="")}}catch(t){r.e(t)}finally{r.f()}return i}function L(t,e){var i=T[e]||(T[e]=function(t){var e=I(t);return function(t){var i,n=_createForOfIteratorHelper(e);try{for(n.s();!(i=n.n()).done;){var r=i.value;if(""===r)break;t=t&&t[r]}}catch(t){n.e(t)}finally{n.f()}return t}}(e));return i(t)}function R(t){return t.charAt(0).toUpperCase()+t.slice(1)}var E=function(t){return void 0!==t},F=function(t){return"function"==typeof t},z=function(t,e){if(t.size!==e.size)return!1;var i,n=_createForOfIteratorHelper(t);try{for(n.s();!(i=n.n()).done;){var r=i.value;if(!e.has(r))return!1}}catch(t){n.e(t)}finally{n.f()}return!0};function V(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}var B=Math.PI,H=2*B,j=H+B,W=Number.POSITIVE_INFINITY,N=B/180,Y=B/2,U=B/4,$=2*B/3,X=Math.log10,G=Math.sign;function K(t,e,i){return Math.abs(t-e)<i}function q(t){var e=Math.round(t);t=K(t,e,t/1e3)?e:t;var i=Math.pow(10,Math.floor(X(t))),n=t/i;return(n<=1?1:n<=2?2:n<=5?5:10)*i}function J(t){var e,i=[],n=Math.sqrt(t);for(e=1;e<n;e++)t%e==0&&(i.push(e),i.push(t/e));return n===(0|n)&&i.push(n),i.sort(function(t,e){return t-e}).pop(),i}function Z(t){return!function(t){return"symbol"==_typeof(t)||"object"==_typeof(t)&&null!==t&&!(Symbol.toPrimitive in t||"toString"in t||"valueOf"in t)}(t)&&!isNaN(parseFloat(t))&&isFinite(t)}function Q(t,e){var i=Math.round(t);return i-e<=t&&i+e>=t}function tt(t,e,i){var n,r,a;for(n=0,r=t.length;n<r;n++)a=t[n][i],isNaN(a)||(e.min=Math.min(e.min,a),e.max=Math.max(e.max,a))}function et(t){return t*(B/180)}function it(t){return t*(180/B)}function nt(t){if(y(t)){for(var e=1,i=0;Math.round(t*e)/e!==t;)e*=10,i++;return i}}function rt(t,e){var i=e.x-t.x,n=e.y-t.y,r=Math.sqrt(i*i+n*n),a=Math.atan2(n,i);return a<-.5*B&&(a+=H),{angle:a,distance:r}}function at(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function ot(t,e){return(t-e+j)%H-B}function st(t){return(t%H+H)%H}function lt(t,e,i,n){var r=st(t),a=st(e),o=st(i),s=st(a-r),l=st(o-r),h=st(r-a),u=st(r-o);return r===a||r===o||n&&a===o||s>l&&h<u}function ht(t,e,i){return Math.max(e,Math.min(i,t))}function ut(t){return ht(t,-32768,32767)}function ct(t,e,i){var n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:1e-6;return t>=Math.min(e,i)-n&&t<=Math.max(e,i)+n}function ft(t,e,i){i=i||function(i){return t[i]<e};for(var n,r=t.length-1,a=0;r-a>1;)i(n=a+r>>1)?a=n:r=n;return{lo:a,hi:r}}var dt=function(t,e,i,n){return ft(t,i,n?function(n){var r=t[n][e];return r<i||r==Line truncated
|
||||
/*!
|
||||
* @kurkle/color v0.3.2
|
||||
* https://github.com/kurkle/color#readme
|
||||
* (c) 2023 Jukka Kurkela
|
||||
* Released under the MIT License
|
||||
*/
|
||||
function Dt(t){return t+.5|0}var Tt=function(t,e,i){return Math.max(Math.min(t,i),e)};function It(t){return Tt(Dt(2.55*t),0,255)}function Lt(t){return Tt(Dt(255*t),0,255)}function Rt(t){return Tt(Dt(t/2.55)/100,0,1)}function Et(t){return Tt(Dt(100*t),0,100)}var Ft={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},zt=_toConsumableArray("0123456789ABCDEF"),Vt=function(t){return zt[15&t]},Bt=function(t){return zt[(240&t)>>4]+zt[15&t]},Ht=function(t){return(240&t)>>4==(15&t)};var jt=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Wt(t,e,i){var n=e*Math.min(i,1-i),r=function(e){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:(e+t/30)%12;return i-n*Math.max(Math.min(r-3,9-r,1),-1)};return[r(0),r(8),r(4)]}function Nt(t,e,i){var n=function(n){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:(n+t/60)%6;return i-i*e*Math.max(Math.min(r,4-r,1),0)};return[n(5),n(3),n(1)]}function Yt(t,e,i){var n,r=Wt(t,1,.5);for(e+i>1&&(e*=n=1/(e+i),i*=n),n=0;n<3;n++)r[n]*=1-e-i,r[n]+=e;return r}function Ut(t){var e,i,n,r=t.r/255,a=t.g/255,o=t.b/255,s=Math.max(r,a,o),l=Math.min(r,a,o),h=(s+l)/2;return s!==l&&(n=s-l,i=h>.5?n/(2-s-l):n/(s+l),e=function(t,e,i,n,r){return t===r?(e-i)/n+(e<i?6:0):e===r?(i-t)/n+2:(t-e)/n+4}(r,a,o,n,s),e=60*e+.5),[0|e,i||0,h]}function $t(t,e,i,n){return(Array.isArray(e)?t(e[0],e[1],e[2]):t(e,i,n)).map(Lt)}function Xt(t,e,i){return $t(Wt,t,e,i)}function Gt(t){return(t%360+360)%360}var Kt,qt={x:"dark",Z:"light",Y:"re",X:"blu",W:"gr",V:"medium",U:"slate",A:"ee",T:"ol",S:"or",B:"ra",C:"lateg",D:"ights",R:"in",Q:"turquois",E:"hi",P:"ro",O:"al",N:"le",M:"de",L:"yello",F:"en",K:"ch",G:"arks",H:"ea",I:"ightg",J:"wh"},Jt={OiceXe:"f0f8ff",antiquewEte:"faebd7",aqua:"ffff",aquamarRe:"7fffd4",azuY:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"0",blanKedOmond:"ffebcd",Xe:"ff",XeviTet:"8a2be2",bPwn:"a52a2a",burlywood:"deb887",caMtXe:"5f9ea0",KartYuse:"7fff00",KocTate:"d2691e",cSO:"ff7f50",cSnflowerXe:"6495ed",cSnsilk:"fff8dc",crimson:"dc143c",cyan:"ffff",xXe:"8b",xcyan:"8b8b",xgTMnPd:"b8860b",xWay:"a9a9a9",xgYF:"6400",xgYy:"a9a9a9",xkhaki:"bdb76b",xmagFta:"8b008b",xTivegYF:"556b2f",xSange:"ff8c00",xScEd:"9932cc",xYd:"8b0000",xsOmon:"e9967a",xsHgYF:"8fbc8f",xUXe:"483d8b",xUWay:"2f4f4f",xUgYy:"2f4f4f",xQe:"ced1",xviTet:"9400d3",dAppRk:"ff1493",dApskyXe:"bfff",dimWay:"696969",dimgYy:"696969",dodgerXe:"1e90ff",fiYbrick:"b22222",flSOwEte:"fffaf0",foYstWAn:"228b22",fuKsia:"ff00ff",gaRsbSo:"dcdcdc",ghostwEte:"f8f8ff",gTd:"ffd700",gTMnPd:"daa520",Way:"808080",gYF:"8000",gYFLw:"adff2f",gYy:"808080",honeyMw:"f0fff0",hotpRk:"ff69b4",RdianYd:"cd5c5c",Rdigo:"4b0082",ivSy:"fffff0",khaki:"f0e68c",lavFMr:"e6e6fa",lavFMrXsh:"fff0f5",lawngYF:"7cfc00",NmoncEffon:"fffacd",ZXe:"add8e6",ZcSO:"f08080",Zcyan:"e0ffff",ZgTMnPdLw:"fafad2",ZWay:"d3d3d3",ZgYF:"90ee90",ZgYy:"d3d3d3",ZpRk:"ffb6c1",ZsOmon:"ffa07a",ZsHgYF:"20b2aa",ZskyXe:"87cefa",ZUWay:"778899",ZUgYy:"778899",ZstAlXe:"b0c4de",ZLw:"ffffe0",lime:"ff00",limegYF:"32cd32",lRF:"faf0e6",magFta:"ff00ff",maPon:"800000",VaquamarRe:"66cdaa",VXe:"cd",VScEd:"ba55d3",VpurpN:"9370db",VsHgYF:"3cb371",VUXe:"7b68ee",VsprRggYF:"fa9a",VQe:"48d1cc",VviTetYd:"c71585",midnightXe:"191970",mRtcYam:"f5fffa",mistyPse:"ffe4e1",moccasR:"ffe4b5",navajowEte:"ffdead",navy:"80",Tdlace:"fdf5e6",Tive:"808000",TivedBb:"6b8e23",Sange:"ffa500",SangeYd:"ff4500",ScEd:"da70d6",pOegTMnPd:"eee8aa",pOegYF:"98fb98",pOeQe:"afeeee",pOeviTetYd:"db7093",papayawEp:"ffefd5",pHKpuff:"ffdab9",peru:"cd853f",pRk:"ffc0cb",plum:"dda0dd",powMrXe:"b0e0e6",purpN:"800080",YbeccapurpN:"663399",Yd:"ff0000",Psybrown:"bc8f8f",PyOXe:"4169e1",saddNbPwn:"8b4513",sOmon:"fa8072",sandybPwn:"f4a460",sHgYF:"2e8b57",sHshell:"fff5ee",siFna:"a0522d",silver:"c0c0c0",skyXe:"87ceeb",UXe:"6a5acd",UWay:"708090",UgYy:"708090",snow:"fffafa",sprRggYF:"ff7f",stAlXe:"4682b4",tan:"d2b48c",teO:"8080",tEstN:"d8bfd8",tomato:"ff6347",Qe:"40e0d0",viTet:"ee82ee",JHt:"f5deb3",wEte:"ffffff",wEtesmoke:"f5f5f5",Lw:"ffff00",LwgYF:"9acd32"};var Zt=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/,Qt=function(t){return t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055},te=function(t){return t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)};function ee(t,e,i){if(t){var n=Ut(t);n[e]=Math.max(0,Math.min(n[e]+n[e]*i,0===e?360:1)),n=Xt(n),t.r=n[0],t.g=n[1],t.b=n[2]}}function ie(t,e){return t?Object.assign(e||{},t):t}function ne(t){var e={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(e={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(e.a=Lt(t[3]))):(e=ie(t,{r:0,g:0,b:0,a:1})).a=Lt(e.a),e}function re(t){return"r"===t.charAt(0)?function(t){var e,i,n,r=Zt.exec(t),a=255;if(r){if(r[7]!==e){var o=+r[7];a=r[8]?It(o):Tt(255*o,0,255)}return e=+r[1],i=+r[3],n=+r[5],{r:e=255&(r[2]?It(e):Tt(e,0,255)),g:i=255&(r[4]?It(i):Tt(i,0,255)),b:n=255&(r[6]?It(n):Tt(n,0,255)),a:a}}}(t):function(t){var e,i=jt.exec(tLine truncated
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! jQuery v3.6.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */
|
||||
!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,y=n.hasOwnProperty,a=y.toString,l=a.call(Object),v={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.1",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0<t&&t-1 in e)}S.fn=S.prototype={jquery:f,constructor:S,length:0,toArray:function(){return s.call(this)},get:function(e){return null==e?s.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=S.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return S.each(this,e)},map:function(n){return this.pushStack(S.map(this,function(e,t){return n.call(e,t,e)}))},slice:function(){return this.pushStack(s.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(S.grep(this,function(e,t){return(t+1)%2}))},odd:function(){return this.pushStack(S.grep(this,function(e,t){return t%2}))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(0<=n&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:u,sort:t.sort,splice:t.splice},S.extend=S.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for("boolean"==typeof a&&(l=a,a=arguments[s]||{},s++),"object"==typeof a||m(a)||(a={}),s===u&&(a=this,s--);s<u;s++)if(null!=(e=arguments[s]))for(t in e)r=e[t],"__proto__"!==t&&a!==r&&(l&&r&&(S.isPlainObject(r)||(i=Array.isArray(r)))?(n=a[t],o=i&&!Array.isArray(n)?[]:i||S.isPlainObject(n)?n:{},i=!1,a[t]=S.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},S.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||"[object Object]"!==o.call(e))&&(!(t=r(e))||"function"==typeof(n=y.call(t,"constructor")&&t.constructor)&&a.call(n)===l)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e,t,n){b(e,{nonce:t&&t.nonce},n)},each:function(e,t){var n,r=0;if(p(e)){for(n=e.length;r<n;r++)if(!1===t.call(e[r],r,e[r]))break}else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},makeArray:function(e,t){var n=t||[];return null!=e&&(p(Object(e))?S.merge(n,"string"==typeof e?[e]:e):u.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:i.call(t,e,n)},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r=[],i=0,o=e.length,a=!n;i<o;i++)!t(e[i],i)!==a&&r.push(e[i]);return r},map:function(e,t,n){var r,i,o=0,a=[];if(p(e))for(r=e.length;o<r;o++)null!=(i=t(e[o],o,n))&&a.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&a.push(i);return g(a)},guid:1,support:v}),"function"==typeof Symbol&&(S.fn[Symbol.iterator]=t[Symbol.iterator]),S.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(e,t){n["[object "+t+"]"]=t.toLowerCase()});var d=function(n){var e,d,b,o,i,h,f,g,w,u,l,T,C,a,E,y,s,c,v,S="sizzle"+1*new Date,p=n.document,k=0,r=0,m=ue(),x=ue(),A=ue(),N=ue(),j=function(e,t){return e===t&&(l=!0),0},D={}.hasOwnProperty,t=[],q=t.pop,L=t.push,H=t.push,O=t.slice,P=function(e,t){for(var n=0,r=e.length;n<r;n++)if(e[n]===t)return n;return-1},R="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",I="(?:\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+",W="\\["+M+"*("+I+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+I+"))|)"+M+"*\\]",F=":("+I+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+W+")*)|.*)\\)|)",$=new RegExp(M+"+","g"),B=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),_=new RegExp("^"+M+"*,"+M+"*"),z=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^Line truncated
|
||||
@@ -0,0 +1,2 @@
|
||||
"use strict";function _typeof(e){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},_typeof(e)
|
||||
/*! jQuery v3.6.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */}!function(e,t){"object"==("undefined"==typeof module?"undefined":_typeof(module))&&"object"==_typeof(module.exports)?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:void 0,function(e,t){var n=[],r=Object.getPrototypeOf,i=n.slice,o=n.flat?function(e){return n.flat.call(e)}:function(e){return n.concat.apply([],e)},a=n.push,s=n.indexOf,u={},l=u.toString,c=u.hasOwnProperty,f=c.toString,p=f.call(Object),d={},h=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},g=function(e){return null!=e&&e===e.window},v=e.document,y={type:!0,src:!0,nonce:!0,noModule:!0};function m(e,t,n){var r,i,o=(n=n||v).createElement("script");if(o.text=e,t)for(r in y)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==_typeof(e)||"function"==typeof e?u[l.call(e)]||"object":_typeof(e)}var b="3.6.1",w=function(e,t){return new w.fn.init(e,t)};function T(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!h(e)&&!g(e)&&("array"===n||0===t||"number"==typeof t&&0<t&&t-1 in e)}w.fn=w.prototype={jquery:b,constructor:w,length:0,toArray:function(){return i.call(this)},get:function(e){return null==e?i.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(i.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(w.grep(this,function(e,t){return(t+1)%2}))},odd:function(){return this.pushStack(w.grep(this,function(e,t){return t%2}))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(0<=n&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:a,sort:n.sort,splice:n.splice},w.extend=w.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for("boolean"==typeof a&&(l=a,a=arguments[s]||{},s++),"object"==_typeof(a)||h(a)||(a={}),s===u&&(a=this,s--);s<u;s++)if(null!=(e=arguments[s]))for(t in e)r=e[t],"__proto__"!==t&&a!==r&&(l&&r&&(w.isPlainObject(r)||(i=Array.isArray(r)))?(n=a[t],o=i&&!Array.isArray(n)?[]:i||w.isPlainObject(n)?n:{},i=!1,a[t]=w.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},w.extend({expando:"jQuery"+(b+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||"[object Object]"!==l.call(e)||(t=r(e))&&("function"!=typeof(n=c.call(t,"constructor")&&t.constructor)||f.call(n)!==p))},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e,t,n){m(e,{nonce:t&&t.nonce},n)},each:function(e,t){var n,r=0;if(T(e))for(n=e.length;r<n&&!1!==t.call(e[r],r,e[r]);r++);else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},makeArray:function(e,t){var n=t||[];return null!=e&&(T(Object(e))?w.merge(n,"string"==typeof e?[e]:e):a.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:s.call(t,e,n)},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r=[],i=0,o=e.length,a=!n;i<o;i++)!t(e[i],i)!==a&&r.push(e[i]);return r},map:function(e,t,n){var r,i,a=0,s=[];if(T(e))for(r=e.length;a<r;a++)null!=(i=t(e[a],a,n))&&s.push(i);else for(a in e)null!=(i=t(e[a],a,n))&&s.push(i);return o(s)},guid:1,support:d}),"function"==typeof Symbol&&(w.fn[Symbol.iterator]=n[Symbol.iterator]),w.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(e,t){u["[object "+t+"]"]=t.toLowerCase()});var C=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,v,y,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ue(),S=ue(),k=ue(),A=ue(),N=function(e,t){return e===t&&(f=!0),0},j={}.hasOwnProperty,D=[],q=D.pop,L=D.push,H=D.push,O=D.slice,P=function(e,t){for(var n=0,r=e.length;n<r;n++)if(e[n]===t)return n;return-1},R="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",I="(?:\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+",_="\\["+M+"*("+I+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+I+"))|)"+M+"*\\]",W=":("+I+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+_+")*)|.*)\\)|)",F=new RegExp(M+"+","g"),$=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),B=new RegExp("^"+M+"*,"+M+"*"),z=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(W),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.(Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
"use strict";function _typeof(e){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},_typeof(e)}function _toConsumableArray(e){return _arrayWithoutHoles(e)||_iterableToArray(e)||_unsupportedIterableToArray(e)||_nonIterableSpread()}function _nonIterableSpread(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function _iterableToArray(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}function _arrayWithoutHoles(e){if(Array.isArray(e))return _arrayLikeToArray(e)}function _slicedToArray(e,t){return _arrayWithHoles(e)||_iterableToArrayLimit(e,t)||_unsupportedIterableToArray(e,t)||_nonIterableRest()}function _nonIterableRest(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function _unsupportedIterableToArray(e,t){if(e){if("string"==typeof e)return _arrayLikeToArray(e,t);var r={}.toString.call(e).slice(8,-1);return"Object"===r&&e.constructor&&(r=e.constructor.name),"Map"===r||"Set"===r?Array.from(e):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?_arrayLikeToArray(e,t):void 0}}function _arrayLikeToArray(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=Array(t);r<t;r++)n[r]=e[r];return n}function _iterableToArrayLimit(e,t){var r=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=r){var n,a,o,i,l=[],u=!0,s=!1;try{if(o=(r=r.call(e)).next,0===t){if(Object(r)!==r)return;u=!1}else for(;!(u=(n=o.call(r)).done)&&(l.push(n.value),l.length!==t);u=!0);}catch(e){s=!0,a=e}finally{try{if(!u&&null!=r.return&&(i=r.return(),Object(i)!==i))return}finally{if(s)throw a}}return l}}function _arrayWithHoles(e){if(Array.isArray(e))return e}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _defineProperties(e,t){for(var r=0;r<t.length;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,_toPropertyKey(n.key),n)}}function _createClass(e,t,r){return t&&_defineProperties(e.prototype,t),r&&_defineProperties(e,r),Object.defineProperty(e,"prototype",{writable:!1}),e}function _toPropertyKey(e){var t=_toPrimitive(e,"string");return"symbol"==_typeof(t)?t:t+""}function _toPrimitive(e,t){if("object"!=_typeof(e)||!e)return e;var r=e[Symbol.toPrimitive];if(void 0!==r){var n=r.call(e,t||"default");if("object"!=_typeof(n))return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}var ScheduleManager=function(){return _createClass(function e(){_classCallCheck(this,e),this.lastActiveItem=null,this.updateInterval=6e4},[{key:"getCurrentTimeInMinutes",value:function(){var e=new Date,t=60*e.getHours()+e.getMinutes();return{now:e,minutes:t}}},{key:"parseTimeToMinutes",value:function(e){var t=_slicedToArray(e.split(":").map(Number),2);return 60*t[0]+t[1]}},{key:"getItemTimeRange",value:function(e){var t=e.dataset,r=t.start,n=t.end;return r&&n?{start:this.parseTimeToMinutes(r),end:this.parseTimeToMinutes(n)}:null}},{key:"determineItemStatus",value:function(e,t){return t>=e.start&&t<=e.end?"active":t>e.end?"completed":"upcoming"}},{key:"updateBadge",value:function(e,t){var r=e.querySelector(".current-time-badge");if(t&&!r){var n=document.createElement("span");n.className="current-time-badge",n.textContent="진행중",e.appendChild(n)}else!t&&r&&r.remove()}},{key:"updateItemClasses",value:function(e,t){var r,n,a={active:{add:["active"],remove:["completed"]},completed:{add:["completed"],remove:["active"]},upcoming:{add:[],remove:["active","completed"]}}[t],o=a.add,i=a.remove;(r=e.classList).remove.apply(r,_toConsumableArray(i)),(n=e.classList).add.apply(n,_toConsumableArray(o))}},{key:"updateItemState",value:function(e,t){var r=this.getItemTimeRange(e);if(!r)return!1;var n=this.determineItemStatus(r,t),a="active"===n;return this.updateItemClasses(e,n),a}},{key:"scrollToElement",value:function(e,t){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:20;t&&e.scrollTo({top:t.offsetTop-r,behavior:"smooth"})}},{key:"getTodayDateString",value:function(e){return e.getFullYear()+"-"+String(e.getMonth()+1).padStart(2,"0")+"-"+String(e.getDate()).padStart(2,"0")}},{key:"updateCardItems",value:function(e,t){var r=this,n=null;return e.querySelectorAll(".day-list li").forEach(function(e){r.updateItemState(e,t)&&!n&&(n=e)}),n}},{key:"processAllCards",value:function(e,t){var r=this,n=null,a=null;return document.querySelectorAll(".day-card").forEach(function(o){var i=o.dataset.date===e;o.classList.toggle("today",i),i?(a=o,n=r.updateCardItems(o,t)):r.updateCardItems(o,t)}),{firstActive:n,todayCarLine truncated
|
||||
@@ -0,0 +1,22 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, viewport-fit=cover"
|
||||
/>
|
||||
<meta http-equiv="X-UA-compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<!-- <meta http-equiv="pragma" content="no-cache"> -->
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/svg+xml"
|
||||
href="./assets/images/favicon.svg"
|
||||
/>
|
||||
<link rel="stylesheet" type="text/css" href="./assets/css/common.css" />
|
||||
<link rel="stylesheet" type="text/css" href="./assets/css/style.css" />
|
||||
|
||||
<script src="./assets/js/lib/jquery-3.6.1.min.js"></script>
|
||||
<script src="./assets/js/lib/chart.js"></script>
|
||||
<script src="./assets/js/chart-plugin.js"></script>
|
||||
|
||||
<script src="./assets/js/common.min.js"></script>
|
||||
<title>EG-BIM 판매현황</title>
|
||||
@@ -0,0 +1,178 @@
|
||||
<!-- header -->
|
||||
<!-- <header class="header">
|
||||
<button class="logo" onclick="fullScreen();" title="">EG-BIM</button>
|
||||
<h1 class="title" data-text="2025년 판매 상세 현황">2025년 판매 상세 현황</h1>
|
||||
<div class="date">업데이트 : 2025.11.21(금) 18:00</div>
|
||||
</header> -->
|
||||
<?php
|
||||
$DB_HOST = "localhost";
|
||||
$DB_NAME = "egbim";
|
||||
$DB_USER = "egbim";
|
||||
$DB_PASS = "baron3840!!";
|
||||
|
||||
try {
|
||||
$pdo = new PDO(
|
||||
"mysql:host={$DB_HOST};dbname={$DB_NAME};charset=utf8mb4",
|
||||
$DB_USER,
|
||||
$DB_PASS,
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode([
|
||||
"status" => "fail",
|
||||
"message" => "DB 연결 실패: " . $e->getMessage()
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
// 기준 연도
|
||||
$START_YEAR = 2025;
|
||||
|
||||
// 현재 연도 (URL 우선)
|
||||
$year = isset($_GET['year']) ? (int)$_GET['year'] : date('Y');
|
||||
if ($year < $START_YEAR) $year = $START_YEAR;
|
||||
|
||||
// 누적 기간
|
||||
$startDate = $START_YEAR . '-01-01';
|
||||
$endDate = $year . '-12-31';
|
||||
|
||||
// 누적 판매 copy 계산
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT COALESCE(SUM(quantity), 0) AS total
|
||||
FROM sales_results
|
||||
WHERE sales_date BETWEEN ? AND ?
|
||||
");
|
||||
$stmt->execute([$startDate, $endDate]);
|
||||
$cumulativeCopy = (int)$stmt->fetchColumn();
|
||||
?>
|
||||
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<button class="logo" onclick="fullScreen()">EG-BIM</button>
|
||||
|
||||
<div class="cumulative-copy">
|
||||
<span class="label">[총 판매 실적 :</span>
|
||||
<span><?= number_format($cumulativeCopy) ?></span>
|
||||
<span>copy]</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 연도 이동 영역 -->
|
||||
<div class="title-wrap yearly_button" id="yearly_button">
|
||||
<button class="year-nav-btn" id="btn-prev-year" title="작년">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#424242"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M15 18l-6-6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<h1 class="title" data-text="">
|
||||
<span id="current-year"></span>년 판매 상세 현황
|
||||
</h1>
|
||||
|
||||
<button class="year-nav-btn" id="btn-next-year" title="내년">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#424242"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M9 18l6-6-6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="date"></div>
|
||||
</header>
|
||||
<!--// header -->
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
/* -------------------------------
|
||||
1) 연도 처리 (URL 우선)
|
||||
-------------------------------- */
|
||||
const params = new URLSearchParams(location.search);
|
||||
// let currentYear = Number(params.get("year")) || new Date().getFullYear();
|
||||
const START_YEAR = 2025;
|
||||
const CURRENT_YEAR = new Date().getFullYear();
|
||||
|
||||
let currentYear = Number(params.get("year")) || CURRENT_YEAR;
|
||||
if (currentYear < START_YEAR) currentYear = START_YEAR;
|
||||
|
||||
const yearSpan = document.getElementById("current-year");
|
||||
const titleEl = document.querySelector(".header .title");
|
||||
|
||||
function updateTitle() {
|
||||
const text = `${currentYear}년 판매 상세 현황`;
|
||||
yearSpan.textContent = currentYear;
|
||||
titleEl.dataset.text = text;
|
||||
}
|
||||
|
||||
updateTitle();
|
||||
|
||||
document.getElementById("btn-prev-year")?.addEventListener("click", () => {
|
||||
if (currentYear <= START_YEAR) return; // 🔒 2025 이하 차단
|
||||
currentYear--;
|
||||
moveYear();
|
||||
});
|
||||
|
||||
document.getElementById("btn-next-year")?.addEventListener("click", () => {
|
||||
currentYear++;
|
||||
moveYear();
|
||||
});
|
||||
|
||||
function moveYear() {
|
||||
location.href = `?year=${currentYear}`;
|
||||
}
|
||||
|
||||
/* -------------------------------
|
||||
2) 업데이트 날짜 자동 반영
|
||||
-------------------------------- */
|
||||
const now = new Date();
|
||||
|
||||
const y = now.getFullYear();
|
||||
const m = String(now.getMonth() + 1).padStart(2, "0");
|
||||
const d = String(now.getDate()).padStart(2, "0");
|
||||
const hh = String(now.getHours()).padStart(2, "0");
|
||||
const mm = String(now.getMinutes()).padStart(2, "0");
|
||||
|
||||
const weekday = ["일", "월", "화", "수", "목", "금", "토"][now.getDay()];
|
||||
const dateText = `업데이트 : ${y}.${m}.${d}(${weekday}) ${hh}:${mm}`;
|
||||
|
||||
const dateEl = document.querySelector(".header .date");
|
||||
if (dateEl) dateEl.textContent = dateText;
|
||||
});
|
||||
|
||||
function updateTitle() {
|
||||
const text = `${currentYear}년 판매 상세 현황`;
|
||||
yearSpan.textContent = currentYear;
|
||||
titleEl.dataset.text = text;
|
||||
|
||||
const prevBtn = document.getElementById("btn-prev-year");
|
||||
if (prevBtn) {
|
||||
prevBtn.classList.toggle("disabled", currentYear <= START_YEAR);
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 연도 (모든 include에서 사용)
|
||||
window.APP_YEAR = (() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const y = parseInt(params.get("year"), 10);
|
||||
const nowY = new Date().getFullYear();
|
||||
|
||||
if (!isNaN(y) && y >= 2025) return y;
|
||||
return nowY;
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
ini_set('display_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?php include __DIR__ . '/html/_include/_head.php'; ?>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<?php include __DIR__ . '/html/_include/_header.php'; ?>
|
||||
<!-- container -->
|
||||
<div class="container">
|
||||
<div class="main-contents">
|
||||
<div class="main-grid">
|
||||
<!-- 연간 총 판매 실적 -->
|
||||
<?php include __DIR__ . '/html/main_sub/yearly_chart.php'; ?>
|
||||
<!-- //연간 총 판매 실적 -->
|
||||
|
||||
<!-- 연간 개별 판매 실적 -->
|
||||
<?php include __DIR__ . '/html/main_sub/individual_chart.php'; ?>
|
||||
<!-- //연간 개별 판매 실적 -->
|
||||
|
||||
<!-- 연간 영업 판매 실적 -->
|
||||
<?php include __DIR__ . '/html/main_sub/sales_chart.php'; ?>
|
||||
<!-- //연간 영업 판매 실적 -->
|
||||
|
||||
<!-- 월 판매 실적 -->
|
||||
<?php include __DIR__ . '/html/main_sub/month_chart.php'; ?>
|
||||
<!-- //월 판매 실적 -->
|
||||
</div>
|
||||
|
||||
<!-- 주간 영업 계획 -->
|
||||
<?php include __DIR__ . '/html/main_sub/weekly_plan.php'; ?>
|
||||
<!-- //주간 영업 계획 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(() => {
|
||||
// 리사이즈 이벤트 리스너 (디바운스 적용)
|
||||
let resizeTimer;
|
||||
window.addEventListener("resize", () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
createYearlyChart();
|
||||
createIndividualChart();
|
||||
createMonthChart();
|
||||
createIndustryChart();
|
||||
}, 250); // 250ms 디바운스
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,545 @@
|
||||
<?php
|
||||
$pdo = new PDO("mysql:host=localhost;dbname=egbim;charset=utf8mb4","egbim","baron3840!!");
|
||||
|
||||
$rows = $pdo->query("
|
||||
SELECT
|
||||
s.schedule_date,
|
||||
s.time_period,
|
||||
s.content,
|
||||
s.emp_no,
|
||||
m.emp_name,
|
||||
c.client_name
|
||||
FROM sales_schedules s
|
||||
LEFT JOIN sales_members m ON s.emp_no = m.emp_no
|
||||
LEFT JOIN sales_clients c ON s.client_code = c.client_code
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 모든 emp_no는 소문자로 통일!
|
||||
$tagMap = [
|
||||
"j01201" => "권",
|
||||
"223070" => "염",
|
||||
"m21430" => "김",
|
||||
"b21367" => "윤"
|
||||
];
|
||||
|
||||
$eventsData = [];
|
||||
|
||||
foreach ($rows as $r) {
|
||||
|
||||
$date = $r["schedule_date"];
|
||||
$empNo = strtolower($r["emp_no"]); // ★ emp_no를 소문자로 통일
|
||||
|
||||
// 날짜 키가 없으면 배열 초기화
|
||||
if (!isset($eventsData[$date])) {
|
||||
$eventsData[$date] = [];
|
||||
}
|
||||
|
||||
// 여러 개가 정상적으로 PUSH됨
|
||||
$eventsData[$date][] = [
|
||||
"time" => $r["time_period"],
|
||||
"title" => $r["content"],
|
||||
"tags" => [ $tagMap[$empNo] ?? "" ]
|
||||
];
|
||||
}
|
||||
|
||||
echo "<script>var eventsData = " . json_encode($eventsData, JSON_UNESCAPED_UNICODE) . ";</script>";
|
||||
?>
|
||||
|
||||
<div id="scheduleModal" class="modal calendar">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="filter-options">
|
||||
<div class="checkbox-group green">
|
||||
<input type="checkbox" id="check1" value="green" checked />
|
||||
<label for="check1">권혁진 수석</label>
|
||||
</div>
|
||||
<div class="checkbox-group purple">
|
||||
<input type="checkbox" id="check2" value="purple" checked />
|
||||
<label for="check2">염승호 수석</label>
|
||||
</div>
|
||||
<div class="checkbox-group teal">
|
||||
<input type="checkbox" id="check3" value="teal" checked />
|
||||
<label for="check3">김지영 선임</label>
|
||||
</div>
|
||||
<div class="checkbox-group black">
|
||||
<input type="checkbox" id="check4" value="black" checked />
|
||||
<label for="check4">윤준수 선임</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="date-navigation">
|
||||
<button class="prev-btn" id="prevMonth">‹</button>
|
||||
<div class="current-date" id="currentDate">2025.11</div>
|
||||
<button class="next-btn" id="nextMonth">›</button>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<div class="toggle-area">
|
||||
<input type="radio" id="option1" name="toggle" value="접기" checked />
|
||||
<label for="option1">접기</label>
|
||||
|
||||
<input type="radio" id="option2" name="toggle" value="펼치기" />
|
||||
<label for="option2">펼치기</label>
|
||||
<div class="toggle-slider"></div>
|
||||
</div>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body" id="modalBody">
|
||||
<div class="weekdays">
|
||||
<div class="weekday">일</div>
|
||||
<div class="weekday">월</div>
|
||||
<div class="weekday">화</div>
|
||||
<div class="weekday">수</div>
|
||||
<div class="weekday">목</div>
|
||||
<div class="weekday">금</div>
|
||||
<div class="weekday">토</div>
|
||||
</div>
|
||||
<div class="calendar-days">
|
||||
<div class="days collapsed" id="calendarDays"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 모달 배경 클릭시 닫기
|
||||
const modal = document.getElementById("scheduleModal");
|
||||
modal.addEventListener("click", function (e) {
|
||||
if (e.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
function initializeCalendar() {
|
||||
const tagMapping = {
|
||||
"권": "green",
|
||||
"염": "purple",
|
||||
"김": "teal",
|
||||
"윤": "black"
|
||||
};
|
||||
// 사람 고정 순서 (우선순위 낮을수록 뒤로)
|
||||
const PERSON_ORDER = {
|
||||
"권": 1, // 권혁진
|
||||
"염": 2, // 염승호
|
||||
"김": 3, // 김지영
|
||||
"윤": 4 // 윤준수
|
||||
};
|
||||
|
||||
function sortEventsByPerson(events) {
|
||||
return events.slice().sort((a, b) => {
|
||||
const aTag = a.tags[0] || "";
|
||||
const bTag = b.tags[0] || "";
|
||||
|
||||
return (PERSON_ORDER[aTag] || 99) - (PERSON_ORDER[bTag] || 99);
|
||||
});
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
오전 / 오후 판별 (핵심)
|
||||
→ title 문자열 기준
|
||||
=============================== */
|
||||
function isMorning(event) {
|
||||
return event.time === "오전";
|
||||
}
|
||||
|
||||
function isAfternoon(event) {
|
||||
return event.time === "오후";
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
let currentYear = today.getFullYear(); // 2025 대신
|
||||
let currentMonth = today.getMonth(); // 11 대신
|
||||
let selectedFilters = ["green", "purple", "teal", "black"];
|
||||
|
||||
function renderCalendar() {
|
||||
const calendarDays = document.getElementById("calendarDays");
|
||||
const currentDate = document.getElementById("currentDate");
|
||||
|
||||
currentDate.textContent = `${currentYear}.${String(currentMonth + 1).padStart(2, "0")}`;
|
||||
|
||||
const firstDay = new Date(currentYear, currentMonth, 1);
|
||||
const lastDay = new Date(currentYear, currentMonth + 1, 0);
|
||||
const prevLastDay = new Date(currentYear, currentMonth, 0);
|
||||
|
||||
const firstDayOfWeek = firstDay.getDay();
|
||||
const lastDate = lastDay.getDate();
|
||||
const prevLastDate = prevLastDay.getDate();
|
||||
|
||||
// 오늘 날짜의 주차 계산
|
||||
const today = new Date();
|
||||
let todayWeekStart = -1;
|
||||
let todayWeekEnd = -1;
|
||||
|
||||
// 현재 달력에 오늘이 포함되어 있는지 확인
|
||||
if (
|
||||
currentMonth === today.getMonth() &&
|
||||
currentYear === today.getFullYear()
|
||||
) {
|
||||
const todayDate = today.getDate();
|
||||
const currentDate = new Date(currentYear, currentMonth, todayDate);
|
||||
const dayOfWeek = currentDate.getDay();
|
||||
|
||||
// 이번 주의 시작(일요일)과 끝(토요일) 날짜 계산
|
||||
todayWeekStart = todayDate - dayOfWeek;
|
||||
todayWeekEnd = todayDate + (6 - dayOfWeek);
|
||||
}
|
||||
|
||||
let weeks = "";
|
||||
let currentWeekDays = "";
|
||||
let dayCount = 0;
|
||||
let weekNumber = 0;
|
||||
|
||||
// Previous month days
|
||||
const prevMonth = currentMonth === 0 ? 11 : currentMonth - 1;
|
||||
const prevYear = currentMonth === 0 ? currentYear - 1 : currentYear;
|
||||
|
||||
for (let i = firstDayOfWeek - 1; i >= 0; i--) {
|
||||
const dayNum = prevLastDate - i;
|
||||
const dateKey = `${prevYear}-${String(prevMonth + 1).padStart(2, "0")}-${String(dayNum).padStart(2, "0")}`;
|
||||
const dayEvents = eventsData[dateKey] || [];
|
||||
|
||||
const filteredEvents = sortEventsByPerson(
|
||||
dayEvents.filter((event) =>
|
||||
event.tags.some((tag) => selectedFilters.includes(tagMapping[tag]))
|
||||
)
|
||||
);
|
||||
|
||||
let morningHtml = "";
|
||||
let afternoonHtml = "";
|
||||
|
||||
filteredEvents.forEach((event) => {
|
||||
|
||||
const tagsHtml = event.tags
|
||||
.filter((tag) => selectedFilters.includes(tagMapping[tag]))
|
||||
.map(
|
||||
(tag) =>
|
||||
`<span class="event-tag badge-${tagMapping[tag]}">${tag}</span>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
const html = `
|
||||
<div class="event-item">
|
||||
<span class="event-title">${tagsHtml}${event.title}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (isMorning(event)) {
|
||||
morningHtml += html;
|
||||
}
|
||||
|
||||
if (isAfternoon(event)) {
|
||||
afternoonHtml += html;
|
||||
}
|
||||
});
|
||||
|
||||
currentWeekDays += `<div class="day other-month">
|
||||
<div class="day-number">${dayNum}</div>
|
||||
<dl>
|
||||
<dt>오전</dt>
|
||||
<dd class="day-content">${morningHtml}</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>오후</dt>
|
||||
<dd class="day-content">${afternoonHtml}</dd>
|
||||
</dl>
|
||||
</div>`;
|
||||
dayCount++;
|
||||
}
|
||||
|
||||
// Current month days
|
||||
for (let i = 1; i <= lastDate; i++) {
|
||||
const isToday =
|
||||
i === today.getDate() &&
|
||||
currentMonth === today.getMonth() &&
|
||||
currentYear === today.getFullYear();
|
||||
|
||||
const isCurrentWeek =
|
||||
todayWeekStart >= 0 && i >= todayWeekStart && i <= todayWeekEnd;
|
||||
|
||||
const dateKey = `${currentYear}-${String(currentMonth + 1).padStart(2, "0")}-${String(i).padStart(2, "0")}`;
|
||||
const dayEvents = eventsData[dateKey] || [];
|
||||
|
||||
const filteredEvents = sortEventsByPerson(
|
||||
dayEvents.filter((event) =>
|
||||
event.tags.some((tag) => selectedFilters.includes(tagMapping[tag]))
|
||||
)
|
||||
);
|
||||
|
||||
filteredEvents.forEach((event) => {
|
||||
console.log(event.title);
|
||||
});
|
||||
let morningHtml = "";
|
||||
let afternoonHtml = "";
|
||||
|
||||
filteredEvents.forEach((event) => {
|
||||
|
||||
const tagsHtml = event.tags
|
||||
.filter((tag) => selectedFilters.includes(tagMapping[tag]))
|
||||
.map(
|
||||
(tag) =>
|
||||
`<span class="event-tag badge-${tagMapping[tag]}">${tag}</span>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
const html = `
|
||||
<div class="event-item">
|
||||
|
||||
<span class="event-title">${tagsHtml}${event.title}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (isMorning(event)) {
|
||||
morningHtml += html;
|
||||
}
|
||||
|
||||
if (isAfternoon(event)) {
|
||||
afternoonHtml += html;
|
||||
}
|
||||
});
|
||||
|
||||
currentWeekDays += `<div class="day ${isToday ? "today" : ""} ${isCurrentWeek ? "current-week" : ""}">
|
||||
<div class="day-number">${i}</div>
|
||||
<dl>
|
||||
<dt>오전</dt>
|
||||
<dd class="day-content">${morningHtml}</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>오후</dt>
|
||||
<dd class="day-content">${afternoonHtml}</dd>
|
||||
</dl>
|
||||
</div>`;
|
||||
dayCount++;
|
||||
|
||||
// 7일마다 한 주 완성
|
||||
if (dayCount === 7) {
|
||||
const hasCurrentWeek =
|
||||
currentWeekDays.includes('class="day today"') ||
|
||||
currentWeekDays.includes("current-week");
|
||||
weeks += `<div class="week ${hasCurrentWeek ? "current-week-row" : ""}">${currentWeekDays}</div>`;
|
||||
currentWeekDays = "";
|
||||
dayCount = 0;
|
||||
weekNumber++;
|
||||
}
|
||||
}
|
||||
|
||||
// Next month days
|
||||
const nextMonth = currentMonth === 11 ? 0 : currentMonth + 1;
|
||||
const nextYear = currentMonth === 11 ? currentYear + 1 : currentYear;
|
||||
const remainingDays = 42 - (firstDayOfWeek + lastDate);
|
||||
|
||||
for (let i = 1; i <= remainingDays; i++) {
|
||||
const dateKey = `${nextYear}-${String(nextMonth + 1).padStart(2, "0")}-${String(i).padStart(2, "0")}`;
|
||||
const dayEvents = eventsData[dateKey] || [];
|
||||
|
||||
const filteredEvents = sortEventsByPerson(
|
||||
dayEvents.filter((event) =>
|
||||
event.tags.some((tag) => selectedFilters.includes(tagMapping[tag]))
|
||||
)
|
||||
);
|
||||
|
||||
let morningHtml = "";
|
||||
let afternoonHtml = "";
|
||||
|
||||
filteredEvents.forEach((event) => {
|
||||
|
||||
const tagsHtml = event.tags
|
||||
.filter((tag) => selectedFilters.includes(tagMapping[tag]))
|
||||
.map(
|
||||
(tag) =>
|
||||
`<span class="event-tag badge-${tagMapping[tag]}">${tag}</span>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
const html = `
|
||||
<div class="event-item">
|
||||
|
||||
<span class="event-title">${tagsHtml}${event.title}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (isMorning(event)) morningHtml += html;
|
||||
if (isAfternoon(event)) afternoonHtml += html;
|
||||
});
|
||||
|
||||
currentWeekDays += `<div class="day other-month">
|
||||
<div class="day-number">${i}</div>
|
||||
<dl>
|
||||
<dt>오전</dt>
|
||||
<dd class="day-content">${morningHtml}</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>오후</dt>
|
||||
<dd class="day-content">${afternoonHtml}</dd>
|
||||
</dl>
|
||||
</div>`;
|
||||
dayCount++;
|
||||
|
||||
// 7일마다 한 주 완성
|
||||
if (dayCount === 7) {
|
||||
weeks += `<div class="week">${currentWeekDays}</div>`;
|
||||
currentWeekDays = "";
|
||||
dayCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
calendarDays.innerHTML = weeks;
|
||||
|
||||
document.querySelectorAll(".day:not(.other-month)").forEach((day) => {
|
||||
day.addEventListener("click", function () {
|
||||
document
|
||||
.querySelectorAll(".day")
|
||||
.forEach((d) => d.classList.remove("selected"));
|
||||
this.classList.add("selected");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkOverflow() {
|
||||
const calendarDaysEl = document.getElementById("calendarDays");
|
||||
if (!calendarDaysEl.classList.contains("collapsed")) return;
|
||||
|
||||
document.querySelectorAll(".day-content").forEach((content) => {
|
||||
// 스크롤 높이가 실제 높이보다 크면 overflow 발생
|
||||
if (content.scrollHeight > content.clientHeight) {
|
||||
content.classList.add("has-overflow");
|
||||
} else {
|
||||
content.classList.remove("has-overflow");
|
||||
}
|
||||
});
|
||||
}
|
||||
// document.getElementById("prevMonth").addEventListener("click", () => {
|
||||
// currentMonth--;
|
||||
// if (currentMonth < 0) {
|
||||
// currentMonth = 11;
|
||||
// currentYear--;
|
||||
// }
|
||||
// renderCalendar();
|
||||
// checkOverflow();
|
||||
// });
|
||||
|
||||
// document.getElementById("nextMonth").addEventListener("click", () => {
|
||||
// currentMonth++;
|
||||
// if (currentMonth > 11) {
|
||||
// currentMonth = 0;
|
||||
// currentYear++;
|
||||
// }
|
||||
// renderCalendar();
|
||||
// checkOverflow();
|
||||
// });
|
||||
|
||||
// 필터 체크박스
|
||||
const filterCheckboxes = document.querySelectorAll(
|
||||
'.filter-options input[type="checkbox"]',
|
||||
);
|
||||
filterCheckboxes.forEach((checkbox) => {
|
||||
checkbox.addEventListener("change", () => {
|
||||
selectedFilters = Array.from(filterCheckboxes)
|
||||
.filter((cb) => cb.checked)
|
||||
.map((cb) => cb.value);
|
||||
renderCalendar();
|
||||
checkOverflow();
|
||||
});
|
||||
});
|
||||
|
||||
// 접기/펼치기 토글
|
||||
const calendarDaysEl = document.getElementById("calendarDays");
|
||||
const toggleRadios = document.querySelectorAll('input[name="toggle"]');
|
||||
|
||||
toggleRadios.forEach((radio) => {
|
||||
radio.addEventListener("change", (e) => {
|
||||
if (e.target.value === "접기") {
|
||||
calendarDaysEl.classList.add("collapsed");
|
||||
} else {
|
||||
calendarDaysEl.classList.remove("collapsed");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
calendarDaysEl.classList.add("collapsed");
|
||||
renderCalendar();
|
||||
checkOverflow();
|
||||
bindCalendarEvents();
|
||||
|
||||
//바인드 함수 컨트롤
|
||||
function bindCalendarEvents() {
|
||||
|
||||
// 이전 월
|
||||
const prevBtn = document.getElementById("prevMonth");
|
||||
if (prevBtn) {
|
||||
prevBtn.onclick = () => {
|
||||
currentMonth--;
|
||||
if (currentMonth < 0) {
|
||||
currentMonth = 11;
|
||||
currentYear--;
|
||||
}
|
||||
renderCalendar();
|
||||
checkOverflow();
|
||||
};
|
||||
}
|
||||
|
||||
// 다음 월
|
||||
const nextBtn = document.getElementById("nextMonth");
|
||||
if (nextBtn) {
|
||||
nextBtn.onclick = () => {
|
||||
currentMonth++;
|
||||
if (currentMonth > 11) {
|
||||
currentMonth = 0;
|
||||
currentYear++;
|
||||
}
|
||||
renderCalendar();
|
||||
checkOverflow();
|
||||
};
|
||||
}
|
||||
|
||||
// 필터 체크박스
|
||||
const filterCheckboxes = document.querySelectorAll('.filter-options input[type="checkbox"]');
|
||||
filterCheckboxes.forEach((checkbox) => {
|
||||
checkbox.onclick = () => {
|
||||
selectedFilters = Array.from(filterCheckboxes)
|
||||
.filter((cb) => cb.checked)
|
||||
.map((cb) => cb.value);
|
||||
|
||||
renderCalendar();
|
||||
checkOverflow();
|
||||
};
|
||||
});
|
||||
|
||||
// 접기&펼치기 토글
|
||||
const radios = document.querySelectorAll('input[name="toggle"]');
|
||||
|
||||
radios.forEach((radio) => {
|
||||
radio.addEventListener("change", (e) => {
|
||||
const calendarDaysEl = document.getElementById("calendarDays");
|
||||
if (e.target.value === "접기") {
|
||||
calendarDaysEl.classList.add("collapsed");
|
||||
} else {
|
||||
calendarDaysEl.classList.remove("collapsed");
|
||||
|
||||
// 스크롤을 맨 아래로 이동
|
||||
setTimeout(() => {
|
||||
const scheduleList = document.querySelector(".calendar-days");
|
||||
const scheduleItem = document.querySelector(".current-week-row");
|
||||
if (scheduleList && scheduleItem) {
|
||||
// 현재 주가 보이도록 + 살짝 아래로 여유 있게
|
||||
scheduleList.scrollTo({
|
||||
top:
|
||||
scheduleItem.offsetTop -
|
||||
scheduleList.offsetHeight / 2 +
|
||||
scheduleItem.offsetHeight / 2,
|
||||
// 또는 그냥 맨 아래로 내리고 싶으면:
|
||||
// top: scheduleList.scrollHeight,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 모달이 로드되면 자동 실행
|
||||
initializeCalendar();
|
||||
</script>
|
||||
</div>
|
||||
@@ -0,0 +1,275 @@
|
||||
<section class="card individual">
|
||||
<div class="chart-wrap">
|
||||
<div class="card-header">
|
||||
<h3>연간 개인별 판매 실적<span class="tail"></span></h3>
|
||||
<div class="legend">
|
||||
<span>
|
||||
<span class="legend-dot line-area01"></span>
|
||||
권혁진 수석
|
||||
<span class="badge-individual01"></span>
|
||||
</span>
|
||||
<span>
|
||||
<span class="legend-dot line-area02"></span>
|
||||
염승호 수석
|
||||
<span class="badge-individual02"></span>
|
||||
</span>
|
||||
<span class="unit">(단위:copy) </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-body">
|
||||
<!-- <div class="badge-box">
|
||||
|
||||
|
||||
</div> -->
|
||||
<canvas id="individual_chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="person-box">
|
||||
<div class="person-name"><span>권혁진</span> <small>수석</small></div>
|
||||
<div class="stat-row individual01">
|
||||
<div class="label-box">
|
||||
<span class="label">누적 판매수</span>
|
||||
<!-- <div class="change-down">전월대비 <em>27</em></div> -->
|
||||
</div>
|
||||
<div>
|
||||
<span class="big-num">162</span><span class="unit copy">copy</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<div class="label-box">
|
||||
<span class="label">목표 달성률</span>
|
||||
<!-- <div class="change-up">전월대비 <em>15p</em></div> -->
|
||||
</div>
|
||||
<div><span class="big-num">81</span><span class="unit">%</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="person-box">
|
||||
<div class="person-name">
|
||||
<span>염승호</span>
|
||||
<small>수석</small>
|
||||
</div>
|
||||
<div class="stat-row individual02">
|
||||
<div class="label-box">
|
||||
<span class="label">누적 판매수</span>
|
||||
<!-- <div class="change-up">전월대비 <em>105</em></div> -->
|
||||
</div>
|
||||
<div>
|
||||
<span class="big-num">508</span><span class="unit copy">copy</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<div class="label-box">
|
||||
<span class="label">목표 달성률</span>
|
||||
<!-- <div class="change-up">전월대비 <em>63p</em></div> -->
|
||||
</div>
|
||||
<div><span class="big-num">169.3</span><span class="unit">%</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
(() => {
|
||||
|
||||
let INDIVIDUAL_DATA = null;
|
||||
|
||||
// -----------------------------
|
||||
// 1) UI 숫자 반영
|
||||
// -----------------------------
|
||||
function updateIndividualUI() {
|
||||
if (!INDIVIDUAL_DATA || INDIVIDUAL_DATA.length < 2) return;
|
||||
|
||||
const p1 = INDIVIDUAL_DATA[0];
|
||||
const p2 = INDIVIDUAL_DATA[1];
|
||||
|
||||
// 이름
|
||||
document.querySelector('.person-box:nth-child(1) .person-name span').textContent = p1.name;
|
||||
document.querySelector('.person-box:nth-child(2) .person-name span').textContent = p2.name;
|
||||
|
||||
// 누적 판매수
|
||||
document.querySelector('.individual01 .big-num').textContent = p1.total;
|
||||
document.querySelector('.individual02 .big-num').textContent = p2.total;
|
||||
|
||||
// 달성률
|
||||
document.querySelector('.person-box:nth-child(1) .stat-row:nth-child(3) .big-num').textContent = p1.rate;
|
||||
document.querySelector('.person-box:nth-child(2) .stat-row:nth-child(3) .big-num').textContent = p2.rate;
|
||||
|
||||
// 개인별 목표(배지)
|
||||
document.querySelector('.badge-individual01').textContent = `목표 ${p1.target_qty}`;
|
||||
document.querySelector('.badge-individual02').textContent = `목표 ${p2.target_qty}`;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// 2) 차트 생성
|
||||
// -----------------------------
|
||||
const labels12 = Array.from({ length: 12 }, (_, i) => String(i + 1));
|
||||
|
||||
const currentMonth = new Date().getMonth() + 1;
|
||||
const currentMonthStr = String(currentMonth);
|
||||
|
||||
let individualInstance = null;
|
||||
|
||||
function createIndividualChart() {
|
||||
const ctx = document.getElementById("individual_chart");
|
||||
if (!ctx || !INDIVIDUAL_DATA || INDIVIDUAL_DATA.length < 2) return;
|
||||
|
||||
if (individualInstance) individualInstance.destroy();
|
||||
|
||||
individualInstance = new Chart(ctx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: labels12,
|
||||
datasets: [
|
||||
{
|
||||
label: "권혁진 수석",
|
||||
data: [...INDIVIDUAL_DATA[0].monthly, null],
|
||||
fill: true,
|
||||
backgroundColor: (ctx) => {
|
||||
const a = ctx.chart.chartArea;
|
||||
return !a
|
||||
? cssVar("--bg-chart-area01")
|
||||
: safeGrad(
|
||||
ctx.chart.ctx,
|
||||
a,
|
||||
[
|
||||
{ stop: 0, color: cssVar("--bg-chart-area01") },
|
||||
{ stop: 1, color: "rgba(255, 255, 255, 0.3)" },
|
||||
],
|
||||
true
|
||||
);
|
||||
},
|
||||
borderColor: cssVar("--bg-chart-linearea01"),
|
||||
borderWidth: 1,
|
||||
pointRadius: 4,
|
||||
tension: 0,
|
||||
},
|
||||
{
|
||||
label: "염승호 수석",
|
||||
data: [...INDIVIDUAL_DATA[1].monthly, null],
|
||||
fill: true,
|
||||
backgroundColor: (ctx) => {
|
||||
const a = ctx.chart.chartArea;
|
||||
return !a
|
||||
? cssVar("--bg-chart-area02")
|
||||
: safeGrad(
|
||||
ctx.chart.ctx,
|
||||
a,
|
||||
[
|
||||
{ stop: 0, color: cssVar("--bg-chart-area02") },
|
||||
{ stop: 1, color: "rgba(255, 255, 255, 0.3)" },
|
||||
],
|
||||
true
|
||||
);
|
||||
},
|
||||
borderColor: cssVar("--bg-chart-linearea02"),
|
||||
borderWidth: 1,
|
||||
pointRadius: 4,
|
||||
tension: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
layout: { padding: { top: 16 } },
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: { enabled: false },
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
afterBuildTicks: (scale) => {
|
||||
const maxValue = scale.chart.data.datasets
|
||||
.flatMap((d) => d.data)
|
||||
.filter((n) => typeof n === "number");
|
||||
|
||||
const realMax = Math.max(...maxValue);
|
||||
const rawMax = realMax + 50;
|
||||
const targetMax = Math.ceil(rawMax / 100) * 100;
|
||||
const mid = Math.round(targetMax / 2);
|
||||
|
||||
scale.max = targetMax;
|
||||
scale.min = 0;
|
||||
scale.ticks = [
|
||||
{ value: 0 },
|
||||
{ value: mid },
|
||||
{ value: targetMax },
|
||||
];
|
||||
scale._midIndex = 1;
|
||||
},
|
||||
grid: {
|
||||
drawTicks: false,
|
||||
color: (ctx) =>
|
||||
ctx.index === ctx.scale._midIndex
|
||||
? cssVar("--grid-border-dashed")
|
||||
: "rgba(0,0,0,0)",
|
||||
lineWidth: (ctx) =>
|
||||
ctx.index === ctx.scale._midIndex ? 1 : 0,
|
||||
},
|
||||
border: { display: false },
|
||||
ticks: {
|
||||
padding: 4,
|
||||
color: cssVar("--chart-base"),
|
||||
},
|
||||
},
|
||||
x: {
|
||||
offset: true,
|
||||
grid: {
|
||||
drawTicks: false,
|
||||
display: true,
|
||||
color: cssVar("--grid-border") || "#e0e0e0",
|
||||
drawOnChartArea: true,
|
||||
z: -1,
|
||||
},
|
||||
ticks: {
|
||||
color: (ctx) =>
|
||||
ctx.tick.label === currentMonthStr
|
||||
? cssVar("--chart-point")
|
||||
: cssVar("--chart-base"),
|
||||
font: (ctx) =>
|
||||
ctx.tick.label === currentMonthStr
|
||||
? { weight: "bold", size: 18 }
|
||||
: { weight: "normal", size: 18 },
|
||||
padding: 10,
|
||||
callback: (_, index) => labels12[index],
|
||||
},
|
||||
border: {
|
||||
display: true,
|
||||
dash: [3, 3],
|
||||
color: cssVar("--grid-border-dashed"),
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
highlightXFactory(currentMonthStr),
|
||||
hideNullPointsPlugin,
|
||||
createPointGradientPluginWithBorder(),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// 3) 개인별 데이터 로딩 함수 (🔥 핵심)
|
||||
// -----------------------------
|
||||
function loadIndividualDashboardData() {
|
||||
return fetch(`/egbim/bbs/sales_dashboard.php?year=${window.APP_YEAR}&_=${Date.now()}`)
|
||||
.then(r => r.json())
|
||||
.then(json => {
|
||||
INDIVIDUAL_DATA = json.individual || [];
|
||||
updateIndividualUI();
|
||||
createIndividualChart();
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// 4) 전역 등록 + 최초 실행
|
||||
// -----------------------------
|
||||
window.loadIndividualDashboardData = loadIndividualDashboardData;
|
||||
loadIndividualDashboardData();
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
<section class="card month">
|
||||
<div class="card-header">
|
||||
<h3>11월 판매 실적<span class="tail"></span></h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="stat-row month">
|
||||
<div class="label-box">
|
||||
<span class="label">당월 판매수</span>
|
||||
<!-- <span class="change-down">전주대비 <em>114</em></span> -->
|
||||
</div>
|
||||
<div>
|
||||
<span class="big-num" id="month_sales_value">0</span><span class="unit copy">copy</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-row">
|
||||
<div class="label-box">
|
||||
<span class="label">당월 매출액</span>
|
||||
<!-- <span class="change">전주대비 <em>0</em></span> -->
|
||||
</div>
|
||||
<div><span class="big-num" id="month_amount_value">0</span><span class="unit">만원</span></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-row">
|
||||
<div class="label-box">
|
||||
<span class="label">목표 달성률</span>
|
||||
<!-- <span class="change-up">전주대비 <em>3p</em></span> -->
|
||||
</div>
|
||||
<div><span class="big-num" id="month_rate_value">0</span><span class="unit">%</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<div class="chart-header">
|
||||
<!-- <span class="badge-month">목표 125</span> -->
|
||||
<div class="legend">
|
||||
<span class="unit">(단위:copy) </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-body">
|
||||
<canvas id="weekly_chart"></canvas>
|
||||
<div id="weekly_legend"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
let weeklyInstance = null;
|
||||
|
||||
// ------------------------------
|
||||
// 1) 초기값 (0으로 먼저 렌더)
|
||||
// ------------------------------
|
||||
let cumulative = [0, 0, 0, 0, 0];
|
||||
let goal = 0;
|
||||
|
||||
// ------------------------------
|
||||
// 📌 현재 월 텍스트로 변경
|
||||
// ------------------------------
|
||||
const now = new Date();
|
||||
const month = now.getMonth() + 1;
|
||||
const monthText = `${month}월 판매 실적`;
|
||||
|
||||
document.querySelector(".card.month .card-header h3").innerHTML =
|
||||
monthText + '<span class="tail"></span>';
|
||||
|
||||
// ------------------------------
|
||||
// 📌 오늘 날짜 기준 몇 주차인지 계산
|
||||
// ------------------------------
|
||||
function getWeekOfMonth(date = new Date()) {
|
||||
const firstDay = new Date(date.getFullYear(), date.getMonth(), 1);
|
||||
|
||||
// 월요일 기준 (월=1, 일=7)
|
||||
const firstDayOfWeek = firstDay.getDay() === 0 ? 7 : firstDay.getDay();
|
||||
const offset = firstDayOfWeek - 1;
|
||||
|
||||
return Math.floor((date.getDate() + offset) / 7);
|
||||
}
|
||||
|
||||
const CURRENT_WEEK = getWeekOfMonth(); // 1~5
|
||||
|
||||
// ------------------------------
|
||||
// 차트 생성 함수
|
||||
// ------------------------------
|
||||
function createMonthChart() {
|
||||
const ctx = document.getElementById("weekly_chart");
|
||||
if (!ctx) return;
|
||||
|
||||
if (weeklyInstance) weeklyInstance.destroy();
|
||||
|
||||
const total = cumulative.reduce((a, b) => a + b, 0);
|
||||
const remaining = Math.max(0, goal - total);
|
||||
|
||||
weeklyInstance = new Chart(ctx, {
|
||||
type: "doughnut",
|
||||
data: {
|
||||
labels: ["1주차", "2주차", "3주차", "4주차", "5주차", "남은 목표"],
|
||||
datasets: [{
|
||||
data: [...cumulative, remaining],
|
||||
backgroundColor: [
|
||||
"#FFD311", "#FFA808", "#F58C03",
|
||||
"#E76C00", "#B65500", "#e1e1e1"
|
||||
],
|
||||
borderWidth: 2,
|
||||
borderColor: "#fff",
|
||||
circumference: 180,
|
||||
rotation: 270,
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: { enabled: false },
|
||||
htmlLegend: {
|
||||
containerID: "weekly_legend",
|
||||
activeWeek: CURRENT_WEEK,
|
||||
},
|
||||
},
|
||||
cutout: "45%",
|
||||
layout: {
|
||||
padding: { top: 20, bottom: 40, left: 40, right: 40 }
|
||||
}
|
||||
},
|
||||
plugins: [htmlLegendPlugin, gaugeValuePlugin],
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// 2) 초기 0 데이터로 차트 1회 렌더
|
||||
// ------------------------------
|
||||
createMonthChart();
|
||||
|
||||
// ------------------------------
|
||||
// 3) 월간 데이터 로딩 함수 (🔥 핵심)
|
||||
// ------------------------------
|
||||
function loadMonthDashboardData() {
|
||||
return fetch(`/egbim/bbs/sales_dashboard.php?year=${window.APP_YEAR}&_=${Date.now()}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
|
||||
// (1) 숫자 UI 반영
|
||||
document.getElementById("month_sales_value").textContent =
|
||||
data.month_sales ?? 0;
|
||||
|
||||
document.getElementById("month_amount_value").textContent =
|
||||
Math.round(data.month_sales_amount / 10000);
|
||||
|
||||
document.getElementById("month_rate_value").textContent =
|
||||
data.target_rate ?? 0;
|
||||
|
||||
// (2) 주차별 수량
|
||||
const week = data.weekly || {};
|
||||
cumulative = [
|
||||
week[1] ?? 0,
|
||||
week[2] ?? 0,
|
||||
week[3] ?? 0,
|
||||
week[4] ?? 0,
|
||||
week[5] ?? 0,
|
||||
];
|
||||
|
||||
// (3) 목표 수량
|
||||
goal = data.target_qty ?? 0;
|
||||
|
||||
// (4) 차트 재렌더
|
||||
createMonthChart();
|
||||
});
|
||||
console.log('월데이터 성공');
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// 4) 전역 등록 + 최초 실행
|
||||
// ------------------------------
|
||||
window.loadMonthDashboardData = loadMonthDashboardData;
|
||||
loadMonthDashboardData();
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
<section class="card sales">
|
||||
<div class="card-header">
|
||||
<h3>영업 업체 현황<span class="tail"></span></h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="stat-row sales">
|
||||
<div class="label-box">
|
||||
<span class="label">영업 업체수</span>
|
||||
<!-- <span class="change-up">전월대비 <em>53</em></span> -->
|
||||
</div>
|
||||
<div><span class="big-num">148</span><span class="unit">개</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<div class="legend">
|
||||
<span class="unit">(단위:업체수)</span>
|
||||
</div>
|
||||
<div class="chart-body">
|
||||
<h4 class="chart-tit">산업/업종별</h4>
|
||||
<canvas id="industry_chart"></canvas>
|
||||
</div>
|
||||
<div class="chart-body">
|
||||
<h4 class="chart-tit">기관별</h4>
|
||||
<canvas id="institution_chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
(() => {
|
||||
|
||||
let industryInstance = null;
|
||||
let institutionInstance = null;
|
||||
|
||||
// ⭐ 업종: 설계, 교육, 제작, 감리, 시공, 기타 → 총 6개
|
||||
let industryData = [0, 0, 0, 0, 0, 0];
|
||||
|
||||
// ⭐ 기관: 기존 그대로 4개
|
||||
let institutionData = [0, 0, 0, 0];
|
||||
|
||||
/* --------------------------------------------------------
|
||||
1) API 로딩
|
||||
-------------------------------------------------------- */
|
||||
function loadSalesDashboardData() {
|
||||
fetch("/egbim/bbs/sales_dashboard.php")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
|
||||
const clientCountEl = document.querySelector(".stat-row.sales .big-num");
|
||||
if (clientCountEl) clientCountEl.textContent = data.client_count ?? 0;
|
||||
|
||||
// 업종 6개
|
||||
industryData = [
|
||||
data.industry?.설계 ?? 0,
|
||||
data.industry?.교육 ?? 0,
|
||||
data.industry?.제작 ?? 0,
|
||||
data.industry?.감리 ?? 0,
|
||||
data.industry?.시공 ?? 0,
|
||||
data.industry?.기타 ?? 0,
|
||||
];
|
||||
|
||||
// 기관 4개
|
||||
institutionData = [
|
||||
data.institution?.일반기업 ?? 0,
|
||||
data.institution?.교육기관 ?? 0,
|
||||
data.institution?.공공기관 ?? 0,
|
||||
data.institution?.기타 ?? 0,
|
||||
];
|
||||
|
||||
createIndustryChart();
|
||||
});
|
||||
}
|
||||
window.loadSalesDashboardData = loadSalesDashboardData;
|
||||
loadSalesDashboardData();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* --------------------------------------------------------
|
||||
2) 차트 생성 (+ 0 데이터 제거)
|
||||
-------------------------------------------------------- */
|
||||
function createIndustryChart() {
|
||||
const industry_chart = document.getElementById("industry_chart");
|
||||
const institution_chart = document.getElementById("institution_chart");
|
||||
|
||||
// 기존 차트 제거
|
||||
if (industryInstance) industryInstance.destroy();
|
||||
if (institutionInstance) institutionInstance.destroy();
|
||||
|
||||
/* ----------------------------------------------------
|
||||
🔵 업종 차트 (6개)
|
||||
→ 0 이상 항목만 표시
|
||||
---------------------------------------------------- */
|
||||
const industryLabelsAll = ["설계", "교육", "제작", "감리", "시공", "기타"];
|
||||
const industryColorsAll = ["#EBA170", "#E08042", "#BB5E21", "#8A471B", "#603213", "#3F1A01"];
|
||||
|
||||
const industryFiltered = industryData
|
||||
.map((v, i) => ({ value: v, label: industryLabelsAll[i], color: industryColorsAll[i] }))
|
||||
.filter(item => item.value > 0);
|
||||
|
||||
const industryLabels = industryFiltered.map(i => i.label);
|
||||
const industryValues = industryFiltered.map(i => i.value);
|
||||
const industryColors = industryFiltered.map(i => i.color);
|
||||
|
||||
industryInstance = new Chart(industry_chart, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: industryLabels,
|
||||
datasets: [
|
||||
{
|
||||
data: industryValues,
|
||||
backgroundColor: industryColors,
|
||||
borderWidth: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
indexAxis: "y",
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
cutout: "50%",
|
||||
layout: {
|
||||
padding: { top: 40, bottom: 16, left: 0, right: 10 },
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: { enabled: false },
|
||||
datalabels: {
|
||||
anchor: "end",
|
||||
align: "end",
|
||||
color: function (context) {
|
||||
return industryColors[context.dataIndex];
|
||||
},
|
||||
font: {
|
||||
size: 16,
|
||||
weight: "500",
|
||||
},
|
||||
formatter: function (value, context) {
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = Math.round((value / total) * 100);
|
||||
return value + " (" + percentage + "%)";
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
afterBuildTicks: (scale) => {
|
||||
const maxValue = scale.chart.data.datasets
|
||||
.flatMap((d) => d.data)
|
||||
.filter((n) => typeof n === "number");
|
||||
|
||||
const realMax = Math.max(...maxValue);
|
||||
|
||||
// +100 후 백단위 올림
|
||||
const rawMax = realMax + 100;
|
||||
const targetMax = Math.ceil(rawMax / 100) * 100;
|
||||
|
||||
const mid = Math.round(targetMax / 2);
|
||||
|
||||
scale.max = targetMax;
|
||||
scale.min = 0;
|
||||
|
||||
// tick 3개
|
||||
scale.ticks = [
|
||||
{ value: 0 },
|
||||
{ value: mid },
|
||||
{ value: targetMax },
|
||||
];
|
||||
|
||||
scale._midIndex = 1;
|
||||
},
|
||||
beginAtZero: true,
|
||||
border: { display: false },
|
||||
ticks: { padding: -4 },
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
ticks: {
|
||||
color: function (context) {
|
||||
return industryColors[context.index];
|
||||
},
|
||||
font: {
|
||||
size: 16,
|
||||
weight: "500",
|
||||
},
|
||||
padding: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [createCustomGridPlugin(),createCustomLabelPlugin(industryColors)],
|
||||
});
|
||||
|
||||
|
||||
/* ----------------------------------------------------
|
||||
🟤 기관 차트 (4개)
|
||||
→ 0 이상 항목만 표시
|
||||
---------------------------------------------------- */
|
||||
const instLabelsAll = ["일반기업", "교육기관", "공공기관", "기타"];
|
||||
const instColorsAll = ["#EBBA69", "#D59C3D", "#B1802C", "#855B17"];
|
||||
|
||||
const instFiltered = institutionData
|
||||
.map((v, i) => ({ value: v, label: instLabelsAll[i], color: instColorsAll[i] }))
|
||||
.filter(item => item.value > 0);
|
||||
|
||||
const instLabels = instFiltered.map(i => i.label);
|
||||
const instValues = instFiltered.map(i => i.value);
|
||||
const instColors = instFiltered.map(i => i.color);
|
||||
|
||||
institutionInstance = new Chart(institution_chart, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: instLabels,
|
||||
datasets: [
|
||||
{
|
||||
data: instValues,
|
||||
backgroundColor: instColors,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
indexAxis: "y",
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
cutout: "50%",
|
||||
layout: {
|
||||
padding: { top: 40, bottom: 16, left: 0, right: 10 },
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: { enabled: false },
|
||||
datalabels: {
|
||||
anchor: "end",
|
||||
align: "end",
|
||||
color: function (context) {
|
||||
return instColors[context.dataIndex];
|
||||
},
|
||||
font: {
|
||||
size: 16,
|
||||
weight: "500",
|
||||
},
|
||||
formatter: function (value, context) {
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = Math.round((value / total) * 100);
|
||||
return value + " (" + percentage + "%)";
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
afterBuildTicks: (scale) => {
|
||||
const maxValue = scale.chart.data.datasets
|
||||
.flatMap((d) => d.data)
|
||||
.filter((n) => typeof n === "number");
|
||||
|
||||
const realMax = Math.max(...maxValue);
|
||||
|
||||
// +100 후 백단위 올림
|
||||
const rawMax = realMax + 100;
|
||||
const targetMax = Math.ceil(rawMax / 100) * 100;
|
||||
|
||||
const mid = Math.round(targetMax / 2);
|
||||
|
||||
scale.max = targetMax;
|
||||
scale.min = 0;
|
||||
|
||||
// tick 3개
|
||||
scale.ticks = [
|
||||
{ value: 0 },
|
||||
{ value: mid },
|
||||
{ value: targetMax },
|
||||
];
|
||||
|
||||
scale._midIndex = 1;
|
||||
},
|
||||
beginAtZero: true,
|
||||
border: { display: false },
|
||||
ticks: { padding: -4 },
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
ticks: {
|
||||
color: function (context) {
|
||||
return instColors[context.index];
|
||||
},
|
||||
font: {
|
||||
size: 16,
|
||||
weight: "500",
|
||||
},
|
||||
padding: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [createCustomGridPlugin(),createCustomLabelPlugin(instColors)],
|
||||
});
|
||||
|
||||
|
||||
} // function 끝
|
||||
|
||||
})(); // 즉시실행 함수 끝
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,477 @@
|
||||
<?php
|
||||
$pdo = new PDO("mysql:host=localhost;dbname=egbim;charset=utf8mb4","egbim","baron3840!!");
|
||||
|
||||
$rows = $pdo->query("
|
||||
SELECT
|
||||
s.schedule_date,
|
||||
s.time_period,
|
||||
s.content,
|
||||
s.emp_no,
|
||||
m.emp_name,
|
||||
c.client_name
|
||||
FROM sales_schedules s
|
||||
LEFT JOIN sales_members m ON s.emp_no = m.emp_no
|
||||
LEFT JOIN sales_clients c ON s.client_code = c.client_code
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 사번 매핑
|
||||
$tagMap = [
|
||||
"j01201" => "권",
|
||||
"223070" => "염",
|
||||
"m21430" => "김",
|
||||
"b21367" => "윤",
|
||||
];
|
||||
|
||||
$eventsData = [];
|
||||
|
||||
foreach ($rows as $r) {
|
||||
$date = $r["schedule_date"];
|
||||
$empNo = strtolower($r["emp_no"]);
|
||||
|
||||
if (!isset($eventsData[$date])) {
|
||||
$eventsData[$date] = [];
|
||||
}
|
||||
|
||||
$time = trim($r["time_period"]); // 공백 제거
|
||||
$time = str_replace(["\r", "\n", "\t"], "", $time); // 숨은 문자 제거
|
||||
|
||||
$eventsData[$date][] = [
|
||||
"time" => $time, // ⭐ 핵심
|
||||
"title" => $r["content"],
|
||||
"tags" => [ $tagMap[$empNo] ?? "" ]
|
||||
];
|
||||
}
|
||||
// JS로 전달
|
||||
echo "<script>var eventsData = " . json_encode($eventsData, JSON_UNESCAPED_UNICODE) . ";</script>";
|
||||
?>
|
||||
|
||||
<div class="main-weekly weekly" id="weekly">
|
||||
<div class="weekly-grid">
|
||||
<div class="weekly-header">
|
||||
<div class="weekly-title">
|
||||
<p>주간 영업 계획</p>
|
||||
<div class="week-navigation">
|
||||
<button class="week-nav-btn" id="prevWeek" title="이전 주">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#424242"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M15 18l-6-6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
<h3 id="weekTitle">11월 5주차</h3>
|
||||
<button class="week-nav-btn" id="nextWeek" title="다음 주">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#424242"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M9 18l6-6-6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="person-list">
|
||||
<li>
|
||||
<span class="badge-green">권</span><span class="name">권혁진 <small>수석</small></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="badge-purple">염</span><span class="name">염승호 <small>수석</small></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="badge-teal">김</span><span class="name">김지영 <small>선임</small></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="badge-black">윤</span><span class="name">윤준수 <small>선임</small></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="schedule" id="scheduleContainer"></div>
|
||||
</div>
|
||||
<div class="main-issue">
|
||||
<div class="issue-header">
|
||||
<h3>주요 이슈 사항</h3>
|
||||
</div>
|
||||
<div class="issue">
|
||||
<ul class="issue-list">
|
||||
<li>주요 이슈 A</li>
|
||||
<li>주요 이슈 B</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./assets/js/schedule-manager.min.js"></script>
|
||||
<!-- <script src="./assets/js/events-data.min.js"></script> -->
|
||||
<script>
|
||||
const personOrder = {
|
||||
"권": 1,
|
||||
"염": 2,
|
||||
"김": 3,
|
||||
"윤": 4,
|
||||
};
|
||||
|
||||
function isMorning(event) {
|
||||
return event.time === "오전";
|
||||
}
|
||||
|
||||
function isAfternoon(event) {
|
||||
return event.time === "오후";
|
||||
}
|
||||
|
||||
// 태그 매핑
|
||||
const tagMapping = {
|
||||
권: "green",
|
||||
염: "purple",
|
||||
김: "teal",
|
||||
윤: "black",
|
||||
};
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ⭐ scheduleManager를 상단에 선언 (초기화는 나중에)
|
||||
let scheduleManager;
|
||||
async function loadIssueText() {
|
||||
|
||||
// ⭐ 현재 주간 달 기준으로 변경
|
||||
const year = currentWeekStart.getFullYear();
|
||||
const month = String(currentWeekStart.getMonth() + 1).padStart(2, "0");
|
||||
const ym = `${year}-${month}`;
|
||||
|
||||
let res = await fetch(`/egbim/bbs/sales_issue.php?action=get&issue_month=${ym}`);
|
||||
let json = await res.json();
|
||||
|
||||
const issue = json.issue_text ?? "";
|
||||
|
||||
const ul = document.querySelector(".issue-list");
|
||||
ul.innerHTML = "";
|
||||
|
||||
if (issue.trim() === "") {
|
||||
ul.innerHTML = "<li>이번 달 주요 이슈가 없습니다.</li>";
|
||||
return;
|
||||
}
|
||||
|
||||
issue.split("\n").forEach(line => {
|
||||
if (line.trim() !== "") {
|
||||
ul.innerHTML += `<li>${line}</li>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 현재 날짜에서 이번 주 월요일 구하기
|
||||
function getThisMonday() {
|
||||
const today = new Date();
|
||||
const day = today.getDay(); // 0(일) ~ 6(토)
|
||||
const diff = day === 0 ? -6 : 1 - day; // 일요일이면 -6, 아니면 1-day
|
||||
|
||||
const monday = new Date(today);
|
||||
monday.setDate(today.getDate() + diff);
|
||||
monday.setHours(0, 0, 0, 0); // 시간 초기화
|
||||
|
||||
return monday;
|
||||
}
|
||||
|
||||
// 현재 주차 상태
|
||||
let currentWeekStart = getThisMonday();
|
||||
|
||||
// 주차 타이틀 업데이트
|
||||
function updateWeekTitle() {
|
||||
const weekStart = new Date(currentWeekStart);
|
||||
const month = weekStart.getMonth() + 1;
|
||||
const startDate = weekStart.getDate();
|
||||
|
||||
// 해당 월의 몇 번째 주인지 계산
|
||||
const firstDayOfMonth = new Date(
|
||||
weekStart.getFullYear(),
|
||||
weekStart.getMonth(),
|
||||
1,
|
||||
);
|
||||
|
||||
const firstDayOfWeek = firstDayOfMonth.getDay() < 2 ? 7 : firstDayOfMonth.getDay();
|
||||
const offset = firstDayOfWeek - 1;
|
||||
|
||||
//const weekOfMonth = Math.ceil((startDate + firstDayOfMonth.getDay()) / 7);
|
||||
const weekOfMonth = Math.floor((startDate + offset) / 7);
|
||||
|
||||
document.getElementById("weekTitle").textContent =
|
||||
`${month}월 ${weekOfMonth}주차`;
|
||||
}
|
||||
|
||||
// 요일 이름 가져오기
|
||||
function getDayName(dayIndex) {
|
||||
const days = ["일", "월", "화", "수", "목", "금", "토"];
|
||||
return days[dayIndex];
|
||||
}
|
||||
|
||||
|
||||
// 주간 일정 렌더링
|
||||
function renderWeeklySchedule() {
|
||||
const container = document.getElementById("scheduleContainer");
|
||||
container.innerHTML = "";
|
||||
|
||||
// 월요일부터 금요일까지 (5일)
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const currentDate = new Date(currentWeekStart);
|
||||
currentDate.setDate(currentWeekStart.getDate() + i);
|
||||
|
||||
const year = currentDate.getFullYear();
|
||||
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(currentDate.getDate()).padStart(2, "0");
|
||||
const dateKey = `${year}-${month}-${day}`;
|
||||
|
||||
const dayOfWeek = getDayName(currentDate.getDay());
|
||||
const dayEvents = eventsData[dateKey] || [];
|
||||
|
||||
dayEvents.sort((a, b) => {
|
||||
const aTag = a.tags[0] ?? "";
|
||||
const bTag = b.tags[0] ?? "";
|
||||
|
||||
return (personOrder[aTag] || 99) - (personOrder[bTag] || 99);
|
||||
});
|
||||
|
||||
// 카드 생성
|
||||
const dayCard = document.createElement("div");
|
||||
dayCard.className = "day-card";
|
||||
dayCard.dataset.date = dateKey;
|
||||
|
||||
let morningHTML = "";
|
||||
let afternoonHTML = "";
|
||||
|
||||
dayEvents.forEach((event) => {
|
||||
console.log(
|
||||
dateKey,
|
||||
event.time,
|
||||
JSON.stringify(event.time),
|
||||
isMorning(event),
|
||||
isAfternoon(event)
|
||||
);
|
||||
});
|
||||
|
||||
dayEvents.forEach((event) => {
|
||||
const tagsHTML = event.tags
|
||||
.map(tag => `<span class="badge-${tagMapping[tag]}">${tag}</span>`)
|
||||
.join("");
|
||||
|
||||
const itemHTML = `
|
||||
<li>
|
||||
<a href="#" class="schedule-item">
|
||||
${tagsHTML}
|
||||
${event.title}
|
||||
</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
if (isMorning(event)) {
|
||||
morningHTML += itemHTML;
|
||||
}
|
||||
|
||||
if (isAfternoon(event)) {
|
||||
afternoonHTML += itemHTML;
|
||||
}
|
||||
});
|
||||
|
||||
let dayHTML = `
|
||||
<div class="day-title">${day}일 <span>(${dayOfWeek})</span></div>
|
||||
|
||||
<div class="day">
|
||||
<dl>
|
||||
<dt>오전</dt>
|
||||
<dd class="day-content">
|
||||
${morningHTML}
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<dl>
|
||||
<dt>오후</dt>
|
||||
<dd class="day-content">
|
||||
${afternoonHTML}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
`;
|
||||
|
||||
dayCard.innerHTML = dayHTML;
|
||||
container.appendChild(dayCard);
|
||||
}
|
||||
|
||||
updateWeekTitle();
|
||||
if (scheduleManager && typeof scheduleManager.initialize === "function") {
|
||||
scheduleManager.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 이벤트 리스너 등록 함수
|
||||
function setupEventListeners() {
|
||||
const prevWeekBtn = document.getElementById("prevWeek");
|
||||
const nextWeekBtn = document.getElementById("nextWeek");
|
||||
|
||||
// 이전 주로 이동
|
||||
if (prevWeekBtn) {
|
||||
prevWeekBtn.addEventListener("click", () => {
|
||||
currentWeekStart.setDate(currentWeekStart.getDate() - 7);
|
||||
renderWeeklySchedule();
|
||||
loadIssueText();
|
||||
});
|
||||
}
|
||||
|
||||
// 다음 주로 이동
|
||||
if (nextWeekBtn) {
|
||||
nextWeekBtn.addEventListener("click", () => {
|
||||
currentWeekStart.setDate(currentWeekStart.getDate() + 7);
|
||||
renderWeeklySchedule();
|
||||
loadIssueText();
|
||||
});
|
||||
}
|
||||
}
|
||||
// ⭐ 초기화 함수
|
||||
function init() {
|
||||
// ScheduleManager가 존재하는지 확인
|
||||
if (typeof ScheduleManager !== 'undefined') {
|
||||
scheduleManager = new ScheduleManager();
|
||||
scheduleManager.initialize();
|
||||
}
|
||||
|
||||
// 이벤트 리스너 등록
|
||||
setupEventListeners();
|
||||
|
||||
// 초기 렌더링
|
||||
renderWeeklySchedule();
|
||||
loadIssueText();
|
||||
}
|
||||
|
||||
// ⭐ DOM 로드 완료 후 실행
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
let isCalendarLoaded = false;
|
||||
// 모달 열기
|
||||
function openModal() {
|
||||
const existingModal = document.getElementById("scheduleModal");
|
||||
|
||||
// 기존 모달이 있으면 제거
|
||||
if (existingModal) {
|
||||
existingModal.remove();
|
||||
}
|
||||
|
||||
// 항상 새로 로드 (데이터 최신화)
|
||||
fetchScheduleDetail();
|
||||
}
|
||||
|
||||
// 모달 닫기
|
||||
function closeModal() {
|
||||
const modal = document.getElementById("scheduleModal");
|
||||
modal.classList.add("closing");
|
||||
isCalendarLoaded = false;
|
||||
setTimeout(() => {
|
||||
modal.remove();
|
||||
}, 400);
|
||||
}
|
||||
|
||||
// AJAX로 calendar.html 페이지 가져오기
|
||||
let calendarScriptsLoaded = false;
|
||||
|
||||
function fetchScheduleDetail() {
|
||||
const weekly = document.getElementById("weekly");
|
||||
|
||||
fetch(`./html/main_sub/calendar.php`)
|
||||
.then((response) => response.text())
|
||||
.then((html) => {
|
||||
// 기존 모달 제거
|
||||
const existingModal = document.getElementById("scheduleModal");
|
||||
if (existingModal) existingModal.remove();
|
||||
|
||||
// 새로운 모달 삽입
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, "text/html");
|
||||
|
||||
// ⭐ body의 자식 노드들을 추가
|
||||
doc.body.childNodes.forEach(node => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE) {
|
||||
weekly.appendChild(node.cloneNode(true));
|
||||
}
|
||||
});
|
||||
|
||||
// ⭐ 스크립트 처리 개선
|
||||
const scripts = doc.querySelectorAll("script");
|
||||
|
||||
if (!calendarScriptsLoaded) {
|
||||
// 첫 번째 로딩: 모든 스크립트 실행
|
||||
scripts.forEach(script => {
|
||||
const newScript = document.createElement("script");
|
||||
if (script.src) {
|
||||
newScript.src = script.src;
|
||||
} else {
|
||||
newScript.textContent = script.textContent;
|
||||
}
|
||||
document.body.appendChild(newScript);
|
||||
});
|
||||
calendarScriptsLoaded = true;
|
||||
} else {
|
||||
// ⭐ 두 번째 이후: 인라인 스크립트만 재실행
|
||||
scripts.forEach(script => {
|
||||
if (!script.src) {
|
||||
// src가 없는 인라인 스크립트만 실행
|
||||
try {
|
||||
eval(script.textContent);
|
||||
} catch (error) {
|
||||
console.error('Script execution error:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ⭐ 모달이 추가된 후 초기화 함수 명시적 호출
|
||||
setTimeout(() => {
|
||||
if (typeof initializeCalendar === "function") {
|
||||
initializeCalendar();
|
||||
}
|
||||
if (typeof renderCalendar === "function") {
|
||||
renderCalendar();
|
||||
}
|
||||
if (typeof checkOverflow === "function") {
|
||||
checkOverflow();
|
||||
}
|
||||
if (typeof bindToggleEvents === "function") {
|
||||
bindToggleEvents();
|
||||
}
|
||||
}, 100);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Calendar loading error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 일정 항목 클릭 이벤트
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.schedule-item')) {
|
||||
e.preventDefault();
|
||||
const scheduleItem = e.target.closest('.schedule-item');
|
||||
const scheduleId = scheduleItem.getAttribute("data-id");
|
||||
openModal(scheduleId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,245 @@
|
||||
<section class="card yearly">
|
||||
<div class="card-header">
|
||||
<h3>연간 총 판매 실적<span class="tail"></span></h3>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- 누적 판매수 -->
|
||||
<div class="stat-row accrue">
|
||||
<div class="label-box">
|
||||
<span class="label">누적 판매수</span>
|
||||
<span class="change-up">전월대비 <em id="year_compare_copy">0</em></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="big-num" id="year_total_copy">0</span>
|
||||
<span class="unit copy">copy</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 누적 판매액 -->
|
||||
<div class="stat-row">
|
||||
<div class="label-box">
|
||||
<span class="label">누적 판매액</span>
|
||||
<span class="change">전월대비 <em id="year_compare_amount">0</em></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="big-num" id="year_total_amount">0.0</span>
|
||||
<span class="unit">만원</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 목표 달성률 -->
|
||||
<div class="stat-row">
|
||||
<div class="label-box">
|
||||
<span class="label">목표 달성률</span>
|
||||
<span class="change-up">전월대비 <em id="year_compare_rate">0</em></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="big-num" id="year_target_rate">0</span>
|
||||
<span class="unit">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-wrap">
|
||||
<div class="chart-header">
|
||||
<span class="badge-year">목표 <span id="year_target_qty">0</span></span>
|
||||
<div class="legend">
|
||||
<span><span class="legend-dot mix-bar"></span>월별 판매수</span>
|
||||
<span><span class="legend-dot mix-line"></span>누적 판매수</span>
|
||||
<span class="unit">(단위:copy) </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-body">
|
||||
<canvas id="yearly_chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
|
||||
// -------------------------------
|
||||
// 1) 기본값 초기화 (시작 시 반드시 0으로 렌더)
|
||||
// -------------------------------
|
||||
const currentMonth = String(new Date().getMonth() + 1);
|
||||
window.yearlyMonthly = Array(12).fill(0);
|
||||
window.yearlyCumulative = Array(12).fill(0);
|
||||
|
||||
const labels12 = Array.from({ length: 12 }, (_, i) => String(i + 1));
|
||||
let yearlyInstance = null;
|
||||
// -------------------------------
|
||||
// 4) API 로딩 → 데이터 반영
|
||||
// -------------------------------
|
||||
function loadYearlyDashboardData() {
|
||||
return fetch(`/egbim/bbs/sales_dashboard.php?year=${window.APP_YEAR}&_=${Date.now()}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
|
||||
const monthlyArr = [];
|
||||
for (let i = 1; i <= 12; i++) {
|
||||
monthlyArr.push(data.monthly[i] ?? 0);
|
||||
}
|
||||
|
||||
const cumulativeArr = [];
|
||||
monthlyArr.reduce((acc, cur) => {
|
||||
cumulativeArr.push(acc + cur);
|
||||
return acc + cur;
|
||||
}, 0);
|
||||
|
||||
const lastIdx = monthlyArr.reduce((l, v, i) => v > 0 ? i : l, -1);
|
||||
window.yearlyMonthly = monthlyArr;
|
||||
window.yearlyCumulative = cumulativeArr.map((v, i) => i <= lastIdx ? v : null);
|
||||
|
||||
// 수치 업데이트
|
||||
year_target_rate.textContent = data.yearly_rate;
|
||||
year_target_qty.textContent = data.yearly_target_qty;
|
||||
year_total_copy.textContent = data.total_copy;
|
||||
year_total_amount.textContent = Math.round(data.total_amount / 10000);
|
||||
|
||||
updateDiff("year_compare_copy", data.diff_copy);
|
||||
updateDiff("year_compare_amount", Math.floor(data.diff_amount / 10000));
|
||||
updateDiff("year_compare_rate", Math.round(data.diff_rate * 10) / 10);
|
||||
|
||||
// 🔥 차트 갱신
|
||||
createYearlyChart();
|
||||
});
|
||||
}
|
||||
window.loadYearlyDashboardData = loadYearlyDashboardData;
|
||||
loadYearlyDashboardData();
|
||||
// -------------------------------
|
||||
// 2) 차트 생성 / 갱신 함수
|
||||
// -------------------------------
|
||||
function createYearlyChart() {
|
||||
const canvas = document.getElementById("yearly_chart");
|
||||
if (!canvas) return;
|
||||
|
||||
// ✅ 최초 1회만 생성
|
||||
if (!yearlyInstance) {
|
||||
yearlyInstance = new Chart(canvas, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: labels12,
|
||||
datasets: [
|
||||
{
|
||||
type: "bar",
|
||||
order: 2,
|
||||
data: window.yearlyMonthly,
|
||||
backgroundColor: cssVar("--chart-mixbar"),
|
||||
borderRadius: { topLeft: 2, topRight: 2 },
|
||||
},
|
||||
{
|
||||
type: "line",
|
||||
order: 1,
|
||||
data: window.yearlyCumulative,
|
||||
borderColor: cssVar("--chart-mixline"),
|
||||
borderWidth: 1,
|
||||
pointRadius: 4,
|
||||
fill: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: { mode: null },
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: { enabled: false },
|
||||
},
|
||||
layout: {
|
||||
padding: { top: 16 },
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
afterBuildTicks: (scale) => {
|
||||
const allValues = scale.chart.data.datasets
|
||||
.flatMap((d) => d.data)
|
||||
.filter((v) => typeof v === "number");
|
||||
|
||||
const maxVal = Math.max(...allValues, 0);
|
||||
const rawMax = maxVal + 50;
|
||||
const targetMax = Math.ceil(rawMax / 100) * 100;
|
||||
const midVal = Math.round(targetMax / 2);
|
||||
|
||||
scale.max = targetMax;
|
||||
scale.min = 0;
|
||||
scale.ticks = [
|
||||
{ value: 0 },
|
||||
{ value: midVal },
|
||||
{ value: targetMax },
|
||||
];
|
||||
scale._midIndex = 1;
|
||||
},
|
||||
grid: {
|
||||
drawTicks: false,
|
||||
color: (ctx) =>
|
||||
ctx.index === ctx.scale._midIndex
|
||||
? cssVar("--grid-border-dashed")
|
||||
: "rgba(0,0,0,0)",
|
||||
lineWidth: (ctx) =>
|
||||
ctx.index === ctx.scale._midIndex ? 1 : 0,
|
||||
},
|
||||
border: { display: false },
|
||||
ticks: {
|
||||
padding: 4,
|
||||
color: cssVar("--chart-base"),
|
||||
},
|
||||
},
|
||||
x: {
|
||||
grid: {
|
||||
drawTicks: false,
|
||||
display: true,
|
||||
drawOnChartArea: true,
|
||||
color: (ctx) =>
|
||||
ctx.index === 0 || ctx.index == null
|
||||
? "rgba(0,0,0,0)"
|
||||
: cssVar("--grid-border-dashed"),
|
||||
lineWidth: (ctx) =>
|
||||
ctx.index === 0 || ctx.index == null ? 0 : 1,
|
||||
},
|
||||
ticks: {
|
||||
padding: 10,
|
||||
color: (ctx) =>
|
||||
ctx.tick.label === currentMonth
|
||||
? cssVar("--chart-point")
|
||||
: cssVar("--chart-base"),
|
||||
font: (ctx) =>
|
||||
ctx.tick.label === currentMonth
|
||||
? { weight: "bold", size: 18 }
|
||||
: { weight: "normal", size: 18 },
|
||||
},
|
||||
border: {
|
||||
display: true,
|
||||
dash: [3, 3],
|
||||
color: cssVar("--grid-border-dashed"),
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
createMixedValuePlugin(12),
|
||||
createMixedPointGradientPlugin(),
|
||||
highlightXFactory(currentMonth),
|
||||
hideNullPointsPlugin,
|
||||
],
|
||||
});
|
||||
}
|
||||
// 🔥 여기만 추가됨 (기존 destroy 제거)
|
||||
else {
|
||||
yearlyInstance.data.datasets[0].data = window.yearlyMonthly;
|
||||
yearlyInstance.data.datasets[1].data = window.yearlyCumulative;
|
||||
yearlyInstance.update();
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// 3) 초기 렌더
|
||||
// -------------------------------
|
||||
createYearlyChart();
|
||||
loadYearlyDashboardData(); // 최초 데이터
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
ini_set('display_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?php include __DIR__ . '/html/_include/_head.php'; ?>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<?php include __DIR__ . '/html/_include/_header.php'; ?>
|
||||
<!-- container -->
|
||||
<div class="container">
|
||||
<div class="main-contents">
|
||||
<div class="main-grid">
|
||||
<!-- 연간 총 판매 실적 -->
|
||||
<?php include __DIR__ . '/html/main_sub/yearly_chart.php'; ?>
|
||||
<!-- //연간 총 판매 실적 -->
|
||||
|
||||
<!-- 연간 개별 판매 실적 -->
|
||||
<?php include __DIR__ . '/html/main_sub/individual_chart.php'; ?>
|
||||
<!-- //연간 개별 판매 실적 -->
|
||||
|
||||
<!-- 연간 영업 판매 실적 -->
|
||||
<?php include __DIR__ . '/html/main_sub/sales_chart.php'; ?>
|
||||
<!-- //연간 영업 판매 실적 -->
|
||||
|
||||
<!-- 월 판매 실적 -->
|
||||
<?php include __DIR__ . '/html/main_sub/month_chart.php'; ?>
|
||||
<!-- //월 판매 실적 -->
|
||||
</div>
|
||||
|
||||
<!-- 주간 영업 계획 -->
|
||||
<?php include __DIR__ . '/html/main_sub/weekly_plan.php'; ?>
|
||||
<!-- //주간 영업 계획 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(() => {
|
||||
// 리사이즈 이벤트 리스너 (디바운스 적용)
|
||||
let resizeTimer;
|
||||
window.addEventListener("resize", () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
createYearlyChart();
|
||||
createIndividualChart();
|
||||
createMonthChart();
|
||||
createIndustryChart();
|
||||
}, 250); // 250ms 디바운스
|
||||
});
|
||||
})();
|
||||
|
||||
/* =========================================================
|
||||
공통: 헤더 업데이트 시간
|
||||
========================================================= */
|
||||
function updateHeaderDate() {
|
||||
const now = new Date();
|
||||
|
||||
const y = now.getFullYear();
|
||||
const m = String(now.getMonth() + 1).padStart(2, "0");
|
||||
const d = String(now.getDate()).padStart(2, "0");
|
||||
const hh = String(now.getHours()).padStart(2, "0");
|
||||
const mm = String(now.getMinutes()).padStart(2, "0");
|
||||
const weekday = ["일","월","화","수","목","금","토"][now.getDay()];
|
||||
|
||||
const dateEl = document.querySelector(".header .date");
|
||||
if (dateEl) {
|
||||
dateEl.textContent = `업데이트 : ${y}.${m}.${d}(${weekday}) ${hh}:${mm}`;
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
공통: 증감 표시 UI
|
||||
========================================================= */
|
||||
function updateDiff(id, value) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
|
||||
const wrap = el.parentElement;
|
||||
|
||||
wrap.classList.remove("change", "change-up", "change-down");
|
||||
|
||||
if (value > 0) {
|
||||
wrap.classList.add("change-up");
|
||||
} else if (value < 0) {
|
||||
wrap.classList.add("change-down");
|
||||
} else {
|
||||
wrap.classList.add("change");
|
||||
}
|
||||
|
||||
el.textContent = Math.abs(value);
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
🔥 대시보드 전체 갱신 컨트롤 타워
|
||||
========================================================= */
|
||||
function refreshDashboard() {
|
||||
Promise.all([
|
||||
// 연간 영업 판매 현황
|
||||
typeof loadSalesDashboardData === "function"
|
||||
? loadSalesDashboardData()
|
||||
: Promise.resolve(),
|
||||
|
||||
// 연간 총 판매 실적
|
||||
typeof loadYearlyDashboardData === "function"
|
||||
? loadYearlyDashboardData()
|
||||
: Promise.resolve(),
|
||||
|
||||
// 월 판매 실적
|
||||
typeof loadMonthDashboardData === "function"
|
||||
? loadMonthDashboardData()
|
||||
: Promise.resolve(),
|
||||
|
||||
// 개인별 연간 실적
|
||||
typeof loadIndividualDashboardData === "function"
|
||||
? loadIndividualDashboardData()
|
||||
: Promise.resolve(),
|
||||
|
||||
// 주간 영업계획 (있을 때만)
|
||||
typeof loadWeeklyPlanData === "function"
|
||||
? loadWeeklyPlanData()
|
||||
: Promise.resolve(),
|
||||
]).then(() => {
|
||||
updateHeaderDate();
|
||||
});
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
🔄 자동 갱신 제어
|
||||
========================================================= */
|
||||
let autoRefreshTimer = null;
|
||||
|
||||
function startAutoRefresh() {
|
||||
if (autoRefreshTimer) return;
|
||||
|
||||
// 5분(300초)
|
||||
autoRefreshTimer = setInterval(refreshDashboard, 60000);
|
||||
}
|
||||
|
||||
function stopAutoRefresh() {
|
||||
if (autoRefreshTimer) {
|
||||
clearInterval(autoRefreshTimer);
|
||||
autoRefreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
탭 비활성화 시 갱신 중단
|
||||
========================================================= */
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.hidden) {
|
||||
stopAutoRefresh();
|
||||
} else {
|
||||
refreshDashboard();
|
||||
startAutoRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
/* =========================================================
|
||||
초기 실행
|
||||
========================================================= */
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
refreshDashboard(); // 최초 1회
|
||||
startAutoRefresh(); // 이후 자동 갱신
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
function sales_layout_start($title = "영업 관리") {
|
||||
$current = basename($_SERVER['PHP_SELF']); // ← 현재 페이지 파일명
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title><?= $title ?></title>
|
||||
<link rel="stylesheet" href="/egbim/css/style_sales.css">
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<!-- <link rel="stylesheet" href="https://rawgit.com/vitmalina/w2ui/master/dist/w2ui.min.css"> -->
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/vitmalina/w2ui@master/dist/w2ui.min.css"/>
|
||||
<link rel="stylesheet" href="https://rawcdn.githack.com/vitmalina/w2ui/master/dist/w2ui.min.css">
|
||||
<!-- <script src="https://rawgit.com/vitmalina/w2ui/master/dist/w2ui.min.js"></script> -->
|
||||
|
||||
<!-- <link rel="stylesheet" href="https://rawcdn.githack.com/sweetalert2/sweetalert2/master/dist/sweetalert2.min.css"> -->
|
||||
<!-- <script src="https://rawcdn.githack.com/sweetalert2/sweetalert2/master/dist/sweetalert2.all.min.js"></script> -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="sales-container">
|
||||
|
||||
<!-- 🔵 왼쪽 메뉴 -->
|
||||
<aside class="sales-sidebar">
|
||||
<div class="sales-logo">
|
||||
EGBIM<br>관리 페이지
|
||||
</div>
|
||||
|
||||
<ul class="sales-menu">
|
||||
<li class="<?= $current == 'sales_schedules.skin.php' ? 'active' : '' ?>">
|
||||
<a href="sales_schedules.skin.php">스케줄</a>
|
||||
</li>
|
||||
<li class="<?= $current == 'sales_results.skin.php' ? 'active' : '' ?>">
|
||||
<a href="sales_results.skin.php">영업실적</a>
|
||||
</li>
|
||||
<li class="<?= $current == 'sales_index.skin.php' ? 'active' : '' ?>">
|
||||
<a href="https://eg-bim.co.kr/egbim/skin/sales/index.php#" target="_blank" rel="noopener noreferrer">DASH board</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="sidebar-title"> 관리영역
|
||||
<ul class="sales-menu">
|
||||
<li class="<?= $current == 'sales_clients.skin.php' ? 'active' : '' ?>">
|
||||
<a href="sales_clients.skin.php">거래처 관리</a>
|
||||
</li>
|
||||
<li class="<?= $current == 'sales_members.skin.php' ? 'active' : '' ?>">
|
||||
<a href="sales_members.skin.php">영업담당자 관리</a>
|
||||
</li>
|
||||
<li class="<?= $current == 'sales_targets.skin.php' ? 'active' : '' ?>">
|
||||
<a href="sales_targets.skin.php">영업 목표 설정</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 🔵 오른쪽 페이지 영역 시작 -->
|
||||
<main class="sales-main">
|
||||
<?php } ?>
|
||||
|
||||
|
||||
<?php
|
||||
function sales_layout_end() { ?>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<?php } ?>
|
||||
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,394 @@
|
||||
<?php
|
||||
include __DIR__ . '/layout_sales.php';
|
||||
sales_layout_start("거래처 관리");
|
||||
?>
|
||||
|
||||
<h2 class="text-2xl font-bold mb-4">거래처 관리</h2>
|
||||
|
||||
<div id="grid_clients" style="width: 100%; height: 720px;"></div>
|
||||
|
||||
<script type="module">
|
||||
import { w2grid } from "https://cdn.jsdelivr.net/gh/vitmalina/w2ui@master/dist/w2ui.es6.min.js";
|
||||
|
||||
/* ------------------------------------------------------
|
||||
⭐ 전화번호 자동 포맷 함수
|
||||
------------------------------------------------------- */
|
||||
function formatPhone(number) {
|
||||
if (!number) return "";
|
||||
|
||||
number = number.replace(/\D/g, ""); // 숫자만 유지
|
||||
|
||||
// 휴대폰 010/011/016/017/018/019
|
||||
if (/^01[016789]\d{7,8}$/.test(number)) {
|
||||
if (number.length === 10)
|
||||
return number.replace(/(01[016789])(\d{3})(\d{4})/, "$1-$2-$3");
|
||||
return number.replace(/(01[016789])(\d{4})(\d{4})/, "$1-$2-$3");
|
||||
}
|
||||
|
||||
// 070 인터넷전화
|
||||
if (/^070\d{7,8}$/.test(number)) {
|
||||
return number.replace(/(070)(\d{4})(\d{4})/, "$1-$2-$3");
|
||||
}
|
||||
|
||||
// 서울 02
|
||||
if (number.startsWith("02")) {
|
||||
if (number.length === 9)
|
||||
return number.replace(/(02)(\d{3})(\d{4})/, "$1-$2-$3");
|
||||
if (number.length === 10)
|
||||
return number.replace(/(02)(\d{4})(\d{4})/, "$1-$2-$3");
|
||||
}
|
||||
|
||||
// 기타 지역번호 031~064
|
||||
if (/^0[3-6]\d/.test(number)) {
|
||||
return number.replace(/(0\d{2})(\d{3,4})(\d{4})/, "$1-$2-$3");
|
||||
}
|
||||
|
||||
return number;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------
|
||||
1) 기관구분 고정 배열
|
||||
------------------------------------------------------- */
|
||||
const orgTypes = [
|
||||
{ id: '공공기관', text: '공공기관' },
|
||||
{ id: '일반기업', text: '일반기업' },
|
||||
{ id: '교육기관', text: '교육기관' },
|
||||
{ id: '기타', text: '기타' },
|
||||
];
|
||||
|
||||
const businessTypes = [
|
||||
{ id: '설계', text: '설계' },
|
||||
{ id: '제작', text: '제작' },
|
||||
{ id: '교육', text: '교육' },
|
||||
{ id: '감리', text: '감리' },
|
||||
{ id: '시공', text: '시공' },
|
||||
{ id: '기타', text: '기타' },
|
||||
];
|
||||
|
||||
/* ------------------------------------------------------
|
||||
2) 영업담당자 리스트 로딩
|
||||
------------------------------------------------------- */
|
||||
let employeeList = [];
|
||||
|
||||
async function loadEmployees() {
|
||||
let res = await fetch('/egbim/bbs/sales_members.php?action=list');
|
||||
let json = await res.json();
|
||||
|
||||
if (json.status === "ok") {
|
||||
employeeList = json.records.map(m => ({
|
||||
id: m.emp_no,
|
||||
text: `${m.emp_name} (${m.emp_no})`
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------
|
||||
3) 직원 먼저 불러와야 grid가 정상 동작
|
||||
------------------------------------------------------- */
|
||||
await loadEmployees();
|
||||
|
||||
/* ------------------------------------------------------
|
||||
4) GRID 생성
|
||||
------------------------------------------------------- */
|
||||
let grid = new w2grid({
|
||||
name: 'grid_clients',
|
||||
box: '#grid_clients',
|
||||
|
||||
show: {
|
||||
toolbar: true,
|
||||
footer: true,
|
||||
lineNumbers: true,
|
||||
toolbarSave: true,
|
||||
toolbarReload: true,
|
||||
toolbarSearch: true,
|
||||
toolbarColumns: true
|
||||
},
|
||||
|
||||
multiSearch: true,
|
||||
|
||||
searches: [
|
||||
{ field: 'client_code', label: '거래처코드', type: 'text' },
|
||||
{ field: 'client_name', label: '거래처명', type: 'text', operator: 'contains' },
|
||||
{ field: 'business_type', label: '업종', type: 'text' },
|
||||
{ field: 'org_type', label: '기관구분', type: 'text' },
|
||||
{ field: 'manager_emp_no', label: '영업담당자', type: 'text' },
|
||||
{ field: 'biz_number', label: '사업자등록번호', size: '120px', editable: { type: 'text' }},
|
||||
{ field: 'address', label: '주소', type: 'text' },
|
||||
{ field: 'phone', label: '전화번호', type: 'text' },
|
||||
{ field: 'remarks', label: '비고', type: 'text' },
|
||||
{ field: 'created_at', label: '등록일', type: 'date' },
|
||||
],
|
||||
|
||||
columns: [
|
||||
{ field: 'client_code', text: '거래처코드', size: '120px',
|
||||
editable: false, resizable: true, sortable: true },
|
||||
|
||||
{ field: 'client_name', text: '거래처명', size: '180px',
|
||||
editable: { type: 'text' }, resizable: true, sortable: true },
|
||||
|
||||
/* -----------------------------------------------------
|
||||
업종 — 콤보박스 적용
|
||||
------------------------------------------------------ */
|
||||
{
|
||||
field: 'business_type',
|
||||
text: '업종',
|
||||
size: '120px',
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
editable: { type: 'combo', items: businessTypes, showAll: true },
|
||||
},
|
||||
|
||||
/* -----------------------------------------------------
|
||||
기관구분 — 콤보박스 적용
|
||||
------------------------------------------------------ */
|
||||
{
|
||||
field: 'org_type',
|
||||
text: '기관구분',
|
||||
size: '140px',
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
editable: { type: 'combo', items: orgTypes, showAll: true },
|
||||
},
|
||||
|
||||
/* -----------------------------------------------------
|
||||
영업담당자 — 직원 목록 기반 콤보박스
|
||||
------------------------------------------------------ */
|
||||
{
|
||||
field: 'manager_emp_no',
|
||||
text: '영업담당자',
|
||||
size: '160px',
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
editable: { type: 'combo', items: employeeList, filter: true },
|
||||
},
|
||||
{ field: 'biz_number', text: '사업자등록번호', size: '120px', editable: { type: 'text' }, resizable: true, sortable: true },
|
||||
{ field: 'address', text: '주소', size: '220px', editable: { type: 'text' }, resizable: true, sortable: true },
|
||||
{ field: 'phone', text: '전화번호', size: '120px', editable: { type: 'text' }, resizable: true, sortable: true },
|
||||
{ field: 'remarks', text: '비고', size: '200px', editable: { type: 'text' }, resizable: true, sortable: true },
|
||||
{ field: 'created_at', text: '등록일', size: '160px', resizable: true, sortable: true },
|
||||
],
|
||||
|
||||
toolbar: {
|
||||
items: [
|
||||
{ id: 'add', type: 'button', text: '추가', icon: 'w2ui-icon-plus' },
|
||||
{ id: 'btn_del', type: 'button', text: '삭제', icon: 'w2ui-icon-cross' }
|
||||
],
|
||||
|
||||
onClick(event) {
|
||||
const g = this.owner;
|
||||
|
||||
if (event.target === 'w2ui-reload') {
|
||||
loadClients();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target === 'add') {
|
||||
|
||||
$.post("/egbim/bbs/sales_clients.php", {
|
||||
action: "insert"
|
||||
}, function (res) {
|
||||
|
||||
const code = res.client_code;
|
||||
|
||||
g.add({
|
||||
recid: code,
|
||||
client_code: code,
|
||||
client_name: "",
|
||||
business_type: "",
|
||||
org_type: "",
|
||||
manager_emp_no: ""
|
||||
});
|
||||
|
||||
g.refresh();
|
||||
alert("새 거래처가 추가되었습니다. (코드: " + code + ")");
|
||||
//alert(grid.records.length + 1);
|
||||
g.scrollIntoView(grid.records.length + 1);
|
||||
//g.editField(grid.records.length + 1, 2);
|
||||
}, "json");
|
||||
}
|
||||
|
||||
if (event.target === 'btn_del') {
|
||||
|
||||
console.log("삭제 버튼 클릭됨!");
|
||||
const g = this.owner;
|
||||
|
||||
let sel = g.getSelection();
|
||||
if (!sel.length) return;
|
||||
|
||||
if (!confirm("정말 삭제하시겠습니까?")) return;
|
||||
|
||||
sel.forEach(code => {
|
||||
|
||||
$.post("/egbim/bbs/sales_clients.php", {
|
||||
action: "delete",
|
||||
client_code: code
|
||||
}, function(res) {
|
||||
console.log("삭제 응답:", res);
|
||||
}, "json");
|
||||
|
||||
g.remove(code);
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
onChange(event) {
|
||||
if (event.column === 'manager_emp_no') {
|
||||
let newValue = event.value_new?.text || event.value_new || '';
|
||||
event.record.manager_emp_no = newValue;
|
||||
this.set(event.recid, { manager_emp_no: newValue });
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/* ------------------------------------------------------
|
||||
onSave() — DB 업데이트 전송
|
||||
------------------------------------------------------ */
|
||||
onSave(event) {
|
||||
let changes = this.getChanges();
|
||||
|
||||
if (!changes.length) {
|
||||
alert("변경된 내용이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.all(
|
||||
changes.map(ch => {
|
||||
|
||||
// 전화번호 자동 포맷
|
||||
if (ch.phone !== undefined) {
|
||||
let raw = (ch.phone + "").replace(/[^0-9]/g, "");
|
||||
ch.phone = formatPhone(raw);
|
||||
}
|
||||
|
||||
/* ------------------------------
|
||||
🔵 1) 기관구분(org_type) 유효성 확인
|
||||
------------------------------ */
|
||||
if (ch.org_type !== undefined) {
|
||||
let validOrg = orgTypes.map(o => o.id);
|
||||
if (!validOrg.includes(ch.org_type)) {
|
||||
alert("리스트에 있는 기관구분만 선택해주세요.");
|
||||
throw "invalid org_type";
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------
|
||||
🔵 2) 업종(business_type) 유효성 확인
|
||||
------------------------------ */
|
||||
if (ch.business_type !== undefined) {
|
||||
let validBiz = businessTypes.map(b => b.id);
|
||||
if (!validBiz.includes(ch.business_type)) {
|
||||
alert("리스트에 있는 업종만 선택해주세요.");
|
||||
throw "invalid business_type";
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// 🔵 3) 영업담당자(manager_emp_no) 유효성 검사 + 원본 저장
|
||||
// ------------------------------
|
||||
if (ch.manager_emp_no !== undefined) {
|
||||
|
||||
let rawValue = ch.manager_emp_no;
|
||||
|
||||
// 콤보 선택 시 object 형태 → 텍스트로 변환하여 DB 저장
|
||||
if (typeof rawValue === "object" && rawValue !== null) {
|
||||
// 예: { id: "01201", text: "홍길동 (01201)" }
|
||||
rawValue = rawValue.text;
|
||||
}
|
||||
|
||||
// "" 허용
|
||||
if (rawValue !== "") {
|
||||
|
||||
// rawValue 안에서 "(사번)" 부분만 추출
|
||||
let match = rawValue.match(/\((.*?)\)/); // 괄호 안의 문자 추출
|
||||
|
||||
if (!match) {
|
||||
alert("영업담당자 형식이 올바르지 않습니다. 예: 홍길동 (01201)");
|
||||
throw "invalid manager_emp_no";
|
||||
}
|
||||
|
||||
let extractedEmpNo = match[1]; // 괄호 안의 사번
|
||||
|
||||
let validEmployees = employeeList.map(e => e.id);
|
||||
|
||||
if (!validEmployees.includes(extractedEmpNo)) {
|
||||
alert("영업담당자는 직원 목록에 있는 사람만 선택할 수 있습니다.");
|
||||
throw "invalid manager_emp_no";
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 DB에는 사용자가 선택한 그대로 저장 (rawValue)
|
||||
ch.manager_emp_no = rawValue;
|
||||
}
|
||||
|
||||
if (ch.biz_number !== undefined) {
|
||||
|
||||
let raw = (ch.biz_number + "").replace(/[^0-9]/g, "");
|
||||
|
||||
if (raw.length !== 10) {
|
||||
alert("사업자등록번호는 숫자 10자리여야 합니다. 예: 1234567890 또는 123-45-67890");
|
||||
throw "invalid biz number";
|
||||
}
|
||||
|
||||
// 10자리 → 하이픈 추가
|
||||
let fmt = raw.replace(/(\d{3})(\d{2})(\d{5})/, "$1-$2-$3");
|
||||
|
||||
ch.biz_number = fmt; // 🔥 저장할 값 수정
|
||||
}
|
||||
|
||||
let orgValue = (typeof ch.org_type === 'object' && ch.org_type?.id)
|
||||
? ch.org_type.id
|
||||
: ch.org_type;
|
||||
|
||||
let empValue = (typeof ch.manager_emp_no === 'object' && ch.manager_emp_no?.id)
|
||||
? ch.manager_emp_no.id
|
||||
: ch.manager_emp_no;
|
||||
|
||||
return $.post("/egbim/bbs/sales_clients.php", {
|
||||
action: "update",
|
||||
client_code: ch.recid,
|
||||
|
||||
client_name: ch.client_name,
|
||||
business_type: ch.business_type,
|
||||
org_type: orgValue,
|
||||
manager_emp_no: empValue,
|
||||
|
||||
biz_number: ch.biz_number,
|
||||
address: ch.address,
|
||||
phone: ch.phone,
|
||||
remarks: ch.remarks
|
||||
});
|
||||
|
||||
})
|
||||
).then(() => {
|
||||
loadClients();
|
||||
alert("저장되었습니다.");
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
/* -------------------------------------------------
|
||||
🔵 loadClients() — DB 조회
|
||||
-------------------------------------------------- */
|
||||
function loadClients() {
|
||||
fetch("/egbim/bbs/sales_clients.php?action=list")
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.status === "ok") {
|
||||
grid.records = res.records.map(row => ({
|
||||
recid: row.client_code,
|
||||
...row
|
||||
}));
|
||||
grid.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadClients();
|
||||
</script>
|
||||
|
||||
|
||||
<?php
|
||||
sales_layout_end();
|
||||
?>
|
||||
Whitespace-only changes.
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
include __DIR__ . '/layout_sales.php';
|
||||
sales_layout_start("DASHBOARD");
|
||||
?>
|
||||
|
||||
<h2 class="text-2xl font-bold mb-4">DASH BOARD</h2>
|
||||
|
||||
<div id="grid_clients" style="width: 100%; height: 720px;"></div>
|
||||
|
||||
|
||||
<?php
|
||||
sales_layout_end();
|
||||
?>
|
||||
Whitespace-only changes.
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
include __DIR__ . '/layout_sales.php';
|
||||
sales_layout_start("영업담당자 관리");
|
||||
?>
|
||||
|
||||
<h2 class="text-2xl font-bold mb-4">영업담당자 관리</h2>
|
||||
|
||||
<div id="grid_members" style="width: 100%; height: 720px;"></div>
|
||||
|
||||
<!-- =====================================================
|
||||
🔥 JS 직접 포함 (CDN 문제 방지 / 로드 순서 문제 해결)
|
||||
====================================================== -->
|
||||
<link rel="stylesheet" type="text/css"
|
||||
href="https://rawgit.com/vitmalina/w2ui/master/dist/w2ui.min.css">
|
||||
|
||||
<script type="module">
|
||||
import { w2grid } from "https://cdn.jsdelivr.net/gh/vitmalina/w2ui@master/dist/w2ui.es6.min.js";
|
||||
|
||||
function clean(v) {
|
||||
return (v === undefined || v === null) ? '' : v;
|
||||
}
|
||||
|
||||
let grid = new w2grid({
|
||||
name: 'grid_members',
|
||||
box: '#grid_members',
|
||||
|
||||
show: {
|
||||
toolbar: true,
|
||||
footer: true,
|
||||
lineNumbers: true,
|
||||
toolbarSave: true,
|
||||
toolbarReload: true,
|
||||
toolbarSearch: true,
|
||||
toolbarColumns: true
|
||||
},
|
||||
|
||||
multiSearch: true,
|
||||
searches: [
|
||||
{ field: 'emp_no', label: '사번', type: 'text' },
|
||||
{ field: 'emp_name', label: '이름', type: 'text' },
|
||||
{ field: 'position', label: '직위', type: 'text' },
|
||||
{ field: 'department', label: '부서', type: 'text' },
|
||||
{ field: 'created_at', label: '등록일', type: 'date' }
|
||||
],
|
||||
|
||||
columns: [
|
||||
{ field: 'emp_no', text: '사번', size: '120px', editable: { type: 'text' }, resizable: true, sortable: true },
|
||||
{ field: 'emp_name', text: '이름', size: '150px', editable: { type: 'text' }, resizable: true, sortable: true },
|
||||
{ field: 'position', text: '직위', size: '120px', editable: { type: 'text' }, resizable: true, sortable: true },
|
||||
{ field: 'department', text: '부서', size: '180px', editable: { type: 'text' }, resizable: true, sortable: true },
|
||||
{ field: 'created_at', text: '등록일', size: '160px', resizable: true, sortable: true }
|
||||
],
|
||||
|
||||
toolbar: {
|
||||
items: [
|
||||
{ id: 'add', type: 'button', text: '추가', icon: 'w2ui-icon-plus' },
|
||||
// { id: 'delete', type: 'button', text: '삭제', icon: 'w2ui-icon-cross' }
|
||||
],
|
||||
|
||||
onClick(event) {
|
||||
const g = this.owner;
|
||||
|
||||
// 🔄 reload
|
||||
if (event.target === 'w2ui-reload') {
|
||||
loadMembers();
|
||||
return;
|
||||
}
|
||||
|
||||
// ➕ 추가 (DB insert 아직 X)
|
||||
if (event.target === 'add') {
|
||||
// recid를 숫자가 아닌 'new-' 접두어를 붙여서 생성합니다.
|
||||
const newId = "new-" + Math.random().toString(36).substr(2, 9);
|
||||
g.add({
|
||||
recid: newId,
|
||||
emp_no: "",
|
||||
emp_name: "",
|
||||
position: "",
|
||||
department: "",
|
||||
created_at: ""
|
||||
});
|
||||
g.scrollIntoView(newId);
|
||||
g.editField(newId, 0); // 사번 컬럼부터 바로 편집 모드
|
||||
}
|
||||
|
||||
// ❌ 삭제
|
||||
if (event.target === 'delete') {
|
||||
|
||||
const sel = g.getSelection(); // ✅ sel 변수를 반드시 선언
|
||||
if (!sel.length) return;
|
||||
|
||||
if (!confirm("정말 삭제하시겠습니까?")) return;
|
||||
|
||||
sel.forEach(id => {
|
||||
|
||||
// 신규행(new-*)는 DB 삭제 안 함
|
||||
if (String(id).startsWith("new-")) {
|
||||
g.remove(id);
|
||||
return;
|
||||
}
|
||||
|
||||
fetch("/egbim/bbs/sales_members.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
action: "delete",
|
||||
emp_no: id
|
||||
})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
console.log("삭제 응답:", res);
|
||||
})
|
||||
.catch(err => console.error("삭제 오류:", err));
|
||||
|
||||
g.remove(id);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 💾 onSave — INSERT + UPDATE
|
||||
onSave(event) {
|
||||
const changes = this.getChanges();
|
||||
if (!changes.length) {
|
||||
alert("변경된 내용이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.all(
|
||||
changes.map(ch => {
|
||||
const isNew = String(ch.recid).startsWith("new-");
|
||||
|
||||
// 신규 데이터일 경우 사용자가 grid에 입력한 emp_no를 가져와야 함
|
||||
// 만약 ch.emp_no가 없다면 grid.get(ch.recid).emp_no 확인
|
||||
const targetData = this.get(ch.recid);
|
||||
const empNo = isNew ? (ch.emp_no || targetData.emp_no) : ch.recid;
|
||||
|
||||
return fetch("/egbim/bbs/sales_members.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
action: isNew ? "insert" : "update",
|
||||
emp_no: clean(empNo),
|
||||
emp_name: clean(ch.emp_name || targetData.emp_name),
|
||||
position: clean(ch.position || targetData.position),
|
||||
department: clean(ch.department || targetData.department)
|
||||
})
|
||||
});
|
||||
})
|
||||
).then(() => {
|
||||
loadMembers();
|
||||
alert("저장되었습니다.");
|
||||
}).catch(err => {
|
||||
console.error("저장 오류:", err);
|
||||
alert("저장 중 오류가 발생했습니다.");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// --------------------------------------------
|
||||
// 🔵 loadMembers() — 데이터 불러오기
|
||||
// --------------------------------------------
|
||||
function loadMembers() {
|
||||
fetch("/egbim/bbs/sales_members.php?action=list")
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.status === "ok") {
|
||||
grid.records = res.records.map(row => ({
|
||||
recid: row.emp_no,
|
||||
emp_no: clean(row.emp_no),
|
||||
emp_name: clean(row.emp_name),
|
||||
position: clean(row.position),
|
||||
department: clean(row.department),
|
||||
created_at: clean(row.created_at)
|
||||
}));
|
||||
|
||||
grid.reload(); // ★★★ refresh 말고 reload 사용
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadMembers();
|
||||
</script>
|
||||
|
||||
<?php
|
||||
sales_layout_end();
|
||||
?>
|
||||
Whitespace-only changes.
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
include __DIR__ . '/layout_sales.php';
|
||||
sales_layout_start("영업실적");
|
||||
?>
|
||||
|
||||
<h2 class="text-2xl font-bold mb-4">영업실적</h2>
|
||||
|
||||
<button class="bg-blue-600 text-white px-4 py-2 rounded mb-3" id="btn-add-result">
|
||||
신규 실적 등록
|
||||
</button>
|
||||
|
||||
<div id="grid_results" style="width:100%;height:720px;"></div>
|
||||
|
||||
<script type="module">
|
||||
import { w2grid } from "https://rawgit.com/vitmalina/w2ui/master/dist/w2ui.es6.min.js";
|
||||
|
||||
let grid = new w2grid({
|
||||
name: 'grid_results',
|
||||
box: '#grid_results',
|
||||
|
||||
show: {
|
||||
toolbar: true,
|
||||
footer: true,
|
||||
toolbarSave: true,
|
||||
toolbarReload: true,
|
||||
toolbarSearch: true,
|
||||
toolbarColumns: true,
|
||||
lineNumbers: true
|
||||
},
|
||||
|
||||
multiSearch: true,
|
||||
|
||||
searches: [
|
||||
{ field: 'sales_date', label: '실적일', type: 'date' },
|
||||
{ field: 'emp_no', label: '사번', type: 'text' },
|
||||
{ field: 'client_code', label: '거래처코드', type: 'text' },
|
||||
{ field: 'product_code', label: '제품코드', type: 'text' },
|
||||
{ field: 'remarks', label: '비고', type: 'text' },
|
||||
],
|
||||
|
||||
columns: [
|
||||
// { field: 'seq_no', text: '번호', size: '70px' },
|
||||
{ field: 'sales_date', text: '실적일', size: '110px', editable: { type: 'date' }, sortable: true },
|
||||
{ field: 'emp_no', text: '사번', size: '110px', editable: { type: 'text' }, sortable: true },
|
||||
{ field: 'client_code', text: '거래처코드', size: '120px', editable: { type: 'text' }, sortable: true },
|
||||
{ field: 'product_code', text: '제품코드', size: '120px', editable: { type: 'text' }, sortable: true },
|
||||
|
||||
{ field: 'quantity', text: '수량', size: '80px',
|
||||
editable: { type: 'int' }, sortable: true,
|
||||
style: 'text-align:right' },
|
||||
|
||||
{ field: 'unit_price', text: '단가', size: '100px',
|
||||
editable: { type: 'float' }, sortable: true,
|
||||
style: 'text-align:right' },
|
||||
|
||||
{ field: 'discount', text: '할인액', size: '100px',
|
||||
editable: { type: 'float' }, sortable: true,
|
||||
style: 'text-align:right' },
|
||||
|
||||
{ field: 'total_amount', text: '총금액', size: '120px',
|
||||
editable: { type: 'float' }, sortable: true,
|
||||
style: 'text-align:right' },
|
||||
|
||||
{ field: 'remarks', text: '비고', size: '200px', editable: { type: 'text' }, sortable: true },
|
||||
{ field: 'created_at', text: '등록일', size: '160px', sortable: true }
|
||||
],
|
||||
|
||||
toolbar: {
|
||||
items: [
|
||||
{ id: 'add', type: 'button', text: '추가', icon: 'w2ui-icon-plus' },
|
||||
{ id: 'delete', type: 'button', text: '삭제', icon: 'w2ui-icon-cross' }
|
||||
],
|
||||
|
||||
onClick(event) {
|
||||
const g = this.owner;
|
||||
|
||||
// reload
|
||||
if (event.target === 'w2ui-reload') {
|
||||
loadResults();
|
||||
return;
|
||||
}
|
||||
|
||||
// ADD
|
||||
if (event.target === 'add') {
|
||||
const newId = "new-" + Math.random().toString(36).substr(2, 9);
|
||||
|
||||
g.add({
|
||||
recid: newId,
|
||||
sales_date: "",
|
||||
emp_no: "",
|
||||
client_code: "",
|
||||
product_code: "",
|
||||
quantity: 0,
|
||||
unit_price: 0,
|
||||
discount: 0,
|
||||
total_amount: 0,
|
||||
remarks: "",
|
||||
created_at: ""
|
||||
});
|
||||
|
||||
g.refresh();
|
||||
updateSummary();
|
||||
return;
|
||||
}
|
||||
|
||||
// DELETE
|
||||
if (event.target === 'delete') {
|
||||
const sel = g.getSelection();
|
||||
if (!sel.length) return;
|
||||
|
||||
w2confirm("정말 삭제하시겠습니까?").yes(() => {
|
||||
sel.forEach(id => {
|
||||
|
||||
// 신규행 삭제
|
||||
if (String(id).startsWith("new-")) {
|
||||
g.remove(id);
|
||||
return;
|
||||
}
|
||||
|
||||
fetch("/egbim/bbs/sales_results.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
action: "delete",
|
||||
seq_no: id
|
||||
})
|
||||
});
|
||||
|
||||
g.remove(id);
|
||||
});
|
||||
updateSummary();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// SAVE (insert + update)
|
||||
onSave(event) {
|
||||
const changes = this.getChanges();
|
||||
if (!changes.length) return;
|
||||
|
||||
Promise.all(
|
||||
changes.map(ch => {
|
||||
|
||||
if (String(ch.recid).startsWith("new-")) {
|
||||
// INSERT
|
||||
return fetch("/egbim/bbs/sales_results.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
action: "insert",
|
||||
sales_date: ch.sales_date,
|
||||
emp_no: ch.emp_no,
|
||||
client_code: ch.client_code,
|
||||
product_code: ch.product_code,
|
||||
quantity: ch.quantity,
|
||||
unit_price: ch.unit_price,
|
||||
discount: ch.discount,
|
||||
total_amount: ch.total_amount,
|
||||
remarks: ch.remarks
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// UPDATE
|
||||
return fetch("/egbim/bbs/sales_results.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
action: "update",
|
||||
seq_no: ch.recid,
|
||||
sales_date: ch.sales_date,
|
||||
emp_no: ch.emp_no,
|
||||
client_code: ch.client_code,
|
||||
product_code: ch.product_code,
|
||||
quantity: ch.quantity,
|
||||
unit_price: ch.unit_price,
|
||||
discount: ch.discount,
|
||||
total_amount: ch.total_amount,
|
||||
remarks: ch.remarks
|
||||
})
|
||||
});
|
||||
|
||||
})
|
||||
).then(() => {
|
||||
loadResults();
|
||||
w2alert("저장되었습니다!");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// LOAD
|
||||
function loadResults() {
|
||||
fetch("/egbim/bbs/sales_results.php?action=list")
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.status === "ok") {
|
||||
grid.records = res.records.map(r => ({
|
||||
recid: r.seq_no,
|
||||
...r
|
||||
}));
|
||||
updateSummary();
|
||||
grid.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadResults();
|
||||
|
||||
//합계
|
||||
function updateSummary() {
|
||||
|
||||
let sumQty = 0;
|
||||
let sumUnit = 0;
|
||||
let sumDiscount = 0;
|
||||
let sumTotal = 0;
|
||||
|
||||
grid.records.forEach(r => {
|
||||
sumQty += Number(r.quantity || 0);
|
||||
sumUnit += Number(r.unit_price || 0);
|
||||
sumDiscount += Number(r.discount || 0);
|
||||
sumTotal += Number(r.total_amount || 0);
|
||||
});
|
||||
|
||||
grid.summary = [
|
||||
{
|
||||
recid: 'summary',
|
||||
sales_date: '<span style="float:right;font-weight:bold;">합계</span>',
|
||||
quantity: sumQty,
|
||||
unit_price: sumUnit,
|
||||
discount: sumDiscount,
|
||||
total_amount: sumTotal,
|
||||
w2ui: { summary: true }
|
||||
}
|
||||
];
|
||||
|
||||
grid.refresh();
|
||||
}
|
||||
|
||||
//천단위
|
||||
function numberFormat(num) {
|
||||
if (num === null || num === undefined || num === "") return "-";
|
||||
return Number(num).toLocaleString('ko-KR');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php sales_layout_end(); ?>
|
||||
@@ -0,0 +1,469 @@
|
||||
<?php
|
||||
include __DIR__ . '/layout_sales.php';
|
||||
sales_layout_start("영업실적");
|
||||
?>
|
||||
|
||||
<h2 class="text-2xl font-bold mb-4">영업실적</h2>
|
||||
|
||||
<!-- <button class="bg-blue-600 text-white px-4 py-2 rounded mb-3" id="btn-add-result">
|
||||
신규 실적 등록
|
||||
</button> -->
|
||||
|
||||
<div id="grid_results" style="width:100%;height:720px;"></div>
|
||||
|
||||
<script type="module">
|
||||
import { w2grid } from "https://cdn.jsdelivr.net/gh/vitmalina/w2ui@master/dist/w2ui.es6.min.js";
|
||||
|
||||
/* ------------------------------------------------------
|
||||
공통 유틸
|
||||
------------------------------------------------------ */
|
||||
function clean(v) {
|
||||
return (v === undefined || v === null) ? '' : v;
|
||||
}
|
||||
function cleanNumber(v) {
|
||||
if (v === undefined || v === null || v === '' || isNaN(v)) return 0;
|
||||
return Number(v);
|
||||
}
|
||||
function normalizeListValue(v) {
|
||||
if (typeof v === "object" && v !== null && v.id) return v.id;
|
||||
if (typeof v === "string") return v;
|
||||
return '';
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------
|
||||
🔵 직원 목록 / 거래처 목록 로드
|
||||
------------------------------------------------------ */
|
||||
let employeeList = [];
|
||||
let clientList = [];
|
||||
let productList = [];
|
||||
|
||||
async function loadEmployees() {
|
||||
let res = await fetch('/egbim/bbs/sales_members.php?action=list');
|
||||
let json = await res.json();
|
||||
|
||||
if (json.status === "ok") {
|
||||
employeeList = json.records.map(m => ({
|
||||
id: m.emp_no,
|
||||
text: `${m.emp_name} (${m.emp_no})`
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async function loadClients() {
|
||||
let res = await fetch('/egbim/bbs/sales_clients.php?action=list');
|
||||
let json = await res.json();
|
||||
|
||||
if (json.status === "ok") {
|
||||
clientList = json.records.map(c => ({
|
||||
id: c.client_code,
|
||||
text: `${c.client_name} (${c.client_code})`
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProducts() {
|
||||
let res = await fetch('/egbim/bbs/sales_products.php?action=list');
|
||||
let json = await res.json();
|
||||
|
||||
if (json.status === 'ok') {
|
||||
productList = json.records.map(p => ({
|
||||
id: p.code, // 저장용
|
||||
text: `${p.name} (${p.code})` // 화면 표시용
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------
|
||||
🔵 직원 + 거래처 모두 로드 후 GRID 생성
|
||||
------------------------------------------------------ */
|
||||
await loadEmployees();
|
||||
await loadClients();
|
||||
await loadProducts();
|
||||
|
||||
/* ------------------------------------------------------
|
||||
🔵 GRID
|
||||
------------------------------------------------------ */
|
||||
let grid = new w2grid({
|
||||
name: 'grid_results',
|
||||
box: '#grid_results',
|
||||
|
||||
show: {
|
||||
toolbar: true,
|
||||
footer: true,
|
||||
toolbarSave: true,
|
||||
toolbarReload: true,
|
||||
toolbarSearch: true,
|
||||
toolbarColumns: true,
|
||||
lineNumbers: true
|
||||
},
|
||||
|
||||
multiSearch: true,
|
||||
|
||||
searches: [
|
||||
{ field: 'sales_date', label: '실적일', type: 'date' },
|
||||
{ field: 'emp_no', label: '영업담당자', type: 'text' },
|
||||
{ field: 'client_code', label: '거래처명', type: 'text', operator: 'contains' },
|
||||
{ field: 'product_code', label: '제품명', type: 'text' },
|
||||
{ field: 'remarks', label: '비고', type: 'text' },
|
||||
],
|
||||
|
||||
columns: [
|
||||
{ field: 'sales_date', text: '실적일', size: '110px', editable: { type: 'date' }, sortable: true },
|
||||
|
||||
/* 🔵 영업담당자 콤보 */
|
||||
{
|
||||
field: 'emp_no',
|
||||
text: '영업담당자',
|
||||
size: '150px',
|
||||
editable: { type: 'combo', items: employeeList, showAll: true, filter: true},
|
||||
resizable: true,
|
||||
// render(record) {
|
||||
// // 1) combo 선택 직후: record.emp_no = { id, text }
|
||||
// if (typeof record.emp_no === "object" && record.emp_no !== null) {
|
||||
// return record.emp_no.text; // ← 즉시 표시됨
|
||||
// }
|
||||
|
||||
// // 2) 서버에서 불러온 값은 string → employeeList에서 매칭
|
||||
// const item = employeeList.find(e => e.id == record.emp_no);
|
||||
// return item ? item.text : record.emp_no;
|
||||
// },
|
||||
sortable: true
|
||||
},
|
||||
|
||||
/* 🔵 거래처명 콤보 — DB는 client_code 저장 */
|
||||
{
|
||||
field: 'client_code',
|
||||
text: '거래처명',
|
||||
size: '180px',
|
||||
editable: { type: 'combo', items: clientList, showAll: true, filter: true, match: 'contains', openOnFocus: true },
|
||||
// render(record, extra) {
|
||||
// let id = (typeof record.client_code === "object") ? record.client_code.id : record.client_code;
|
||||
// let item = clientList.find(c => c.id == id);
|
||||
// return item ? item.text : id;
|
||||
// },
|
||||
resizable: true,
|
||||
sortable: true
|
||||
},
|
||||
|
||||
{
|
||||
field: 'product_code',
|
||||
text: '제품명',
|
||||
size: '200px',
|
||||
editable: { type: 'combo', items: productList, filter: true, showAll: true },
|
||||
|
||||
// combo는 render 제거해야 정상표시됨
|
||||
// render(record) {
|
||||
// let item = productList.find(p => p.id === record.product_code);
|
||||
// return item ? item.text : record.product_code;
|
||||
// },
|
||||
resizable: true,
|
||||
sortable: true
|
||||
},
|
||||
|
||||
{ field: 'quantity', text: '수량', size: '80px',
|
||||
editable: { type: 'int' }, sortable: true, render: 'int', resizable: true, style: 'text-align:right' },
|
||||
|
||||
{ field: 'unit_price', text: '단가', size: '100px',
|
||||
editable: { type: 'int' }, sortable: true, render: 'int', resizable: true, style: 'text-align:right' },
|
||||
|
||||
{ field: 'discount', text: '할인액', size: '100px',
|
||||
editable: { type: 'float' }, sortable: true, render: 'int', resizable: true, style: 'text-align:right' },
|
||||
|
||||
{ field: 'total_amount', text: '총금액', size: '120px',
|
||||
editable: { type: 'float' }, sortable: true, render: 'int', resizable: true, style: 'text-align:right' },
|
||||
|
||||
{ field: 'remarks', text: '비고', size: '200px', editable: { type: 'text' }, resizable: true, sortable: true },
|
||||
|
||||
{ field: 'created_at', text: '등록일', size: '160px', resizable: true, sortable: true }
|
||||
],
|
||||
|
||||
toolbar: {
|
||||
items: [
|
||||
{ id: 'add', type: 'button', text: '추가', icon: 'w2ui-icon-plus' },
|
||||
{ id: 'delete', type: 'button', text: '삭제', icon: 'w2ui-icon-cross' }
|
||||
],
|
||||
|
||||
onClick(event) {
|
||||
const g = this.owner;
|
||||
|
||||
if (event.target === 'w2ui-reload') {
|
||||
loadResults();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target === 'add') {
|
||||
const newId = "new-" + Math.random().toString(36).substr(2, 9);
|
||||
g.add({
|
||||
recid: newId,
|
||||
sales_date: "",
|
||||
emp_no: "",
|
||||
client_code: "",
|
||||
product_code: "",
|
||||
quantity: 0,
|
||||
unit_price: 0,
|
||||
discount: 0,
|
||||
total_amount: 0,
|
||||
remarks: "",
|
||||
created_at: ""
|
||||
});
|
||||
g.refresh();
|
||||
updateSummary();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target === 'delete') {
|
||||
|
||||
const sel = g.getSelection();
|
||||
if (!sel.length) return;
|
||||
|
||||
if (!confirm("정말 삭제하시겠습니까?")) return;
|
||||
|
||||
sel.forEach(id => {
|
||||
|
||||
// 신규(new-xxx)는 DB 삭제 X
|
||||
if (String(id).startsWith("new-")) {
|
||||
g.remove(id);
|
||||
return;
|
||||
}
|
||||
|
||||
fetch("/egbim/bbs/sales_results.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
action: "delete",
|
||||
seq_no: id
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(json => {
|
||||
console.log("삭제 응답:", json);
|
||||
})
|
||||
.catch(err => console.error("삭제 오류:", err));
|
||||
|
||||
g.remove(id);
|
||||
});
|
||||
|
||||
updateSummary();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onChange(event) {
|
||||
event.onComplete = function(ev) {
|
||||
let rec = grid.get(ev.recid);
|
||||
if (!rec) return;
|
||||
|
||||
let field = grid.columns[ev.column].field;
|
||||
let val = ev.value_new;
|
||||
|
||||
// combo object → text 강제 변환
|
||||
if (typeof val === "object" && val !== null) {
|
||||
val = val.text;
|
||||
}
|
||||
|
||||
grid.set(ev.recid, { [field]: val });
|
||||
|
||||
// 계산 필드 자동 업데이트
|
||||
if (field === 'quantity' || field === 'unit_price' || field === 'discount') {
|
||||
let qty = parseInt(rec.quantity) || 0;
|
||||
let unit = parseInt(rec.unit_price) || 0;
|
||||
let dc = parseInt(rec.discount) || 0;
|
||||
|
||||
let total = Math.max(0, qty * unit - dc);
|
||||
grid.set(ev.recid, { total_amount: total });
|
||||
}
|
||||
|
||||
grid.refresh();
|
||||
};
|
||||
},
|
||||
|
||||
/* ------------------------------------------------------
|
||||
🔥 저장 처리 (insert + update)
|
||||
------------------------------------------------------ */
|
||||
onSave(event) {
|
||||
const changes = this.getChanges();
|
||||
if (!changes.length) return;
|
||||
|
||||
Promise.all(
|
||||
changes.map(ch => {
|
||||
|
||||
let rec = this.get(ch.recid); // 기존 record 전체 값
|
||||
|
||||
|
||||
/* -------------------------------------------
|
||||
🔵 1) 영업담당자(emp_no) 유효성 검사 및 변환
|
||||
------------------------------------------- */
|
||||
|
||||
// raw 값 가져오기 (object, string 모두 처리)
|
||||
let empRaw = ch.emp_no ?? rec.emp_no;
|
||||
console.log('1111333');
|
||||
|
||||
// 콤보일 경우 object → text 로 변환 ("홍길동 (01201)")
|
||||
if (typeof empRaw === "object" && empRaw !== null) {
|
||||
empRaw = empRaw.text;
|
||||
console.log('1111222');
|
||||
}
|
||||
|
||||
// 빈 값은 허용
|
||||
if (empRaw !== "") {
|
||||
console.log('1111444');
|
||||
// "(사번)" 추출
|
||||
let match = empRaw.match(/\((.*?)\)/);
|
||||
if (!match) {
|
||||
console.log('11115');
|
||||
alert("영업담당자 형식이 잘못되었습니다. 예: 홍길동 (01201)");
|
||||
throw "invalid emp_no format";
|
||||
}
|
||||
|
||||
let extractedEmpNo = match[1];
|
||||
|
||||
// 사번 유효성 체크
|
||||
let validEmpList = employeeList.map(e => e.id);
|
||||
if (!validEmpList.includes(extractedEmpNo)) {
|
||||
console.log('11116');
|
||||
alert("영업담당자는 목록에 있는 사람만 선택할 수 있습니다.");
|
||||
throw "invalid emp_no";
|
||||
}
|
||||
}
|
||||
|
||||
// ✔ 최종 DB에는 "홍길동 (01201)" 그대로 저장
|
||||
ch.emp_no = empRaw;
|
||||
|
||||
/* -------------------------------------------
|
||||
🔵 2) 거래처(client_code) 유효성 검사
|
||||
------------------------------------------- */
|
||||
let client = normalizeListValue(ch.client_code ?? rec.client_code);
|
||||
let validClientList = clientList.map(c => c.id);
|
||||
|
||||
let clientRaw = ch.client_code ?? rec.client_code;
|
||||
|
||||
if (typeof clientRaw === "object" && clientRaw !== null) {
|
||||
clientRaw = clientRaw.text;
|
||||
}
|
||||
|
||||
if (clientRaw !== "") {
|
||||
let match = clientRaw.match(/\(([^()]*)\)(?!.*\([^()]*\))/);
|
||||
if (!match) {
|
||||
alert("거래처명 형식이 잘못되었습니다. 예: 현대건설 (C002)");
|
||||
throw "invalid_client_code_format";
|
||||
}
|
||||
let code = match[1];
|
||||
|
||||
let validList = clientList.map(c => c.id);
|
||||
if (!validList.includes(code)) {
|
||||
alert("거래처명은 목록에서 선택해야 합니다.");
|
||||
throw "invalid_client_code";
|
||||
}
|
||||
}
|
||||
|
||||
ch.client_code = clientRaw;
|
||||
|
||||
/* -------------------------------------------
|
||||
🔵 3) 제품명(product_code) 유효성 검사
|
||||
------------------------------------------- */
|
||||
let product = normalizeListValue(ch.product_code ?? rec.product_code);
|
||||
let validProductList = productList.map(p => p.id);
|
||||
|
||||
let productRaw = ch.product_code ?? rec.product_code;
|
||||
|
||||
if (typeof productRaw === "object" && productRaw !== null) {
|
||||
productRaw = productRaw.text;
|
||||
}
|
||||
|
||||
if (productRaw !== "") {
|
||||
let match = productRaw.match(/\((.*?)\)$/);
|
||||
if (!match) {
|
||||
alert("제품명 형식이 잘못되었습니다. 예: EG-BIM (P001)");
|
||||
throw "invalid_product_code_format";
|
||||
}
|
||||
|
||||
let code = match[1];
|
||||
let validList = productList.map(p => p.id);
|
||||
|
||||
if (!validList.includes(code)) {
|
||||
alert("제품명은 목록에서 선택해야 합니다.");
|
||||
throw "invalid_product_code";
|
||||
}
|
||||
}
|
||||
|
||||
ch.product_code = productRaw;
|
||||
|
||||
let body = {
|
||||
action: String(ch.recid).startsWith("new-") ? "insert" : "update",
|
||||
seq_no: ch.recid,
|
||||
|
||||
sales_date: clean(ch.sales_date ?? rec.sales_date),
|
||||
emp_no: ch.emp_no,
|
||||
client_code: ch.client_code,
|
||||
product_code: ch.product_code,
|
||||
|
||||
quantity: cleanNumber(ch.quantity ?? rec.quantity),
|
||||
unit_price: cleanNumber(ch.unit_price ?? rec.unit_price),
|
||||
discount: cleanNumber(ch.discount ?? rec.discount),
|
||||
total_amount: cleanNumber(ch.total_amount ?? rec.total_amount),
|
||||
remarks: clean(ch.remarks ?? rec.remarks)
|
||||
};
|
||||
|
||||
return fetch("/egbim/bbs/sales_results.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams(body)
|
||||
});
|
||||
|
||||
})
|
||||
).then(() => {
|
||||
loadResults();
|
||||
alert("저장되었습니다!");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------
|
||||
🔵 데이터 로드 + 합계
|
||||
------------------------------------------------------ */
|
||||
function loadResults() {
|
||||
fetch("/egbim/bbs/sales_results.php?action=list")
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.status === "ok") {
|
||||
grid.records = res.records.map(r => ({
|
||||
recid: r.seq_no,
|
||||
...r
|
||||
}));
|
||||
updateSummary();
|
||||
grid.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
loadResults();
|
||||
|
||||
function updateSummary() {
|
||||
let sumQty = 0, sumUnit = 0, sumDiscount = 0, sumTotal = 0;
|
||||
|
||||
grid.records.forEach(r => {
|
||||
sumQty += Number(r.quantity || 0);
|
||||
sumUnit += Number(r.unit_price || 0);
|
||||
sumDiscount += Number(r.discount || 0);
|
||||
sumTotal += Number(r.total_amount || 0);
|
||||
});
|
||||
|
||||
grid.summary = [
|
||||
{
|
||||
recid: 'summary',
|
||||
sales_date: '<span style="float:right;font-weight:bold;">합계</span>',
|
||||
quantity: sumQty,
|
||||
unit_price: sumUnit,
|
||||
discount: sumDiscount,
|
||||
total_amount: sumTotal,
|
||||
w2ui: { summary: true }
|
||||
}
|
||||
];
|
||||
|
||||
grid.refresh();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<?php sales_layout_end(); ?>
|
||||
@@ -0,0 +1,350 @@
|
||||
<?php
|
||||
include __DIR__ . '/layout_sales.php';
|
||||
sales_layout_start("영업 스케줄표");
|
||||
?>
|
||||
|
||||
<h2 class="text-2xl font-bold mb-3">영업 스케줄표</h2>
|
||||
|
||||
<!-- 월 선택 -->
|
||||
<div class="flex gap-2 mb-4">
|
||||
<select id="sel-year" class="border p-1">
|
||||
<?php
|
||||
for ($y=date("Y")-1; $y<=date("Y")+2; $y++) {
|
||||
echo "<option value='$y' ".($y==date("Y")?"selected":"").">$y 년</option>";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
||||
<select id="sel-month" class="border p-1">
|
||||
<?php
|
||||
for ($m=1;$m<=12;$m++) {
|
||||
$mm = sprintf("%02d",$m);
|
||||
echo "<option value='$mm' ".($mm==date("m")?"selected":"").">$mm 월</option>";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
||||
<button id="btn-load" class="bg-blue-600 text-white px-3 py-1 rounded">불러오기</button>
|
||||
</div>
|
||||
|
||||
<div id="month-container"></div>
|
||||
|
||||
<script type="module">
|
||||
import { w2grid,w2popup } from "https://cdn.jsdeliver.net/npm/w2ui@2.0.0/dist/w2ui.es6.min.js";
|
||||
|
||||
let allSchedules = [];
|
||||
let employees = [];
|
||||
let weekGroups = [];
|
||||
|
||||
/* =========================================================
|
||||
평일(월~금) 기준 주차 생성
|
||||
========================================================= */
|
||||
function generateWeeks(year, month) {
|
||||
let lastDay = new Date(year, month, 0).getDate();
|
||||
let businessDays = [];
|
||||
|
||||
for (let d=1; d<=lastDay; d++) {
|
||||
let date = new Date(`${year}-${month}-${String(d).padStart(2,'0')}`);
|
||||
let dow = date.getDay();
|
||||
|
||||
if (dow >= 1 && dow <= 5) {
|
||||
businessDays.push({
|
||||
day: d,
|
||||
dow: ["","월","화","수","목","금"][dow],
|
||||
full: `${year}-${month}-${String(d).padStart(2,'0')}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let weeks = [];
|
||||
for (let i=0; i<businessDays.length; i+=5) {
|
||||
weeks.push(businessDays.slice(i, i+5));
|
||||
}
|
||||
return weeks;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
월 전체 로드
|
||||
========================================================= */
|
||||
async function loadMonth() {
|
||||
let year = document.getElementById("sel-year").value;
|
||||
let month = document.getElementById("sel-month").value;
|
||||
|
||||
let res = await fetch(`/egbim/bbs/sales_schedules.php?action=month&year=${year}&month=${month}`);
|
||||
let json = await res.json();
|
||||
|
||||
if (json.status !== "ok") return;
|
||||
|
||||
employees = json.employees;
|
||||
allSchedules = json.schedules;
|
||||
|
||||
weekGroups = generateWeeks(year, month);
|
||||
|
||||
renderMonth();
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
월 → 주차 렌더링
|
||||
========================================================= */
|
||||
function renderMonth() {
|
||||
let container = document.getElementById("month-container");
|
||||
container.innerHTML = "";
|
||||
|
||||
weekGroups.forEach((week, idx) => {
|
||||
|
||||
container.insertAdjacentHTML("beforeend", `
|
||||
<div class="font-bold text-lg mt-6 mb-2">
|
||||
${idx+1}주차 : ${week[0].day}일 ~ ${week.at(-1).day}일
|
||||
</div>
|
||||
<div id="week-grid-${idx}" style="width:100%;height:360px;"></div>
|
||||
`);
|
||||
|
||||
renderWeekGrid(week, idx);
|
||||
});
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
주차 Grid
|
||||
========================================================= */
|
||||
function renderWeekGrid(week, idx) {
|
||||
|
||||
let columns = [
|
||||
{ field: "emp_name", text: "담당자", size: "120px", frozen: true }
|
||||
];
|
||||
|
||||
week.forEach(d => {
|
||||
columns.push({
|
||||
field: "day_" + d.day,
|
||||
text: `${d.day}일(${d.dow})`,
|
||||
size: "260px",
|
||||
render: rec => rec["day_" + d.day] || ""
|
||||
});
|
||||
});
|
||||
|
||||
let records = employees.map(emp => {
|
||||
let row = {
|
||||
recid: emp.emp_no + "_" + idx,
|
||||
emp_name: emp.emp_name,
|
||||
emp_no: emp.emp_no
|
||||
};
|
||||
|
||||
week.forEach(d => {
|
||||
let items = allSchedules.filter(s =>
|
||||
s.emp_no === emp.emp_no && s.schedule_date === d.full
|
||||
);
|
||||
|
||||
row["day_" + d.day] = items.map(it => `
|
||||
<div style="margin-bottom:4px;">
|
||||
• ${it.content}
|
||||
<div style="font-size:11px;color:#777;">${it.client_code}</div>
|
||||
<div style="font-size:11px;color:#0a0;">${it.status}</div>
|
||||
</div>
|
||||
`).join("");
|
||||
});
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
new w2grid({
|
||||
name: `grid_week_${idx}`,
|
||||
box: `#week-grid-${idx}`,
|
||||
show: { toolbar: false, footer: false },
|
||||
columns,
|
||||
records,
|
||||
|
||||
/* 셀 클릭 → 일정 팝업 */
|
||||
onClick(event) {
|
||||
|
||||
let rec = this.get(event.recid);
|
||||
if (!rec) return;
|
||||
|
||||
let col = this.columns[event.column];
|
||||
if (!col || !col.field) return; // ← 헤더 클릭 오류 방지
|
||||
|
||||
let field = col.field;
|
||||
if (!field.startsWith("day_")) return;
|
||||
|
||||
let dayNum = field.replace("day_", "");
|
||||
let fullDate = week.find(w => w.day == dayNum).full;
|
||||
|
||||
openSchedulePopup(rec.emp_no, rec.emp_name, fullDate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
일정 리스트 팝업
|
||||
========================================================= */
|
||||
function openSchedulePopup(emp_no, emp_name, date) {
|
||||
|
||||
let items = allSchedules.filter(s => s.emp_no == emp_no && s.schedule_date == date);
|
||||
|
||||
let html = `
|
||||
<div class="p-3">
|
||||
<div class="mb-2 font-bold">${date} - ${emp_name}</div>
|
||||
|
||||
<button class="bg-blue-600 text-white px-3 py-1 rounded mb-3"
|
||||
onclick="window.addSchedule('${emp_no}','${date}')">+ 신규 일정 추가</button>
|
||||
|
||||
${items.map(it => `
|
||||
<div class="border rounded p-2 mb-2">
|
||||
<div><b>${it.content}</b></div>
|
||||
<div class="text-xs text-gray-600">${it.client_code}</div>
|
||||
<div class="text-xs mt-1">상태: ${it.status}</div>
|
||||
|
||||
<button onclick="window.editSchedule(${it.seq_no})"
|
||||
class="bg-yellow-500 text-white px-2 py-1 rounded mt-2">수정</button>
|
||||
|
||||
<button onclick="window.deleteSchedule(${it.seq_no})"
|
||||
class="bg-red-600 text-white px-2 py-1 rounded mt-2 ml-2">삭제</button>
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>
|
||||
`;
|
||||
|
||||
w2popup.open({
|
||||
title: `${date} - 일정`,
|
||||
body: html,
|
||||
width: 420,
|
||||
height: 500
|
||||
});
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
신규 일정 추가
|
||||
========================================================= */
|
||||
window.addSchedule = function(emp_no, date) {
|
||||
|
||||
let html = `
|
||||
<div class="p-3">
|
||||
<label>내용</label>
|
||||
<input id="s-content" class="border p-2 w-full mb-2">
|
||||
|
||||
<label>거래처 코드</label>
|
||||
<input id="s-client" class="border p-2 w-full mb-2">
|
||||
|
||||
<label>상태</label>
|
||||
<select id="s-status" class="border p-2 w-full mb-3">
|
||||
<option value="예정">예정</option>
|
||||
<option value="완료">완료</option>
|
||||
</select>
|
||||
|
||||
<button class="bg-blue-600 text-white px-3 py-2 rounded w-full"
|
||||
onclick="window.saveNew('${emp_no}','${date}')">등록</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
w2popup.open({
|
||||
title: "신규 일정 추가",
|
||||
body: html,
|
||||
width: 400,
|
||||
height: 350
|
||||
});
|
||||
};
|
||||
|
||||
window.saveNew = async function(emp_no, date) {
|
||||
|
||||
let res = await fetch("/egbim/bbs/sales_schedules.php", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type":"application/x-www-form-urlencoded"},
|
||||
body: new URLSearchParams({
|
||||
action: "insert",
|
||||
emp_no,
|
||||
schedule_date: date,
|
||||
client_code: document.getElementById("s-client").value,
|
||||
content: document.getElementById("s-content").value,
|
||||
status: document.getElementById("s-status").value
|
||||
})
|
||||
});
|
||||
|
||||
let json = await res.json();
|
||||
if (json.status === "ok") {
|
||||
w2popup.close();
|
||||
loadMonth();
|
||||
}
|
||||
};
|
||||
|
||||
/* =========================================================
|
||||
일정 수정
|
||||
========================================================= */
|
||||
window.editSchedule = async function(seq_no) {
|
||||
|
||||
let item = allSchedules.find(s => s.seq_no == seq_no);
|
||||
|
||||
let html = `
|
||||
<div class="p-3">
|
||||
<label>내용</label>
|
||||
<input id="e-content" class="border p-2 w-full mb-2" value="${item.content}">
|
||||
|
||||
<label>거래처 코드</label>
|
||||
<input id="e-client" class="border p-2 w-full mb-2" value="${item.client_code}">
|
||||
|
||||
<label>상태</label>
|
||||
<select id="e-status" class="border p-2 w-full mb-3">
|
||||
<option value="예정" ${item.status=="예정"?"selected":""}>예정</option>
|
||||
<option value="완료" ${item.status=="완료"?"selected":""}>완료</option>
|
||||
</select>
|
||||
|
||||
<button class="bg-blue-600 text-white px-3 py-2 rounded w-full"
|
||||
onclick="window.saveEdit(${seq_no})">저장</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
w2popup.open({
|
||||
title: "일정 수정",
|
||||
body: html,
|
||||
width: 400,
|
||||
height: 350
|
||||
});
|
||||
};
|
||||
|
||||
window.saveEdit = async function(seq_no) {
|
||||
|
||||
let res = await fetch("/egbim/bbs/sales_schedules.php", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type":"application/x-www-form-urlencoded"},
|
||||
body: new URLSearchParams({
|
||||
action: "update",
|
||||
seq_no,
|
||||
content: document.getElementById("e-content").value,
|
||||
client_code: document.getElementById("e-client").value,
|
||||
status: document.getElementById("e-status").value
|
||||
})
|
||||
});
|
||||
|
||||
let json = await res.json();
|
||||
if (json.status === "ok") {
|
||||
w2popup.close();
|
||||
loadMonth();
|
||||
}
|
||||
};
|
||||
|
||||
/* =========================================================
|
||||
일정 삭제
|
||||
========================================================= */
|
||||
window.deleteSchedule = async function(seq_no) {
|
||||
if (!confirm("삭제하시겠습니까?")) return;
|
||||
|
||||
let res = await fetch("/egbim/bbs/sales_schedules.php", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type":"application/x-www-form-urlencoded"},
|
||||
body: new URLSearchParams({
|
||||
action: "delete",
|
||||
seq_no
|
||||
})
|
||||
});
|
||||
|
||||
let json = await res.json();
|
||||
if (json.status === "ok") {
|
||||
w2popup.close();
|
||||
loadMonth();
|
||||
}
|
||||
};
|
||||
|
||||
/* 초기 로드 */
|
||||
document.getElementById("btn-load").onclick = loadMonth;
|
||||
loadMonth();
|
||||
|
||||
</script>
|
||||
|
||||
<?php sales_layout_end(); ?>
|
||||
@@ -0,0 +1,602 @@
|
||||
<?php
|
||||
include __DIR__ . '/layout_sales.php';
|
||||
sales_layout_start("영업 스케줄표");
|
||||
?>
|
||||
|
||||
<!-- SweetAlert2 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<h2 class="text-2xl font-bold mb-3">영업 스케줄표</h2>
|
||||
|
||||
<!-- 월 선택 -->
|
||||
<div class="flex gap-2 mb-4">
|
||||
<select id="sel-year" class="border p-1">
|
||||
<?php
|
||||
for ($y=date("Y")-1; $y<=date("Y")+2; $y++) {
|
||||
echo "<option value='$y' ".($y==date("Y")?"selected":"").">$y 년</option>";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
||||
<select id="sel-month" class="border p-1">
|
||||
<?php
|
||||
for ($m=1;$m<=12;$m++) {
|
||||
$mm = sprintf("%02d",$m);
|
||||
echo "<option value='$mm' ".($mm==date("m")?"selected":"").">$mm 월</option>";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
||||
<!-- <button id="btn-load" class="bg-blue-600 text-white px-3 py-1 rounded">
|
||||
불러오기
|
||||
</button> -->
|
||||
</div>
|
||||
|
||||
<div id="month-container"></div>
|
||||
|
||||
<!-- ⭐ 주요 이슈 사항 영역 -->
|
||||
<div id="issue-box" class="mt-10 hidden">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h3 id="issue-title" class="text-xl font-bold">📌 주요 이슈 사항</h3>
|
||||
<button id="btn-save-issue"
|
||||
class="px-3 py-1 bg-amber-600 text-white rounded hover:bg-amber-700">
|
||||
저장
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<textarea id="issue-textarea"
|
||||
class="w-full border border-gray-300 rounded p-3"
|
||||
placeholder="이번 달 주요 이슈를 입력하세요."
|
||||
style="height:150px;"></textarea>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let allSchedules = [];
|
||||
let employees = [];
|
||||
let weekGroups = [];
|
||||
let clientList = [];
|
||||
|
||||
/* ===============================
|
||||
거래처 로드
|
||||
=============================== */
|
||||
async function loadClients() {
|
||||
let res = await fetch('/egbim/bbs/sales_clients.php?action=list');
|
||||
let json = await res.json();
|
||||
|
||||
if (json.status === "ok") {
|
||||
clientList = json.records.map(c => ({
|
||||
id: c.client_code,
|
||||
text: `${c.client_name} (${c.client_code})`
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
초기 로드
|
||||
=============================== */
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
await loadClients();
|
||||
loadMonth();
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
주요 이슈 로드
|
||||
=============================== */
|
||||
async function loadIssue(year, month) {
|
||||
const ym = `${year}-${month}`;
|
||||
|
||||
try {
|
||||
let res = await fetch(`/egbim/bbs/sales_issue.php?action=get&issue_month=${ym}`);
|
||||
let json = await res.json();
|
||||
|
||||
document.getElementById("issue-title").innerText = `📌 ${month}월 주요 이슈 사항`;
|
||||
document.getElementById("issue-textarea").value = json.issue_text ?? "";
|
||||
} catch (e) {
|
||||
document.getElementById("issue-textarea").value = "";
|
||||
}
|
||||
|
||||
document.getElementById("issue-box").classList.remove("hidden");
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
주요 이슈 저장
|
||||
=============================== */
|
||||
document.getElementById("btn-save-issue").onclick = async function () {
|
||||
const year = document.getElementById("sel-year").value;
|
||||
const month = document.getElementById("sel-month").value;
|
||||
|
||||
let res = await fetch("/egbim/bbs/sales_issue.php", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/x-www-form-urlencoded"},
|
||||
body: new URLSearchParams({
|
||||
action: "save",
|
||||
issue_month: `${year}-${month}`,
|
||||
issue_text: document.getElementById("issue-textarea").value
|
||||
})
|
||||
});
|
||||
|
||||
let json = await res.json();
|
||||
if (json.success) Swal.fire("저장 완료!", "", "success");
|
||||
};
|
||||
|
||||
/* ===============================
|
||||
평일 기준 주차 생성
|
||||
=============================== */
|
||||
function generateWeeks(year, month) {
|
||||
const weeks = [];
|
||||
const firstDay = new Date(year, month - 1, 1);
|
||||
const lastDay = new Date(year, month, 0);
|
||||
|
||||
let currentWeek = [];
|
||||
let weekNo = 1;
|
||||
|
||||
for (let d = 1; d <= lastDay.getDate(); d++) {
|
||||
const date = new Date(year, month - 1, d);
|
||||
const dow = date.getDay(); // 0=일, 1=월, ... 6=토
|
||||
|
||||
// 월요일이고, 이전 주차에 데이터 있으면 새 주차 시작
|
||||
if (dow === 1 && currentWeek.length) {
|
||||
weeks.push({
|
||||
weekNo: weekNo++,
|
||||
days: currentWeek
|
||||
});
|
||||
currentWeek = [];
|
||||
}
|
||||
|
||||
// 평일만 포함
|
||||
if (dow >= 1 && dow <= 5) {
|
||||
currentWeek.push({
|
||||
day: d,
|
||||
dow: ["","월","화","수","목","금"][dow],
|
||||
full: `${year}-${String(month).padStart(2,"0")}-${String(d).padStart(2,"0")}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 마지막 주차 push
|
||||
if (currentWeek.length) {
|
||||
weeks.push({
|
||||
weekNo: weekNo,
|
||||
days: currentWeek
|
||||
});
|
||||
}
|
||||
|
||||
return weeks;
|
||||
}
|
||||
|
||||
|
||||
/* ===============================
|
||||
현재 주차 index 계산
|
||||
=============================== */
|
||||
function getCurrentWeekIndex(year, month, weeks) {
|
||||
const today = new Date();
|
||||
const todayStr = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,"0")}-${String(today.getDate()).padStart(2,"0")}`;
|
||||
|
||||
if (`${year}-${month}` !== todayStr.slice(0,7)) return null;
|
||||
|
||||
for (let i = 0; i < weeks.length; i++) {
|
||||
if (weeks[i].days.some(d => d.full === todayStr)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
주차 재정렬
|
||||
=============================== */
|
||||
function reorderWeeks(weeks, currentIdx) {
|
||||
if (currentIdx === null) return weeks;
|
||||
|
||||
const current = weeks[currentIdx];
|
||||
const before = weeks.slice(0, currentIdx);
|
||||
const after = weeks.slice(currentIdx + 1);
|
||||
|
||||
return [current, ...before, ...after];
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
월 전체 로드
|
||||
=============================== */
|
||||
async function loadMonth() {
|
||||
let year = document.getElementById("sel-year").value;
|
||||
let month = document.getElementById("sel-month").value;
|
||||
|
||||
let res = await fetch(`/egbim/bbs/sales_schedules.php?action=month&year=${year}&month=${month}`);
|
||||
let json = await res.json();
|
||||
|
||||
employees = json.employees;
|
||||
allSchedules = json.schedules;
|
||||
|
||||
weekGroups = generateWeeks(year, month);
|
||||
|
||||
const idx = getCurrentWeekIndex(year, month, weekGroups);
|
||||
|
||||
const currentWeekNo = getCurrentWeekNo(year, month, weekGroups);
|
||||
window.__CURRENT_WEEK_NO__ = currentWeekNo;
|
||||
|
||||
weekGroups = reorderWeeks(weekGroups, idx);
|
||||
|
||||
renderMonth();
|
||||
await loadIssue(year, month);
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
월 → 주차 렌더링
|
||||
=============================== */
|
||||
function renderMonth() {
|
||||
let container = document.getElementById("month-container");
|
||||
container.innerHTML = "";
|
||||
|
||||
weekGroups.forEach(week => {
|
||||
let html = `
|
||||
<div class="mt-10 ${week.weekNo === window.__CURRENT_WEEK_NO__ ? 'week-current' : ''}">
|
||||
<div class="font-bold text-xl mb-3 week-title">
|
||||
${week.weekNo}주차 : ${week.days[0].day}일 ~ ${week.days.at(-1).day}일
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow border overflow-hidden">
|
||||
<table class="w-full text-sm sales-schedule-table">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
<th class="border-r px-3 py-2 w-28 text-center">담당자</th>
|
||||
${week.days.map(d => `
|
||||
<th class="border-r px-3 py-2 text-center">
|
||||
${d.day}일(${d.dow})
|
||||
</th>
|
||||
`).join("")}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${employees.map(emp => `
|
||||
<tr>
|
||||
<td class="border px-3 py-2 text-center bg-gray-50">${emp.emp_name}</td>
|
||||
${week.days.map(d => {
|
||||
let items = allSchedules.filter(s =>
|
||||
s.emp_no == emp.emp_no && s.schedule_date == d.full
|
||||
);
|
||||
return `
|
||||
<td class="border px-2 py-2 align-top cursor-pointer"
|
||||
onclick="openSchedulePopup('${emp.emp_no}','${emp.emp_name}','${d.full}')">
|
||||
${
|
||||
items.length
|
||||
? items.map(it => `
|
||||
<div class="mb-1 p-2 border rounded bg-gray-50">
|
||||
<div class="font-medium">${it.content}</div>
|
||||
<div class="text-xs text-gray-500">${it.client_code}</div>
|
||||
<span class="text-xs px-2 py-0.5 rounded ${
|
||||
it.status === '완료'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-yellow-100 text-yellow-700'
|
||||
}">${it.status}</span>
|
||||
</div>
|
||||
`).join("")
|
||||
: `<div class="text-xs text-gray-300">(없음)</div>`
|
||||
}
|
||||
</td>
|
||||
`;
|
||||
}).join("")}
|
||||
</tr>
|
||||
`).join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`;
|
||||
container.insertAdjacentHTML("beforeend", html);
|
||||
});
|
||||
}
|
||||
|
||||
//현재 주차 번호 구하기
|
||||
function getCurrentWeekNo(year, month, weeks) {
|
||||
const today = new Date();
|
||||
const todayStr = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,"0")}-${String(today.getDate()).padStart(2,"0")}`;
|
||||
|
||||
if (`${year}-${month}` !== todayStr.slice(0,7)) return null;
|
||||
|
||||
for (let w of weeks) {
|
||||
if (w.days.some(d => d.full === todayStr)) {
|
||||
return w.weekNo;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/* =========================================================
|
||||
일정 리스트 팝업 (SweetAlert2)
|
||||
========================================================= */
|
||||
function openSchedulePopup(emp_no, emp_name, date) {
|
||||
|
||||
let items = allSchedules.filter(
|
||||
s => s.emp_no == emp_no && s.schedule_date == date
|
||||
);
|
||||
|
||||
const listHtml = items.length
|
||||
? items.map(it => `
|
||||
<div class="swal-schedule-card">
|
||||
<div class="swal-schedule-content">
|
||||
${it.content}
|
||||
</div>
|
||||
<div class="swal-schedule-meta">
|
||||
${it.client_code} · ${it.time_period}
|
||||
<span class="ml-2 px-2 py-0.5 rounded text-xs ${
|
||||
it.status === '완료'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-yellow-100 text-yellow-700'
|
||||
}">${it.status}</span>
|
||||
</div>
|
||||
|
||||
<div class="swal-schedule-actions">
|
||||
<button onclick="editSchedule(${it.seq_no})"
|
||||
class="px-2 py-1 text-xs rounded bg-amber-400 text-white">
|
||||
수정
|
||||
</button>
|
||||
<button onclick="deleteSchedule(${it.seq_no})"
|
||||
class="px-2 py-1 text-xs rounded bg-red-500 text-white">
|
||||
삭제
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join("")
|
||||
: `<div style="color:#9ca3af;">등록된 일정이 없습니다.</div>`;
|
||||
|
||||
Swal.fire({
|
||||
width: 480,
|
||||
showConfirmButton: false,
|
||||
html: `
|
||||
<div class="swal-schedule-header">
|
||||
<div>
|
||||
<div class="swal-schedule-title">${date}</div>
|
||||
<div class="swal-schedule-sub">${emp_name}</div>
|
||||
</div>
|
||||
|
||||
<button onclick="addSchedule('${emp_no}','${date}')"
|
||||
class="px-3 py-1 text-sm rounded bg-indigo-500 text-white">
|
||||
+ 일정 추가
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="swal-schedule-list">
|
||||
${listHtml}
|
||||
</div>
|
||||
`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/* =========================================================
|
||||
신규 일정 추가
|
||||
========================================================= */
|
||||
window.addSchedule = function(emp_no, date) {
|
||||
|
||||
let clientOptions = clientList
|
||||
.map(c => `<option value="${c.id}">${c.text}</option>`)
|
||||
.join("");
|
||||
|
||||
Swal.fire({
|
||||
title: "신규 일정 추가",
|
||||
html: `
|
||||
<div class="swal-form">
|
||||
|
||||
<div class="swal-group">
|
||||
<label class="title">시간대</label>
|
||||
<div class="swal-radio">
|
||||
<label><input type="radio" name="s_period" value="오전" checked> 오전</label>
|
||||
<label><input type="radio" name="s_period" value="오후"> 오후</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swal-group">
|
||||
<label class="title">상태</label>
|
||||
<div class="swal-radio">
|
||||
<label><input type="radio" name="s_status" value="예정" checked> 예정</label>
|
||||
<label><input type="radio" name="s_status" value="완료"> 완료</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swal-group">
|
||||
<label class="title">거래처</label>
|
||||
<select id="s-client" class="swal-select">
|
||||
<option value="">선택하세요</option>
|
||||
${clientOptions}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="swal-group">
|
||||
<label class="title">내용</label>
|
||||
<textarea id="s-content"
|
||||
class="swal-textarea"
|
||||
placeholder="내용을 입력하세요"></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "등록",
|
||||
cancelButtonText: "취소",
|
||||
width: 420
|
||||
}).then(async result => {
|
||||
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
let status = document.querySelector('input[name="s_status"]:checked').value;
|
||||
let period = document.querySelector('input[name="s_period"]:checked').value;
|
||||
let client_code = document.getElementById("s-client").value;
|
||||
let content = document.getElementById("s-content").value;
|
||||
|
||||
let res = await fetch("/egbim/bbs/sales_schedules.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
action: "insert",
|
||||
emp_no,
|
||||
schedule_date: date,
|
||||
time_period: period,
|
||||
client_code,
|
||||
content,
|
||||
status
|
||||
})
|
||||
});
|
||||
|
||||
let j = await res.json();
|
||||
if (j.status === "ok") {
|
||||
Swal.close();
|
||||
loadMonth();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/* =========================================================
|
||||
일정 수정
|
||||
========================================================= */
|
||||
window.editSchedule = function(seq_no) {
|
||||
|
||||
let item = allSchedules.find(s => s.seq_no == seq_no);
|
||||
|
||||
let clientOptions = clientList
|
||||
.map(c => `
|
||||
<option value="${c.id}" ${c.id == item.client_code ? "selected" : ""}>
|
||||
${c.text}
|
||||
</option>
|
||||
`)
|
||||
.join("");
|
||||
|
||||
Swal.fire({
|
||||
title: "일정 수정",
|
||||
html: `
|
||||
<div class="swal-form">
|
||||
|
||||
<div class="swal-group">
|
||||
<label class="title">시간대</label>
|
||||
<div class="swal-radio">
|
||||
<label>
|
||||
<input type="radio" name="e_period" value="오전"
|
||||
${item.time_period === "오전" ? "checked" : ""}>
|
||||
오전
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="e_period" value="오후"
|
||||
${item.time_period === "오후" ? "checked" : ""}>
|
||||
오후
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swal-group">
|
||||
<label class="title">상태</label>
|
||||
<div class="swal-radio">
|
||||
<label>
|
||||
<input type="radio" name="e_status" value="예정"
|
||||
${item.status === "예정" ? "checked" : ""}>
|
||||
예정
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="e_status" value="완료"
|
||||
${item.status === "완료" ? "checked" : ""}>
|
||||
완료
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swal-group">
|
||||
<label class="title">거래처</label>
|
||||
<select id="e-client" class="swal-select">
|
||||
<option value="">선택하세요</option>
|
||||
${clientOptions}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="swal-group">
|
||||
<label class="title">내용</label>
|
||||
<textarea id="e-content"
|
||||
class="swal-textarea"
|
||||
placeholder="내용을 입력하세요">${item.content}</textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "저장",
|
||||
cancelButtonText: "취소",
|
||||
width: 420
|
||||
}).then(async result => {
|
||||
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
let status = document.querySelector('input[name="e_status"]:checked').value;
|
||||
let period = document.querySelector('input[name="e_period"]:checked').value;
|
||||
let client_code = document.getElementById("e-client").value;
|
||||
let content = document.getElementById("e-content").value;
|
||||
|
||||
let res = await fetch("/egbim/bbs/sales_schedules.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
action: "update",
|
||||
seq_no,
|
||||
emp_no: item.emp_no,
|
||||
schedule_date: item.schedule_date,
|
||||
time_period: period,
|
||||
client_code,
|
||||
content,
|
||||
status
|
||||
})
|
||||
});
|
||||
|
||||
let j = await res.json();
|
||||
if (j.status === "ok") {
|
||||
Swal.close();
|
||||
loadMonth();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
/* =========================================================
|
||||
일정 삭제
|
||||
========================================================= */
|
||||
window.deleteSchedule = function(seq_no) {
|
||||
|
||||
Swal.fire({
|
||||
title:"삭제하시겠습니까?",
|
||||
icon:"warning",
|
||||
showCancelButton:true,
|
||||
confirmButtonText:"삭제"
|
||||
}).then(async r=>{
|
||||
if(!r.isConfirmed) return;
|
||||
|
||||
let res = await fetch("/egbim/bbs/sales_schedules.php",{
|
||||
method:"POST",
|
||||
headers:{"Content-Type":"application/x-www-form-urlencoded"},
|
||||
body:new URLSearchParams({
|
||||
action:"delete",
|
||||
seq_no
|
||||
})
|
||||
});
|
||||
|
||||
let j = await res.json();
|
||||
if(j.status==="ok"){
|
||||
Swal.close();
|
||||
loadMonth();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//document.getElementById("btn-load").onclick = loadMonth;
|
||||
loadMonth();
|
||||
|
||||
/* ===============================
|
||||
연도 / 월 변경 시 자동 로드
|
||||
=============================== */
|
||||
document.getElementById("sel-year").addEventListener("change", () => {
|
||||
loadMonth();
|
||||
});
|
||||
|
||||
document.getElementById("sel-month").addEventListener("change", () => {
|
||||
loadMonth();
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php sales_layout_end(); ?>
|
||||
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
include __DIR__ . '/layout_sales.php';
|
||||
sales_layout_start("영업 목표 설정");
|
||||
?>
|
||||
|
||||
<h2 class="text-2xl font-bold mb-4">영업 목표 설정</h2>
|
||||
|
||||
<div id="grid_targets" style="width:100%;height:720px;"></div>
|
||||
|
||||
<script type="module">
|
||||
import { w2grid } from "https://cdn.jsdelivr.net/gh/vitmalina/w2ui@master/dist/w2ui.es6.min.js";
|
||||
|
||||
/* ------------------------------------------------------
|
||||
공통 유틸
|
||||
------------------------------------------------------ */
|
||||
function clean(v) {
|
||||
return (v === undefined || v === null) ? '' : v;
|
||||
}
|
||||
function cleanNumber(v) {
|
||||
if (v === undefined || v === null || v === '' || isNaN(v)) return 0;
|
||||
return Number(v);
|
||||
}
|
||||
function normalizeListValue(v) {
|
||||
if (typeof v === "object" && v !== null && v.id) return v.id;
|
||||
return v ?? '';
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------
|
||||
직원 목록 로드
|
||||
------------------------------------------------------ */
|
||||
let employeeList = [];
|
||||
|
||||
async function loadEmployees() {
|
||||
let res = await fetch('/egbim/bbs/sales_members.php?action=list');
|
||||
let json = await res.json();
|
||||
|
||||
if (json.status === "ok") {
|
||||
employeeList = json.records.map(m => ({
|
||||
id: m.emp_no,
|
||||
text: `${m.emp_name} (${m.emp_no})`
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
await loadEmployees();
|
||||
|
||||
/* ------------------------------------------------------
|
||||
GRID 정의
|
||||
------------------------------------------------------ */
|
||||
let grid = new w2grid({
|
||||
name: 'grid_targets',
|
||||
box: '#grid_targets',
|
||||
|
||||
show: {
|
||||
toolbar: true,
|
||||
footer: true,
|
||||
toolbarSave: true,
|
||||
toolbarReload: true,
|
||||
toolbarSearch: true,
|
||||
toolbarColumns: true,
|
||||
lineNumbers: false
|
||||
},
|
||||
|
||||
multiSearch: true,
|
||||
|
||||
searches: [
|
||||
{ field: 'target_month', label: '목표월', type: 'text' },
|
||||
{ field: 'emp_no', label: '영업담당자', type: 'text' }
|
||||
],
|
||||
|
||||
columns: [
|
||||
{ field: 'target_month', text: '목표월', size: '120px',
|
||||
editable: { type: 'text' }, sortable: true },
|
||||
|
||||
{
|
||||
field: 'emp_no',
|
||||
text: '영업담당자',
|
||||
size: '160px',
|
||||
editable: { type: 'combo', items: employeeList, showAll: true, filter: true },
|
||||
sortable: true,
|
||||
// render(record) {
|
||||
// if (typeof record.emp_no === "object" && record.emp_no !== null)
|
||||
// return record.emp_no.text;
|
||||
|
||||
// const item = employeeList.find(e => e.id == record.emp_no);
|
||||
// return item ? item.text : record.emp_no;
|
||||
// }
|
||||
},
|
||||
|
||||
{ field: 'target_qty', text: '목표수량', size: '120px',
|
||||
editable: { type: 'int' }, sortable: true, render: 'int', style: 'text-align:right' },
|
||||
|
||||
{ field: 'target_amount', text: '목표금액', size: '150px',
|
||||
editable: { type: 'float' }, sortable: true, render: 'int', style: 'text-align:right' },
|
||||
|
||||
{ field: 'created_at', text: '등록일', size: '160px', sortable: true }
|
||||
],
|
||||
|
||||
toolbar: {
|
||||
items: [
|
||||
{ id: 'add', type: 'button', text: '추가', icon: 'w2ui-icon-plus' },
|
||||
{ id: 'delete', type: 'button', text: '삭제', icon: 'w2ui-icon-cross' }
|
||||
],
|
||||
|
||||
onClick(event) {
|
||||
const g = this.owner;
|
||||
|
||||
if (event.target === 'w2ui-reload') {
|
||||
loadTargets();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target === 'add') {
|
||||
let recid = grid.records.length + 2;
|
||||
g.add({recid:recid,target_month:'',emp_no:'',target_qty:0,target_amount:0,created_at:''});
|
||||
g.scrollIntoView(recid);
|
||||
g.editField(recid, 0);
|
||||
/*
|
||||
const newId = "new-" + Math.random().toString(36).substr(2, 9);
|
||||
g.add({
|
||||
recid: newId,
|
||||
target_month: "",
|
||||
emp_no: "",
|
||||
target_qty: 0,
|
||||
target_amount: 0,
|
||||
created_at: ""
|
||||
});
|
||||
g.refresh();
|
||||
return;
|
||||
*/
|
||||
}
|
||||
|
||||
if (event.target === 'delete') {
|
||||
|
||||
const sel = g.getSelection();
|
||||
if (!sel.length) return;
|
||||
|
||||
if (!confirm("정말 삭제하시겠습니까?")) return;
|
||||
|
||||
sel.forEach(id => {
|
||||
|
||||
if (String(id).startsWith("new-")) {
|
||||
g.remove(id);
|
||||
return;
|
||||
}
|
||||
|
||||
fetch("/egbim/bbs/sales_targets.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
action: "delete",
|
||||
id: id
|
||||
})
|
||||
});
|
||||
|
||||
g.remove(id);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/* ------------------------------------------------------
|
||||
저장 처리
|
||||
------------------------------------------------------ */
|
||||
onSave(event) {
|
||||
const changes = this.getChanges();
|
||||
if (!changes.length) return;
|
||||
|
||||
Promise.all(
|
||||
changes.map(ch => {
|
||||
let rec = this.get(ch.recid);
|
||||
|
||||
let empRaw = ch.emp_no ?? rec.emp_no;
|
||||
let empText = ""; // 저장될 값: 이름(사번)
|
||||
let empId = ""; // 검증용 사번
|
||||
|
||||
/* ------------------------------------------------------
|
||||
1) combo 선택한 경우 (object)
|
||||
------------------------------------------------------ */
|
||||
if (typeof empRaw === "object" && empRaw !== null) {
|
||||
empId = String(empRaw.id);
|
||||
|
||||
// combo text를 그대로 믿지 말고 employeeList 기준으로 재확정
|
||||
const found = employeeList.find(e => String(e.id) === empId);
|
||||
if (found) {
|
||||
empText = found.text;
|
||||
} else {
|
||||
empText = empRaw.text; // fallback
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------
|
||||
2) 직접 입력한 경우
|
||||
- 이름(사번)
|
||||
- 이름 (사번)
|
||||
- 사번만 입력 → 허용 X (반드시 이름(사번)으로 변환)
|
||||
------------------------------------------------------ */
|
||||
else {
|
||||
let typed = String(empRaw ?? "").trim();
|
||||
|
||||
// (1) 이름(사번) 패턴
|
||||
let match = typed.match(/(.+)\(([A-Za-z0-9]+)\)$/);
|
||||
if (match) {
|
||||
empText = typed;
|
||||
empId = match[2];
|
||||
}
|
||||
else {
|
||||
// (2) 사번만 입력 → 리스트에서 검색하여 이름(사번) 변환
|
||||
let foundById = employeeList.find(e => e.id === typed);
|
||||
if (foundById) {
|
||||
empId = foundById.id;
|
||||
empText = foundById.text; // 이름(사번)
|
||||
}
|
||||
else {
|
||||
// (3) 이름만 입력 → 리스트에서 검색하여 이름(사번) 변환
|
||||
let foundByName = employeeList.find(e =>
|
||||
e.text.startsWith(typed + "(") // "이름(" 패턴 확인
|
||||
);
|
||||
|
||||
if (foundByName) {
|
||||
empId = foundByName.id;
|
||||
empText = foundByName.text;
|
||||
} else {
|
||||
alert("⚠ 올바른 이름 또는 이름(사번)을 입력해주세요.");
|
||||
throw 'invalid-format';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------
|
||||
3) employeeList 존재 여부 체크
|
||||
------------------------------------------------------ */
|
||||
const normalize = v => String(v).replace(/\s+/g, '').trim();
|
||||
|
||||
const exists = employeeList.some(e =>
|
||||
normalize(e.text) === normalize(empText)
|
||||
);
|
||||
|
||||
if (!exists) {
|
||||
alert(`영업담당자 '${empText}' 은(는) 등록된 직원이 아닙니다.`);
|
||||
throw 'invalid-emp';
|
||||
}
|
||||
|
||||
let body = {
|
||||
target_month: clean(ch.target_month ?? rec.target_month),
|
||||
emp_no: empText,
|
||||
target_qty: cleanNumber(ch.target_qty ?? rec.target_qty),
|
||||
target_amount: cleanNumber(ch.target_amount ?? rec.target_amount)
|
||||
};
|
||||
|
||||
if (!/^\d{4}-(0[1-9]|1[0-2])$/.test(body.target_month)) {
|
||||
alert("목표월은 YYYY-MM 형식이어야 합니다.");
|
||||
throw 'invalid-month';
|
||||
}
|
||||
|
||||
if (String(ch.recid).startsWith("new-")) {
|
||||
body.action = "insert";
|
||||
} else {
|
||||
body.action = "update";
|
||||
body.id = ch.recid;
|
||||
}
|
||||
|
||||
return fetch("/egbim/bbs/sales_targets.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams(body)
|
||||
});
|
||||
|
||||
})
|
||||
).then(() => {
|
||||
loadTargets();
|
||||
w2alert("저장되었습니다!");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------
|
||||
데이터 로드
|
||||
------------------------------------------------------ */
|
||||
function loadTargets() {
|
||||
fetch("/egbim/bbs/sales_targets.php?action=list")
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.status === "ok") {
|
||||
grid.records = res.records.map(r => ({
|
||||
recid: r.id,
|
||||
...r
|
||||
}));
|
||||
grid.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
loadTargets();
|
||||
</script>
|
||||
|
||||
<?php sales_layout_end(); ?>
|
||||
Reference in new issue
Block a user