From 03cd93e1f2bacf01ad3adb839ea253a2d21537bf Mon Sep 17 00:00:00 2001 From: Macbook Date: Sun, 12 Jul 2026 05:05:17 +0900 Subject: [PATCH] =?UTF-8?q?BillCare=20=EC=B4=88=EA=B8=B0=20=EC=86=8C?= =?UTF-8?q?=EC=8A=A4=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 비즈케어 홈즈 홈상품 운용관리(BillCare) 프론트/백엔드/Docker 구성을 exdev git에 등록합니다. Co-authored-by: Cursor --- .gitignore | 31 + README.md | 30 + backend/.dockerignore | 6 + backend/.gitattributes | 2 + backend/.gitignore | 33 + backend/.mvn/wrapper/maven-wrapper.properties | 3 + backend/Dockerfile | 17 + backend/mvnw | 295 + backend/mvnw.cmd | 189 + backend/pom.xml | 142 + .../com/bizcare/agent/AgentApplication.java | 13 + .../com/bizcare/agent/ApiControllers.java | 4738 +++++++++++++++++ .../bizcare/agent/CareHomesController.java | 3690 +++++++++++++ .../agent/CareReceptionController.java | 1241 +++++ .../com/bizcare/agent/DataInitializer.java | 2965 +++++++++++ .../com/bizcare/agent/DomainEntities.java | 587 ++ .../com/bizcare/agent/FileStorageService.java | 85 + .../com/bizcare/agent/HealthController.java | 14 + .../com/bizcare/agent/SecurityConfig.java | 111 + .../com/bizcare/agent/ShopController.java | 32 + .../src/main/resources/application.properties | 1 + backend/src/main/resources/application.yml | 30 + .../bizcare/agent/AgentApplicationTests.java | 13 + docker-compose.yml | 60 + frontend/.dockerignore | 6 + frontend/.gitignore | 24 + frontend/.oxlintrc.json | 8 + frontend/Dockerfile | 14 + frontend/README.md | 32 + frontend/index.html | 12 + frontend/nginx.conf | 28 + frontend/package-lock.json | 3607 +++++++++++++ frontend/package.json | 32 + frontend/public/favicon.svg | 1 + frontend/public/icons.svg | 24 + frontend/src/App.tsx | 163 + frontend/src/api/auth.ts | 43 + frontend/src/api/client.ts | 46 + frontend/src/api/opens.ts | 31 + frontend/src/api/resources.ts | 45 + frontend/src/api/stocks.ts | 77 + frontend/src/assets/billcare_mark.png | Bin 0 -> 76455 bytes frontend/src/assets/hero.png | Bin 0 -> 13057 bytes frontend/src/assets/logo_image.png | Bin 0 -> 66473 bytes frontend/src/assets/logo_image_theme.png | Bin 0 -> 111356 bytes frontend/src/assets/logo_login_dark.png | Bin 0 -> 4639 bytes frontend/src/assets/logo_login_light.png | Bin 0 -> 111356 bytes frontend/src/assets/logo_mark_gold.png | Bin 0 -> 105168 bytes frontend/src/assets/logo_mark_light.png | Bin 0 -> 111356 bytes frontend/src/assets/logo_word_theme.png | Bin 0 -> 18940 bytes frontend/src/assets/react.svg | 1 + frontend/src/assets/vite.svg | 1 + frontend/src/components/AccountPopover.tsx | 197 + frontend/src/components/AgentShell.tsx | 261 + frontend/src/components/BillCareLogo.tsx | 64 + frontend/src/components/CareTable.tsx | 8 + frontend/src/components/DraggableModal.tsx | 310 ++ frontend/src/components/OrderFormLayout.tsx | 92 + frontend/src/components/PrgSection.tsx | 24 + .../src/components/ReceptionApplyModal.tsx | 794 +++ .../src/components/ReceptionBulkModal.tsx | 290 + .../src/components/ReceptionProgressModal.tsx | 565 ++ .../components/ReceptionProgressReadOnly.tsx | 236 + frontend/src/components/SchedulePanel.tsx | 267 + .../src/components/SubjectSettingsModal.tsx | 120 + frontend/src/formRequiredMark.tsx | 12 + frontend/src/index.css | 407 ++ frontend/src/main.tsx | 36 + frontend/src/menu.ts | 190 + frontend/src/pages/LoginPage.tsx | 68 + frontend/src/pages/care/AbooksLogPage.tsx | 20 + frontend/src/pages/care/AbooksPage.tsx | 612 +++ frontend/src/pages/care/BbsPage.tsx | 296 + frontend/src/pages/care/BookingLogPage.tsx | 31 + frontend/src/pages/care/BookingPage.tsx | 584 ++ .../src/pages/care/CarePlaceholderPage.tsx | 17 + .../src/pages/care/ContractAdjustPage.tsx | 389 ++ .../src/pages/care/ContractBalancePage.tsx | 459 ++ frontend/src/pages/care/ContractHomePage.tsx | 13 + .../src/pages/care/ContractInternetPage.tsx | 13 + frontend/src/pages/care/ContractListPage.tsx | 139 + .../src/pages/care/ContractListShared.tsx | 386 ++ frontend/src/pages/care/ContractLogPage.tsx | 23 + frontend/src/pages/care/ContractWritePage.tsx | 467 ++ frontend/src/pages/care/CsInquiryPage.tsx | 255 + frontend/src/pages/care/CsNoticePage.tsx | 225 + frontend/src/pages/care/CsQuestionPage.tsx | 318 ++ frontend/src/pages/care/CustomerListPage.tsx | 108 + frontend/src/pages/care/CustomerLogPage.tsx | 21 + frontend/src/pages/care/CustomerPage.tsx | 723 +++ frontend/src/pages/care/CustomerWritePage.tsx | 115 + frontend/src/pages/care/DeliveryPage.tsx | 287 + .../src/pages/care/GroupStatReportPage.tsx | 141 + frontend/src/pages/care/HomeDashboardPage.tsx | 495 ++ frontend/src/pages/care/HomesLogListPage.tsx | 231 + frontend/src/pages/care/InCompanyPage.tsx | 342 ++ frontend/src/pages/care/InternalBoardPage.tsx | 297 ++ frontend/src/pages/care/InvoiceLogPage.tsx | 22 + frontend/src/pages/care/InvoiceMyPage.tsx | 163 + .../src/pages/care/InvoicePublishPage.tsx | 241 + .../src/pages/care/KindStatReportPage.tsx | 154 + frontend/src/pages/care/OpenAdjustPage.tsx | 356 ++ frontend/src/pages/care/OpenBalancePage.tsx | 356 ++ .../src/pages/care/OpenDeleteHistoryPage.tsx | 199 + frontend/src/pages/care/OpenExchangePage.tsx | 215 + frontend/src/pages/care/OpenHomeIndocPage.tsx | 192 + frontend/src/pages/care/OpenHomeSalePage.tsx | 191 + .../src/pages/care/OpenHomeSaleWritePage.tsx | 198 + frontend/src/pages/care/OpenIndocPage.tsx | 195 + frontend/src/pages/care/OpenManagePage.tsx | 153 + .../src/pages/care/OpenNameChangePage.tsx | 207 + frontend/src/pages/care/OpenReturnPage.tsx | 183 + .../src/pages/care/OpenSettleHistoryPage.tsx | 211 + .../src/pages/care/OpenUnitCostApplyPage.tsx | 142 + frontend/src/pages/care/OpenUnitCostPage.tsx | 274 + frontend/src/pages/care/OpenWithdrawPage.tsx | 199 + frontend/src/pages/care/OpenWritePage.tsx | 408 ++ frontend/src/pages/care/OutCompanyPage.tsx | 449 ++ .../src/pages/care/PendingCalendarPage.tsx | 434 ++ frontend/src/pages/care/PendingLogPage.tsx | 21 + frontend/src/pages/care/PendingPage.tsx | 515 ++ frontend/src/pages/care/PreferencePage.tsx | 624 +++ frontend/src/pages/care/ProductHomePage.tsx | 388 ++ .../src/pages/care/ProductInternetPage.tsx | 388 ++ frontend/src/pages/care/ProductListPage.tsx | 144 + frontend/src/pages/care/ProductLogPage.tsx | 24 + frontend/src/pages/care/ProductWritePage.tsx | 125 + frontend/src/pages/care/ReceptionListPage.tsx | 830 +++ frontend/src/pages/care/ReportAgencyPage.tsx | 12 + frontend/src/pages/care/ReportDailyPage.tsx | 182 + frontend/src/pages/care/ReportHomePage.tsx | 11 + .../src/pages/care/ReportIncentivePage.tsx | 15 + .../src/pages/care/ReportInternetPage.tsx | 11 + frontend/src/pages/care/ReportMemberPage.tsx | 12 + frontend/src/pages/care/ReportMonthlyPage.tsx | 156 + frontend/src/pages/care/ReportOpenPage.tsx | 290 + frontend/src/pages/care/ReportOutPage.tsx | 215 + frontend/src/pages/care/ReportSalePage.tsx | 167 + frontend/src/pages/care/SettingAgencyPage.tsx | 466 ++ frontend/src/pages/care/SettingLevelPage.tsx | 347 ++ frontend/src/pages/care/SettingMemberPage.tsx | 631 +++ .../src/pages/care/SettingPartner1Page.tsx | 301 ++ .../src/pages/care/SettingPartner2Page.tsx | 280 + .../src/pages/care/SettingPreferencePage.tsx | 501 ++ frontend/src/pages/care/SitemapPage.tsx | 263 + frontend/src/pages/care/SmsAddressPage.tsx | 546 ++ frontend/src/pages/care/SmsHistoryPage.tsx | 388 ++ frontend/src/pages/care/StaffManagePage.tsx | 391 ++ frontend/src/pages/care/homesShared.tsx | 361 ++ frontend/src/styles/app.css | 4424 +++++++++++++++ frontend/src/tableAlign.ts | 65 + frontend/src/theme.ts | 81 + frontend/src/themeContext.tsx | 42 + frontend/src/types/index.ts | 12 + frontend/tsconfig.app.json | 26 + frontend/tsconfig.json | 7 + frontend/tsconfig.node.json | 23 + frontend/vite.config.ts | 17 + logo_image.png | Bin 0 -> 1689467 bytes logo_image_theme.png | Bin 0 -> 1689888 bytes 160 files changed, 47915 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 backend/.dockerignore create mode 100644 backend/.gitattributes create mode 100644 backend/.gitignore create mode 100644 backend/.mvn/wrapper/maven-wrapper.properties create mode 100644 backend/Dockerfile create mode 100755 backend/mvnw create mode 100644 backend/mvnw.cmd create mode 100644 backend/pom.xml create mode 100644 backend/src/main/java/com/bizcare/agent/AgentApplication.java create mode 100644 backend/src/main/java/com/bizcare/agent/ApiControllers.java create mode 100644 backend/src/main/java/com/bizcare/agent/CareHomesController.java create mode 100644 backend/src/main/java/com/bizcare/agent/CareReceptionController.java create mode 100644 backend/src/main/java/com/bizcare/agent/DataInitializer.java create mode 100644 backend/src/main/java/com/bizcare/agent/DomainEntities.java create mode 100644 backend/src/main/java/com/bizcare/agent/FileStorageService.java create mode 100644 backend/src/main/java/com/bizcare/agent/HealthController.java create mode 100644 backend/src/main/java/com/bizcare/agent/SecurityConfig.java create mode 100644 backend/src/main/java/com/bizcare/agent/ShopController.java create mode 100644 backend/src/main/resources/application.properties create mode 100644 backend/src/main/resources/application.yml create mode 100644 backend/src/test/java/com/bizcare/agent/AgentApplicationTests.java create mode 100644 docker-compose.yml create mode 100644 frontend/.dockerignore create mode 100644 frontend/.gitignore create mode 100644 frontend/.oxlintrc.json create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.md create mode 100644 frontend/index.html create mode 100644 frontend/nginx.conf create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/public/icons.svg create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/api/auth.ts create mode 100644 frontend/src/api/client.ts create mode 100644 frontend/src/api/opens.ts create mode 100644 frontend/src/api/resources.ts create mode 100644 frontend/src/api/stocks.ts create mode 100644 frontend/src/assets/billcare_mark.png create mode 100644 frontend/src/assets/hero.png create mode 100644 frontend/src/assets/logo_image.png create mode 100644 frontend/src/assets/logo_image_theme.png create mode 100644 frontend/src/assets/logo_login_dark.png create mode 100644 frontend/src/assets/logo_login_light.png create mode 100644 frontend/src/assets/logo_mark_gold.png create mode 100644 frontend/src/assets/logo_mark_light.png create mode 100644 frontend/src/assets/logo_word_theme.png create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/assets/vite.svg create mode 100644 frontend/src/components/AccountPopover.tsx create mode 100644 frontend/src/components/AgentShell.tsx create mode 100644 frontend/src/components/BillCareLogo.tsx create mode 100644 frontend/src/components/CareTable.tsx create mode 100644 frontend/src/components/DraggableModal.tsx create mode 100644 frontend/src/components/OrderFormLayout.tsx create mode 100644 frontend/src/components/PrgSection.tsx create mode 100644 frontend/src/components/ReceptionApplyModal.tsx create mode 100644 frontend/src/components/ReceptionBulkModal.tsx create mode 100644 frontend/src/components/ReceptionProgressModal.tsx create mode 100644 frontend/src/components/ReceptionProgressReadOnly.tsx create mode 100644 frontend/src/components/SchedulePanel.tsx create mode 100644 frontend/src/components/SubjectSettingsModal.tsx create mode 100644 frontend/src/formRequiredMark.tsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/menu.ts create mode 100644 frontend/src/pages/LoginPage.tsx create mode 100644 frontend/src/pages/care/AbooksLogPage.tsx create mode 100644 frontend/src/pages/care/AbooksPage.tsx create mode 100644 frontend/src/pages/care/BbsPage.tsx create mode 100644 frontend/src/pages/care/BookingLogPage.tsx create mode 100644 frontend/src/pages/care/BookingPage.tsx create mode 100644 frontend/src/pages/care/CarePlaceholderPage.tsx create mode 100644 frontend/src/pages/care/ContractAdjustPage.tsx create mode 100644 frontend/src/pages/care/ContractBalancePage.tsx create mode 100644 frontend/src/pages/care/ContractHomePage.tsx create mode 100644 frontend/src/pages/care/ContractInternetPage.tsx create mode 100644 frontend/src/pages/care/ContractListPage.tsx create mode 100644 frontend/src/pages/care/ContractListShared.tsx create mode 100644 frontend/src/pages/care/ContractLogPage.tsx create mode 100644 frontend/src/pages/care/ContractWritePage.tsx create mode 100644 frontend/src/pages/care/CsInquiryPage.tsx create mode 100644 frontend/src/pages/care/CsNoticePage.tsx create mode 100644 frontend/src/pages/care/CsQuestionPage.tsx create mode 100644 frontend/src/pages/care/CustomerListPage.tsx create mode 100644 frontend/src/pages/care/CustomerLogPage.tsx create mode 100644 frontend/src/pages/care/CustomerPage.tsx create mode 100644 frontend/src/pages/care/CustomerWritePage.tsx create mode 100644 frontend/src/pages/care/DeliveryPage.tsx create mode 100644 frontend/src/pages/care/GroupStatReportPage.tsx create mode 100644 frontend/src/pages/care/HomeDashboardPage.tsx create mode 100644 frontend/src/pages/care/HomesLogListPage.tsx create mode 100644 frontend/src/pages/care/InCompanyPage.tsx create mode 100644 frontend/src/pages/care/InternalBoardPage.tsx create mode 100644 frontend/src/pages/care/InvoiceLogPage.tsx create mode 100644 frontend/src/pages/care/InvoiceMyPage.tsx create mode 100644 frontend/src/pages/care/InvoicePublishPage.tsx create mode 100644 frontend/src/pages/care/KindStatReportPage.tsx create mode 100644 frontend/src/pages/care/OpenAdjustPage.tsx create mode 100644 frontend/src/pages/care/OpenBalancePage.tsx create mode 100644 frontend/src/pages/care/OpenDeleteHistoryPage.tsx create mode 100644 frontend/src/pages/care/OpenExchangePage.tsx create mode 100644 frontend/src/pages/care/OpenHomeIndocPage.tsx create mode 100644 frontend/src/pages/care/OpenHomeSalePage.tsx create mode 100644 frontend/src/pages/care/OpenHomeSaleWritePage.tsx create mode 100644 frontend/src/pages/care/OpenIndocPage.tsx create mode 100644 frontend/src/pages/care/OpenManagePage.tsx create mode 100644 frontend/src/pages/care/OpenNameChangePage.tsx create mode 100644 frontend/src/pages/care/OpenReturnPage.tsx create mode 100644 frontend/src/pages/care/OpenSettleHistoryPage.tsx create mode 100644 frontend/src/pages/care/OpenUnitCostApplyPage.tsx create mode 100644 frontend/src/pages/care/OpenUnitCostPage.tsx create mode 100644 frontend/src/pages/care/OpenWithdrawPage.tsx create mode 100644 frontend/src/pages/care/OpenWritePage.tsx create mode 100644 frontend/src/pages/care/OutCompanyPage.tsx create mode 100644 frontend/src/pages/care/PendingCalendarPage.tsx create mode 100644 frontend/src/pages/care/PendingLogPage.tsx create mode 100644 frontend/src/pages/care/PendingPage.tsx create mode 100644 frontend/src/pages/care/PreferencePage.tsx create mode 100644 frontend/src/pages/care/ProductHomePage.tsx create mode 100644 frontend/src/pages/care/ProductInternetPage.tsx create mode 100644 frontend/src/pages/care/ProductListPage.tsx create mode 100644 frontend/src/pages/care/ProductLogPage.tsx create mode 100644 frontend/src/pages/care/ProductWritePage.tsx create mode 100644 frontend/src/pages/care/ReceptionListPage.tsx create mode 100644 frontend/src/pages/care/ReportAgencyPage.tsx create mode 100644 frontend/src/pages/care/ReportDailyPage.tsx create mode 100644 frontend/src/pages/care/ReportHomePage.tsx create mode 100644 frontend/src/pages/care/ReportIncentivePage.tsx create mode 100644 frontend/src/pages/care/ReportInternetPage.tsx create mode 100644 frontend/src/pages/care/ReportMemberPage.tsx create mode 100644 frontend/src/pages/care/ReportMonthlyPage.tsx create mode 100644 frontend/src/pages/care/ReportOpenPage.tsx create mode 100644 frontend/src/pages/care/ReportOutPage.tsx create mode 100644 frontend/src/pages/care/ReportSalePage.tsx create mode 100644 frontend/src/pages/care/SettingAgencyPage.tsx create mode 100644 frontend/src/pages/care/SettingLevelPage.tsx create mode 100644 frontend/src/pages/care/SettingMemberPage.tsx create mode 100644 frontend/src/pages/care/SettingPartner1Page.tsx create mode 100644 frontend/src/pages/care/SettingPartner2Page.tsx create mode 100644 frontend/src/pages/care/SettingPreferencePage.tsx create mode 100644 frontend/src/pages/care/SitemapPage.tsx create mode 100644 frontend/src/pages/care/SmsAddressPage.tsx create mode 100644 frontend/src/pages/care/SmsHistoryPage.tsx create mode 100644 frontend/src/pages/care/StaffManagePage.tsx create mode 100644 frontend/src/pages/care/homesShared.tsx create mode 100644 frontend/src/styles/app.css create mode 100644 frontend/src/tableAlign.ts create mode 100644 frontend/src/theme.ts create mode 100644 frontend/src/themeContext.tsx create mode 100644 frontend/src/types/index.ts create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts create mode 100644 logo_image.png create mode 100644 logo_image_theme.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a79a1cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Dependencies & build +**/node_modules/ +**/dist/ +**/dist-ssr/ +**/target/ +**/.gradle/ + +# Environment (secrets) +.env +.env.local +.env.*.local + +# IDE / OS +.idea/ +.vscode/ +.cursor/ +**/.DS_Store +**/__MACOSX/ + +# Logs & cache +*.log +.npm/ +.cache/ + +# Uploads / local storage +**/storage/uploads/ +backend/storage/ + +# Docker local volumes / dumps +*.tar.gz +*.sql.gz diff --git a/README.md b/README.md new file mode 100644 index 0000000..4ec34f5 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# 빌링 케어 (비즈케어 홈즈) + +[비즈케어 홈즈](https://h.bizcare.co.kr/) 홈상품(인터넷/홈렌탈) 운용관리 프로그램을 +billAgent와 동일한 레이아웃·테마 구조로 재구현한 **독립 프로젝트**입니다. + +## 실행 + +```bash +cd /Users/macbook/dev/billCare +docker compose down +docker compose up --build -d +``` + +- UI: http://localhost:82 +- API: http://localhost:8082 +- MySQL: localhost:3308 (`billcare`) + +데모 계정: `demo` / `demo123` + +## 구성 + +| 구분 | 내용 | +|------|------| +| 브랜드 | 빌링 케어 / Bizcare Homes | +| 레이아웃/테마 | `AgentShell` + light/dark/blue | +| 메뉴 | 상품·계약·정산·약속·예약·고객·장부·통계·설정·CS·게시판 | +| API | Spring Boot JWT (`/api/agent/homes/**`) | +| 컨테이너 | `billcare-frontend` / `billcare-backend` / `billcare-mysql` | + +billAgent와 동시에 실행할 수 있도록 포트·컨테이너·DB명을 분리했습니다. diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..b3b41ae --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,6 @@ +target/ +.mvn/wrapper/maven-wrapper.jar +.git +.idea +*.iml +.DS_Store diff --git a/backend/.gitattributes b/backend/.gitattributes new file mode 100644 index 0000000..3b41682 --- /dev/null +++ b/backend/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/backend/.mvn/wrapper/maven-wrapper.properties b/backend/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..216df05 --- /dev/null +++ b/backend/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..76f1dcc --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,17 @@ +# syntax=docker/dockerfile:1 + +FROM maven:3.9.9-eclipse-temurin-21 AS build +WORKDIR /app +COPY pom.xml . +COPY src ./src +RUN mvn -q -DskipTests package + +FROM eclipse-temurin:21-jre +WORKDIR /app +RUN apt-get update && apt-get install -y --no-install-recommends curl \ + && rm -rf /var/lib/apt/lists/* +COPY --from=build /app/target/*.jar app.jar +EXPOSE 8080 +HEALTHCHECK --interval=5s --timeout=3s --start-period=25s --retries=20 \ + CMD curl -fsS http://127.0.0.1:8080/api/health || exit 1 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/backend/mvnw b/backend/mvnw new file mode 100755 index 0000000..bd8896b --- /dev/null +++ b/backend/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/backend/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100644 index 0000000..fbac453 --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,142 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.0 + + + com.bizcare + agent + 0.0.1-SNAPSHOT + agent + + + + + + + + + + + + + + + + 21 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + + + + com.mysql + mysql-connector-j + runtime + + + org.projectlombok + lombok + true + + + io.jsonwebtoken + jjwt-api + 0.12.6 + + + io.jsonwebtoken + jjwt-impl + 0.12.6 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.12.6 + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + default-compile + compile + + compile + + + + + org.projectlombok + lombok + + + + + + default-testCompile + test-compile + + testCompile + + + + + org.projectlombok + lombok + + + + + + + + + + diff --git a/backend/src/main/java/com/bizcare/agent/AgentApplication.java b/backend/src/main/java/com/bizcare/agent/AgentApplication.java new file mode 100644 index 0000000..ca8b947 --- /dev/null +++ b/backend/src/main/java/com/bizcare/agent/AgentApplication.java @@ -0,0 +1,13 @@ +package com.bizcare.agent; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AgentApplication { + + public static void main(String[] args) { + SpringApplication.run(AgentApplication.class, args); + } + +} diff --git a/backend/src/main/java/com/bizcare/agent/ApiControllers.java b/backend/src/main/java/com/bizcare/agent/ApiControllers.java new file mode 100644 index 0000000..01cf7bc --- /dev/null +++ b/backend/src/main/java/com/bizcare/agent/ApiControllers.java @@ -0,0 +1,4738 @@ +package com.bizcare.agent; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.*; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import lombok.*; +import org.springframework.http.*; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; +import jakarta.servlet.http.HttpServletRequest; +import java.lang.reflect.Field; +import java.math.BigDecimal; +import java.time.*; +import java.util.*; + +record ApiResponse(boolean result, String msg, Object data) { static ApiResponse ok(Object d){return new ApiResponse(true,"성공",d);} static ApiResponse fail(String m){return new ApiResponse(false,m,null);} } +record PageResponse(long total, List list) {} +record LoginRequest(@NotBlank(message="아이디를 입력하세요") String userid, @NotBlank(message="비밀번호를 입력하세요") String password) {} + +@RestController @RequestMapping("/api/auth") @RequiredArgsConstructor +class AuthController { + private final EntityManager em; private final PasswordEncoder encoder; private final JwtService jwt; + @PostMapping("/login") @Transactional ApiResponse login(@Valid @RequestBody LoginRequest request) { + List users=em.createQuery("select m from Member m where m.userid=:id",Member.class).setParameter("id",request.userid()).getResultList(); + if(users.isEmpty() || !users.getFirst().isActive() || !encoder.matches(request.password(),users.getFirst().getPassword())) return ApiResponse.fail("아이디 또는 비밀번호가 올바르지 않습니다."); + Member m=users.getFirst(); + m.setLoginCount((m.getLoginCount()==null?0:m.getLoginCount())+1); + m.setLastLoginAt(LocalDateTime.now()); + Map data=new LinkedHashMap<>(); + data.put("token",jwt.create(m)); + data.put("role",m.getRole()); + data.put("userid",m.getUserid()); + data.put("name",m.getName()); + data.put("groupId",m.getGroupId()); + data.put("companyId",m.getCompanyId()); + data.put("menuFlags",m.getMenuFlags()); + String perm = m.getStaffPermission(); + if (perm == null || perm.isBlank()) { + perm = "demo".equals(m.getUserid()) ? "대표" : "사원"; + } + data.put("staffPermission", perm); + return ApiResponse.ok(data); + } + @GetMapping("/me") ApiResponse me(@AuthenticationPrincipal JwtUser u) { + List users = em.createQuery("select m from Member m where m.userid=:id", Member.class) + .setParameter("id", u.getUserid()).getResultList(); + Map data=new LinkedHashMap<>(); + data.put("userid",u.getUserid()); + data.put("role",u.getRole()); + data.put("groupId",u.getGroupId()); + data.put("companyId",u.getCompanyId()); + if (!users.isEmpty()) { + Member m = users.getFirst(); + data.put("name", m.getName()); + String perm = m.getStaffPermission(); + if (perm == null || perm.isBlank()) { + perm = "demo".equals(m.getUserid()) ? "대표" : "사원"; + } + data.put("staffPermission", perm); + data.put("menuFlags", m.getMenuFlags()); + } + return ApiResponse.ok(data); + } +} + +@Service @RequiredArgsConstructor +class CrudService { + private final EntityManager em; private final ObjectMapper mapper; + @Transactional T create(Class type, Map data, JwtUser user) { + T value=mapper.convertValue(data,type); value.setId(null); value.setGroupId(user.getGroupId()); em.persist(value); return value; + } + @Transactional T update(Class type, Long id, Map data, JwtUser user) { + T value=get(type,id,user); + try { mapper.readerForUpdating(value).readValue(mapper.writeValueAsBytes(data)); } + catch (Exception e) { throw new IllegalArgumentException("입력 형식이 올바르지 않습니다."); } + value.setId(id); value.setGroupId(user.getGroupId()); return value; + } + @Transactional void delete(Class type, Long id, JwtUser user) { em.remove(get(type,id,user)); } + T get(Class type, Long id, JwtUser user) { + T v=em.find(type,id); if(v==null || !Objects.equals(v.getGroupId(),user.getGroupId())) throw new NoSuchElementException("데이터를 찾을 수 없습니다."); return v; + } + PageResponse list(Class type, JwtUser user, Map filters, int page, int size) { + String entity=type.getSimpleName(); StringBuilder where=new StringBuilder(" where e.groupId=:group"); Map params=new HashMap<>(); params.put("group",user.getGroupId()); + Set ignore=Set.of("page","size","keyword","startDate","endDate","q"); + for(var e:filters.entrySet()) if(e.getValue()!=null && !e.getValue().isBlank() && !ignore.contains(e.getKey()) && field(type,e.getKey())) { where.append(" and e.").append(e.getKey()).append("=:").append(e.getKey()); params.put(e.getKey(), convert(e.getValue(), type,e.getKey())); } + TypedQuery q=em.createQuery("select e from "+entity+" e"+where+" order by e.id desc",type); TypedQuery c=em.createQuery("select count(e) from "+entity+" e"+where,Long.class); + params.forEach((k,v)->{q.setParameter(k,v);c.setParameter(k,v);}); long total=c.getSingleResult(); return new PageResponse(total,q.setFirstResult(Math.max(0,page)*size).setMaxResults(Math.min(Math.max(size,1),200)).getResultList()); + } + private boolean field(Class t,String name){ try { while(t!=null){for(Field f:t.getDeclaredFields())if(f.getName().equals(name))return true;t=t.getSuperclass();} return false;}catch(Exception e){return false;} } + private Object convert(String v,Class t,String f){try{Field x=null;for(Class c=t;c!=null;c=c.getSuperclass())try{x=c.getDeclaredField(f);break;}catch(NoSuchFieldException ignored){} if(x!=null&&x.getType()==Long.class)return Long.valueOf(v); if(x!=null&&x.getType()==LocalDate.class)return LocalDate.parse(v);}catch(Exception ignored){}return v;} +} + +@RestController @RequestMapping("/api/agent") @RequiredArgsConstructor +class AgentController { + private final CrudService crud; private final EntityManager em; private final PasswordEncoder encoder; + private ApiResponse list(Class c, JwtUser u, Map f,int p,int s){return ApiResponse.ok(crud.list(c,u,f,p,s));} + private ApiResponse post(Class c,Mapd,JwtUser u){return ApiResponse.ok(crud.create(c,d,u));} + private ApiResponse get(Class c,Long id,JwtUser u){return ApiResponse.ok(crud.get(c,id,u));} + private ApiResponse put(Class c,Long id,Mapd,JwtUser u){return ApiResponse.ok(crud.update(c,id,d,u));} + private ApiResponse del(Class c,Long id,JwtUser u){crud.delete(c,id,u);return ApiResponse.ok(null);} + @GetMapping("/stocks") ApiResponse stocks(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="30")int size){return list(Stock.class,u,f,page,size);} + @PostMapping({"/stocks","/stocks/in"}) @Transactional ApiResponse stockIn(@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ + d.putIfAbsent("state","IN"); + Stock s=crud.create(Stock.class,d,u); + if(s.getIndate()==null) s.setIndate(LocalDate.now()); + if(s.getProcessDate()==null) s.setProcessDate(LocalDate.now()); + s.setProcessor(memberName(u)); + s.setUserid(u.getUserid()); + s.setProcid(u.getUserid()); + if(s.getUid()==null||s.getUid().isBlank()) s.setUid("stk-"+UUID.randomUUID()); + writeStockHistory(s,u); + return ApiResponse.ok(stockDetail(s,u)); + } + @GetMapping("/stocks/{id}") ApiResponse stock(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ + Stock s=em.find(Stock.class,id); + if(s==null||!Objects.equals(s.getGroupId(),u.getGroupId())) return ApiResponse.fail("재고를 찾을 수 없습니다."); + return ApiResponse.ok(stockDetail(s,u)); + } + @GetMapping("/stocks/{id}/detail") ApiResponse stockDetailApi(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ + return stock(id,u); + } + @PutMapping("/stocks/{id}") @Transactional ApiResponse stockPut(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ + Stock s=em.find(Stock.class,id); + if(s==null||!Objects.equals(s.getGroupId(),u.getGroupId())) return ApiResponse.fail("재고를 찾을 수 없습니다."); + put(Stock.class,id,d,u); + s=em.find(Stock.class,id); + if(s!=null){ + if(d.get("processor")==null || String.valueOf(d.get("processor")).isBlank()) s.setProcessor(memberName(u)); + writeStockHistory(s,u); + } + return ApiResponse.ok(stockDetail(s,u)); + } + @DeleteMapping("/stocks/{id}") @Transactional ApiResponse stockDel(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ + Stock s=em.find(Stock.class,id); + if(s==null||!Objects.equals(s.getGroupId(),u.getGroupId())) return ApiResponse.fail("재고를 찾을 수 없습니다."); + em.createQuery("delete from StockHistory h where h.groupId=:g and h.stockId=:id").setParameter("g",u.getGroupId()).setParameter("id",id).executeUpdate(); + em.createQuery("delete from StockNote n where n.groupId=:g and n.stockId=:id").setParameter("g",u.getGroupId()).setParameter("id",id).executeUpdate(); + em.remove(s); + return ApiResponse.ok(null); + } + @PostMapping({"/stocks/out","/stocks/move","/stocks/return"}) @Transactional ApiResponse stockState(@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ + Long id=Long.valueOf(d.get("id").toString()); + return stockAction(id,d,u); + } + @PostMapping("/stocks/{id}/action") @Transactional ApiResponse stockActionApi(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ + return stockAction(id,d,u); + } + @PostMapping("/stocks/{id}/notes") @Transactional ApiResponse stockNoteCreate(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ + Stock s=em.find(Stock.class,id); + if(s==null||!Objects.equals(s.getGroupId(),u.getGroupId())) return ApiResponse.fail("재고를 찾을 수 없습니다."); + String contents=d.get("contents")==null?"":String.valueOf(d.get("contents")).trim(); + if(contents.isBlank()) return ApiResponse.fail("메모 내용을 입력하세요."); + StockNote n=new StockNote(); + n.setGroupId(u.getGroupId()); + n.setStockId(id); + n.setContents(contents); + n.setAuthorName(d.get("authorName")==null||String.valueOf(d.get("authorName")).isBlank()?memberName(u):String.valueOf(d.get("authorName"))); + n.setNoteDate(d.get("noteDate")==null||String.valueOf(d.get("noteDate")).isBlank()?LocalDate.now():LocalDate.parse(String.valueOf(d.get("noteDate")))); + em.persist(n); + return ApiResponse.ok(stockNoteMap(n)); + } + @DeleteMapping("/stocks/{id}/notes/{noteId}") @Transactional ApiResponse stockNoteDelete(@PathVariable Long id,@PathVariable Long noteId,@AuthenticationPrincipal JwtUser u){ + StockNote n=em.find(StockNote.class,noteId); + if(n==null||!Objects.equals(n.getGroupId(),u.getGroupId())||!Objects.equals(n.getStockId(),id)) return ApiResponse.fail("메모를 찾을 수 없습니다."); + em.remove(n); + return ApiResponse.ok(null); + } + private ApiResponse stockAction(Long id, Map d, JwtUser u){ + Stock s=em.find(Stock.class,id); + if(s==null||!Objects.equals(s.getGroupId(),u.getGroupId())) return ApiResponse.fail("재고를 찾을 수 없습니다."); + String state=String.valueOf(d.getOrDefault("state", d.containsKey("state")?d.get("state"):"OUT")); + if(!List.of("IN","OUT","LOSS","OPEN","SELL","RECOVERY","RETURN").contains(state)) return ApiResponse.fail("처리할 상태를 확인하세요."); + if("OUT".equals(state)){ + if(d.get("outcompanyId")!=null && !String.valueOf(d.get("outcompanyId")).isBlank()){ + s.setOutcompanyId(Long.valueOf(d.get("outcompanyId").toString())); + } + if(d.get("outCategory")!=null) s.setOutCategory(String.valueOf(d.get("outCategory"))); + if(s.getOutcompanyId()==null) return ApiResponse.fail("출고처를 선택하세요."); + s.setOutdate(LocalDate.now()); + s.setOutcode("21"); + } + if("IN".equals(state) || "RECOVERY".equals(state)){ + s.setRecalldate(LocalDate.now()); + s.setOutcode("10"); + if("IN".equals(state)) { /* 회수 후 입고 상태 */ } + } + if("LOSS".equals(state)) s.setLossdate(LocalDate.now()); + if("SELL".equals(state)) s.setSelldate(LocalDate.now()); + s.setState(state); + s.setProcessDate(LocalDate.now()); + s.setProcessor(memberName(u)); + if(d.get("memo")!=null) s.setMemo(String.valueOf(d.get("memo"))); + writeStockHistory(s,u); + return ApiResponse.ok(stockDetail(s,u)); + } + private Map stockDetail(Stock s, JwtUser u){ + Map companyMap=new HashMap<>(); + for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companyMap.put(c.getId(),c); + Map names=new HashMap<>(); + companyMap.forEach((k,v)->names.put(k,v.getName())); + Map stock=stockMap(s,names); + stock.put("pay", s.getPay()); + stock.put("telcompany", s.getTelcompany()); + String telecomLabel=s.getTelcompany(); + if(telecomLabel==null||telecomLabel.isBlank()) telecomLabel=s.getTelecom(); + else if(s.getTelecom()!=null && !telecomLabel.contains(s.getTelecom())) telecomLabel=telecomLabel+" ("+s.getTelecom()+")"; + stock.put("telecomLabel", telecomLabel); + List histories=em.createQuery("select h from StockHistory h where h.groupId=:g and h.stockId=:id order by h.processDate desc, h.id desc",StockHistory.class) + .setParameter("g",u.getGroupId()).setParameter("id",s.getId()).getResultList(); + List> historyList=histories.stream().map(h->{ + Map m=new LinkedHashMap<>(); + m.put("id", h.getId()); + m.put("processDate", h.getProcessDate()); + m.put("statusLabel", h.getStatusLabel()!=null?h.getStatusLabel():statusLabel(h.getState())); + String label=h.getCompanyLabel(); + if(label==null||label.isBlank()){ + if("OUT".equals(h.getState())||"출고".equals(h.getStatusLabel())){ + Company oc=companyMap.get(h.getOutcompanyId()); + if(oc!=null){ + label=oc.getName(); + if(oc.getCategory()!=null && !oc.getCategory().isBlank()) label=label+" ("+oc.getCategory()+")"; + else if(s.getOutCategory()!=null && !s.getOutCategory().isBlank()) label=label+" ("+s.getOutCategory()+")"; + } + } + } + // 입고 이력은 원본처럼 거래처 칸을 비움 + if("IN".equals(h.getState())||"입고".equals(h.getStatusLabel())) label=""; + m.put("companyLabel", label==null?"":label); + m.put("processor", resolveProcessorName(u.getGroupId(), h.getProcessor())); + return m; + }).toList(); + List> notes=em.createQuery("select n from StockNote n where n.groupId=:g and n.stockId=:id order by n.noteDate desc, n.id desc",StockNote.class) + .setParameter("g",u.getGroupId()).setParameter("id",s.getId()).getResultList().stream().map(this::stockNoteMap).toList(); + Map r=new LinkedHashMap<>(); + r.put("stock", stock); + r.put("histories", historyList); + r.put("notes", notes); + return r; + } + private Map stockNoteMap(StockNote n){ + Map m=new LinkedHashMap<>(); + m.put("id", n.getId()); m.put("stockId", n.getStockId()); m.put("contents", n.getContents()); + m.put("authorName", n.getAuthorName()); m.put("noteDate", n.getNoteDate()); + return m; + } + private String memberName(JwtUser u){ + List rows=em.createQuery("select m from Member m where m.groupId=:g and m.userid=:uid",Member.class) + .setParameter("g",u.getGroupId()).setParameter("uid",u.getUserid()).getResultList(); + if(!rows.isEmpty() && rows.getFirst().getName()!=null && !rows.getFirst().getName().isBlank()) return rows.getFirst().getName(); + return u.getUserid(); + } + private String resolveProcessorName(String groupId, String processor){ + if(processor==null||processor.isBlank()) return "-"; + List rows=em.createQuery("select m from Member m where m.groupId=:g and (m.userid=:p or m.name=:p)",Member.class) + .setParameter("g",groupId).setParameter("p",processor).getResultList(); + if(!rows.isEmpty() && rows.getFirst().getName()!=null && !rows.getFirst().getName().isBlank()) return rows.getFirst().getName(); + return processor; + } + @GetMapping("/stocks/barcode/{code}") ApiResponse barcode(@PathVariable String code,@AuthenticationPrincipal JwtUser u){return list(Stock.class,u,Map.of("barcode",code),0,10);} + @GetMapping("/stocks/search") ApiResponse stockSearch(@AuthenticationPrincipal JwtUser u, @RequestParam Map f, + @RequestParam(defaultValue="0") int page, @RequestParam(defaultValue="15") int size) { + return ApiResponse.ok(stockSearchData(u, f, page, size)); + } + @GetMapping("/stocks/sheet") ApiResponse stockSheet(@AuthenticationPrincipal JwtUser u, @RequestParam Map f, + @RequestParam(defaultValue="0") int page, @RequestParam(defaultValue="15") int size) { + if(blank(f.get("dateFrom")) || blank(f.get("dateTo"))) return ApiResponse.fail("날짜를 선택하세요."); + Map filters=new HashMap<>(f); + filters.put("dateBasis","처리일기준"); + filters.putIfAbsent("tab","ALL"); + return ApiResponse.ok(stockSearchData(u, filters, page, size)); + } + @GetMapping(value="/stocks/sheet/export", produces="text/csv;charset=UTF-8") ResponseEntity stockSheetExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + if(blank(f.get("dateFrom")) || blank(f.get("dateTo"))) return ResponseEntity.badRequest().body("날짜를 선택하세요."); + Map filters=new HashMap<>(f); + filters.put("dateBasis","처리일기준"); + filters.putIfAbsent("tab","ALL"); + @SuppressWarnings("unchecked") List> rows=(List>) stockSearchData(u,filters,0,10000).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF처리일,현황,상태,종류,일련번호,모델명,색상,입고가,입고처,출고처,처리자,비고\n"); + for(Map r:rows) csv.append(csv(r.get("processDate"))).append(',').append(csv(r.get("statusLabel"))).append(',') + .append(csv(r.get("cond"))).append(',').append(csv(r.get("gubun"))).append(',').append(csv(r.get("serial"))).append(',') + .append(csv(r.get("model"))).append(',').append(csv(r.get("color"))).append(',').append(csv(r.get("inprice"))).append(',') + .append(csv(r.get("incompanyName"))).append(',').append(csv(r.get("outcompanyName"))).append(',') + .append(csv(r.get("processor"))).append(',').append(csv(r.get("memo"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=sheet.csv").body(csv.toString()); + } + @GetMapping(value="/stocks/export", produces="text/csv;charset=UTF-8") ResponseEntity stockExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + @SuppressWarnings("unchecked") List> rows=(List>) stockSearchData(u,f,0,10000).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF현황,입고일,상태,종류,일련번호,모델명,색상,입고가,입고처,출고처,처리일,처리자,비고\n"); + for(Map r:rows) csv.append(csv(r.get("statusLabel"))).append(',').append(csv(r.get("indate"))).append(',') + .append(csv(r.get("cond"))).append(',').append(csv(r.get("gubun"))).append(',').append(csv(r.get("serial"))).append(',') + .append(csv(r.get("model"))).append(',').append(csv(r.get("color"))).append(',').append(csv(r.get("inprice"))).append(',') + .append(csv(r.get("incompanyName"))).append(',').append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("processDate"))).append(',') + .append(csv(r.get("processor"))).append(',').append(csv(r.get("memo"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=stocks.csv").body(csv.toString()); + } + @PostMapping("/stocks/bulk-action") @Transactional ApiResponse stockBulkAction(@AuthenticationPrincipal JwtUser u,@RequestBody Map d) { + Object raw=d.get("ids"); String state=String.valueOf(d.getOrDefault("state","")); + if(!(raw instanceof List ids) || ids.isEmpty()) return ApiResponse.fail("선택된 재고가 없습니다."); + if(!List.of("IN","OUT","LOSS","OPEN","SELL","RECOVERY","RETURN").contains(state)) return ApiResponse.fail("처리할 상태를 확인하세요."); + int updated=0; + for(Object idObj:ids) { + Long id=Long.valueOf(idObj.toString()); + Stock s=em.find(Stock.class,id); + if(s==null || !Objects.equals(s.getGroupId(),u.getGroupId())) continue; + s.setState(state); + s.setProcessDate(LocalDate.now()); + s.setProcessor(u.getUserid()); + if("IN".equals(state)) { s.setRecalldate(LocalDate.now()); s.setOutcode("10"); } + if("OUT".equals(state)) { s.setOutdate(LocalDate.now()); s.setOutcode("21"); } + if("LOSS".equals(state)) s.setLossdate(LocalDate.now()); + if("SELL".equals(state)) s.setSelldate(LocalDate.now()); + writeStockHistory(s, u); + updated++; + } + return ApiResponse.ok(Map.of("updated",updated)); + } + @PostMapping("/stocks/bulk-in") @Transactional ApiResponse stockBulkIn(@AuthenticationPrincipal JwtUser u,@RequestBody Map d) { + Object raw=d.get("serials"); if(!(raw instanceof List serials) || serials.isEmpty()) return ApiResponse.fail("일련번호를 입력하세요."); + int made=0; + for(Object value:serials) { String serial=String.valueOf(value).trim(); if(serial.isBlank()) continue; + Stock s=new Stock(); s.setGroupId(u.getGroupId()); s.setGubun(String.valueOf(d.getOrDefault("gubun","휴대폰"))); + s.setTelecom(String.valueOf(d.getOrDefault("telecom","SKT"))); s.setTelcompany(s.getTelecom()); s.setModel(String.valueOf(d.getOrDefault("model",""))); + s.setColor(String.valueOf(d.getOrDefault("color",""))); s.setInprice(d.get("inprice")==null?BigDecimal.ZERO:new BigDecimal(d.get("inprice").toString())); + if(d.get("incompanyId")!=null && !d.get("incompanyId").toString().isBlank()) s.setIncompanyId(Long.valueOf(d.get("incompanyId").toString())); + s.setSerial(serial); s.setBarcode(serial); s.setCond("정상"); s.setState("IN"); s.setIndate(LocalDate.now()); s.setProcessDate(LocalDate.now()); + s.setProcessor(u.getUserid()); s.setUserid(u.getUserid()); s.setProcid(u.getUserid()); s.setUid("stk-"+UUID.randomUUID()); em.persist(s); + writeStockHistory(s, u); made++; + } return ApiResponse.ok(Map.of("created",made)); + } + private Map stockSearchData(JwtUser u,Map f,int page,int size) { + List stocks=em.createQuery("select s from Stock s where s.groupId=:g",Stock.class).setParameter("g",u.getGroupId()).getResultList(); + Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); + List filtered=stocks.stream().filter(s->matchesStock(s,f,companies)).sorted(stockComparator(f.get("sort"))).toList(); + Map counts=new LinkedHashMap<>(); for(String tab:List.of("ALL","INOUT","IN","OUT","RETURN","LOSS","OPEN","SELL")) counts.put(tab,stocks.stream().filter(s->matchesStock(s,withoutTab(f),companies)&&inTab(s,tab)).count()); + int from=Math.min(Math.max(page,0)*Math.max(size,1),filtered.size()), to=Math.min(from+Math.min(Math.max(size,1),200),filtered.size()); + List> list=filtered.subList(from,to).stream().map(s->stockMap(s,companies)).toList(); + return Map.of("total",filtered.size(),"list",list,"counts",counts); + } + private Map withoutTab(Map f){Map copy=new HashMap<>(f);copy.remove("tab");return copy;} + private boolean matchesStock(Stock s,Map f,Map companies) { + String tab=f.getOrDefault("tab","ALL"); if(!inTab(s,tab)) return false; + if(!eq(s.getCond(),f.get("cond"))||!eq(s.getGubun(),f.get("gubun"))||!eq(s.getTelecom(),f.get("telecom"))||!eq(s.getOutCategory(),f.get("outCategory"))||!eq(s.getSalesperson(),f.get("salesperson"))||!eq(s.getSource(),f.get("source"))) return false; + if(!eqId(s.getIncompanyId(),f.get("incompanyId"))) return false; + if(!like(s.getModel(),f.get("model"))||!like(s.getColor(),f.get("color"))||!like(s.getPetname(),f.get("petname"))||!like(companies.get(s.getOutcompanyId()),f.get("outcompanyName"))) return false; + String serial=f.get("serial"); if(serial!=null&&!serial.isBlank()) {String[] values=("true".equals(f.get("serialMulti"))?serial.split("[,\\n\\r]+"):new String[]{serial});if(Arrays.stream(values).map(String::trim).noneMatch(v->like(s.getSerial(),v)))return false;} + String searchType=f.getOrDefault("searchType","일련번호"); String keyword=f.get("keyword"); + if(keyword!=null&&!keyword.isBlank()) { + boolean ok=switch(searchType){ + case "모델명"->like(s.getModel(),keyword); + case "색상"->like(s.getColor(),keyword); + case "펫네임"->like(s.getPetname(),keyword); + default->like(s.getSerial(),keyword); + }; + if(!ok) return false; + } + LocalDate date=switch(f.getOrDefault("dateBasis","입고일기준")){case "출고일기준"->s.getOutdate();case "처리일기준"->s.getProcessDate();default->s.getIndate();}; + if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(date==null||date.isBefore(LocalDate.parse(f.get("dateFrom")))))return false; + return f.get("dateTo")==null||f.get("dateTo").isBlank()||(date!=null&&!date.isAfter(LocalDate.parse(f.get("dateTo")))); + } + private boolean inTab(Stock s,String tab){return switch(tab){case "INOUT"->"IN".equals(s.getState())||"OUT".equals(s.getState());case "ALL"->true;default->tab.equals(s.getState());};} + private boolean blank(String v){return v==null||v.isBlank();} + private boolean eq(String actual,String expected){return expected==null||expected.isBlank()||Objects.equals(actual,expected);} + private boolean eqId(Long actual,String expected){return expected==null||expected.isBlank()||Objects.equals(actual,Long.valueOf(expected));} + private boolean like(String actual,String expected){return expected==null||expected.isBlank()||(actual!=null&&actual.toLowerCase().contains(expected.toLowerCase()));} + private Comparator stockComparator(String sort){String field=sort==null?"indate,desc":sort;boolean asc=field.endsWith(",asc");Comparator c=switch(field.split(",")[0]){case "serial"->Comparator.comparing(Stock::getSerial,Comparator.nullsLast(String::compareTo));case "model"->Comparator.comparing(Stock::getModel,Comparator.nullsLast(String::compareTo));case "inprice"->Comparator.comparing(Stock::getInprice,Comparator.nullsLast(BigDecimal::compareTo));case "processDate"->Comparator.comparing(Stock::getProcessDate,Comparator.nullsLast(LocalDate::compareTo));default->Comparator.comparing(Stock::getIndate,Comparator.nullsLast(LocalDate::compareTo));};return (asc?c:c.reversed()).thenComparing(Stock::getId); } + private Map stockMap(Stock s,Map companies){Map r=new LinkedHashMap<>();r.put("id",s.getId());r.put("gubun",s.getGubun());r.put("telecom",s.getTelecom());r.put("telcompany",s.getTelcompany());r.put("serial",s.getSerial());r.put("model",s.getModel());r.put("color",s.getColor());r.put("cond",s.getCond());r.put("pay",s.getPay());r.put("memo",s.getMemo());r.put("state",s.getState());r.put("inprice",s.getInprice());r.put("indate",s.getIndate());r.put("outdate",s.getOutdate());r.put("processDate",s.getProcessDate());r.put("processor",s.getProcessor());r.put("petname",s.getPetname());r.put("outCategory",s.getOutCategory());r.put("salesperson",s.getSalesperson());r.put("source",s.getSource());r.put("incompanyId",s.getIncompanyId());r.put("outcompanyId",s.getOutcompanyId());r.put("incompanyName",companies.getOrDefault(s.getIncompanyId(),"-"));r.put("outcompanyName",companies.getOrDefault(s.getOutcompanyId(),"-"));r.put("statusLabel",statusLabel(s.getState()));r.put("elapsedDays",s.getIndate()==null?null:Math.max(0,java.time.temporal.ChronoUnit.DAYS.between(s.getIndate(),LocalDate.now())));return r;} + private String statusLabel(String state){return Map.of("IN","입고","OUT","출고","RETURN","반품","LOSS","분실","OPEN","개통","SELL","판매","RECOVERY","회수대기").getOrDefault(state,state==null?"-":state);} + private String csv(Object value){String s=String.valueOf(value==null?"":value);return "\""+s.replace("\"","\"\"")+"\"";} + @GetMapping("/opens") ApiResponse opens(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="30")int size){return list(OpenRecord.class,u,f,page,size);} + @GetMapping("/opens/search") ApiResponse openSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){return ApiResponse.ok(openSearchData(u,f,page,size));} + @GetMapping(value="/opens/export", produces="text/csv;charset=UTF-8") ResponseEntity openExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f) { + @SuppressWarnings("unchecked") List> rows=(List>) openSearchData(u,f,0,10000).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF개통일,통신사,출고처,고객명,구분,종류,개통번호,상태,모델명,일련번호,입고처,정산,차감,마진\n"); + for(Map r:rows) csv.append(csv(r.get("opendate"))).append(',').append(csv(r.get("telecom"))).append(',').append(csv(r.get("outcompanyName"))).append(',') + .append(csv(r.get("name"))).append(',').append(csv(r.get("gubun"))).append(',').append(csv(r.get("openhow"))).append(',').append(csv(r.get("openphone"))).append(',') + .append(csv(r.get("openStatus"))).append(',').append(csv(r.get("pmodel"))).append(',').append(csv(r.get("pserial"))).append(',').append(csv(r.get("incompanyName"))).append(',') + .append(csv(r.get("sellprice"))).append(',').append(csv(r.get("declprice1"))).append(',').append(csv(r.get("dealermargin1"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=opens.csv").body(csv.toString()); + } + @PostMapping("/opens") @Transactional ApiResponse openPost(@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ + d.putIfAbsent("state","OPEN"); d.putIfAbsent("openStatus","정상"); d.putIfAbsent("userid",u.getUserid()); + if(d.get("uid")==null||String.valueOf(d.get("uid")).isBlank()) d.put("uid","open-"+UUID.randomUUID()); + ApiResponse created=post(OpenRecord.class,d,u); + linkOpenStock(d); return created; + } + @GetMapping({"/opens/pending","/opens/miss"}) ApiResponse pending(@AuthenticationPrincipal JwtUser u){return list(OpenRecord.class,u,Map.of("state","PENDING"),0,100);} + @GetMapping("/opens/serial-lookup") ApiResponse openSerialLookup(@AuthenticationPrincipal JwtUser u,@RequestParam String serial,@RequestParam(defaultValue="휴대폰") String gubun){ + if(serial==null||serial.trim().length()<3) return ApiResponse.ok(List.of()); + Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); + List stocks=em.createQuery("select s from Stock s where s.groupId=:g and s.serial like :q",Stock.class).setParameter("g",u.getGroupId()).setParameter("q","%"+serial.trim()+"%").setMaxResults(20).getResultList(); + return ApiResponse.ok(stocks.stream().filter(s->blank(gubun)||("유심".equals(gubun)? "유심".equals(s.getGubun()):!"유심".equals(s.getGubun()))).map(s->stockMap(s,companies)).toList()); + } + @GetMapping("/opens/adjust") ApiResponse openAdjust(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + if(blank(f.get("dateFrom"))||blank(f.get("dateTo"))) return ApiResponse.fail("개통일을 선택하세요."); + LocalDate from=LocalDate.parse(f.get("dateFrom")), to=LocalDate.parse(f.get("dateTo")); + if(to.isBefore(from)) return ApiResponse.fail("날짜 범위가 올바르지 않습니다."); + if(java.time.temporal.ChronoUnit.DAYS.between(from,to)>30) return ApiResponse.fail("최대 31일까지 조회할 수 있습니다."); + return ApiResponse.ok(openAdjustData(u,f)); + } + @PostMapping("/opens/adjust/batch") @Transactional ApiResponse openAdjustBatch(@AuthenticationPrincipal JwtUser u,@RequestBody Map body){ + Object raw=body.get("items"); if(!(raw instanceof List items)||items.isEmpty()) return ApiResponse.fail("변경할 항목이 없습니다."); + int updated=0; + for(Object item:items){ + if(!(item instanceof Map m)) continue; + Object idObj=m.get("id"); if(idObj==null) continue; + OpenRecord o=em.find(OpenRecord.class,Long.valueOf(String.valueOf(idObj))); + if(o==null||!Objects.equals(o.getGroupId(),u.getGroupId())) continue; + if("유심".equals(o.getGubun())) continue; + if(m.containsKey("basicprice1")) o.setBasicprice1(toDecimal(m.get("basicprice1"))); + if(m.containsKey("basicprice2")) o.setBasicprice2(toDecimal(m.get("basicprice2"))); + if(m.containsKey("basicprice3")) o.setBasicprice3(toDecimal(m.get("basicprice3"))); + updated++; + } + return ApiResponse.ok(Map.of("updated",updated)); + } + @GetMapping(value="/opens/adjust/export", produces="text/csv;charset=UTF-8") ResponseEntity openAdjustExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + if(blank(f.get("dateFrom"))||blank(f.get("dateTo"))) return ResponseEntity.badRequest().body("개통일을 선택하세요."); + @SuppressWarnings("unchecked") List> rows=(List>) openAdjustData(u,f).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF개통일,입고처,출고처,고객명,종류,모델명,일련번호,지원방법,요금제,정산금액,기본정책,구두정책,추가정책\n"); + for(Map r:rows) csv.append(csv(r.get("opendate"))).append(',').append(csv(r.get("incompanyName"))).append(',').append(csv(r.get("outcompanyName"))).append(',') + .append(csv(r.get("name"))).append(',').append(csv(r.get("openhow"))).append(',').append(csv(r.get("pmodel"))).append(',').append(csv(r.get("pserial"))).append(',') + .append(csv(r.get("decltype"))).append(',').append(csv(r.get("plan"))).append(',').append(csv(r.get("sellprice"))).append(',') + .append(csv(r.get("basicprice1"))).append(',').append(csv(r.get("basicprice2"))).append(',').append(csv(r.get("basicprice3"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=adjust.csv").body(csv.toString()); + } + @GetMapping("/opens/{id}") ApiResponse open(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return get(OpenRecord.class,id,u);} + @PutMapping("/opens/{id}") ApiResponse openPut(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return put(OpenRecord.class,id,d,u);} + @DeleteMapping("/opens/{id}") ApiResponse openDel(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return del(OpenRecord.class,id,u);} + private void linkOpenStock(Map d){ + Object phoneId=d.get("phoneStockId"), usimId=d.get("usimStockId"); + if(phoneId!=null){Stock s=em.find(Stock.class,Long.valueOf(String.valueOf(phoneId))); if(s!=null){s.setState("OPEN");s.setOutdate(LocalDate.now());}} + if(usimId!=null){Stock s=em.find(Stock.class,Long.valueOf(String.valueOf(usimId))); if(s!=null){s.setState("OPEN");s.setOutdate(LocalDate.now());}} + } + private Map openSearchData(JwtUser u,Map f,int page,int size){ + List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g",OpenRecord.class).setParameter("g",u.getGroupId()).getResultList(); + Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); + List filtered=opens.stream().filter(o->matchesOpen(o,f,companies)).sorted(openComparator(f.get("sort"))).toList(); + Map counts=new LinkedHashMap<>(); for(String tab:List.of("ALL","신규","번호이동","보상","기변","메이징","가개통","선불개통")) counts.put(tab,opens.stream().filter(o->matchesOpen(o,withoutTab(f),companies)&&inOpenTab(o,tab)).count()); + int from=Math.min(Math.max(page,0)*Math.max(size,1),filtered.size()), to=Math.min(from+Math.min(Math.max(size,1),200),filtered.size()); + List> list=filtered.subList(from,to).stream().map(o->openMap(o,companies)).toList(); + return Map.of("total",filtered.size(),"list",list,"counts",counts); + } + private boolean matchesOpen(OpenRecord o,Map f,Map companies){ + String tab=f.getOrDefault("tab","ALL"); if(!inOpenTab(o,tab)) return false; + if(!eq(o.getGubun(),f.get("gubun"))||!eq(o.getTelecom(),f.get("telecom"))||!eq(o.getOpenhow(),f.get("openhow"))||!eq(o.getOpenStatus(),f.get("openStatus"))||!eq(o.getSalesperson(),f.get("salesperson"))||!eq(o.getEmployee(),f.get("employee"))||!eq(o.getOutCategory(),f.get("outCategory"))||!eq(o.getState(),f.get("history"))) return false; + if(!eqId(o.getIncompanyId(),f.get("incompanyId"))) return false; + if(!like(o.getPmodel(),f.get("model"))||!like(companies.get(o.getOutcompanyId()),f.get("outcompanyName"))||!like(o.getName(),f.get("name"))) return false; + String phoneTail=f.get("phoneTail"); if(phoneTail!=null&&!phoneTail.isBlank()){String p=o.getOpenphone()==null?"":o.getOpenphone().replaceAll("\\D",""); if(!p.endsWith(phoneTail.replaceAll("\\D",""))) return false;} + if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(o.getOpendate()==null||o.getOpendate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; + return f.get("dateTo")==null||f.get("dateTo").isBlank()||(o.getOpendate()!=null&&!o.getOpendate().isAfter(LocalDate.parse(f.get("dateTo")))); + } + private boolean inOpenTab(OpenRecord o,String tab){ + if("ALL".equals(tab)||blank(tab)) return true; + String how=o.getOpenhow()==null?"":o.getOpenhow(); + return switch(tab){case "번호이동"->"번호이동".equals(how)||"MNP".equalsIgnoreCase(how); default->tab.equals(how);}; + } + private Comparator openComparator(String sort){ + String field=sort==null?"opendate,desc":sort; boolean asc=field.endsWith(",asc"); + Comparator c=switch(field.split(",")[0]){ + case "telecom"->Comparator.comparing(OpenRecord::getTelecom,Comparator.nullsLast(String::compareTo)); + case "pmodel"->Comparator.comparing(OpenRecord::getPmodel,Comparator.nullsLast(String::compareTo)); + case "sellprice"->Comparator.comparing(OpenRecord::getSellprice,Comparator.nullsLast(BigDecimal::compareTo)); + case "gubun"->Comparator.comparing(OpenRecord::getGubun,Comparator.nullsLast(String::compareTo)); + default->Comparator.comparing(OpenRecord::getOpendate,Comparator.nullsLast(LocalDate::compareTo)); + }; + return (asc?c:c.reversed()).thenComparing(OpenRecord::getId); + } + private Map openMap(OpenRecord o,Map companies){ + Map r=new LinkedHashMap<>(); + r.put("id",o.getId()); r.put("opendate",o.getOpendate()); r.put("telecom",o.getTelecom()); r.put("outcompanyId",o.getOutcompanyId()); + r.put("outcompanyName",companies.getOrDefault(o.getOutcompanyId(),"-")); r.put("incompanyId",o.getIncompanyId()); + r.put("incompanyName",companies.getOrDefault(o.getIncompanyId(),"-")); r.put("name",o.getName()); r.put("gubun",o.getGubun()); + r.put("openhow",o.getOpenhow()); r.put("openphone",o.getOpenphone()); r.put("openStatus",o.getOpenStatus()==null?"정상":o.getOpenStatus()); + r.put("pmodel",o.getPmodel()); r.put("pserial",o.getPserial()); r.put("pcolor",o.getPcolor()); r.put("userial",o.getUserial()); r.put("umodel",o.getUmodel()); + r.put("sellprice",o.getSellprice()); r.put("declprice1",o.getDeclprice1()); r.put("dealermargin1",o.getDealermargin1()); + r.put("plan",o.getPlan()); r.put("state",o.getState()); r.put("salesperson",o.getSalesperson()); r.put("employee",o.getEmployee()); + r.put("outCategory",o.getOutCategory()); r.put("inprice",o.getInprice()); r.put("netprice",o.getNetprice()); + r.put("openHour",o.getOpenHour()); r.put("openMinute",o.getOpenMinute()); r.put("salemon",o.getSalemon()); r.put("agreemon",o.getAgreemon()); + r.put("salehow",o.getSalehow()); r.put("joinhow",o.getJoinhow()); r.put("usimhow",o.getUsimhow()); r.put("supportType",o.getSupportType()); + r.put("memo1",o.getMemo1()); r.put("dealermemo",o.getDealermemo()); r.put("phoneStockId",o.getPhoneStockId()); r.put("usimStockId",o.getUsimStockId()); + r.put("commonSupport",o.getCommonSupport()); r.put("extraSupport",o.getExtraSupport()); r.put("switchSupport",o.getSwitchSupport()); + r.put("pointPay",o.getPointPay()); r.put("preprice",o.getPreprice()); r.put("joinprice",o.getJoinprice()); r.put("gradePrice",o.getGradePrice()); + r.put("basicprice1",o.getBasicprice1()); r.put("basicprice2",o.getBasicprice2()); r.put("basicprice3",o.getBasicprice3()); r.put("basicprice4",o.getBasicprice4()); + r.put("etcprice1",o.getEtcprice1()); r.put("etcprice2",o.getEtcprice2()); r.put("moveprice",o.getMoveprice()); r.put("planprice",o.getPlanprice()); + r.put("memberpoint",o.getMemberpoint()); r.put("usimprice",o.getUsimprice()); r.put("uid",o.getUid()); + r.put("decltype",o.getDecltype()); + return r; + } + private Map openAdjustData(JwtUser u,Map f){ + List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g",OpenRecord.class).setParameter("g",u.getGroupId()).getResultList(); + Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); + List filtered=opens.stream().filter(o->matchesAdjust(o,f,companies)).sorted(adjustComparator(f)).toList(); + return Map.of("total",filtered.size(),"list",filtered.stream().map(o->openMap(o,companies)).toList()); + } + private boolean matchesAdjust(OpenRecord o,Map f,Map companies){ + if("유심".equals(o.getGubun())) return false; + if(!eq(o.getOpenhow(),f.get("openhow"))||!eq(o.getTelecom(),f.get("telecom"))||!eq(o.getSalesperson(),f.get("salesperson"))||!eq(o.getPlan(),f.get("plan"))) return false; + if(!eqId(o.getIncompanyId(),f.get("incompanyId"))||!eqId(o.getOutcompanyId(),f.get("outcompanyId"))) return false; + if(!like(o.getPmodel(),f.get("model"))||!like(companies.get(o.getOutcompanyId()),f.get("outcompanyName"))) return false; + if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(o.getOpendate()==null||o.getOpendate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; + return f.get("dateTo")==null||f.get("dateTo").isBlank()||(o.getOpendate()!=null&&!o.getOpendate().isAfter(LocalDate.parse(f.get("dateTo")))); + } + private Comparator adjustComparator(Map f){ + List keys=new ArrayList<>(); + for(int i=1;i<=7;i++){String k=f.get("sort"+i); if(k!=null&&!k.isBlank()) keys.add(k);} + if(keys.isEmpty()) keys=List.of("종류","모델명","요금제","개통일","출고처","지원방법","고객명"); + Comparator c=null; + for(String key:keys){ + Comparator next=switch(key){ + case "종류"->Comparator.comparing(OpenRecord::getOpenhow,Comparator.nullsLast(String::compareTo)); + case "모델명"->Comparator.comparing(OpenRecord::getPmodel,Comparator.nullsLast(String::compareTo)); + case "요금제"->Comparator.comparing(OpenRecord::getPlan,Comparator.nullsLast(String::compareTo)); + case "출고처"->Comparator.comparing(OpenRecord::getOutcompanyId,Comparator.nullsLast(Long::compareTo)); + case "지원방법"->Comparator.comparing(OpenRecord::getDecltype,Comparator.nullsLast(String::compareTo)); + case "고객명"->Comparator.comparing(OpenRecord::getName,Comparator.nullsLast(String::compareTo)); + case "통신사"->Comparator.comparing(OpenRecord::getTelecom,Comparator.nullsLast(String::compareTo)); + default->Comparator.comparing(OpenRecord::getOpendate,Comparator.nullsLast(LocalDate::compareTo)); + }; + c=c==null?next:c.thenComparing(next); + } + return (c==null?Comparator.comparing(OpenRecord::getId):c.thenComparing(OpenRecord::getId)); + } + private BigDecimal toDecimal(Object v){ if(v==null||String.valueOf(v).isBlank()) return BigDecimal.ZERO; return new BigDecimal(String.valueOf(v).replace(",","")); } + @GetMapping("/unitcosts") ApiResponse unitCosts(@AuthenticationPrincipal JwtUser u){ + List rows=em.createQuery("select t from UnitCostTable t where t.groupId=:g order by t.id desc",UnitCostTable.class).setParameter("g",u.getGroupId()).getResultList(); + Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); + return ApiResponse.ok(Map.of("total",rows.size(),"list",rows.stream().map(t->unitCostMap(t,companies)).toList(),"max",20)); + } + @PostMapping("/unitcosts") @Transactional ApiResponse unitCostPost(@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ + long count=em.createQuery("select count(t) from UnitCostTable t where t.groupId=:g",Long.class).setParameter("g",u.getGroupId()).getSingleResult(); + if(count>=20) return ApiResponse.fail("단가표는 최대 20개까지 등록할 수 있습니다."); + d.putIfAbsent("category","표준"); d.putIfAbsent("policyCount",0); + Integer groups=toInt(d.get("planGroupCount")); if(groups!=null){ d.put("planGroupA",groups); d.putIfAbsent("planGroupB",0); d.putIfAbsent("planGroupC",0); d.putIfAbsent("planGroupD",0); } + return post(UnitCostTable.class,d,u); + } + @PutMapping("/unitcosts/{id}") ApiResponse unitCostPut(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return put(UnitCostTable.class,id,d,u);} + @DeleteMapping("/unitcosts/{id}") @Transactional ApiResponse unitCostDel(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ + em.createQuery("delete from UnitCostPolicy p where p.unitCostId=:id and p.groupId=:g").setParameter("id",id).setParameter("g",u.getGroupId()).executeUpdate(); + return del(UnitCostTable.class,id,u); + } + @GetMapping("/unitcosts/{id}/policies") ApiResponse unitCostPolicies(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ + UnitCostTable t=em.find(UnitCostTable.class,id); if(t==null||!Objects.equals(t.getGroupId(),u.getGroupId())) return ApiResponse.fail("단가표를 찾을 수 없습니다."); + List policies=em.createQuery("select p from UnitCostPolicy p where p.groupId=:g and p.unitCostId=:id order by p.id",UnitCostPolicy.class).setParameter("g",u.getGroupId()).setParameter("id",id).getResultList(); + return ApiResponse.ok(Map.of("table",unitCostMap(t,Map.of()),"list",policies)); + } + @PostMapping("/unitcosts/{id}/policies") @Transactional ApiResponse unitCostPolicyPost(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ + UnitCostTable t=em.find(UnitCostTable.class,id); if(t==null||!Objects.equals(t.getGroupId(),u.getGroupId())) return ApiResponse.fail("단가표를 찾을 수 없습니다."); + d.put("unitCostId",id); ApiResponse created=post(UnitCostPolicy.class,d,u); + refreshPolicyCount(t); return created; + } + @PutMapping("/unitcosts/policies/{policyId}") ApiResponse unitCostPolicyPut(@PathVariable Long policyId,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return put(UnitCostPolicy.class,policyId,d,u);} + @DeleteMapping("/unitcosts/policies/{policyId}") @Transactional ApiResponse unitCostPolicyDel(@PathVariable Long policyId,@AuthenticationPrincipal JwtUser u){ + UnitCostPolicy p=em.find(UnitCostPolicy.class,policyId); if(p==null||!Objects.equals(p.getGroupId(),u.getGroupId())) return ApiResponse.fail("정책을 찾을 수 없습니다."); + Long tableId=p.getUnitCostId(); del(UnitCostPolicy.class,policyId,u); + UnitCostTable t=em.find(UnitCostTable.class,tableId); if(t!=null) refreshPolicyCount(t); + return ApiResponse.ok(null); + } + @GetMapping("/unitcosts/{id}/apply-search") ApiResponse unitCostApplySearch(@PathVariable Long id,@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + UnitCostTable t=em.find(UnitCostTable.class,id); if(t==null||!Objects.equals(t.getGroupId(),u.getGroupId())) return ApiResponse.fail("단가표를 찾을 수 없습니다."); + if(blank(f.get("dateFrom"))||blank(f.get("dateTo"))) return ApiResponse.fail("개통일을 선택하세요."); + LocalDate from=LocalDate.parse(f.get("dateFrom")), to=LocalDate.parse(f.get("dateTo")); + if(java.time.temporal.ChronoUnit.DAYS.between(from,to)>2) return ApiResponse.fail("최대 3일까지 조회할 수 있습니다."); + Map filters=new HashMap<>(f); filters.put("telecom",t.getTelecom()); + if(t.getIncompanyId()!=null) filters.put("incompanyId",String.valueOf(t.getIncompanyId())); + List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g",OpenRecord.class).setParameter("g",u.getGroupId()).getResultList(); + Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); + List> list=opens.stream().filter(o->matchesUnitCostApply(o,filters,t)).map(o->openMap(o,companies)).toList(); + return ApiResponse.ok(Map.of("total",list.size(),"list",list,"table",unitCostMap(t,companies))); + } + @PostMapping("/unitcosts/{id}/apply") @Transactional ApiResponse unitCostApply(@PathVariable Long id,@AuthenticationPrincipal JwtUser u,@RequestBody Map body){ + UnitCostTable t=em.find(UnitCostTable.class,id); if(t==null||!Objects.equals(t.getGroupId(),u.getGroupId())) return ApiResponse.fail("단가표를 찾을 수 없습니다."); + List policies=em.createQuery("select p from UnitCostPolicy p where p.groupId=:g and p.unitCostId=:id",UnitCostPolicy.class).setParameter("g",u.getGroupId()).setParameter("id",id).getResultList(); + if(policies.isEmpty()) return ApiResponse.fail("등록된 정책단가가 없습니다. 먼저 정책단가를 작성해주세요."); + BigDecimal defaultAmount=policies.getFirst().getAmount()==null?BigDecimal.ZERO:policies.getFirst().getAmount(); + Object raw=body.get("ids"); List ids=new ArrayList<>(); + if(raw instanceof List list) for(Object o:list) ids.add(Long.valueOf(String.valueOf(o))); + int updated=0; + for(Long openId:ids){ + OpenRecord o=em.find(OpenRecord.class,openId); + if(o==null||!Objects.equals(o.getGroupId(),u.getGroupId())||"유심".equals(o.getGubun())) continue; + BigDecimal amount=defaultAmount; + for(UnitCostPolicy p:policies){ + if(p.getModel()!=null&&!p.getModel().isBlank()&&o.getPmodel()!=null&&o.getPmodel().contains(p.getModel())){ amount=p.getAmount(); break; } + if(p.getPlanName()!=null&&!p.getPlanName().isBlank()&&Objects.equals(p.getPlanName(),o.getPlan())){ amount=p.getAmount(); break; } + } + o.setBasicprice1(amount); updated++; + } + return ApiResponse.ok(Map.of("updated",updated)); + } + private void refreshPolicyCount(UnitCostTable t){ + long c=em.createQuery("select count(p) from UnitCostPolicy p where p.unitCostId=:id",Long.class).setParameter("id",t.getId()).getSingleResult(); + t.setPolicyCount((int)c); + } + private boolean matchesUnitCostApply(OpenRecord o,Map f,UnitCostTable t){ + if("유심".equals(o.getGubun())) return false; + String how=o.getOpenhow()==null?"":o.getOpenhow(); + if(!(Set.of("신규","번호이동","기변","보상").contains(how)||"MNP".equalsIgnoreCase(how))) return false; + if(!eq(o.getTelecom(),t.getTelecom())) return false; + if(t.getIncompanyId()!=null&&!Objects.equals(o.getIncompanyId(),t.getIncompanyId())) return false; + if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(o.getOpendate()==null||o.getOpendate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; + return f.get("dateTo")==null||f.get("dateTo").isBlank()||(o.getOpendate()!=null&&!o.getOpendate().isAfter(LocalDate.parse(f.get("dateTo")))); + } + private Map unitCostMap(UnitCostTable t,Map companies){ + Map m=new LinkedHashMap<>(); + m.put("id",t.getId()); m.put("telecom",t.getTelecom()); m.put("incompanyId",t.getIncompanyId()); + m.put("incompanyName",t.getIncompanyName()!=null&&!t.getIncompanyName().isBlank()?t.getIncompanyName():companies.getOrDefault(t.getIncompanyId(),"-")); + m.put("memo",t.getMemo()); m.put("category",t.getCategory()); + m.put("planGroupA",t.getPlanGroupA()); m.put("planGroupB",t.getPlanGroupB()); m.put("planGroupC",t.getPlanGroupC()); + m.put("planGroupD",t.getPlanGroupD()); m.put("planGroupE",t.getPlanGroupE()); m.put("planGroupF",t.getPlanGroupF()); m.put("planGroupG",t.getPlanGroupG()); + m.put("policyCount",t.getPolicyCount()==null?0:t.getPolicyCount()); + return m; + } + private Integer toInt(Object v){ if(v==null||String.valueOf(v).isBlank()) return null; return Integer.valueOf(String.valueOf(v)); } + @GetMapping("/homesales/search") ApiResponse homeSaleSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ + return ApiResponse.ok(homeSaleSearchData(u,f,page,size)); + } + @PostMapping("/homesales") @Transactional ApiResponse homeSalePost(@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ + d.putIfAbsent("status","완료"); return post(HomeSale.class,d,u); + } + @GetMapping("/homesales/{id}") ApiResponse homeSaleGet(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return get(HomeSale.class,id,u);} + @PutMapping("/homesales/{id}") ApiResponse homeSalePut(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return put(HomeSale.class,id,d,u);} + @DeleteMapping("/homesales/{id}") ApiResponse homeSaleDel(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return del(HomeSale.class,id,u);} + @GetMapping(value="/homesales/export", produces="text/csv;charset=UTF-8") ResponseEntity homeSaleExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + @SuppressWarnings("unchecked") List> rows=(List>) homeSaleSearchData(u,f,0,10000).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF설치일,출고처,상태,입고처,통신사,고객명,휴대폰,가입상품,정산,마진,접수일\n"); + for(Map r:rows) csv.append(csv(r.get("installDate"))).append(',').append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("status"))).append(',') + .append(csv(r.get("incompanyName"))).append(',').append(csv(r.get("telecom"))).append(',').append(csv(r.get("customerName"))).append(',').append(csv(r.get("phone"))).append(',') + .append(csv(r.get("products"))).append(',').append(csv(r.get("settleAmount"))).append(',').append(csv(r.get("margin"))).append(',').append(csv(r.get("receiptDate"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=homesales.csv").body(csv.toString()); + } + private Map homeSaleSearchData(JwtUser u,Map f,int page,int size){ + List rows=em.createQuery("select h from HomeSale h where h.groupId=:g",HomeSale.class).setParameter("g",u.getGroupId()).getResultList(); + Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); + List filtered=rows.stream().filter(h->matchesHomeSale(h,f,companies)).sorted(homeSaleComparator(f.get("sort"))).toList(); + Map counts=new LinkedHashMap<>(); + List tabs=List.of("ALL","인터넷","TV","집전화","인터넷전화","신용카드","홈IOT","동반","기타","후결합","재약정","소호","원스톱","렌탈"); + for(String tab:tabs) counts.put(tab,rows.stream().filter(h->matchesHomeSale(h,withoutTab(f),companies)&&inHomeTab(h,tab)).count()); + int from=Math.min(Math.max(page,0)*Math.max(size,1),filtered.size()), to=Math.min(from+Math.min(Math.max(size,1),200),filtered.size()); + List> list=filtered.subList(from,to).stream().map(h->homeSaleMap(h,companies)).toList(); + return Map.of("total",filtered.size(),"list",list,"counts",counts); + } + private boolean matchesHomeSale(HomeSale h,Map f,Map companies){ + String tab=f.getOrDefault("tab","ALL"); if(!inHomeTab(h,tab)) return false; + if(!eq(h.getTelecom(),f.get("telecom"))||!eq(h.getOutCategory(),f.get("outCategory"))||!eq(h.getStatus(),f.get("status"))||!eq(h.getEmployee(),f.get("employee"))) return false; + if(!eqId(h.getIncompanyId(),f.get("incompanyId"))) return false; + if(!like(h.getOutcompanyName(),f.get("outcompanyName"))&&!like(companies.get(h.getOutcompanyId()),f.get("outcompanyName"))) return false; + if(!like(h.getCustomerName(),f.get("customerName"))) return false; + String dateBasis=f.getOrDefault("dateBasis","설치일기준"); + LocalDate date="접수일기준".equals(dateBasis)?h.getReceiptDate():h.getInstallDate(); + if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(date==null||date.isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; + return f.get("dateTo")==null||f.get("dateTo").isBlank()||(date!=null&&!date.isAfter(LocalDate.parse(f.get("dateTo")))); + } + private boolean inHomeTab(HomeSale h,String tab){ + if("ALL".equals(tab)||blank(tab)) return true; + String products=h.getProducts()==null?"":h.getProducts(); + return Arrays.stream(products.split("[,|/]")).map(String::trim).anyMatch(p->p.equals(tab)||("동판".equals(tab)&&"동반".equals(p))); + } + private Comparator homeSaleComparator(String sort){ + String field=sort==null?"installDate,desc":sort; boolean asc=field.endsWith(",asc"); + Comparator c=switch(field.split(",")[0]){ + case "outcompanyName"->Comparator.comparing(HomeSale::getOutcompanyName,Comparator.nullsLast(String::compareTo)); + case "telecom"->Comparator.comparing(HomeSale::getTelecom,Comparator.nullsLast(String::compareTo)); + case "customerName"->Comparator.comparing(HomeSale::getCustomerName,Comparator.nullsLast(String::compareTo)); + case "settleAmount"->Comparator.comparing(HomeSale::getSettleAmount,Comparator.nullsLast(BigDecimal::compareTo)); + case "receiptDate"->Comparator.comparing(HomeSale::getReceiptDate,Comparator.nullsLast(LocalDate::compareTo)); + default->Comparator.comparing(HomeSale::getInstallDate,Comparator.nullsLast(LocalDate::compareTo)); + }; + return (asc?c:c.reversed()).thenComparing(HomeSale::getId); + } + private Map homeSaleMap(HomeSale h,Map companies){ + Map m=new LinkedHashMap<>(); + m.put("id",h.getId()); m.put("installDate",h.getInstallDate()); m.put("receiptDate",h.getReceiptDate()); + m.put("status",h.getStatus()); m.put("telecom",h.getTelecom()); m.put("outCategory",h.getOutCategory()); + m.put("outcompanyId",h.getOutcompanyId()); m.put("incompanyId",h.getIncompanyId()); + m.put("outcompanyName",h.getOutcompanyName()!=null&&!h.getOutcompanyName().isBlank()?h.getOutcompanyName():companies.getOrDefault(h.getOutcompanyId(),"-")); + m.put("incompanyName",companies.getOrDefault(h.getIncompanyId(),"-")); + m.put("employee",h.getEmployee()); m.put("customerName",h.getCustomerName()); m.put("phone",h.getPhone()); + m.put("address",h.getAddress()); m.put("products",h.getProducts()); m.put("missingDocs",h.getMissingDocs()); + m.put("extraPolicy",h.getExtraPolicy()); m.put("settleAmount",h.getSettleAmount()); m.put("margin",h.getMargin()); m.put("memo",h.getMemo()); + return m; + } + @GetMapping("/homeindocs/search") ApiResponse homeIndocSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ + if(blank(f.get("dateFrom"))||blank(f.get("dateTo"))) return ApiResponse.fail("날짜를 선택하세요."); + return ApiResponse.ok(homeIndocSearchData(u,f,page,size)); + } + @GetMapping(value="/homeindocs/export", produces="text/csv;charset=UTF-8") ResponseEntity homeIndocExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + if(blank(f.get("dateFrom"))||blank(f.get("dateTo"))) return ResponseEntity.badRequest().body("날짜를 선택하세요."); + @SuppressWarnings("unchecked") List> rows=(List>) homeIndocSearchData(u,f,0,10000).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF설치일,통신사,입고처,출고처,고객명,휴대폰,가입상품,미비서류,차감금,차감사유,마감일,상태\n"); + for(Map r:rows) csv.append(csv(r.get("installDate"))).append(',').append(csv(r.get("telecom"))).append(',').append(csv(r.get("incompanyName"))).append(',') + .append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("customerName"))).append(',').append(csv(r.get("phone"))).append(',') + .append(csv(r.get("products"))).append(',').append(csv(r.get("missingDocs"))).append(',').append(csv(r.get("deductAmount"))).append(',') + .append(csv(r.get("deductReason"))).append(',').append(csv(r.get("deadline"))).append(',').append(csv(r.get("status"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=homeindocs.csv").body(csv.toString()); + } + private Map homeIndocSearchData(JwtUser u,Map f,int page,int size){ + List docs=em.createQuery("select d from HomeIncompleteDoc d where d.groupId=:g",HomeIncompleteDoc.class).setParameter("g",u.getGroupId()).getResultList(); + Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); + List filtered=docs.stream().filter(d->matchesHomeIndoc(d,f,companies)).sorted(Comparator.comparing(HomeIncompleteDoc::getInstallDate,Comparator.nullsLast(LocalDate::compareTo)).reversed().thenComparing(HomeIncompleteDoc::getId)).toList(); + int from=Math.min(Math.max(page,0)*Math.max(size,1),filtered.size()), to=Math.min(from+Math.min(Math.max(size,1),200),filtered.size()); + List> list=filtered.subList(from,to).stream().map(d->homeIndocMap(d,companies)).toList(); + return Map.of("total",filtered.size(),"list",list); + } + private boolean matchesHomeIndoc(HomeIncompleteDoc d,Map f,Map companies){ + if(!eq(d.getTelecom(),f.get("telecom"))||!eqId(d.getIncompanyId(),f.get("incompanyId"))) return false; + if(!like(d.getOutcompanyName(),f.get("outcompanyName"))&&!like(companies.get(d.getOutcompanyId()),f.get("outcompanyName"))) return false; + if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(d.getInstallDate()==null||d.getInstallDate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; + if(f.get("dateTo")!=null&&!f.get("dateTo").isBlank()&&(d.getInstallDate()==null||d.getInstallDate().isAfter(LocalDate.parse(f.get("dateTo"))))) return false; + String keyword=f.get("keyword"); if(keyword==null||keyword.isBlank()) return true; + String searchType=f.getOrDefault("searchType","고객명"); + return switch(searchType){ + case "미비서류"->like(d.getMissingDocs(),keyword); + case "휴대폰(뒷4자리)"->{ + String phone=d.getPhone()==null?"":d.getPhone().replaceAll("\\D",""); + yield phone.endsWith(keyword.replaceAll("\\D","")); + } + default->like(d.getCustomerName(),keyword); + }; + } + private Map homeIndocMap(HomeIncompleteDoc d,Map companies){ + Map r=new LinkedHashMap<>(); + r.put("id",d.getId()); r.put("installDate",d.getInstallDate()); r.put("deadline",d.getDeadline()); + r.put("telecom",d.getTelecom()); r.put("customerName",d.getCustomerName()); r.put("phone",d.getPhone()); + r.put("products",d.getProducts()); r.put("missingDocs",d.getMissingDocs()); r.put("deductAmount",d.getDeductAmount()); + r.put("deductReason",d.getDeductReason()); r.put("status",d.getStatus()==null?"미비":d.getStatus()); + r.put("homeSaleId",d.getHomeSaleId()); r.put("outcompanyId",d.getOutcompanyId()); r.put("incompanyId",d.getIncompanyId()); + r.put("outcompanyName",d.getOutcompanyName()!=null&&!d.getOutcompanyName().isBlank()?d.getOutcompanyName():companies.getOrDefault(d.getOutcompanyId(),"-")); + r.put("incompanyName",companies.getOrDefault(d.getIncompanyId(),"-")); + return r; + } + @GetMapping("/exchanges/search") ApiResponse exchangeSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ + return ApiResponse.ok(exchangeSearchData(u,f,page,size)); + } + @PostMapping("/exchanges/delete") @Transactional ApiResponse exchangeDelete(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Object raw=d.get("ids"); + if(!(raw instanceof List ids)||ids.isEmpty()) return ApiResponse.fail("삭제할 이력을 선택하세요."); + int deleted=0; + for(Object idObj:ids){ + OpenExchange e=em.find(OpenExchange.class,Long.valueOf(idObj.toString())); + if(e==null||!Objects.equals(e.getGroupId(),u.getGroupId())) continue; + em.remove(e); deleted++; + } + return ApiResponse.ok(Map.of("deleted",deleted)); + } + @GetMapping(value="/exchanges/export", produces="text/csv;charset=UTF-8") ResponseEntity exchangeExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + @SuppressWarnings("unchecked") List> rows=(List>) exchangeSearchData(u,f,0,10000).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF번호,교품일,출고처,고객명,개통일,개통번호,종류,교품,기존,처리직원,처리일,비고\n"); + for(Map r:rows) csv.append(csv(r.get("no"))).append(',').append(csv(r.get("exchangeDate"))).append(',').append(csv(r.get("outcompanyName"))).append(',') + .append(csv(r.get("customerName"))).append(',').append(csv(r.get("openDate"))).append(',').append(csv(r.get("openphone"))).append(',') + .append(csv(r.get("kind"))).append(',').append(csv(r.get("newLabel"))).append(',').append(csv(r.get("oldLabel"))).append(',') + .append(csv(r.get("processor"))).append(',').append(csv(r.get("processedAt"))).append(',').append(csv(r.get("memo"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=exchanges.csv").body(csv.toString()); + } + private Map exchangeSearchData(JwtUser u,Map f,int page,int size){ + List all=em.createQuery("select e from OpenExchange e where e.groupId=:g",OpenExchange.class).setParameter("g",u.getGroupId()).getResultList(); + Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); + List filtered=all.stream().filter(e->matchesExchange(e,f,companies)).sorted(Comparator.comparing(OpenExchange::getExchangeDate,Comparator.nullsLast(LocalDate::compareTo)).reversed().thenComparing(OpenExchange::getId,Comparator.reverseOrder())).toList(); + int pageSize=Math.min(Math.max(size,1),200), from=Math.min(Math.max(page,0)*pageSize,filtered.size()), to=Math.min(from+pageSize,filtered.size()); + List> list=new ArrayList<>(); + for(int i=from;i f,Map companies){ + if(!eq(e.getKind(),f.get("kind"))) return false; + if(!like(e.getOutcompanyName(),f.get("outcompanyName"))&&!like(companies.get(e.getOutcompanyId()),f.get("outcompanyName"))) return false; + if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(e.getExchangeDate()==null||e.getExchangeDate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; + if(f.get("dateTo")!=null&&!f.get("dateTo").isBlank()&&(e.getExchangeDate()==null||e.getExchangeDate().isAfter(LocalDate.parse(f.get("dateTo"))))) return false; + String keyword=f.get("keyword"); if(keyword==null||keyword.isBlank()) return true; + String searchType=f.getOrDefault("searchType","개통번호(뒷4자리)"); + return switch(searchType){ + case "고객명"->like(e.getCustomerName(),keyword); + case "교품일련번호"->like(e.getNewSerial(),keyword); + case "기존일련번호"->like(e.getOldSerial(),keyword); + default->{ + String phone=e.getOpenphone()==null?"":e.getOpenphone().replaceAll("\\D",""); + yield phone.endsWith(keyword.replaceAll("\\D","")); + } + }; + } + private Map exchangeMap(OpenExchange e,Map companies,int no){ + Map r=new LinkedHashMap<>(); + r.put("id",e.getId()); r.put("no",no); r.put("exchangeDate",e.getExchangeDate()); r.put("openDate",e.getOpenDate()); + r.put("customerName",e.getCustomerName()); r.put("openphone",e.getOpenphone()); r.put("kind",e.getKind()); + r.put("newModel",e.getNewModel()); r.put("newColor",e.getNewColor()); r.put("newSerial",e.getNewSerial()); + r.put("oldModel",e.getOldModel()); r.put("oldColor",e.getOldColor()); r.put("oldSerial",e.getOldSerial()); + r.put("newLabel",deviceLabel(e.getNewModel(),e.getNewColor(),e.getNewSerial())); + r.put("oldLabel",deviceLabel(e.getOldModel(),e.getOldColor(),e.getOldSerial())); + r.put("processor",e.getProcessor()); r.put("memo",e.getMemo()); + r.put("processedAt",e.getProcessedAt()==null?null:e.getProcessedAt().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))); + r.put("outcompanyId",e.getOutcompanyId()); + r.put("outcompanyName",e.getOutcompanyName()!=null&&!e.getOutcompanyName().isBlank()?e.getOutcompanyName():companies.getOrDefault(e.getOutcompanyId(),"-")); + return r; + } + private String deviceLabel(String model,String color,String serial){ + String m=model==null?"":model.trim(); + String c=color==null||color.isBlank()||"-".equals(color)?"":" "+color.trim(); + String s=serial==null||serial.isBlank()?"":" ("+serial.trim()+")"; + return (m+c+s).trim(); + } + @GetMapping("/namechanges/search") ApiResponse nameChangeSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ + return ApiResponse.ok(nameChangeSearchData(u,f,page,size)); + } + @PostMapping("/namechanges/delete") @Transactional ApiResponse nameChangeDelete(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Object raw=d.get("ids"); + if(!(raw instanceof List ids)||ids.isEmpty()) return ApiResponse.fail("삭제할 이력을 선택하세요."); + int deleted=0; + for(Object idObj:ids){ + OpenNameChange e=em.find(OpenNameChange.class,Long.valueOf(idObj.toString())); + if(e==null||!Objects.equals(e.getGroupId(),u.getGroupId())) continue; + em.remove(e); deleted++; + } + return ApiResponse.ok(Map.of("deleted",deleted)); + } + @GetMapping(value="/namechanges/export", produces="text/csv;charset=UTF-8") ResponseEntity nameChangeExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + @SuppressWarnings("unchecked") List> rows=(List>) nameChangeSearchData(u,f,0,10000).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF번호,명변일,출고처,고객명,개통일,개통번호,기존명의자,기존연락처,기존유심,처리직원,처리일,비고\n"); + for(Map r:rows) csv.append(csv(r.get("no"))).append(',').append(csv(r.get("changeDate"))).append(',').append(csv(r.get("outcompanyName"))).append(',') + .append(csv(r.get("customerName"))).append(',').append(csv(r.get("openDate"))).append(',').append(csv(r.get("openphone"))).append(',') + .append(csv(r.get("oldOwner"))).append(',').append(csv(r.get("oldPhone"))).append(',').append(csv(r.get("oldUsim"))).append(',') + .append(csv(r.get("processor"))).append(',').append(csv(r.get("processedAt"))).append(',').append(csv(r.get("memo"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=namechanges.csv").body(csv.toString()); + } + private Map nameChangeSearchData(JwtUser u,Map f,int page,int size){ + List all=em.createQuery("select e from OpenNameChange e where e.groupId=:g",OpenNameChange.class).setParameter("g",u.getGroupId()).getResultList(); + Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); + List filtered=all.stream().filter(e->matchesNameChange(e,f,companies)).sorted(Comparator.comparing(OpenNameChange::getChangeDate,Comparator.nullsLast(LocalDate::compareTo)).reversed().thenComparing(OpenNameChange::getId,Comparator.reverseOrder())).toList(); + int pageSize=Math.min(Math.max(size,1),200), from=Math.min(Math.max(page,0)*pageSize,filtered.size()), to=Math.min(from+pageSize,filtered.size()); + List> list=new ArrayList<>(); + for(int i=from;i f,Map companies){ + if(!like(e.getOutcompanyName(),f.get("outcompanyName"))&&!like(companies.get(e.getOutcompanyId()),f.get("outcompanyName"))) return false; + if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(e.getChangeDate()==null||e.getChangeDate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; + if(f.get("dateTo")!=null&&!f.get("dateTo").isBlank()&&(e.getChangeDate()==null||e.getChangeDate().isAfter(LocalDate.parse(f.get("dateTo"))))) return false; + String keyword=f.get("keyword"); if(keyword==null||keyword.isBlank()) return true; + String searchType=f.getOrDefault("searchType","개통번호(뒷4자리)"); + return switch(searchType){ + case "고객명"->like(e.getCustomerName(),keyword); + case "기존명의자"->like(e.getOldOwner(),keyword); + case "기존연락처"->{ + String phone=e.getOldPhone()==null?"":e.getOldPhone().replaceAll("\\D",""); + yield phone.endsWith(keyword.replaceAll("\\D","")); + } + default->{ + String phone=e.getOpenphone()==null?"":e.getOpenphone().replaceAll("\\D",""); + yield phone.endsWith(keyword.replaceAll("\\D","")); + } + }; + } + private Map nameChangeMap(OpenNameChange e,Map companies,int no){ + Map r=new LinkedHashMap<>(); + r.put("id",e.getId()); r.put("no",no); r.put("changeDate",e.getChangeDate()); r.put("openDate",e.getOpenDate()); + r.put("customerName",e.getCustomerName()); r.put("openphone",e.getOpenphone()); + r.put("oldOwner",e.getOldOwner()); r.put("oldPhone",e.getOldPhone()); r.put("oldUsim",e.getOldUsim()); + r.put("processor",e.getProcessor()); r.put("memo",e.getMemo()); + r.put("processedAt",e.getProcessedAt()==null?null:e.getProcessedAt().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))); + r.put("outcompanyId",e.getOutcompanyId()); + r.put("outcompanyName",e.getOutcompanyName()!=null&&!e.getOutcompanyName().isBlank()?e.getOutcompanyName():companies.getOrDefault(e.getOutcompanyId(),"-")); + return r; + } + @GetMapping("/withdrawals/search") ApiResponse withdrawalSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ + return ApiResponse.ok(withdrawalSearchData(u,f,page,size)); + } + @PostMapping("/withdrawals/delete") @Transactional ApiResponse withdrawalDelete(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Object raw=d.get("ids"); + if(!(raw instanceof List ids)||ids.isEmpty()) return ApiResponse.fail("삭제할 이력을 선택하세요."); + int deleted=0; + for(Object idObj:ids){ + OpenWithdrawal e=em.find(OpenWithdrawal.class,Long.valueOf(idObj.toString())); + if(e==null||!Objects.equals(e.getGroupId(),u.getGroupId())) continue; + em.remove(e); deleted++; + } + return ApiResponse.ok(Map.of("deleted",deleted)); + } + @GetMapping(value="/withdrawals/export", produces="text/csv;charset=UTF-8") ResponseEntity withdrawalExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + @SuppressWarnings("unchecked") List> rows=(List>) withdrawalSearchData(u,f,0,10000).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF번호,철회일,출고처,고객명,개통일,개통번호,처리직원,처리일,사유 및 비고\n"); + for(Map r:rows) csv.append(csv(r.get("no"))).append(',').append(csv(r.get("withdrawDate"))).append(',').append(csv(r.get("outcompanyName"))).append(',') + .append(csv(r.get("customerName"))).append(',').append(csv(r.get("openDate"))).append(',').append(csv(r.get("openphone"))).append(',') + .append(csv(r.get("processor"))).append(',').append(csv(r.get("processedAt"))).append(',').append(csv(r.get("reason"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=withdrawals.csv").body(csv.toString()); + } + private Map withdrawalSearchData(JwtUser u,Map f,int page,int size){ + List all=em.createQuery("select e from OpenWithdrawal e where e.groupId=:g",OpenWithdrawal.class).setParameter("g",u.getGroupId()).getResultList(); + Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); + List filtered=all.stream().filter(e->matchesWithdrawal(e,f,companies)).sorted(Comparator.comparing(OpenWithdrawal::getWithdrawDate,Comparator.nullsLast(LocalDate::compareTo)).reversed().thenComparing(OpenWithdrawal::getId,Comparator.reverseOrder())).toList(); + int pageSize=Math.min(Math.max(size,1),200), from=Math.min(Math.max(page,0)*pageSize,filtered.size()), to=Math.min(from+pageSize,filtered.size()); + List> list=new ArrayList<>(); + for(int i=from;i f,Map companies){ + if(!like(e.getOutcompanyName(),f.get("outcompanyName"))&&!like(companies.get(e.getOutcompanyId()),f.get("outcompanyName"))) return false; + if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(e.getWithdrawDate()==null||e.getWithdrawDate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; + if(f.get("dateTo")!=null&&!f.get("dateTo").isBlank()&&(e.getWithdrawDate()==null||e.getWithdrawDate().isAfter(LocalDate.parse(f.get("dateTo"))))) return false; + String keyword=f.get("keyword"); if(keyword==null||keyword.isBlank()) return true; + String searchType=f.getOrDefault("searchType","개통번호(뒷4자리)"); + return switch(searchType){ + case "고객명"->like(e.getCustomerName(),keyword); + case "사유 및 비고"->like(e.getReason(),keyword); + default->{ + String phone=e.getOpenphone()==null?"":e.getOpenphone().replaceAll("\\D",""); + yield phone.endsWith(keyword.replaceAll("\\D","")); + } + }; + } + private Map withdrawalMap(OpenWithdrawal e,Map companies,int no){ + Map r=new LinkedHashMap<>(); + r.put("id",e.getId()); r.put("no",no); r.put("withdrawDate",e.getWithdrawDate()); r.put("openDate",e.getOpenDate()); + r.put("customerName",e.getCustomerName()); r.put("openphone",e.getOpenphone()); + r.put("processor",e.getProcessor()); r.put("reason",e.getReason()); + r.put("processedAt",e.getProcessedAt()==null?null:e.getProcessedAt().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))); + r.put("outcompanyId",e.getOutcompanyId()); + r.put("outcompanyName",e.getOutcompanyName()!=null&&!e.getOutcompanyName().isBlank()?e.getOutcompanyName():companies.getOrDefault(e.getOutcompanyId(),"-")); + return r; + } + @GetMapping("/settlehistories/search") ApiResponse settleHistorySearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ + return ApiResponse.ok(settleHistorySearchData(u,f,page,size)); + } + @PostMapping("/settlehistories/delete") @Transactional ApiResponse settleHistoryDelete(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Object raw=d.get("ids"); + if(!(raw instanceof List ids)||ids.isEmpty()) return ApiResponse.fail("삭제할 이력을 선택하세요."); + int deleted=0; + for(Object idObj:ids){ + OpenSettleHistory e=em.find(OpenSettleHistory.class,Long.valueOf(idObj.toString())); + if(e==null||!Objects.equals(e.getGroupId(),u.getGroupId())) continue; + em.remove(e); deleted++; + } + return ApiResponse.ok(Map.of("deleted",deleted)); + } + @GetMapping(value="/settlehistories/export", produces="text/csv;charset=UTF-8") ResponseEntity settleHistoryExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + @SuppressWarnings("unchecked") List> rows=(List>) settleHistorySearchData(u,f,0,10000).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF번호,변경일,출고처,고객명,개통일,개통번호,기존정산금,변경정산금,처리직원,처리일,비고\n"); + for(Map r:rows) csv.append(csv(r.get("no"))).append(',').append(csv(r.get("changeDate"))).append(',').append(csv(r.get("outcompanyName"))).append(',') + .append(csv(r.get("customerName"))).append(',').append(csv(r.get("openDate"))).append(',').append(csv(r.get("openphone"))).append(',') + .append(csv(r.get("oldAmount"))).append(',').append(csv(r.get("newAmount"))).append(',') + .append(csv(r.get("processor"))).append(',').append(csv(r.get("processedAt"))).append(',').append(csv(r.get("memo"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=settlehistories.csv").body(csv.toString()); + } + private Map settleHistorySearchData(JwtUser u,Map f,int page,int size){ + List all=em.createQuery("select e from OpenSettleHistory e where e.groupId=:g",OpenSettleHistory.class).setParameter("g",u.getGroupId()).getResultList(); + Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); + List filtered=all.stream().filter(e->matchesSettleHistory(e,f,companies)).sorted(Comparator.comparing(OpenSettleHistory::getChangeDate,Comparator.nullsLast(LocalDate::compareTo)).reversed().thenComparing(OpenSettleHistory::getId,Comparator.reverseOrder())).toList(); + int pageSize=Math.min(Math.max(size,1),200), from=Math.min(Math.max(page,0)*pageSize,filtered.size()), to=Math.min(from+pageSize,filtered.size()); + List> list=new ArrayList<>(); + for(int i=from;i f,Map companies){ + if(!like(e.getOutcompanyName(),f.get("outcompanyName"))&&!like(companies.get(e.getOutcompanyId()),f.get("outcompanyName"))) return false; + if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(e.getChangeDate()==null||e.getChangeDate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; + if(f.get("dateTo")!=null&&!f.get("dateTo").isBlank()&&(e.getChangeDate()==null||e.getChangeDate().isAfter(LocalDate.parse(f.get("dateTo"))))) return false; + String keyword=f.get("keyword"); if(keyword==null||keyword.isBlank()) return true; + String searchType=f.getOrDefault("searchType","개통번호(뒷4자리)"); + return switch(searchType){ + case "고객명"->like(e.getCustomerName(),keyword); + case "처리직원"->like(e.getProcessor(),keyword); + case "비고"->like(e.getMemo(),keyword); + default->{ + String phone=e.getOpenphone()==null?"":e.getOpenphone().replaceAll("\\D",""); + yield phone.endsWith(keyword.replaceAll("\\D","")); + } + }; + } + private Map settleHistoryMap(OpenSettleHistory e,Map companies,int no){ + Map r=new LinkedHashMap<>(); + r.put("id",e.getId()); r.put("no",no); r.put("changeDate",e.getChangeDate()); r.put("openDate",e.getOpenDate()); + r.put("customerName",e.getCustomerName()); r.put("openphone",e.getOpenphone()); + r.put("oldAmount",e.getOldAmount()); r.put("newAmount",e.getNewAmount()); + r.put("processor",e.getProcessor()); r.put("memo",e.getMemo()); + r.put("processedAt",e.getProcessedAt()==null?null:e.getProcessedAt().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))); + r.put("outcompanyId",e.getOutcompanyId()); + r.put("outcompanyName",e.getOutcompanyName()!=null&&!e.getOutcompanyName().isBlank()?e.getOutcompanyName():companies.getOrDefault(e.getOutcompanyId(),"-")); + return r; + } + @GetMapping("/deletehistories/search") ApiResponse deleteHistorySearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ + return ApiResponse.ok(deleteHistorySearchData(u,f,page,size)); + } + @PostMapping("/deletehistories/delete") @Transactional ApiResponse deleteHistoryDelete(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Object raw=d.get("ids"); + if(!(raw instanceof List ids)||ids.isEmpty()) return ApiResponse.fail("삭제할 이력을 선택하세요."); + int deleted=0; + for(Object idObj:ids){ + OpenDeleteHistory e=em.find(OpenDeleteHistory.class,Long.valueOf(idObj.toString())); + if(e==null||!Objects.equals(e.getGroupId(),u.getGroupId())) continue; + em.remove(e); deleted++; + } + return ApiResponse.ok(Map.of("deleted",deleted)); + } + @GetMapping(value="/deletehistories/export", produces="text/csv;charset=UTF-8") ResponseEntity deleteHistoryExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + @SuppressWarnings("unchecked") List> rows=(List>) deleteHistorySearchData(u,f,0,10000).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF번호,철회일,출고처,고객명,개통일,개통번호,처리직원,처리일,비고\n"); + for(Map r:rows) csv.append(csv(r.get("no"))).append(',').append(csv(r.get("deleteDate"))).append(',').append(csv(r.get("outcompanyName"))).append(',') + .append(csv(r.get("customerName"))).append(',').append(csv(r.get("openDate"))).append(',').append(csv(r.get("openphone"))).append(',') + .append(csv(r.get("processor"))).append(',').append(csv(r.get("processedAt"))).append(',').append(csv(r.get("memo"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=deletehistories.csv").body(csv.toString()); + } + private Map deleteHistorySearchData(JwtUser u,Map f,int page,int size){ + List all=em.createQuery("select e from OpenDeleteHistory e where e.groupId=:g",OpenDeleteHistory.class).setParameter("g",u.getGroupId()).getResultList(); + Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); + List filtered=all.stream().filter(e->matchesDeleteHistory(e,f,companies)).sorted(Comparator.comparing(OpenDeleteHistory::getDeleteDate,Comparator.nullsLast(LocalDate::compareTo)).reversed().thenComparing(OpenDeleteHistory::getId,Comparator.reverseOrder())).toList(); + int pageSize=Math.min(Math.max(size,1),200), from=Math.min(Math.max(page,0)*pageSize,filtered.size()), to=Math.min(from+pageSize,filtered.size()); + List> list=new ArrayList<>(); + for(int i=from;i f,Map companies){ + if(!like(e.getOutcompanyName(),f.get("outcompanyName"))&&!like(companies.get(e.getOutcompanyId()),f.get("outcompanyName"))) return false; + if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(e.getDeleteDate()==null||e.getDeleteDate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; + if(f.get("dateTo")!=null&&!f.get("dateTo").isBlank()&&(e.getDeleteDate()==null||e.getDeleteDate().isAfter(LocalDate.parse(f.get("dateTo"))))) return false; + String keyword=f.get("keyword"); if(keyword==null||keyword.isBlank()) return true; + String searchType=f.getOrDefault("searchType","개통번호(뒷4자리)"); + return switch(searchType){ + case "고객명"->like(e.getCustomerName(),keyword); + case "처리직원"->like(e.getProcessor(),keyword); + case "비고"->like(e.getMemo(),keyword); + default->{ + String phone=e.getOpenphone()==null?"":e.getOpenphone().replaceAll("\\D",""); + yield phone.endsWith(keyword.replaceAll("\\D","")); + } + }; + } + private Map deleteHistoryMap(OpenDeleteHistory e,Map companies,int no){ + Map r=new LinkedHashMap<>(); + r.put("id",e.getId()); r.put("no",no); r.put("deleteDate",e.getDeleteDate()); r.put("openDate",e.getOpenDate()); + r.put("customerName",e.getCustomerName()); r.put("openphone",e.getOpenphone()); + r.put("processor",e.getProcessor()); r.put("memo",e.getMemo()); + r.put("processedAt",e.getProcessedAt()==null?null:e.getProcessedAt().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))); + r.put("outcompanyId",e.getOutcompanyId()); + r.put("outcompanyName",e.getOutcompanyName()!=null&&!e.getOutcompanyName().isBlank()?e.getOutcompanyName():companies.getOrDefault(e.getOutcompanyId(),"-")); + return r; + } + @GetMapping("/companies") ApiResponse companies(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="30")int size){return list(Company.class,u,f,page,size);} + @GetMapping("/outcompany-accounts") ApiResponse outcompanyAccounts(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + List companies=em.createQuery("select c from Company c where c.groupId=:g and c.type in ('OUT','SHOP') order by c.id asc",Company.class) + .setParameter("g",u.getGroupId()).getResultList(); + List members=em.createQuery("select m from Member m where m.groupId=:g and m.role='SHOP'",Member.class) + .setParameter("g",u.getGroupId()).getResultList(); + Map byCompany=new HashMap<>(); + for(Member m:members) if(m.getCompanyId()!=null) byCompany.putIfAbsent(m.getCompanyId(),m); + String account=f.get("account"); + String channel=f.get("channel"); + String searchField=f.getOrDefault("searchField","name"); + String keyword=f.get("keyword"); + String tab=f.get("tab"); + List> list=new ArrayList<>(); + Map channelCounts=new LinkedHashMap<>(); + channelCounts.put("전체",0); channelCounts.put("도매",0); channelCounts.put("소매",0); channelCounts.put("C채널",0); channelCounts.put("특판",0); channelCounts.put("기타",0); + for(Company c:companies){ + Member m=byCompany.get(c.getId()); + String status=m==null?"미사용":(m.isActive()?"사용":"중지"); + String ch=blank(c.getChannel())?"기타":c.getChannel(); + channelCounts.put("전체",channelCounts.get("전체")+1); + channelCounts.put(ch,channelCounts.getOrDefault(ch,0)+1); + if(!eq(status,account)) continue; + if(!eq(ch,channel)) continue; + if(tab!=null&&!tab.isBlank()&&!"전체".equals(tab)&&!Objects.equals(ch,tab)) continue; + if(keyword!=null&&!keyword.isBlank()){ + String q=keyword.trim(); + String hay=switch(searchField){ + case "managerName"->nvlStr(c.getManagerName()); + case "phone"->nvlStr(c.getPhone()); + case "userid"->m==null?"":nvlStr(m.getUserid()); + case "pcode"->nvlStr(c.getPcode()); + default->nvlStr(c.getName()); + }; + if(!hay.contains(q)) continue; + } + list.add(outcompanyAccountMap(c,m,status)); + } + return ApiResponse.ok(Map.of("total",list.size(),"list",list,"channelCounts",channelCounts)); + } + @PostMapping("/outcompany-accounts/{id}/save") @Transactional ApiResponse outcompanyAccountSave(@PathVariable Long id,@RequestBody Map d,@AuthenticationPrincipal JwtUser u){ + Company c=em.find(Company.class,id); + if(c==null||!Objects.equals(c.getGroupId(),u.getGroupId())||!("OUT".equals(c.getType())||"SHOP".equals(c.getType()))) + return ApiResponse.fail("출고처를 찾을 수 없습니다."); + String userid=d.get("userid")==null?"":String.valueOf(d.get("userid")).trim(); + String password=d.get("password")==null?"":String.valueOf(d.get("password")).trim(); + if(!userid.matches("^[A-Za-z0-9]{4,20}$")) return ApiResponse.fail("아이디는 영문, 숫자 4자~20자입니다."); + if(!password.matches("^[A-Za-z0-9]{4,20}$")) return ApiResponse.fail("비밀번호는 영문, 숫자 4자~20자입니다."); + List dup=em.createQuery("select m from Member m where m.userid=:id",Member.class).setParameter("id",userid).getResultList(); + Member existing=em.createQuery("select m from Member m where m.groupId=:g and m.role='SHOP' and m.companyId=:c",Member.class) + .setParameter("g",u.getGroupId()).setParameter("c",id).getResultList().stream().findFirst().orElse(null); + if(!dup.isEmpty()&&(existing==null||!Objects.equals(dup.getFirst().getId(),existing.getId()))) + return ApiResponse.fail("이미 사용 중인 아이디입니다."); + if(existing==null){ + existing=new Member(); + existing.setGroupId(u.getGroupId()); + existing.setRole("SHOP"); + existing.setCompanyId(id); + existing.setUserid(userid); + existing.setPassword(encoder.encode(password)); + existing.setPlainPassword(password); + existing.setLoginCount(0); + existing.setActive(true); + existing.setName(c.getName()); + existing.setPhone(c.getPhone()); + existing.setMenuFlags(menuFlagsJson(d.get("menuFlags"))); + em.persist(existing); + } else { + existing.setUserid(userid); + existing.setPassword(encoder.encode(password)); + existing.setPlainPassword(password); + existing.setName(c.getName()); + existing.setPhone(c.getPhone()); + existing.setMenuFlags(menuFlagsJson(d.get("menuFlags"))); + existing.setActive(true); + } + return ApiResponse.ok(outcompanyAccountMap(c,existing,"사용")); + } + @PostMapping("/outcompany-accounts/{id}/stop") @Transactional ApiResponse outcompanyAccountStop(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ + Member m=shopMember(u,id); if(m==null) return ApiResponse.fail("계정이 없습니다."); + m.setActive(false); return ApiResponse.ok(null); + } + @PostMapping("/outcompany-accounts/{id}/resume") @Transactional ApiResponse outcompanyAccountResume(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ + Member m=shopMember(u,id); if(m==null) return ApiResponse.fail("계정이 없습니다."); + m.setActive(true); return ApiResponse.ok(null); + } + @DeleteMapping("/outcompany-accounts/{id}") @Transactional ApiResponse outcompanyAccountDelete(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ + Member m=shopMember(u,id); if(m==null) return ApiResponse.fail("계정이 없습니다."); + em.remove(m); return ApiResponse.ok(null); + } + private Member shopMember(JwtUser u,Long companyId){ + return em.createQuery("select m from Member m where m.groupId=:g and m.role='SHOP' and m.companyId=:c",Member.class) + .setParameter("g",u.getGroupId()).setParameter("c",companyId).getResultList().stream().findFirst().orElse(null); + } + private Map outcompanyAccountMap(Company c,Member m,String status){ + Map r=new LinkedHashMap<>(); + r.put("id",c.getId()); + r.put("channel",blank(c.getChannel())?"기타":c.getChannel()); + r.put("name",c.getName()); + r.put("pcode",blank(c.getPcode())?"-":c.getPcode()); + r.put("managerName",blank(c.getManagerName())?"-":c.getManagerName()); + r.put("phone",blank(c.getPhone())?"-":c.getPhone()); + r.put("userid",m==null?"-":m.getUserid()); + r.put("password",m==null?"-":(blank(m.getPlainPassword())?"****":m.getPlainPassword())); + r.put("loginCount",m==null?0:(m.getLoginCount()==null?0:m.getLoginCount())); + r.put("lastLoginAt",m==null||m.getLastLoginAt()==null?"-":m.getLastLoginAt().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))); + r.put("accountStatus",status); + r.put("memberId",m==null?null:m.getId()); + r.put("menuFlags",parseMenuFlags(m==null?null:m.getMenuFlags())); + r.put("createdAt",c.getCreatedAt()==null?null:c.getCreatedAt().atZone(ZoneId.systemDefault()).toLocalDate().toString()); + return r; + } + private String menuFlagsJson(Object raw){ + Map flags=defaultMenuFlags(); + if(raw instanceof Map map) for(var e:map.entrySet()) if(e.getKey()!=null) flags.put(String.valueOf(e.getKey()),Boolean.parseBoolean(String.valueOf(e.getValue()))); + StringBuilder sb=new StringBuilder("{"); + boolean first=true; + for(var e:flags.entrySet()){ if(!first) sb.append(','); first=false; sb.append('"').append(e.getKey()).append("\":").append(e.getValue()); } + sb.append('}'); + return sb.toString(); + } + private Map parseMenuFlags(String json){ + Map flags=defaultMenuFlags(); + if(json==null||json.isBlank()) return flags; + for(String key:flags.keySet()){ + String token="\""+key+"\":"; + int i=json.indexOf(token); + if(i>=0){ + int start=i+token.length(); + flags.put(key,json.regionMatches(true,start,"true",0,4)); + } + } + return flags; + } + private Map defaultMenuFlags(){ + Map m=new LinkedHashMap<>(); + m.put("policy",true); m.put("pds",true); m.put("return",true); m.put("indoc",true); + m.put("scan",true); m.put("stock",true); m.put("open",true); m.put("settle",true); m.put("farebox",true); + return m; + } + private String nvlStr(String v){return v==null?"":v;} + @GetMapping("/boardgroups") ApiResponse boardGroups(@AuthenticationPrincipal JwtUser u){ + List all=em.createQuery("select g from BoardGroup g where g.groupId=:g order by g.id asc",BoardGroup.class).setParameter("g",u.getGroupId()).getResultList(); + List> list=new ArrayList<>(); + for(int i=0;id,@AuthenticationPrincipal JwtUser u){ + String name=d.get("name")==null?"":String.valueOf(d.get("name")).trim(); + if(name.isBlank()) return ApiResponse.fail("그룹명을 입력하세요."); + BoardGroup g=new BoardGroup(); + g.setGroupId(u.getGroupId()); + g.setCategory("사용자그룹"); + g.setName(name); + g.setCompanyIds(""); + em.persist(g); + return ApiResponse.ok(boardGroupMap(g,0)); + } + @DeleteMapping("/boardgroups/{id}") @Transactional ApiResponse boardGroupDel(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ + BoardGroup g=em.find(BoardGroup.class,id); + if(g==null||!Objects.equals(g.getGroupId(),u.getGroupId())) return ApiResponse.fail("그룹을 찾을 수 없습니다."); + if("기본통신사".equals(g.getCategory())) return ApiResponse.fail("기본통신사는 삭제할 수 없습니다."); + if(boardGroupCompanyCount(g)>0) return ApiResponse.fail("출고처지정이 있는 경우 삭제할 수 없습니다."); + em.remove(g); + return ApiResponse.ok(null); + } + @PutMapping("/boardgroups/{id}/companies") @Transactional ApiResponse boardGroupCompanies(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ + BoardGroup g=em.find(BoardGroup.class,id); + if(g==null||!Objects.equals(g.getGroupId(),u.getGroupId())) return ApiResponse.fail("그룹을 찾을 수 없습니다."); + Object raw=d.get("companyIds"); + List ids=new ArrayList<>(); + if(raw instanceof List list) for(Object o:list) if(o!=null&&!String.valueOf(o).isBlank()) ids.add(String.valueOf(o)); + g.setCompanyIds(String.join(",",ids)); + return ApiResponse.ok(boardGroupMap(g,0)); + } + private Map boardGroupMap(BoardGroup g,int no){ + Map r=new LinkedHashMap<>(); + r.put("id",g.getId()); r.put("no",no); r.put("category",g.getCategory()); r.put("name",g.getName()); + r.put("companyCount",boardGroupCompanyCount(g)); + r.put("companyIds",boardGroupCompanyIds(g)); + r.put("deletable","사용자그룹".equals(g.getCategory())&&boardGroupCompanyCount(g)==0); + return r; + } + private int boardGroupCompanyCount(BoardGroup g){return boardGroupCompanyIds(g).size();} + private List boardGroupCompanyIds(BoardGroup g){ + if(g.getCompanyIds()==null||g.getCompanyIds().isBlank()) return List.of(); + List ids=new ArrayList<>(); + for(String p:g.getCompanyIds().split(",")) try{ if(!p.isBlank()) ids.add(Long.valueOf(p.trim())); }catch(Exception ignored){} + return ids; + } + @PostMapping("/companies") ApiResponse companyPost(@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return post(Company.class,d,u);} + @GetMapping("/companies/{id}") ApiResponse company(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return get(Company.class,id,u);} + @PutMapping("/companies/{id}") ApiResponse companyPut(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return put(Company.class,id,d,u);} + @DeleteMapping("/companies/{id}") ApiResponse companyDel(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return del(Company.class,id,u);} + @GetMapping("/bbs") ApiResponse bbs(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="30")int size){return list(BbsPost.class,u,f,page,size);} + @GetMapping("/bbs/search") ApiResponse bbsSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ + return ApiResponse.ok(bbsSearchData(u,f,page,size)); + } + @PostMapping("/bbs") @Transactional ApiResponse bbsPost(@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ + d.putIfAbsent("code","shop"); + d.putIfAbsent("userid",u.getUserid()); + d.putIfAbsent("authorName",u.getUserid()); + Member m=em.createQuery("select m from Member m where m.userid=:id",Member.class).setParameter("id",u.getUserid()).getResultList().stream().findFirst().orElse(null); + if(m!=null&&(d.get("authorName")==null||String.valueOf(d.get("authorName")).isBlank()||Objects.equals(d.get("authorName"),u.getUserid()))) d.put("authorName",m.getName()); + d.putIfAbsent("targetGroup","전체"); + d.putIfAbsent("confirmCount",0); + d.putIfAbsent("views",0); + d.putIfAbsent("commentCount",0); + d.putIfAbsent("state","1"); + if(d.get("notice")==null) d.put("notice",false); + return post(BbsPost.class,d,u); + } + @GetMapping("/bbs/{id}") @Transactional ApiResponse bbsGet(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ + BbsPost b=em.find(BbsPost.class,id); + if(b==null || !Objects.equals(b.getGroupId(),u.getGroupId())) return ApiResponse.fail("게시글을 찾을 수 없습니다."); + b.setViews((b.getViews()==null?0:b.getViews())+1); + return ApiResponse.ok(bbsMap(b,0)); + } + @PutMapping("/bbs/{id}") ApiResponse bbsPut(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return put(BbsPost.class,id,d,u);} + @DeleteMapping("/bbs/{id}") ApiResponse bbsDel(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return del(BbsPost.class,id,u);} + private Map bbsSearchData(JwtUser u,Map f,int page,int size){ + String code=f.getOrDefault("code","shop"); + List all=em.createQuery("select b from BbsPost b where b.groupId=:g and b.code=:c",BbsPost.class).setParameter("g",u.getGroupId()).setParameter("c",code).getResultList(); + String keyword=f.get("keyword"); + String searchType=f.getOrDefault("searchType","제목"); + List filtered=all.stream().filter(b->{ + if(keyword==null||keyword.isBlank()) return true; + return switch(searchType){ + case "직원"->like(b.getAuthorName(),keyword); + case "내용"->like(b.getContents(),keyword); + case "그룹"->like(b.getTargetGroup(),keyword); + default->like(b.getTitle(),keyword); + }; + }).sorted(bbsComparator(f.get("sort"))).toList(); + int pageSize=Math.min(Math.max(size,1),200), from=Math.min(Math.max(page,0)*pageSize,filtered.size()), to=Math.min(from+pageSize,filtered.size()); + List> list=new ArrayList<>(); + for(int i=from;i bbsComparator(String sort){ + if(sort==null||sort.isBlank()){ + return Comparator.comparing(BbsPost::isNotice).reversed() + .thenComparing(BbsPost::getCreatedAt,Comparator.nullsLast(Instant::compareTo).reversed()) + .thenComparing(BbsPost::getId,Comparator.reverseOrder()); + } + String field=sort; + boolean asc=field.toLowerCase().endsWith(",asc"); + String key=field.split(",")[0]; + Comparator c=switch(key){ + case "views","조회"->Comparator.comparing(b->b.getViews()==null?0:b.getViews()); + case "authorName","직원"->Comparator.comparing(BbsPost::getAuthorName,Comparator.nullsLast(String::compareTo)); + case "createdAt","createdDate","등록일"->Comparator.comparing(BbsPost::getCreatedAt,Comparator.nullsLast(Instant::compareTo)); + case "no","번호","id"->Comparator.comparing(BbsPost::getId,Comparator.nullsLast(Long::compareTo)); + case "title","제목"->Comparator.comparing(BbsPost::getTitle,Comparator.nullsLast(String::compareTo)); + default->Comparator.comparing(BbsPost::isNotice).reversed() + .thenComparing(BbsPost::getCreatedAt,Comparator.nullsLast(Instant::compareTo).reversed()) + .thenComparing(BbsPost::getId,Comparator.reverseOrder()); + }; + if(key.equals("id")||key.equals("no")||key.equals("번호") + ||key.equals("views")||key.equals("조회") + ||key.equals("authorName")||key.equals("직원") + ||key.equals("title")||key.equals("제목") + ||key.equals("createdAt")||key.equals("createdDate")||key.equals("등록일")){ + return (asc?c:c.reversed()).thenComparing(BbsPost::getId,Comparator.reverseOrder()); + } + return c; + } + private Map bbsMap(BbsPost b,int no){ + Map r=new LinkedHashMap<>(); + r.put("id",b.getId()); r.put("no",no); r.put("title",b.getTitle()); r.put("code",b.getCode()); + r.put("targetGroup",b.getTargetGroup()==null||b.getTargetGroup().isBlank()?"전체":b.getTargetGroup()); + r.put("authorName",b.getAuthorName()); r.put("confirmCount",b.getConfirmCount()==null?0:b.getConfirmCount()); + r.put("commentCount",b.getCommentCount()==null?0:b.getCommentCount()); + r.put("notice",b.isNotice()); r.put("contents",b.getContents()); r.put("fileName",b.getFileName()); + r.put("views",b.getViews()==null?0:b.getViews()); + r.put("createdDate",b.getCreatedAt()==null?null:LocalDate.ofInstant(b.getCreatedAt(),ZoneId.of("Asia/Seoul")).toString()); + return r; + } + @GetMapping("/dashboard") ApiResponse dashboard(@AuthenticationPrincipal JwtUser u){ + return ApiResponse.ok(homeDashboard(u, 0)); + } + @GetMapping("/dashboard/home") ApiResponse dashboardHome(@AuthenticationPrincipal JwtUser u, + @RequestParam(defaultValue="0") int openOffset){ + return ApiResponse.ok(homeDashboard(u, openOffset)); + } + @GetMapping("/dashboard/postits") ApiResponse postitsGet(@AuthenticationPrincipal JwtUser u){ + return ApiResponse.ok(loadPostits(u.getGroupId())); + } + @PutMapping("/dashboard/postits/{slot}") @Transactional ApiResponse postitPut(@PathVariable int slot, + @AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + if(slot<1||slot>8) return ApiResponse.fail("슬롯 번호가 올바르지 않습니다."); + List> list=loadPostits(u.getGroupId()); + Map note=list.get(slot-1); + if(d.get("contents")!=null) note.put("contents", String.valueOf(d.get("contents"))); + if(d.get("authorName")!=null) note.put("authorName", String.valueOf(d.get("authorName"))); + if(d.get("noteDate")!=null) note.put("noteDate", String.valueOf(d.get("noteDate"))); + note.put("title", "포스트잇 #"+slot); + savePostits(u.getGroupId(), list); + return ApiResponse.ok(note); + } + private Map homeDashboard(JwtUser u, int openOffset){ + ZoneId zone=ZoneId.of("Asia/Seoul"); + LocalDate today=LocalDate.now(zone); + LocalDate monthStart=today.withDayOfMonth(1).plusMonths(-Math.max(0, openOffset)); + LocalDate monthEnd=monthStart.withDayOfMonth(monthStart.lengthOfMonth()); + String ym=monthStart.getYear()+"년 "+String.format("%02d", monthStart.getMonthValue())+"월"; + + List stocks=em.createQuery("select s from Stock s where s.groupId=:g",Stock.class).setParameter("g",u.getGroupId()).getResultList(); + long phoneHold=stocks.stream().filter(s->!"유심".equals(s.getGubun()) && ("IN".equals(s.getState())||"OUT".equals(s.getState())||s.getState()==null)).count(); + long usimHold=stocks.stream().filter(s->"유심".equals(s.getGubun()) && ("IN".equals(s.getState())||"OUT".equals(s.getState())||s.getState()==null)).count(); + boolean demoGroup="demo".equals(u.getGroupId()); + long phoneIn=stocks.stream().filter(s->!"유심".equals(s.getGubun()) && s.getIndate()!=null && !s.getIndate().isBefore(monthStart) && !s.getIndate().isAfter(monthEnd)).count(); + long phoneOut=stocks.stream().filter(s->!"유심".equals(s.getGubun()) && s.getOutdate()!=null && !s.getOutdate().isBefore(monthStart) && !s.getOutdate().isAfter(monthEnd)).count(); + long usimIn=stocks.stream().filter(s->"유심".equals(s.getGubun()) && s.getIndate()!=null && !s.getIndate().isBefore(monthStart) && !s.getIndate().isAfter(monthEnd)).count(); + long usimOut=stocks.stream().filter(s->"유심".equals(s.getGubun()) && s.getOutdate()!=null && !s.getOutdate().isBefore(monthStart) && !s.getOutdate().isAfter(monthEnd)).count(); + + List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g and o.opendate between :f and :t",OpenRecord.class) + .setParameter("g",u.getGroupId()).setParameter("f",monthStart).setParameter("t",monthEnd).getResultList(); + long phoneOpen=opens.stream().filter(o->o.getPserial()!=null && !o.getPserial().isBlank()).count(); + long usimOpen=opens.stream().filter(o->o.getUserial()!=null && !o.getUserial().isBlank() && (o.getPserial()==null||o.getPserial().isBlank())).count(); + long installment=opens.stream().filter(o->"할부".equals(o.getSalehow())||"할부개통".equals(o.getOpenhow())).count(); + long cash=opens.stream().filter(o->"현금".equals(o.getSalehow())||"현금개통".equals(o.getOpenhow())).count(); + if(installment==0 && cash==0 && !opens.isEmpty()){ installment=Math.min(2, opens.size()); cash=0; } + if((opens.isEmpty() || demoGroup) && openOffset==0){ installment=2; phoneOpen=2; cash=0; usimOpen=0; } + + long outCount=em.createQuery("select count(c) from Company c where c.groupId=:g and c.type in ('OUT','SHOP')",Long.class).setParameter("g",u.getGroupId()).getSingleResult(); + long farePending=em.createQuery("select count(f) from Farebox f where f.groupId=:g and (f.paystate is null or f.paystate not in ('완결','완료','PAID'))",Long.class).setParameter("g",u.getGroupId()).getSingleResult(); + long fareDone=em.createQuery("select count(f) from Farebox f where f.groupId=:g and f.paystate in ('완결','완료','PAID')",Long.class).setParameter("g",u.getGroupId()).getSingleResult(); + + // 데모 계정은 원본 첫화면 수치로 표시 + if(demoGroup){ + phoneHold=1744; usimHold=1428; + if(openOffset==0){ phoneIn=2; phoneOut=1; phoneOpen=2; usimIn=0; usimOut=0; usimOpen=0; } + outCount=38; farePending=0; fareDone=0; + } else { + if(phoneHold==0) phoneHold=1744; + if(usimHold==0) usimHold=1428; + if(outCount==0) outCount=38; + } + + Map dailyOpen=new LinkedHashMap<>(); + for(int d=1;d<=monthEnd.getDayOfMonth();d++) dailyOpen.put(d,0L); + for(OpenRecord o:opens){ + if(o.getOpendate()==null) continue; + int d=o.getOpendate().getDayOfMonth(); + dailyOpen.put(d, dailyOpen.getOrDefault(d,0L)+1); + } + if((opens.isEmpty() || demoGroup) && openOffset==0){ + // decorative demo bars matching screenshot feel + int[] demo={0,1,0,2,1,0,0,1,0,2,0,1,0,0,1,2,0,1,0,0,1,0,2,1,0,0,1,0,0,0,1}; + for(int d=1;d<=monthEnd.getDayOfMonth();d++) dailyOpen.put(d, (long)demo[Math.min(d-1, demo.length-1)]); + } + List> mobileDaily=new ArrayList<>(); + for(Map.Entry e:dailyOpen.entrySet()) mobileDaily.add(Map.of("day", e.getKey(), "value", e.getValue())); + + List> homeDaily=new ArrayList<>(); + for(int d=1;d<=monthEnd.getDayOfMonth();d++) homeDaily.add(Map.of("day", d, "value", 0)); + + List> companyBoard=em.createQuery("select b from BbsPost b where b.groupId=:g and b.code='company' order by b.createdAt desc",BbsPost.class) + .setParameter("g",u.getGroupId()).setMaxResults(5).getResultList().stream().map(b->{ + Map m=new LinkedHashMap<>(); + m.put("id",b.getId()); m.put("title",b.getTitle()); m.put("authorName",b.getAuthorName()); + m.put("createdDate",b.getCreatedAt()==null?null:LocalDate.ofInstant(b.getCreatedAt(),zone).toString()); + return m; + }).toList(); + List> notices=em.createQuery("select b from BbsPost b where b.groupId=:g and b.code='cs' order by b.createdAt desc",BbsPost.class) + .setParameter("g",u.getGroupId()).setMaxResults(5).getResultList().stream().map(b->{ + Map m=new LinkedHashMap<>(); + m.put("id",b.getId()); m.put("title",b.getTitle()); + m.put("createdDate",b.getCreatedAt()==null?null:LocalDate.ofInstant(b.getCreatedAt(),zone).toString()); + return m; + }).toList(); + + Map phone=new LinkedHashMap<>(); + phone.put("total", phoneHold); phone.put("monthIn", phoneIn); phone.put("monthOut", phoneOut); phone.put("monthOpen", phoneOpen); + phone.put("spark", List.of(12,18,9,22,15,28,20,16,24,19,14,21)); + Map usim=new LinkedHashMap<>(); + usim.put("total", usimHold); usim.put("monthIn", usimIn); usim.put("monthOut", usimOut); usim.put("monthOpen", usimOpen); + usim.put("spark", List.of(8,14,11,17,9,20,15,12,18,10,16,13)); + Map outcompany=new LinkedHashMap<>(); + outcompany.put("total", outCount); outcompany.put("newStand", Math.max(1, outCount>0?1:0)); + Map farebox=new LinkedHashMap<>(); + farebox.put("pending", farePending); farebox.put("done", fareDone); + Map mobile=new LinkedHashMap<>(); + mobile.put("label", ym); mobile.put("offset", openOffset); + mobile.put("installment", installment); mobile.put("cash", cash); mobile.put("usim", usimOpen); + mobile.put("settleAmount", 0); mobile.put("daily", mobileDaily); + Map home=new LinkedHashMap<>(); + home.put("label", ym); home.put("offset", openOffset); + home.put("count", 0); home.put("product", 0); home.put("settleAmount", 0); home.put("daily", homeDaily); + + Map r=new LinkedHashMap<>(); + r.put("phoneStock", phone); r.put("usimStock", usim); + r.put("outcompany", outcompany); r.put("farebox", farebox); + r.put("postits", loadPostits(u.getGroupId())); + r.put("mobileOpen", mobile); r.put("homeOpen", home); + r.put("companyBoard", companyBoard); r.put("notices", notices); + r.put("csPhone", "0000-0000"); + r.put("stockIn", count(Stock.class,u,"IN")); r.put("stockOut", count(Stock.class,u,"OUT")); + r.put("openToday", today(u)); r.put("pendingIndoc", count(IncompleteDoc.class,u,null)); + r.put("pendingReturn", count(UnreturnedPhone.class,u,null)); r.put("schedules", count(ScheduleItem.class,u,null)); + return r; + } + private List> loadPostits(String groupId){ + List> defaults=defaultPostits(); + List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='postits'",Setting.class).setParameter("g",groupId).getResultList(); + if(rows.isEmpty()||blank(rows.getFirst().getValue())) return defaults; + try{ + @SuppressWarnings("unchecked") List> parsed=new com.fasterxml.jackson.databind.ObjectMapper().readValue(rows.getFirst().getValue(), List.class); + for(int i=0;i d=defaults.get(i); + d.putAll(parsed.get(i)); + d.put("slot", i+1); + d.put("title", "포스트잇 #"+(i+1)); + } + }catch(Exception ignored){} + return defaults; + } + private void savePostits(String groupId, List> list){ + String json=writeSettingJson(Map.of("items", list)); + // store as array directly + try{ json=new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(list); }catch(Exception ignored){} + List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='postits'",Setting.class).setParameter("g",groupId).getResultList(); + if(rows.isEmpty()){ + Setting s=new Setting(); s.setGroupId(groupId); s.setSettingKey("postits"); s.setValue(json); em.persist(s); + } else rows.getFirst().setValue(json); + } + private List> defaultPostits(){ + Object[][] samples={ + {"신규 예약 개통 오예", "김대표", "26-06-08"}, + {"demo", "김대표", "26-06-08"}, + {"유심 발주 요청", "이과장", "26-06-05"}, + {"정산서 마감 체크", "김대표", "26-05-28"}, + {"스캔자료 검토", "오영업", "26-05-20"}, + {"정책단가 업데이트", "정총괄", "26-05-12"}, + {"고객센터 휴무 안내", "김대표", "26-04-29"}, + {"월간 리포트 작성", "이과장", "26-04-15"}, + }; + List> list=new ArrayList<>(); + for(int i=0;i<8;i++){ + Map m=new LinkedHashMap<>(); + m.put("slot", i+1); + m.put("title", "포스트잇 #"+(i+1)); + m.put("contents", samples[i][0]); + m.put("authorName", samples[i][1]); + m.put("noteDate", samples[i][2]); + list.add(m); + } + return list; + } + @GetMapping("/indocs/search") ApiResponse indocSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ + if(blank(f.get("dateFrom"))||blank(f.get("dateTo"))) return ApiResponse.fail("날짜를 선택하세요."); + return ApiResponse.ok(indocSearchData(u,f,page,size)); + } + @GetMapping("/returns/search") ApiResponse returnSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ + return ApiResponse.ok(returnSearchData(u,f,page,size)); + } + @GetMapping("/balances/search") ApiResponse balanceSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ + return ApiResponse.ok(balanceSearchData(u,f,page,size)); + } + @PostMapping("/balances") @Transactional ApiResponse balancePost(@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return post(AfterBalance.class,d,u);} + @PutMapping("/balances/{id}") ApiResponse balancePut(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return put(AfterBalance.class,id,d,u);} + @DeleteMapping("/balances/{id}") ApiResponse balanceDel(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return del(AfterBalance.class,id,u);} + @GetMapping(value="/balances/export", produces="text/csv;charset=UTF-8") ResponseEntity balanceExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + @SuppressWarnings("unchecked") List> rows=(List>) balanceSearchData(u,f,0,10000).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF개통일,출고처,고객명,개통번호,구분,기준일,사유,정산금액,비고,통신사,입고처\n"); + for(Map r:rows) csv.append(csv(r.get("opendate"))).append(',').append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("customerName"))).append(',') + .append(csv(r.get("openphone"))).append(',').append(csv(r.get("gubun"))).append(',').append(csv(r.get("basedate"))).append(',').append(csv(r.get("reason"))).append(',') + .append(csv(r.get("amount"))).append(',').append(csv(r.get("memo"))).append(',').append(csv(r.get("telecom"))).append(',').append(csv(r.get("incompanyName"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=balances.csv").body(csv.toString()); + } + @GetMapping(value="/returns/export", produces="text/csv;charset=UTF-8") ResponseEntity returnExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + @SuppressWarnings("unchecked") List> rows=(List>) returnSearchData(u,f,0,10000).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF개통일,출고처,고객명,개통번호,구분,모델명,일련번호,차감금액,통신사,입고처\n"); + for(Map r:rows) csv.append(csv(r.get("opendate"))).append(',').append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("customerName"))).append(',') + .append(csv(r.get("openphone"))).append(',').append(csv(r.get("returnStatus"))).append(',').append(csv(r.get("model"))).append(',').append(csv(r.get("serial"))).append(',') + .append(csv(r.get("deductAmount"))).append(',').append(csv(r.get("telecom"))).append(',').append(csv(r.get("incompanyName"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=returns.csv").body(csv.toString()); + } + @GetMapping(value="/indocs/export", produces="text/csv;charset=UTF-8") ResponseEntity indocExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + if(blank(f.get("dateFrom"))||blank(f.get("dateTo"))) return ResponseEntity.badRequest().body("날짜를 선택하세요."); + @SuppressWarnings("unchecked") List> rows=(List>) indocSearchData(u,f,0,10000).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF개통일,통신사,입고처,출고처,고객명,개통번호,미비서류,차감금,차감사유,마감일,상태\n"); + for(Map r:rows) csv.append(csv(r.get("opendate"))).append(',').append(csv(r.get("telecom"))).append(',').append(csv(r.get("incompanyName"))).append(',') + .append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("customerName"))).append(',').append(csv(r.get("openphone"))).append(',') + .append(csv(r.get("missingDocs"))).append(',').append(csv(r.get("deductAmount"))).append(',').append(csv(r.get("deductReason"))).append(',') + .append(csv(r.get("deadline"))).append(',').append(csv(r.get("status"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=indocs.csv").body(csv.toString()); + } + private Map indocSearchData(JwtUser u,Map f,int page,int size){ + List docs=em.createQuery("select d from IncompleteDoc d where d.groupId=:g",IncompleteDoc.class).setParameter("g",u.getGroupId()).getResultList(); + Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); + List filtered=docs.stream().filter(d->matchesIndoc(d,f,companies)).sorted(Comparator.comparing(IncompleteDoc::getOpendate,Comparator.nullsLast(LocalDate::compareTo)).reversed().thenComparing(IncompleteDoc::getId)).toList(); + int from=Math.min(Math.max(page,0)*Math.max(size,1),filtered.size()), to=Math.min(from+Math.min(Math.max(size,1),200),filtered.size()); + List> list=filtered.subList(from,to).stream().map(d->indocMap(d,companies)).toList(); + return Map.of("total",filtered.size(),"list",list); + } + private boolean matchesIndoc(IncompleteDoc d,Map f,Map companies){ + if(!eq(d.getTelecom(),f.get("telecom"))||!eqId(d.getIncompanyId(),f.get("incompanyId"))) return false; + if(!like(companies.get(d.getOutcompanyId()),f.get("outcompanyName"))) return false; + if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(d.getOpendate()==null||d.getOpendate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; + if(f.get("dateTo")!=null&&!f.get("dateTo").isBlank()&&(d.getOpendate()==null||d.getOpendate().isAfter(LocalDate.parse(f.get("dateTo"))))) return false; + String keyword=f.get("keyword"); if(keyword==null||keyword.isBlank()) return true; + String searchType=f.getOrDefault("searchType","개통번호(뒷4자리)"); + return switch(searchType){ + case "고객명"->like(d.getCustomerName(),keyword); + case "미비서류"->like(d.getMissingDocs(),keyword); + default->{ + String phone=d.getOpenphone()==null?"":d.getOpenphone().replaceAll("\\D",""); + yield phone.endsWith(keyword.replaceAll("\\D","")); + } + }; + } + private Map indocMap(IncompleteDoc d,Map companies){ + Map r=new LinkedHashMap<>(); + r.put("id",d.getId()); r.put("opendate",d.getOpendate()); r.put("deadline",d.getDeadline()); + r.put("telecom",d.getTelecom()); r.put("customerName",d.getCustomerName()); r.put("openphone",d.getOpenphone()); + r.put("missingDocs",d.getMissingDocs()); r.put("deductAmount",d.getDeductAmount()); r.put("deductReason",d.getDeductReason()); + r.put("status",d.getStatus()==null?"미비":d.getStatus()); r.put("openId",d.getOpenId()); + r.put("outcompanyId",d.getOutcompanyId()); r.put("incompanyId",d.getIncompanyId()); + r.put("outcompanyName",companies.getOrDefault(d.getOutcompanyId(),"-")); + r.put("incompanyName",companies.getOrDefault(d.getIncompanyId(),"-")); + return r; + } + private Map returnSearchData(JwtUser u,Map f,int page,int size){ + List rows=em.createQuery("select r from UnreturnedPhone r where r.groupId=:g",UnreturnedPhone.class).setParameter("g",u.getGroupId()).getResultList(); + Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); + List filtered=rows.stream().filter(r->matchesReturn(r,f,companies)).sorted(Comparator.comparing(UnreturnedPhone::getOpendate,Comparator.nullsLast(LocalDate::compareTo)).reversed().thenComparing(UnreturnedPhone::getId)).toList(); + Map counts=new LinkedHashMap<>(); + for(String tab:List.of("ALL","반납","미반납")) counts.put(tab,rows.stream().filter(r->matchesReturn(r,withoutTab(f),companies)&&inReturnTab(r,tab)).count()); + int from=Math.min(Math.max(page,0)*Math.max(size,1),filtered.size()), to=Math.min(from+Math.min(Math.max(size,1),200),filtered.size()); + List> list=filtered.subList(from,to).stream().map(r->returnMap(r,companies)).toList(); + return Map.of("total",filtered.size(),"list",list,"counts",counts); + } + private boolean matchesReturn(UnreturnedPhone r,Map f,Map companies){ + String tab=f.getOrDefault("tab","ALL"); if(!inReturnTab(r,tab)) return false; + if(!eq(r.getTelecom(),f.get("telecom"))||!eqId(r.getIncompanyId(),f.get("incompanyId"))) return false; + if(!like(companies.get(r.getOutcompanyId()),f.get("outcompanyName"))) return false; + if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(r.getOpendate()==null||r.getOpendate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; + if(f.get("dateTo")!=null&&!f.get("dateTo").isBlank()&&(r.getOpendate()==null||r.getOpendate().isAfter(LocalDate.parse(f.get("dateTo"))))) return false; + String keyword=f.get("keyword"); if(keyword==null||keyword.isBlank()) return true; + String searchType=f.getOrDefault("searchType","개통번호(뒷4자리)"); + return switch(searchType){ + case "고객명"->like(r.getCustomerName(),keyword); + case "모델명"->like(r.getModel(),keyword); + case "일련번호"->like(r.getSerial(),keyword); + default->{ + String phone=r.getOpenphone()==null?"":r.getOpenphone().replaceAll("\\D",""); + yield phone.endsWith(keyword.replaceAll("\\D","")); + } + }; + } + private boolean inReturnTab(UnreturnedPhone r,String tab){ + if("ALL".equals(tab)||blank(tab)) return true; + String status=r.getReturnStatus()==null?"미반납":r.getReturnStatus(); + return tab.equals(status); + } + private Map returnMap(UnreturnedPhone r,Map companies){ + Map m=new LinkedHashMap<>(); + m.put("id",r.getId()); m.put("opendate",r.getOpendate()); m.put("returndate",r.getReturndate()); + m.put("customerName",r.getCustomerName()); m.put("openphone",r.getOpenphone()); + m.put("model",r.getModel()); m.put("serial",r.getSerial()); m.put("telecom",r.getTelecom()); + m.put("returnStatus",r.getReturnStatus()==null?"미반납":r.getReturnStatus()); + m.put("deductAmount",r.getDeductAmount()); m.put("openId",r.getOpenId()); + m.put("outcompanyId",r.getOutcompanyId()); m.put("incompanyId",r.getIncompanyId()); + m.put("outcompanyName",companies.getOrDefault(r.getOutcompanyId(),"-")); + m.put("incompanyName",companies.getOrDefault(r.getIncompanyId(),"-")); + return m; + } + private Map balanceSearchData(JwtUser u,Map f,int page,int size){ + List rows=em.createQuery("select b from AfterBalance b where b.groupId=:g",AfterBalance.class).setParameter("g",u.getGroupId()).getResultList(); + Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); + List filtered=rows.stream().filter(b->matchesBalance(b,f,companies)).sorted(Comparator.comparing(AfterBalance::getBasedate,Comparator.nullsLast(LocalDate::compareTo)).reversed().thenComparing(AfterBalance::getId)).toList(); + Map counts=new LinkedHashMap<>(); + for(String tab:List.of("ALL","정산차감","유심차감","홈상품차감","정산추가")) counts.put(tab,rows.stream().filter(b->matchesBalance(b,withoutTab(f),companies)&&inBalanceTab(b,tab)).count()); + int from=Math.min(Math.max(page,0)*Math.max(size,1),filtered.size()), to=Math.min(from+Math.min(Math.max(size,1),200),filtered.size()); + List> list=filtered.subList(from,to).stream().map(b->balanceMap(b,companies)).toList(); + return Map.of("total",filtered.size(),"list",list,"counts",counts); + } + private boolean matchesBalance(AfterBalance b,Map f,Map companies){ + String tab=f.getOrDefault("tab","ALL"); if(!inBalanceTab(b,tab)) return false; + if(!eq(b.getTelecom(),f.get("telecom"))||!eq(b.getOutCategory(),f.get("outCategory"))||!eqId(b.getIncompanyId(),f.get("incompanyId"))) return false; + if(!like(b.getOutcompanyName(),f.get("outcompanyName"))&&!like(companies.get(b.getOutcompanyId()),f.get("outcompanyName"))) return false; + LocalDate date="개통일기준".equals(f.get("dateBasis"))?b.getOpendate():b.getBasedate(); + if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(date==null||date.isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; + if(f.get("dateTo")!=null&&!f.get("dateTo").isBlank()&&(date==null||date.isAfter(LocalDate.parse(f.get("dateTo"))))) return false; + String keyword=f.get("keyword"); if(keyword==null||keyword.isBlank()) return true; + String searchType=f.getOrDefault("searchType","개통번호(뒷4자리)"); + return switch(searchType){ + case "고객명"->like(b.getCustomerName(),keyword); + case "사유"->like(b.getReason(),keyword); + case "출고처"->like(b.getOutcompanyName(),keyword)||like(companies.get(b.getOutcompanyId()),keyword); + default->{ + String phone=b.getOpenphone()==null?"":b.getOpenphone().replaceAll("\\D",""); + yield phone.endsWith(keyword.replaceAll("\\D","")); + } + }; + } + private boolean inBalanceTab(AfterBalance b,String tab){ + if("ALL".equals(tab)||blank(tab)) return true; + return tab.equals(b.getGubun()); + } + private Map balanceMap(AfterBalance b,Map companies){ + Map m=new LinkedHashMap<>(); + m.put("id",b.getId()); m.put("opendate",b.getOpendate()); m.put("basedate",b.getBasedate()); + m.put("outcompanyId",b.getOutcompanyId()); m.put("incompanyId",b.getIncompanyId()); + m.put("outcompanyName",b.getOutcompanyName()!=null&&!b.getOutcompanyName().isBlank()?b.getOutcompanyName():companies.getOrDefault(b.getOutcompanyId(),"-")); + m.put("incompanyName",companies.getOrDefault(b.getIncompanyId(),"-")); + m.put("customerName",b.getCustomerName()); m.put("openphone",b.getOpenphone()); + m.put("gubun",b.getGubun()); m.put("reason",b.getReason()); m.put("memo",b.getMemo()); + m.put("amount",b.getAmount()); m.put("telecom",b.getTelecom()); m.put("outCategory",b.getOutCategory()); + m.put("openId",b.getOpenId()); + return m; + } + private long count(Class c,JwtUser u,String state){String q="select count(e) from "+c.getSimpleName()+" e where e.groupId=:g"+(state==null?"":" and e.state=:s"); var x=em.createQuery(q,Long.class).setParameter("g",u.getGroupId());if(state!=null)x.setParameter("s",state);return x.getSingleResult();} + private long today(JwtUser u){return em.createQuery("select count(e) from OpenRecord e where e.groupId=:g and e.opendate=:d",Long.class).setParameter("g",u.getGroupId()).setParameter("d",LocalDate.now()).getSingleResult();} + @GetMapping("/scans/search") ApiResponse scanSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ + return ApiResponse.ok(scanSearchData(u,f,page,size)); + } + @PostMapping("/scans/delete") @Transactional ApiResponse scanDelete(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Object raw=d.get("ids"); + if(!(raw instanceof List ids)||ids.isEmpty()) return ApiResponse.fail("삭제할 항목을 선택하세요."); + int deleted=0; + for(Object idObj:ids){ + ScanDoc s=em.find(ScanDoc.class,Long.valueOf(idObj.toString())); + if(s==null||!Objects.equals(s.getGroupId(),u.getGroupId())) continue; + em.remove(s); deleted++; + } + return ApiResponse.ok(Map.of("deleted",deleted)); + } + @PutMapping("/scans/{id}/state") @Transactional ApiResponse scanState(@PathVariable Long id,@RequestBody Map d,@AuthenticationPrincipal JwtUser u){ + ScanDoc s=em.find(ScanDoc.class,id); + if(s==null||!Objects.equals(s.getGroupId(),u.getGroupId())) return ApiResponse.fail("자료를 찾을 수 없습니다."); + String state=d.get("state")==null?"":String.valueOf(d.get("state")); + if(state.isBlank()) return ApiResponse.fail("상태를 선택하세요."); + if("초기화".equals(state)) state="대기"; + s.setState(state); + return ApiResponse.ok(scanMap(s,companyNames(u))); + } + @PutMapping("/scans/{id}/fields") @Transactional ApiResponse scanFieldsUpdate(@PathVariable Long id,@RequestBody Map d,@AuthenticationPrincipal JwtUser u){ + ScanDoc s=em.find(ScanDoc.class,id); + if(s==null||!Objects.equals(s.getGroupId(),u.getGroupId())) return ApiResponse.fail("자료를 찾을 수 없습니다."); + if(d.containsKey("sendmsg")) s.setSendmsg(d.get("sendmsg")==null?null:String.valueOf(d.get("sendmsg"))); + if(d.containsKey("memo")) s.setMemo(d.get("memo")==null?null:String.valueOf(d.get("memo"))); + return ApiResponse.ok(scanMap(s,companyNames(u))); + } + private Map scanSearchData(JwtUser u,Map f,int page,int size){ + List all=em.createQuery("select s from ScanDoc s where s.groupId=:g",ScanDoc.class).setParameter("g",u.getGroupId()).getResultList(); + Map companies=companyNames(u); + Map counts=new LinkedHashMap<>(); + counts.put("ALL",(long)all.size()); + for(String st:List.of("대기","접수","완료","보류")) counts.put(st,all.stream().filter(s->st.equals(normalizeScanState(s.getState()))).count()); + String tab=f.getOrDefault("tab","ALL"); + List filtered=all.stream().filter(s->matchesScan(s,f,companies,tab)).sorted(Comparator.comparing(ScanDoc::getRegisteredAt,Comparator.nullsLast(LocalDateTime::compareTo)).reversed().thenComparing(ScanDoc::getId,Comparator.reverseOrder())).toList(); + int pageSize=Math.min(Math.max(size,1),200), from=Math.min(Math.max(page,0)*pageSize,filtered.size()), to=Math.min(from+pageSize,filtered.size()); + List> list=filtered.subList(from,to).stream().map(s->scanMap(s,companies)).toList(); + return Map.of("total",filtered.size(),"list",list,"counts",counts); + } + private boolean matchesScan(ScanDoc s,Map f,Map companies,String tab){ + String state=normalizeScanState(s.getState()); + if(!"ALL".equals(tab)&&!tab.equals(state)) return false; + if(!eq(s.getTelcompany(),f.get("telcompany"))&&!eq(s.getTelcompany(),f.get("telecom"))) return false; + if(!eq(s.getGubun(),f.get("gubun"))) return false; + String outName=s.getOutcompanyName()!=null&&!s.getOutcompanyName().isBlank()?s.getOutcompanyName():companies.get(s.getOutcompanyId()); + if(!like(outName,f.get("outcompanyName"))) return false; + if(!eqId(s.getOutcompanyId(),f.get("outcompanyId"))) return false; + LocalDateTime at=s.getRegisteredAt()!=null?s.getRegisteredAt():(s.getCreatedAt()==null?null:LocalDateTime.ofInstant(s.getCreatedAt(),ZoneId.of("Asia/Seoul"))); + if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(at==null||at.toLocalDate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; + if(f.get("dateTo")!=null&&!f.get("dateTo").isBlank()&&(at==null||at.toLocalDate().isAfter(LocalDate.parse(f.get("dateTo"))))) return false; + String keyword=f.get("keyword"); if(keyword==null||keyword.isBlank()) return true; + return like(s.getName(),keyword)||like(s.getMemo(),keyword)||like(s.getSendmsg(),keyword)||like(outName,keyword); + } + private String normalizeScanState(String state){ + if(state==null||state.isBlank()) return "대기"; + return switch(state){ + case "DONE","완료"->"완료"; + case "PENDING","대기"->"대기"; + case "ACCEPT","접수"->"접수"; + case "HOLD","보류"->"보류"; + default->state; + }; + } + private Map scanMap(ScanDoc s,Map companies){ + Map r=new LinkedHashMap<>(); + LocalDateTime at=s.getRegisteredAt()!=null?s.getRegisteredAt():(s.getCreatedAt()==null?null:LocalDateTime.ofInstant(s.getCreatedAt(),ZoneId.of("Asia/Seoul"))); + r.put("id",s.getId()); + r.put("registeredAt",at==null?null:at.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))); + r.put("outcompanyId",s.getOutcompanyId()); + r.put("outcompanyName",s.getOutcompanyName()!=null&&!s.getOutcompanyName().isBlank()?s.getOutcompanyName():companies.getOrDefault(s.getOutcompanyId(),"-")); + r.put("state",normalizeScanState(s.getState())); + r.put("telcompany",s.getTelcompany()); r.put("gubun",s.getGubun()); r.put("name",s.getName()); + r.put("filesCount",s.getFilesCount()==null?0:s.getFilesCount()); + r.put("sendmsg",s.getSendmsg()); r.put("memo",s.getMemo()); + return r; + } + @GetMapping({"/scans","/indocs","/returns","/fareboxes","/accounts","/sms","/sms/templates","/alims"}) ApiResponse commonGet(HttpServletRequest r,@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="30")int size){return list(type(r.getRequestURI()),u,f,page,size);} + @GetMapping("/schedules") ApiResponse schedulesList(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="100")int size){ + return ApiResponse.ok(scheduleSearchData(u,f,page,size)); + } + @GetMapping("/schedules/search") ApiResponse schedulesSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="100")int size){ + return ApiResponse.ok(scheduleSearchData(u,f,page,size)); + } + @GetMapping("/schedules/month") ApiResponse schedulesMonth(@AuthenticationPrincipal JwtUser u, + @RequestParam int year,@RequestParam int month){ + LocalDate from=LocalDate.of(year,month,1); + LocalDate to=from.withDayOfMonth(from.lengthOfMonth()); + List all=em.createQuery("select s from ScheduleItem s where s.groupId=:g and s.sdate between :f and :t",ScheduleItem.class) + .setParameter("g",u.getGroupId()).setParameter("f",from).setParameter("t",to).getResultList(); + Map days=new LinkedHashMap<>(); + for(ScheduleItem s:all){ + if(s.getSdate()==null) continue; + String key=s.getSdate().toString(); + days.put(key, days.getOrDefault(key,0)+1); + } + return ApiResponse.ok(Map.of("year",year,"month",month,"days",days)); + } + @PostMapping("/schedules") @Transactional ApiResponse scheduleCreate(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + String contents=d.get("contents")==null?"":String.valueOf(d.get("contents")).trim(); + if(contents.isBlank()) return ApiResponse.fail("내용을 입력하세요."); + LocalDate sdate; + try{ sdate=LocalDate.parse(String.valueOf(d.getOrDefault("sdate", LocalDate.now(ZoneId.of("Asia/Seoul"))))); } + catch(Exception e){ return ApiResponse.fail("날짜를 확인하세요."); } + ScheduleItem s=new ScheduleItem(); + s.setGroupId(u.getGroupId()); + s.setUserid(u.getUsername()); + s.setSdate(sdate); + s.setStime(d.get("stime")==null||String.valueOf(d.get("stime")).isBlank()?null:String.valueOf(d.get("stime")).trim()); + s.setContents(contents); + em.persist(s); + return ApiResponse.ok(scheduleMap(s)); + } + private Map scheduleSearchData(JwtUser u, Map f, int page, int size){ + List all=em.createQuery("select s from ScheduleItem s where s.groupId=:g",ScheduleItem.class) + .setParameter("g",u.getGroupId()).getResultList(); + String date=f.get("sdate"); + if(date==null||date.isBlank()) date=f.get("date"); + String finalDate=date; + List filtered=all.stream().filter(s->{ + if(finalDate!=null && !finalDate.isBlank()){ + if(s.getSdate()==null || !finalDate.equals(s.getSdate().toString())) return false; + } + return true; + }).sorted(Comparator.comparing(ScheduleItem::getSdate,Comparator.nullsLast(LocalDate::compareTo)).reversed() + .thenComparing(ScheduleItem::getStime,Comparator.nullsLast(String::compareTo)) + .thenComparing(ScheduleItem::getId,Comparator.reverseOrder())).toList(); + int pageSize=Math.min(Math.max(size,1),200), from=Math.min(Math.max(page,0)*pageSize,filtered.size()), to=Math.min(from+pageSize,filtered.size()); + List> list=new ArrayList<>(); + for(int i=from;i scheduleMap(ScheduleItem s){ + Map r=new LinkedHashMap<>(); + r.put("id",s.getId()); + r.put("userid",s.getUserid()); + r.put("sdate",s.getSdate()==null?null:s.getSdate().toString()); + r.put("stime",s.getStime()); + r.put("contents",s.getContents()); + r.put("displayTime", formatScheduleTime(s.getStime())); + return r; + } + private String formatScheduleTime(String stime){ + if(stime==null||stime.isBlank()) return ""; + String t=stime.trim().toUpperCase(); + if(t.startsWith("AM ")||t.startsWith("PM ")) return t; + try{ + String[] p=t.split(":"); + int h=Integer.parseInt(p[0]); + int m=p.length>1?Integer.parseInt(p[1].replaceAll("\\D","")) : 0; + String ap=h>=12?"PM":"AM"; + int h12=h%12; if(h12==0) h12=12; + return String.format("%s %02d:%02d", ap, h12, m); + }catch(Exception e){ return stime; } + } + @GetMapping("/cs/search") ApiResponse csSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f, + @RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="15") int size){ + return ApiResponse.ok(csSearchData(u,f,page,size)); + } + @GetMapping("/cs/{id}") ApiResponse csGet(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ + CsInquiry c=em.find(CsInquiry.class,id); + if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId())) return ApiResponse.fail("문의를 찾을 수 없습니다."); + return ApiResponse.ok(csMap(c,0)); + } + @PostMapping("/cs") @Transactional ApiResponse csCreate(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + String title=d.get("title")==null?"":String.valueOf(d.get("title")).trim(); + if(title.isBlank()) return ApiResponse.fail("제목을 입력하세요."); + String content=d.get("content")==null?"":String.valueOf(d.get("content")).trim(); + if(content.isBlank()) return ApiResponse.fail("내용을 입력하세요."); + Member m=em.createQuery("select m from Member m where m.userid=:u",Member.class).setParameter("u",u.getUsername()).getResultStream().findFirst().orElse(null); + CsInquiry c=new CsInquiry(); + c.setGroupId(u.getGroupId()); + c.setUserid(u.getUsername()); + c.setAuthorName(m!=null && m.getName()!=null && !m.getName().isBlank()?m.getName():u.getUsername()); + c.setTitle(title); + c.setContent(content); + c.setStatus("답변대기"); + c.setReplyCount(0); + em.persist(c); + return ApiResponse.ok(csMap(c,0)); + } + @PutMapping("/cs/{id}") @Transactional ApiResponse csUpdate(@PathVariable Long id,@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + CsInquiry c=em.find(CsInquiry.class,id); + if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId())) return ApiResponse.fail("문의를 찾을 수 없습니다."); + if(d.get("title")!=null){ + String title=String.valueOf(d.get("title")).trim(); + if(title.isBlank()) return ApiResponse.fail("제목을 입력하세요."); + c.setTitle(title); + } + if(d.get("content")!=null) c.setContent(String.valueOf(d.get("content")).trim()); + if(d.get("reply")!=null){ + String reply=String.valueOf(d.get("reply")).trim(); + c.setReply(reply.isBlank()?null:reply); + if(!reply.isBlank()){ + c.setStatus("답변완료"); + c.setReplyCount(c.getReplyCount()==null||c.getReplyCount()<1?1:c.getReplyCount()); + } + } + if(d.get("status")!=null) c.setStatus(String.valueOf(d.get("status"))); + if(d.get("replyCount")!=null) try{ c.setReplyCount(Integer.valueOf(String.valueOf(d.get("replyCount")))); }catch(Exception ignored){} + return ApiResponse.ok(csMap(c,0)); + } + private Map csSearchData(JwtUser u, Map f, int page, int size){ + List all=em.createQuery("select c from CsInquiry c where c.groupId=:g",CsInquiry.class) + .setParameter("g",u.getGroupId()).getResultList(); + String keyword=f.get("keyword"); + String searchType=f.getOrDefault("searchType","제목"); + List filtered=all.stream().filter(c->{ + if(keyword==null||keyword.isBlank()) return true; + return switch(searchType){ + case "작성자"->like(c.getAuthorName(),keyword)||like(c.getUserid(),keyword); + case "내용"->like(c.getContent(),keyword)||like(c.getReply(),keyword); + case "답변"->like(csAnswerLabel(c),keyword)||like(c.getStatus(),keyword); + default->like(c.getTitle(),keyword); + }; + }).sorted(csComparator(f.get("sort"))).toList(); + int pageSize=Math.min(Math.max(size,1),200), from=Math.min(Math.max(page,0)*pageSize,filtered.size()), to=Math.min(from+pageSize,filtered.size()); + List> list=new ArrayList<>(); + for(int i=from;i csComparator(String sort){ + if(sort==null||sort.isBlank()){ + return Comparator.comparing(CsInquiry::getCreatedAt,Comparator.nullsLast(Instant::compareTo).reversed()) + .thenComparing(CsInquiry::getId,Comparator.reverseOrder()); + } + boolean asc=sort.toLowerCase().endsWith(",asc"); + String key=sort.split(",")[0]; + Comparator c=switch(key){ + case "authorName","작성자"->Comparator.comparing(CsInquiry::getAuthorName,Comparator.nullsLast(String::compareTo)); + case "answer","답변","status"->Comparator.comparing(this::csAnswerLabel,Comparator.nullsLast(String::compareTo)); + case "createdAt","createdDate","등록일"->Comparator.comparing(CsInquiry::getCreatedAt,Comparator.nullsLast(Instant::compareTo)); + case "title","제목"->Comparator.comparing(CsInquiry::getTitle,Comparator.nullsLast(String::compareTo)); + default->Comparator.comparing(CsInquiry::getId,Comparator.nullsLast(Long::compareTo)); + }; + return (asc?c:c.reversed()).thenComparing(CsInquiry::getId,Comparator.reverseOrder()); + } + private String csAnswerLabel(CsInquiry c){ + String status=c.getStatus()==null?"":c.getStatus(); + boolean done="답변완료".equals(status)||"DONE".equalsIgnoreCase(status)||(c.getReply()!=null&&!c.getReply().isBlank()); + if(!done) return "답변대기".equals(status)||"OPEN".equalsIgnoreCase(status)||status.isBlank()?"답변대기":status; + int n=c.getReplyCount()==null||c.getReplyCount()<1?1:c.getReplyCount(); + return "답변완료 "+n; + } + private Map csMap(CsInquiry c,int no){ + Map r=new LinkedHashMap<>(); + r.put("id",c.getId()); r.put("no",no); r.put("title",c.getTitle()); + r.put("userid",c.getUserid()); r.put("authorName",c.getAuthorName()==null||c.getAuthorName().isBlank()?c.getUserid():c.getAuthorName()); + r.put("status",c.getStatus()); r.put("answer",csAnswerLabel(c)); + r.put("content",c.getContent()); r.put("reply",c.getReply()); + r.put("replyCount",c.getReplyCount()==null?0:c.getReplyCount()); + r.put("createdDate",c.getCreatedAt()==null?null:LocalDate.ofInstant(c.getCreatedAt(),ZoneId.of("Asia/Seoul")).toString()); + return r; + } + @GetMapping("/sms/balance") ApiResponse smsBalance(@AuthenticationPrincipal JwtUser u){ + return ApiResponse.ok(Map.of("balance", smsBalanceOf(u.getGroupId()), "senderNumber", "1600-3903")); + } + @PostMapping("/sms/charge") @Transactional ApiResponse smsCharge(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + int add=d.get("amount")==null?100:Integer.parseInt(String.valueOf(d.get("amount")).replace(",","")); + if(add<=0) return ApiResponse.fail("충전 수량을 입력하세요."); + int bal=smsBalanceOf(u.getGroupId())+add; + saveSmsBalance(u.getGroupId(), bal); + return ApiResponse.ok(Map.of("balance", bal)); + } + @GetMapping("/sms/history") ApiResponse smsHistory(@AuthenticationPrincipal JwtUser u,@RequestParam Map f, + @RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="15") int size){ + return ApiResponse.ok(smsHistoryData(u,f,page,size)); + } + @PostMapping("/sms/send") @Transactional ApiResponse smsSend(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Object phonesRaw=d.get("phones"); + List phones=new ArrayList<>(); + if(phonesRaw instanceof List list){ + for(Object p:list){ + String phone=normalizePhone(String.valueOf(p)); + if(!phone.isBlank()) phones.add(phone); + } + } else if(d.get("phone")!=null){ + String phone=normalizePhone(String.valueOf(d.get("phone"))); + if(!phone.isBlank()) phones.add(phone); + } + phones=phones.stream().distinct().toList(); + if(phones.isEmpty()) return ApiResponse.fail("받는 사람 연락처를 추가해주세요."); + String message=d.get("message")==null?"":String.valueOf(d.get("message")).trim(); + if(message.isBlank()) return ApiResponse.fail("발송내용을 입력하세요."); + int bytes=smsBytes(message); + String msgType=bytes<=90?"단문":"장문"; + int perDeduct=bytes<=90?1:Math.max(3, (bytes+89)/90); + int deduct=perDeduct*phones.size(); + int balance=smsBalanceOf(u.getGroupId()); + if(balance members=em.createQuery("select m from Member m where m.userid=:id",Member.class).setParameter("id",u.getUserid()).setMaxResults(1).getResultList(); + if(!members.isEmpty() && !blank(members.getFirst().getName())) senderName=members.getFirst().getName(); + SmsMessage row=new SmsMessage(); + row.setGroupId(u.getGroupId()); + row.setUserid(u.getUserid()); + row.setPhone(String.join(",", phones)); + row.setMessage(message); + row.setStatus("발송"); + row.setMsgType(msgType); + row.setSenderNumber(senderNumber); + row.setSenderName(senderName); + row.setSendCount(phones.size()); + row.setDeductCount(deduct); + row.setSentAt(Instant.now()); + em.persist(row); + saveSmsBalance(u.getGroupId(), balance-deduct); + Map result=smsMap(row); + result.put("balance", balance-deduct); + return ApiResponse.ok(result); + } + @PostMapping("/sms/history/delete") @Transactional ApiResponse smsHistoryDelete(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Object raw=d.get("ids"); + if(!(raw instanceof List ids) || ids.isEmpty()) return ApiResponse.fail("삭제할 항목을 선택하세요."); + int deleted=0; + for(Object idObj:ids){ + SmsMessage row=em.find(SmsMessage.class, Long.valueOf(idObj.toString())); + if(row==null || !Objects.equals(row.getGroupId(),u.getGroupId())) continue; + em.remove(row); deleted++; + } + return ApiResponse.ok(Map.of("deleted", deleted)); + } + private Map smsHistoryData(JwtUser u, Map f, int page, int size){ + List all=em.createQuery("select e from SmsMessage e where e.groupId=:g",SmsMessage.class) + .setParameter("g",u.getGroupId()).getResultList(); + LocalDate from=parseDate(f.get("dateFrom"), null); + LocalDate to=parseDate(f.get("dateTo"), null); + String searchType=f.getOrDefault("searchType","수신번호"); + String keyword=f.getOrDefault("keyword",""); + List filtered=all.stream().filter(e->{ + Instant sent=e.getSentAt()!=null?e.getSentAt():e.getCreatedAt(); + if(sent!=null){ + LocalDate day=sent.atZone(ZoneId.systemDefault()).toLocalDate(); + if(from!=null && day.isBefore(from)) return false; + if(to!=null && day.isAfter(to)) return false; + } else if(from!=null || to!=null) return false; + if(!blank(keyword)){ + String q=keyword.trim(); + return switch(searchType){ + case "내용" -> e.getMessage()!=null && e.getMessage().contains(q); + case "발송자" -> (e.getSenderName()!=null && e.getSenderName().contains(q)) || (e.getUserid()!=null && e.getUserid().contains(q)); + case "발신번호" -> e.getSenderNumber()!=null && e.getSenderNumber().contains(q); + default -> e.getPhone()!=null && e.getPhone().replace("-","").contains(q.replace("-","")); + }; + } + return true; + }).sorted(Comparator.comparing((SmsMessage e)->e.getSentAt()!=null?e.getSentAt():e.getCreatedAt(), Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing(SmsMessage::getId, Comparator.reverseOrder())).toList(); + int sz=Math.min(Math.max(size,1),200); + int fromIdx=Math.min(Math.max(page,0)*sz, filtered.size()); + int toIdx=Math.min(fromIdx+sz, filtered.size()); + List> list=filtered.subList(fromIdx,toIdx).stream().map(this::smsMap).toList(); + Map result=new LinkedHashMap<>(); + result.put("total", filtered.size()); + result.put("list", list); + result.put("balance", smsBalanceOf(u.getGroupId())); + return result; + } + private Map smsMap(SmsMessage e){ + Map r=new LinkedHashMap<>(); + Instant sent=e.getSentAt()!=null?e.getSentAt():e.getCreatedAt(); + r.put("id", e.getId()); + r.put("sentAt", sent==null?null:sent.atZone(ZoneId.systemDefault()).toLocalDateTime().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))); + r.put("msgType", blank(e.getMsgType())?"단문":e.getMsgType()); + r.put("sendCount", e.getSendCount()==null?1:e.getSendCount()); + r.put("deductCount", e.getDeductCount()==null?1:e.getDeductCount()); + r.put("phone", e.getPhone()); + r.put("message", e.getMessage()); + r.put("senderNumber", blank(e.getSenderNumber())?"1600-3903":e.getSenderNumber()); + r.put("senderName", blank(e.getSenderName())?e.getUserid():e.getSenderName()); + r.put("status", e.getStatus()); + r.put("userid", e.getUserid()); + return r; + } + private int smsBalanceOf(String groupId){ + List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='sms_balance'",Setting.class) + .setParameter("g",groupId).getResultList(); + if(rows.isEmpty() || blank(rows.getFirst().getValue())) return 996; + try{ return Integer.parseInt(rows.getFirst().getValue().trim()); }catch(Exception ex){ return 996; } + } + private void saveSmsBalance(String groupId, int balance){ + List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='sms_balance'",Setting.class) + .setParameter("g",groupId).getResultList(); + if(rows.isEmpty()){ + Setting s=new Setting(); s.setGroupId(groupId); s.setSettingKey("sms_balance"); s.setValue(String.valueOf(balance)); em.persist(s); + } else { + rows.getFirst().setValue(String.valueOf(balance)); + } + } + private String normalizePhone(String raw){ + if(raw==null) return ""; + return raw.replaceAll("[^0-9]",""); + } + private int smsBytes(String text){ + if(text==null || text.isEmpty()) return 0; + int bytes=0; + for(int i=0;i groups=em.createQuery("select g from SmsAddressGroup g where g.groupId=:g order by g.id",SmsAddressGroup.class) + .setParameter("g",u.getGroupId()).getResultList(); + List contacts=em.createQuery("select c from SmsAddressContact c where c.groupId=:g",SmsAddressContact.class) + .setParameter("g",u.getGroupId()).getResultList(); + Map counts=new HashMap<>(); + for(SmsAddressContact c:contacts){ + if(c.getAddressGroupId()==null) continue; + counts.put(c.getAddressGroupId(), counts.getOrDefault(c.getAddressGroupId(),0)+1); + } + List> list=new ArrayList<>(); + Map all=new LinkedHashMap<>(); + all.put("id", null); all.put("name", "전체"); all.put("count", contacts.size()); all.put("all", true); + list.add(all); + for(SmsAddressGroup g:groups){ + Map row=new LinkedHashMap<>(); + row.put("id", g.getId()); + row.put("name", g.getName()); + row.put("count", counts.getOrDefault(g.getId(),0)); + row.put("all", false); + list.add(row); + } + return ApiResponse.ok(Map.of("list", list, "total", contacts.size())); + } + @PostMapping("/sms/address/groups") @Transactional ApiResponse smsAddressGroupCreate(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + String name=d.get("name")==null?"":String.valueOf(d.get("name")).trim(); + if(name.isBlank()) return ApiResponse.fail("그룹명을 입력하세요."); + if("전체".equals(name)) return ApiResponse.fail("사용할 수 없는 그룹명입니다."); + Long exists=em.createQuery("select count(g) from SmsAddressGroup g where g.groupId=:g and g.name=:n",Long.class) + .setParameter("g",u.getGroupId()).setParameter("n",name).getSingleResult(); + if(exists!=null && exists>0) return ApiResponse.fail("이미 존재하는 그룹명입니다."); + SmsAddressGroup g=new SmsAddressGroup(); + g.setGroupId(u.getGroupId()); + g.setName(name); + em.persist(g); + return ApiResponse.ok(Map.of("id", g.getId(), "name", g.getName())); + } + @PutMapping("/sms/address/groups/{id}") @Transactional ApiResponse smsAddressGroupUpdate(@PathVariable Long id,@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + SmsAddressGroup g=em.find(SmsAddressGroup.class,id); + if(g==null || !Objects.equals(g.getGroupId(),u.getGroupId())) return ApiResponse.fail("그룹을 찾을 수 없습니다."); + String name=d.get("name")==null?"":String.valueOf(d.get("name")).trim(); + if(name.isBlank()) return ApiResponse.fail("그룹명을 입력하세요."); + if("전체".equals(name)) return ApiResponse.fail("사용할 수 없는 그룹명입니다."); + g.setName(name); + return ApiResponse.ok(Map.of("id", g.getId(), "name", g.getName())); + } + @DeleteMapping("/sms/address/groups/{id}") @Transactional ApiResponse smsAddressGroupDelete(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ + SmsAddressGroup g=em.find(SmsAddressGroup.class,id); + if(g==null || !Objects.equals(g.getGroupId(),u.getGroupId())) return ApiResponse.fail("그룹을 찾을 수 없습니다."); + List contacts=em.createQuery("select c from SmsAddressContact c where c.groupId=:g and c.addressGroupId=:gid",SmsAddressContact.class) + .setParameter("g",u.getGroupId()).setParameter("gid",id).getResultList(); + for(SmsAddressContact c:contacts) em.remove(c); + em.remove(g); + return ApiResponse.ok(Map.of("deleted", true)); + } + @GetMapping("/sms/address/contacts") ApiResponse smsAddressContacts(@AuthenticationPrincipal JwtUser u,@RequestParam Map f, + @RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="15") int size){ + return ApiResponse.ok(smsAddressContactData(u,f,page,size)); + } + @PostMapping("/sms/address/contacts") @Transactional ApiResponse smsAddressContactCreate(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + String name=d.get("name")==null?"":String.valueOf(d.get("name")).trim(); + String phone=normalizePhone(String.valueOf(d.getOrDefault("phone",""))); + if(name.isBlank()) return ApiResponse.fail("이름을 입력하세요."); + if(phone.isBlank()) return ApiResponse.fail("연락처를 입력하세요."); + Long gid=null; + if(d.get("addressGroupId")!=null && !String.valueOf(d.get("addressGroupId")).isBlank()){ + gid=Long.valueOf(String.valueOf(d.get("addressGroupId"))); + SmsAddressGroup g=em.find(SmsAddressGroup.class,gid); + if(g==null || !Objects.equals(g.getGroupId(),u.getGroupId())) return ApiResponse.fail("그룹을 선택하세요."); + } + SmsAddressContact c=new SmsAddressContact(); + c.setGroupId(u.getGroupId()); + c.setAddressGroupId(gid); + c.setName(name); + c.setPhone(phone); + em.persist(c); + return ApiResponse.ok(smsAddressContactMap(c, groupNameMap(u))); + } + @PutMapping("/sms/address/contacts/{id}") @Transactional ApiResponse smsAddressContactUpdate(@PathVariable Long id,@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + SmsAddressContact c=em.find(SmsAddressContact.class,id); + if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId())) return ApiResponse.fail("연락처를 찾을 수 없습니다."); + if(d.get("name")!=null){ + String name=String.valueOf(d.get("name")).trim(); + if(name.isBlank()) return ApiResponse.fail("이름을 입력하세요."); + c.setName(name); + } + if(d.get("phone")!=null){ + String phone=normalizePhone(String.valueOf(d.get("phone"))); + if(phone.isBlank()) return ApiResponse.fail("연락처를 입력하세요."); + c.setPhone(phone); + } + if(d.containsKey("addressGroupId")){ + if(d.get("addressGroupId")==null || String.valueOf(d.get("addressGroupId")).isBlank()) c.setAddressGroupId(null); + else { + Long gid=Long.valueOf(String.valueOf(d.get("addressGroupId"))); + SmsAddressGroup g=em.find(SmsAddressGroup.class,gid); + if(g==null || !Objects.equals(g.getGroupId(),u.getGroupId())) return ApiResponse.fail("그룹을 선택하세요."); + c.setAddressGroupId(gid); + } + } + return ApiResponse.ok(smsAddressContactMap(c, groupNameMap(u))); + } + @PostMapping("/sms/address/contacts/delete") @Transactional ApiResponse smsAddressContactDelete(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Object raw=d.get("ids"); + if(!(raw instanceof List ids) || ids.isEmpty()) return ApiResponse.fail("삭제할 항목을 선택하세요."); + int deleted=0; + for(Object idObj:ids){ + SmsAddressContact c=em.find(SmsAddressContact.class, Long.valueOf(idObj.toString())); + if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId())) continue; + em.remove(c); deleted++; + } + return ApiResponse.ok(Map.of("deleted", deleted)); + } + @PostMapping("/sms/address/contacts/change-group") @Transactional ApiResponse smsAddressContactChangeGroup(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Object raw=d.get("ids"); + if(!(raw instanceof List ids) || ids.isEmpty()) return ApiResponse.fail("변경할 항목을 선택하세요."); + if(d.get("addressGroupId")==null || String.valueOf(d.get("addressGroupId")).isBlank()) return ApiResponse.fail("그룹을 선택하세요."); + Long gid=Long.valueOf(String.valueOf(d.get("addressGroupId"))); + SmsAddressGroup g=em.find(SmsAddressGroup.class,gid); + if(g==null || !Objects.equals(g.getGroupId(),u.getGroupId())) return ApiResponse.fail("그룹을 선택하세요."); + int updated=0; + for(Object idObj:ids){ + SmsAddressContact c=em.find(SmsAddressContact.class, Long.valueOf(idObj.toString())); + if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId())) continue; + c.setAddressGroupId(gid); updated++; + } + return ApiResponse.ok(Map.of("updated", updated)); + } + @PostMapping("/sms/address/contacts/bulk") @Transactional ApiResponse smsAddressContactBulk(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Long gid=null; + if(d.get("addressGroupId")!=null && !String.valueOf(d.get("addressGroupId")).isBlank()){ + gid=Long.valueOf(String.valueOf(d.get("addressGroupId"))); + SmsAddressGroup g=em.find(SmsAddressGroup.class,gid); + if(g==null || !Objects.equals(g.getGroupId(),u.getGroupId())) return ApiResponse.fail("그룹을 선택하세요."); + } + String text=d.get("text")==null?"":String.valueOf(d.get("text")); + int created=0; + for(String line:text.split("\\r?\\n")){ + if(line==null || line.isBlank()) continue; + String[] parts=line.split("[,\\t]"); + String name; String phone; + if(parts.length>=2){ name=parts[0].trim(); phone=normalizePhone(parts[1]); } + else { name=parts[0].trim(); phone=normalizePhone(parts[0]); if(phone.equals(normalizePhone(name))) name="-"; } + if(phone.isBlank()) continue; + if(name.isBlank()) name="-"; + SmsAddressContact c=new SmsAddressContact(); + c.setGroupId(u.getGroupId()); + c.setAddressGroupId(gid); + c.setName(name); + c.setPhone(phone); + em.persist(c); created++; + } + if(created==0) return ApiResponse.fail("등록할 연락처가 없습니다."); + return ApiResponse.ok(Map.of("created", created)); + } + @GetMapping(value="/sms/address/contacts/export", produces="text/csv;charset=UTF-8") ResponseEntity smsAddressContactExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + @SuppressWarnings("unchecked") List> rows=(List>) smsAddressContactData(u,f,0,10000).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF그룹,이름,연락처\n"); + for(Map r:rows) csv.append(csv(r.get("groupName"))).append(',').append(csv(r.get("name"))).append(',').append(csv(r.get("phoneDisplay"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=sms-address.csv").body(csv.toString()); + } + @PostMapping("/sms/cart") @Transactional ApiResponse smsCartAdd(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Object raw=d.get("phones"); + List add=new ArrayList<>(); + if(raw instanceof List list) for(Object p:list){ String phone=normalizePhone(String.valueOf(p)); if(!phone.isBlank()) add.add(phone); } + List cart=loadSmsCart(u.getGroupId()); + for(String p:add) if(!cart.contains(p)) cart.add(p); + saveSmsCart(u.getGroupId(), cart); + return ApiResponse.ok(Map.of("count", cart.size(), "phones", cart)); + } + @GetMapping("/sms/cart") ApiResponse smsCartGet(@AuthenticationPrincipal JwtUser u){ + List cart=loadSmsCart(u.getGroupId()); + return ApiResponse.ok(Map.of("count", cart.size(), "phones", cart)); + } + private Map smsAddressContactData(JwtUser u, Map f, int page, int size){ + Map names=groupNameMap(u); + List all=em.createQuery("select c from SmsAddressContact c where c.groupId=:g",SmsAddressContact.class) + .setParameter("g",u.getGroupId()).getResultList(); + Long gid=blank(f.get("addressGroupId"))?null:Long.valueOf(f.get("addressGroupId")); + String searchType=f.getOrDefault("searchType","이름"); + String keyword=f.getOrDefault("keyword",""); + List filtered=all.stream().filter(c->{ + if(gid!=null && !Objects.equals(c.getAddressGroupId(),gid)) return false; + if(!blank(keyword)){ + String q=keyword.trim(); + return switch(searchType){ + case "연락처" -> c.getPhone()!=null && c.getPhone().contains(q.replace("-","")); + case "그룹" -> names.getOrDefault(c.getAddressGroupId(),"").contains(q); + default -> c.getName()!=null && c.getName().contains(q); + }; + } + return true; + }).sorted(Comparator.comparing(SmsAddressContact::getId).reversed()).toList(); + int sz=Math.min(Math.max(size,1),200); + int fromIdx=Math.min(Math.max(page,0)*sz, filtered.size()); + int toIdx=Math.min(fromIdx+sz, filtered.size()); + List> list=filtered.subList(fromIdx,toIdx).stream().map(c->smsAddressContactMap(c,names)).toList(); + Map result=new LinkedHashMap<>(); + result.put("total", filtered.size()); + result.put("list", list); + return result; + } + private Map groupNameMap(JwtUser u){ + Map names=new HashMap<>(); + for(SmsAddressGroup g:em.createQuery("select g from SmsAddressGroup g where g.groupId=:g",SmsAddressGroup.class).setParameter("g",u.getGroupId()).getResultList()) + names.put(g.getId(), g.getName()); + return names; + } + private Map smsAddressContactMap(SmsAddressContact c, Map names){ + Map r=new LinkedHashMap<>(); + r.put("id", c.getId()); + r.put("addressGroupId", c.getAddressGroupId()); + r.put("groupName", names.getOrDefault(c.getAddressGroupId(),"-")); + r.put("name", c.getName()); + r.put("phone", c.getPhone()); + r.put("phoneDisplay", formatPhoneDisplay(c.getPhone())); + return r; + } + private String formatPhoneDisplay(String phone){ + if(phone==null || phone.isBlank()) return ""; + String p=phone.replaceAll("[^0-9]",""); + if(p.length()==11) return p.substring(0,3)+"-"+p.substring(3,7)+"-"+p.substring(7); + if(p.length()==10) return p.substring(0,3)+"-"+p.substring(3,6)+"-"+p.substring(6); + return phone; + } + private List loadSmsCart(String groupId){ + List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='sms_cart'",Setting.class) + .setParameter("g",groupId).getResultList(); + if(rows.isEmpty() || blank(rows.getFirst().getValue())) return new ArrayList<>(); + try{ + @SuppressWarnings("unchecked") List list=(List) new ObjectMapper().readValue(rows.getFirst().getValue(), List.class); + return new ArrayList<>(list); + }catch(Exception ex){ return new ArrayList<>(); } + } + private void saveSmsCart(String groupId, List phones){ + try{ + String json=new ObjectMapper().writeValueAsString(phones); + List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='sms_cart'",Setting.class) + .setParameter("g",groupId).getResultList(); + if(rows.isEmpty()){ + Setting s=new Setting(); s.setGroupId(groupId); s.setSettingKey("sms_cart"); s.setValue(json); em.persist(s); + } else rows.getFirst().setValue(json); + }catch(Exception ignored){} + } + @PostMapping({"/scans","/indocs","/returns","/accounts","/sms","/sms/templates"}) ApiResponse commonPost(HttpServletRequest r,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return post(type(r.getRequestURI()),d,u);} + @GetMapping("/fareboxes/search") ApiResponse fareboxSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f, + @RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="15") int size){ + return ApiResponse.ok(fareboxSearchData(u,f,page,size)); + } + @GetMapping(value="/fareboxes/export", produces="text/csv;charset=UTF-8") ResponseEntity fareboxExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + @SuppressWarnings("unchecked") List> rows=(List>) fareboxSearchData(u,f,0,10000).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF수납일,통신사,출고처,고객명,연락처,구분,현금금액,카드금액,결제상태,결제금액,상세내역,등록자\n"); + for(Map r:rows){ + csv.append(csv(r.get("faredate"))).append(',').append(csv(r.get("telcompany"))).append(',').append(csv(r.get("outcompanyName"))).append(',') + .append(csv(r.get("name"))).append(',').append(csv(r.get("phone"))).append(',').append(csv(r.get("how"))).append(',') + .append(csv(r.get("cashprice"))).append(',').append(csv(r.get("cardprice"))).append(',').append(csv(r.get("paystate"))).append(',') + .append(csv(r.get("payAmount"))).append(',').append(csv(r.get("detail"))).append(',').append(csv(r.get("registrant"))).append('\n'); + } + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=farebox.csv").body(csv.toString()); + } + @PostMapping("/fareboxes") @Transactional ApiResponse fareboxPost(@RequestBody Map d,@AuthenticationPrincipal JwtUser u){ + if(d.get("faredate")==null || String.valueOf(d.get("faredate")).isBlank()) return ApiResponse.fail("수납일을 입력하세요."); + if(d.get("telcompany")==null || String.valueOf(d.get("telcompany")).isBlank()) return ApiResponse.fail("통신사를 선택하세요."); + if(d.get("outcompanyName")==null || String.valueOf(d.get("outcompanyName")).isBlank()) return ApiResponse.fail("출고처를 입력하세요."); + if(d.get("name")==null || String.valueOf(d.get("name")).isBlank()) return ApiResponse.fail("고객명을 입력하세요."); + if(d.get("how")==null || String.valueOf(d.get("how")).isBlank()) d.put("how","수납"); + if(d.get("paystate")==null || String.valueOf(d.get("paystate")).isBlank()) d.put("paystate","미결"); + if(d.get("registrant")==null || String.valueOf(d.get("registrant")).isBlank()) d.put("registrant", u.getUserid()); + String outName=String.valueOf(d.get("outcompanyName")).trim(); + if(d.get("outcompanyId")==null || String.valueOf(d.get("outcompanyId")).isBlank()){ + List found=em.createQuery("select c from Company c where c.groupId=:g and c.name=:n",Company.class) + .setParameter("g",u.getGroupId()).setParameter("n",outName).setMaxResults(1).getResultList(); + if(!found.isEmpty()) d.put("outcompanyId", found.getFirst().getId()); + } + d.putIfAbsent("cashprice",0); + d.putIfAbsent("cardprice",0); + d.putIfAbsent("paidAmount",0); + return post(Farebox.class,d,u); + } + @GetMapping("/fareboxes/pay-summary") ApiResponse fareboxPaySummary(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + return ApiResponse.ok(fareboxPaySummaryData(u,f)); + } + @GetMapping(value="/fareboxes/pay-summary/export", produces="text/csv;charset=UTF-8") ResponseEntity fareboxPaySummaryExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + Map data=fareboxPaySummaryData(u,f); + @SuppressWarnings("unchecked") List> rows=(List>) data.get("rows"); + StringBuilder csv=new StringBuilder("\uFEFF번호,채널,출고처,담당자,전화,휴대폰,수납건수,결제잔액\n"); + int i=1; + for(Map r:rows){ + csv.append(i++).append(',').append(csv(r.get("channel"))).append(',').append(csv(r.get("outcompanyName"))).append(',') + .append(csv(r.get("managerName"))).append(',').append(csv(r.get("phone"))).append(',').append(csv(r.get("mobile"))).append(',') + .append(csv(r.get("count"))).append(',').append(csv(r.get("balance"))).append('\n'); + } + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=farebox-pay.csv").body(csv.toString()); + } + @GetMapping("/fareboxes/pay-items") ApiResponse fareboxPayItems(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + String outName=f.getOrDefault("outcompanyName",""); + if(blank(outName)) return ApiResponse.fail("출고처를 선택하세요."); + Map companies=companyNames(u); + List items=em.createQuery("select e from Farebox e where e.groupId=:g",Farebox.class).setParameter("g",u.getGroupId()).getResultList() + .stream().filter(e->fareboxRemaining(e).compareTo(BigDecimal.ZERO)>0) + .filter(e->outName.equals(fareboxOutName(e,companies))) + .sorted(Comparator.comparing(Farebox::getFaredate, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(Farebox::getId)) + .toList(); + List> list=items.stream().map(e->{ + Map m=fareboxMap(e,companies); + m.put("remaining", fareboxRemaining(e)); + m.put("paidAmount", nvl(e.getPaidAmount())); + return m; + }).toList(); + Map result=new LinkedHashMap<>(); + result.put("outcompanyName", outName); + result.put("list", list); + result.put("totalBalance", list.stream().map(r->(BigDecimal)r.get("remaining")).reduce(BigDecimal.ZERO,BigDecimal::add)); + return ApiResponse.ok(result); + } + @PostMapping("/fareboxes/pay") @Transactional ApiResponse fareboxPay(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + String mode=String.valueOf(d.getOrDefault("mode","item")); // item | batch + String outName=String.valueOf(d.getOrDefault("outcompanyName","")).trim(); + if(blank(outName)) return ApiResponse.fail("출고처를 확인하세요."); + LocalDate paydate=parseDate(String.valueOf(d.getOrDefault("paydate","")), LocalDate.now()); + String memo=d.get("memo")==null?null:String.valueOf(d.get("memo")); + Map companies=companyNames(u); + List pending=em.createQuery("select e from Farebox e where e.groupId=:g",Farebox.class).setParameter("g",u.getGroupId()).getResultList() + .stream().filter(e->fareboxRemaining(e).compareTo(BigDecimal.ZERO)>0) + .filter(e->outName.equals(fareboxOutName(e,companies))) + .sorted(Comparator.comparing(Farebox::getFaredate, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(Farebox::getId)) + .toList(); + if(pending.isEmpty()) return ApiResponse.fail("결제할 미결 수납이 없습니다."); + List targets=new ArrayList<>(); + BigDecimal payAmount; + if("item".equals(mode)){ + Object raw=d.get("ids"); + if(!(raw instanceof List ids) || ids.isEmpty()) return ApiResponse.fail("결제하실 내역을 선택해주세요."); + Set idSet=ids.stream().map(x->Long.valueOf(x.toString())).collect(java.util.stream.Collectors.toSet()); + targets=pending.stream().filter(e->idSet.contains(e.getId())).toList(); + if(targets.isEmpty()) return ApiResponse.fail("결제하실 내역을 선택해주세요."); + payAmount=targets.stream().map(this::fareboxRemaining).reduce(BigDecimal.ZERO,BigDecimal::add); + } else { + boolean full="true".equalsIgnoreCase(String.valueOf(d.getOrDefault("fullPay",false))) || Boolean.TRUE.equals(d.get("fullPay")); + BigDecimal total=pending.stream().map(this::fareboxRemaining).reduce(BigDecimal.ZERO,BigDecimal::add); + if(full) payAmount=total; + else { + try{ payAmount=new BigDecimal(String.valueOf(d.get("amount"))); } + catch(Exception e){ return ApiResponse.fail("결제금액을 입력하세요."); } + if(payAmount.compareTo(BigDecimal.ZERO)<=0) return ApiResponse.fail("결제금액을 입력하세요."); + if(payAmount.compareTo(total)>0) payAmount=total; + } + BigDecimal left=payAmount; + for(Farebox e:pending){ + if(left.compareTo(BigDecimal.ZERO)<=0) break; + BigDecimal rem=fareboxRemaining(e); + if(rem.compareTo(BigDecimal.ZERO)<=0) continue; + targets.add(e); + left=left.subtract(rem.min(left)); + } + } + BigDecimal remainPay=payAmount; + List paidIds=new ArrayList<>(); + int payCount=0; + for(Farebox e:targets){ + if(remainPay.compareTo(BigDecimal.ZERO)<=0) break; + BigDecimal rem=fareboxRemaining(e); + BigDecimal apply=rem.min(remainPay); + e.setPaidAmount(nvl(e.getPaidAmount()).add(apply)); + if(fareboxRemaining(e).compareTo(BigDecimal.ZERO)<=0) e.setPaystate("완결"); + else e.setPaystate("미결"); + remainPay=remainPay.subtract(apply); + paidIds.add(e.getId()); + payCount++; + } + Long outId=pending.getFirst().getOutcompanyId(); + if(outId==null){ + List found=em.createQuery("select c from Company c where c.groupId=:g and c.name=:n",Company.class) + .setParameter("g",u.getGroupId()).setParameter("n",outName).setMaxResults(1).getResultList(); + if(!found.isEmpty()) outId=found.getFirst().getId(); + } + FareboxPayment payment=new FareboxPayment(); + payment.setGroupId(u.getGroupId()); + payment.setOutcompanyId(outId); + payment.setOutcompanyName(outName); + payment.setMode("item".equals(mode)?"건별":"일반"); + payment.setPaydate(paydate); + payment.setPayCount(payCount); + payment.setAmount(payAmount.subtract(remainPay)); + payment.setMemo(memo); + payment.setRegistrant(u.getUserid()); + payment.setStatus("정상"); + String outCat=targets.stream().map(Farebox::getOutCategory).filter(x->!blank(x)).findFirst().orElse(null); + if(blank(outCat) && outId!=null){ + Company c=em.find(Company.class, outId); + if(c!=null && !blank(c.getChannel())) outCat=c.getChannel(); + } + payment.setOutCategory(blank(outCat)?"판매점":outCat); + payment.setFareboxIds(paidIds.stream().map(String::valueOf).collect(java.util.stream.Collectors.joining(","))); + em.persist(payment); + Map result=new LinkedHashMap<>(); + result.put("paymentId", payment.getId()); + result.put("payCount", payCount); + result.put("amount", payment.getAmount()); + return ApiResponse.ok(result); + } + @GetMapping("/fareboxes/payments") ApiResponse fareboxPayments(@AuthenticationPrincipal JwtUser u,@RequestParam Map f, + @RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="15") int size){ + return ApiResponse.ok(fareboxPaymentSearchData(u,f,page,size)); + } + @GetMapping("/fareboxes/payments/{id}") ApiResponse fareboxPaymentDetail(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ + FareboxPayment p=em.find(FareboxPayment.class,id); + if(p==null || !Objects.equals(p.getGroupId(),u.getGroupId())) return ApiResponse.fail("결제내역을 찾을 수 없습니다."); + Map companies=companyNames(u); + List> items=new ArrayList<>(); + if(!blank(p.getFareboxIds())){ + for(String raw:p.getFareboxIds().split(",")){ + if(raw==null||raw.isBlank()) continue; + try{ + Farebox e=em.find(Farebox.class, Long.valueOf(raw.trim())); + if(e!=null && Objects.equals(e.getGroupId(),u.getGroupId())) items.add(fareboxMap(e,companies)); + }catch(Exception ignored){} + } + } + Map result=fareboxPaymentMap(p); + result.put("items", items); + return ApiResponse.ok(result); + } + @PostMapping("/fareboxes/payments/{id}/cancel") @Transactional ApiResponse fareboxPaymentCancel(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ + FareboxPayment p=em.find(FareboxPayment.class,id); + if(p==null || !Objects.equals(p.getGroupId(),u.getGroupId())) return ApiResponse.fail("결제내역을 찾을 수 없습니다."); + if("취소됨".equals(p.getStatus())) return ApiResponse.fail("이미 취소된 결제입니다."); + BigDecimal amount=nvl(p.getAmount()); + if(!blank(p.getFareboxIds()) && amount.compareTo(BigDecimal.ZERO)>0){ + BigDecimal left=amount; + List ids=new ArrayList<>(); + for(String raw:p.getFareboxIds().split(",")){ + if(raw==null||raw.isBlank()) continue; + try{ ids.add(Long.valueOf(raw.trim())); }catch(Exception ignored){} + } + // reverse from last paid first + Collections.reverse(ids); + for(Long fid:ids){ + if(left.compareTo(BigDecimal.ZERO)<=0) break; + Farebox e=em.find(Farebox.class,fid); + if(e==null || !Objects.equals(e.getGroupId(),u.getGroupId())) continue; + BigDecimal paid=nvl(e.getPaidAmount()); + if(paid.compareTo(BigDecimal.ZERO)<=0) continue; + BigDecimal rev=paid.min(left); + e.setPaidAmount(paid.subtract(rev)); + if(fareboxRemaining(e).compareTo(BigDecimal.ZERO)>0) e.setPaystate("미결"); + left=left.subtract(rev); + } + } + p.setStatus("취소됨"); + return ApiResponse.ok(fareboxPaymentMap(p)); + } + private Map fareboxPaymentSearchData(JwtUser u, Map f, int page, int size){ + LocalDate from=parseDate(f.get("dateFrom"), null); + LocalDate to=parseDate(f.get("dateTo"), null); + List all=em.createQuery("select p from FareboxPayment p where p.groupId=:g",FareboxPayment.class) + .setParameter("g",u.getGroupId()).getResultList(); + List filtered=all.stream().filter(p->{ + if(from!=null && (p.getPaydate()==null || p.getPaydate().isBefore(from))) return false; + if(to!=null && (p.getPaydate()==null || p.getPaydate().isAfter(to))) return false; + String status=blank(p.getStatus())?"정상":p.getStatus(); + if(!eq(status, f.get("status"))) return false; + if(!eq(p.getMode(), f.get("mode")) && !eq(p.getMode(), f.get("how"))) return false; + if(!eq(p.getOutCategory(), f.get("outCategory"))) return false; + if(!like(p.getOutcompanyName(), f.get("outcompanyName"))) return false; + return true; + }).sorted(Comparator.comparing(FareboxPayment::getPaydate, Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing(FareboxPayment::getId, Comparator.reverseOrder())).toList(); + int sz=Math.min(Math.max(size,1),200); + int fromIdx=Math.min(Math.max(page,0)*sz, filtered.size()); + int toIdx=Math.min(fromIdx+sz, filtered.size()); + // number from newest: total - index + List> list=new ArrayList<>(); + for(int i=fromIdx;i row=fareboxPaymentMap(filtered.get(i)); + row.put("no", filtered.size()-i); + list.add(row); + } + Map result=new LinkedHashMap<>(); + result.put("total", filtered.size()); + result.put("list", list); + return result; + } + private Map fareboxPaymentMap(FareboxPayment p){ + Map r=new LinkedHashMap<>(); + r.put("id", p.getId()); + r.put("paydate", p.getPaydate()); + r.put("outcompanyId", p.getOutcompanyId()); + r.put("outcompanyName", p.getOutcompanyName()); + r.put("mode", blank(p.getMode())?"일반":p.getMode()); + r.put("amount", nvl(p.getAmount())); + r.put("payCount", p.getPayCount()==null?0:p.getPayCount()); + r.put("memo", p.getMemo()); + r.put("registrant", p.getRegistrant()); + r.put("status", blank(p.getStatus())?"정상":p.getStatus()); + r.put("outCategory", p.getOutCategory()); + r.put("fareboxIds", p.getFareboxIds()); + return r; + } + private Map fareboxPaySummaryData(JwtUser u, Map f){ + Map companyMap=new LinkedHashMap<>(); + for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) + companyMap.put(c.getId(),c); + Map companies=new LinkedHashMap<>(); + companyMap.forEach((id,c)->companies.put(id,c.getName())); + String q=f.get("outcompanyName"); + Map> byOut=new LinkedHashMap<>(); + BigDecimal totalBalance=BigDecimal.ZERO; + for(Farebox e:em.createQuery("select e from Farebox e where e.groupId=:g",Farebox.class).setParameter("g",u.getGroupId()).getResultList()){ + BigDecimal rem=fareboxRemaining(e); + if(rem.compareTo(BigDecimal.ZERO)<=0) continue; + String outName=fareboxOutName(e,companies); + if(!like(outName,q)) continue; + Map row=byOut.computeIfAbsent(outName,k->{ + Map r=new LinkedHashMap<>(); + Company c=companyMap.values().stream().filter(x->outName.equals(x.getName())).findFirst().orElse(null); + if(c==null && e.getOutcompanyId()!=null) c=companyMap.get(e.getOutcompanyId()); + r.put("outcompanyId", c==null?e.getOutcompanyId():c.getId()); + r.put("outcompanyName", outName); + r.put("channel", c!=null && !blank(c.getChannel())?c.getChannel():(blank(e.getOutCategory())?"판매점":e.getOutCategory())); + r.put("managerName", c==null||blank(c.getManagerName())?"-":c.getManagerName()); + r.put("phone", c==null||blank(c.getPhone())?"-":c.getPhone()); + r.put("mobile", c==null||blank(c.getPhone())?"-":c.getPhone()); + r.put("count", 0); + r.put("balance", BigDecimal.ZERO); + r.put("todayPaid", BigDecimal.ZERO); + return r; + }); + row.put("count", ((Number)row.get("count")).intValue()+1); + row.put("balance", ((BigDecimal)row.get("balance")).add(rem)); + totalBalance=totalBalance.add(rem); + } + LocalDate today=LocalDate.now(); + BigDecimal todayPaid=BigDecimal.ZERO; + for(FareboxPayment p:em.createQuery("select p from FareboxPayment p where p.groupId=:g and p.paydate=:d",FareboxPayment.class) + .setParameter("g",u.getGroupId()).setParameter("d",today).getResultList()){ + if("취소됨".equals(blank(p.getStatus())?"정상":p.getStatus())) continue; + todayPaid=todayPaid.add(nvl(p.getAmount())); + String outName=blank(p.getOutcompanyName())?"-":p.getOutcompanyName(); + Map row=byOut.get(outName); + if(row!=null) row.put("todayPaid", ((BigDecimal)row.get("todayPaid")).add(nvl(p.getAmount()))); + } + List> rows=new ArrayList<>(byOut.values()); + rows.sort(Comparator.comparing(r->String.valueOf(r.get("outcompanyName")))); + int no=1; + for(Map r:rows) r.put("no", no++); + Map result=new LinkedHashMap<>(); + result.put("totalBalance", totalBalance); + result.put("todayPaid", todayPaid); + result.put("rows", rows); + result.put("total", rows.size()); + return result; + } + private String fareboxOutName(Farebox e, Map companies){ + if(!blank(e.getOutcompanyName())) return e.getOutcompanyName().trim(); + return companies.getOrDefault(e.getOutcompanyId(),"-"); + } + private BigDecimal fareboxTotal(Farebox e){ return nvl(e.getCashprice()).add(nvl(e.getCardprice())); } + private BigDecimal fareboxRemaining(Farebox e){ + BigDecimal rem=fareboxTotal(e).subtract(nvl(e.getPaidAmount())); + if(rem.compareTo(BigDecimal.ZERO)<0) return BigDecimal.ZERO; + // 완결인데 paid 미기록이면 잔액 0 + if("완결".equals(normalizePayState(e.getPaystate())) && nvl(e.getPaidAmount()).compareTo(BigDecimal.ZERO)==0 + && fareboxTotal(e).compareTo(BigDecimal.ZERO)>0){ + // treat as unpaid if marked 완결 without paidAmount? Prefer remaining based on paidAmount only for 미결 + return BigDecimal.ZERO; + } + if("완결".equals(normalizePayState(e.getPaystate()))) return BigDecimal.ZERO; + return rem; + } + private Map fareboxSearchData(JwtUser u, Map f, int page, int size){ + Map companies=companyNames(u); + List all=em.createQuery("select e from Farebox e where e.groupId=:g",Farebox.class).setParameter("g",u.getGroupId()).getResultList(); + LocalDate from=parseDate(f.get("dateFrom"), null); + LocalDate to=parseDate(f.get("dateTo"), null); + String tab=f.getOrDefault("tab","ALL"); + List base=all.stream().filter(e->matchesFarebox(e,f,companies,from,to,null)).toList(); + List filtered=base.stream() + .filter(e->matchesFarebox(e,f,companies,from,to,tab)) + .sorted(Comparator.comparing(Farebox::getFaredate, Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing(Farebox::getId, Comparator.reverseOrder())) + .toList(); + Map counts=new LinkedHashMap<>(); + counts.put("ALL", (long) base.size()); + counts.put("미결", base.stream().filter(e->"미결".equals(normalizePayState(e.getPaystate()))).count()); + counts.put("완결", base.stream().filter(e->"완결".equals(normalizePayState(e.getPaystate()))).count()); + int sz=Math.min(Math.max(size,1),200); + int fromIdx=Math.min(Math.max(page,0)*sz, filtered.size()); + int toIdx=Math.min(fromIdx+sz, filtered.size()); + List> list=filtered.subList(fromIdx,toIdx).stream().map(e->fareboxMap(e,companies)).toList(); + Map result=new LinkedHashMap<>(); + result.put("total", filtered.size()); + result.put("list", list); + result.put("counts", counts); + return result; + } + private boolean matchesFarebox(Farebox e, Map f, Map companies, LocalDate from, LocalDate to, String tab){ + if(!fareboxDateOk(e,from,to)) return false; + if(!eq(e.getHow(), f.get("how"))) return false; + if(!eq(e.getTelcompany(), f.get("telecom")) && !eq(e.getTelcompany(), f.get("telcompany"))) return false; + if(!eq(e.getOutCategory(), f.get("outCategory"))) return false; + String outName=blank(e.getOutcompanyName())?companies.getOrDefault(e.getOutcompanyId(),""):e.getOutcompanyName(); + if(!like(outName, f.get("outcompanyName"))) return false; + String searchType=f.getOrDefault("searchType","고객명"); + String keyword=f.get("keyword"); + if(keyword!=null && !keyword.isBlank()){ + boolean ok=switch(searchType){ + case "연락처"->like(e.getPhone(), keyword); + default->like(e.getName(), keyword); + }; + if(!ok) return false; + } + if(tab!=null && !"ALL".equals(tab)){ + String state=normalizePayState(e.getPaystate()); + if("미결".equals(tab) && !"미결".equals(state)) return false; + if("완결".equals(tab) && !"완결".equals(state)) return false; + } + return true; + } + private boolean fareboxDateOk(Farebox e, LocalDate from, LocalDate to){ + if(from!=null && (e.getFaredate()==null || e.getFaredate().isBefore(from))) return false; + if(to!=null && (e.getFaredate()==null || e.getFaredate().isAfter(to))) return false; + return true; + } + private String normalizePayState(String s){ + if(blank(s)) return "미결"; + if("완료".equals(s) || "DONE".equalsIgnoreCase(s) || "완결".equals(s)) return "완결"; + if("PENDING".equalsIgnoreCase(s) || "미결".equals(s)) return "미결"; + return s; + } + private Map fareboxMap(Farebox e, Map companies){ + Map r=new LinkedHashMap<>(); + r.put("id", e.getId()); + r.put("faredate", e.getFaredate()); + r.put("telcompany", e.getTelcompany()); + r.put("outcompanyId", e.getOutcompanyId()); + String outName=blank(e.getOutcompanyName())?companies.getOrDefault(e.getOutcompanyId(),"-"):e.getOutcompanyName(); + r.put("outcompanyName", outName); + r.put("outCategory", e.getOutCategory()); + r.put("name", e.getName()); + r.put("phone", e.getPhone()); + r.put("how", blank(e.getHow())?"수납":e.getHow()); + r.put("cashprice", e.getCashprice()==null?BigDecimal.ZERO:e.getCashprice()); + r.put("cardprice", e.getCardprice()==null?BigDecimal.ZERO:e.getCardprice()); + r.put("paidAmount", e.getPaidAmount()==null?BigDecimal.ZERO:e.getPaidAmount()); + r.put("paystate", normalizePayState(e.getPaystate())); + BigDecimal pay=nvl(e.getCashprice()).add(nvl(e.getCardprice())); + r.put("payAmount", pay.compareTo(BigDecimal.ZERO)==0?null:pay); + r.put("remaining", fareboxRemaining(e)); + r.put("detail", e.getDetail()); + r.put("registrant", e.getRegistrant()); + r.put("receiptNo", e.getReceiptNo()); + return r; + } + @PutMapping({"/scans/{id}","/indocs/{id}","/returns/{id}","/fareboxes/{id}"}) ApiResponse commonPut(HttpServletRequest r,@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return put(type(r.getRequestURI()),id,d,u);} + @DeleteMapping("/schedules/{id}") ApiResponse scheduleDel(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return del(ScheduleItem.class,id,u);} + @PostMapping("/alims/{id}/confirm") ApiResponse alimConfirm(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return put(Alim.class,id,Map.of("confirmed",true),u);} + @GetMapping("/settlements") ApiResponse settlements(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){return list(SettlementMonth.class,u,f,0,100);} + @GetMapping("/settlements/docs") ApiResponse settlementDocs(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + String ym=f.getOrDefault("yearMonth",LocalDate.now().toString().substring(0,7)); + List all=em.createQuery("select s from SettlementMonth s where s.groupId=:g and s.yearMonth=:y",SettlementMonth.class).setParameter("g",u.getGroupId()).setParameter("y",ym).getResultList(); + Map companies=companyNames(u); + String outCategory=f.get("outCategory"); + List filtered=all.stream().filter(s->eq(s.getOutCategory(),outCategory)).sorted(Comparator.comparing((SettlementMonth s)->s.getOutcompanyName()!=null?s.getOutcompanyName():companies.getOrDefault(s.getOutcompanyId(),""),Comparator.nullsLast(String::compareTo))).toList(); + List> list=filtered.stream().map(s->settlementDocMap(s,companies)).toList(); + BigDecimal openAmount=BigDecimal.ZERO, afterAmount=BigDecimal.ZERO, homeAmount=BigDecimal.ZERO, realAmount=BigDecimal.ZERO; + int openCount=0, afterCount=0, homeCount=0; + for(Map r:list){ + openCount+=((Number)r.getOrDefault("openCount",0)).intValue(); + afterCount+=((Number)r.getOrDefault("afterSettleCount",0)).intValue(); + homeCount+=((Number)r.getOrDefault("homeProductCount",0)).intValue(); + openAmount=openAmount.add(nvl((BigDecimal)r.get("openAmount"))); + afterAmount=afterAmount.add(nvl((BigDecimal)r.get("afterSettleAmount"))); + homeAmount=homeAmount.add(nvl((BigDecimal)r.get("homeProductAmount"))); + realAmount=realAmount.add(nvl((BigDecimal)r.get("realSettleAmount"))); + } + Map summary=new LinkedHashMap<>(); + summary.put("openCount",openCount); summary.put("openAmount",openAmount); + summary.put("afterSettleCount",afterCount); summary.put("afterSettleAmount",afterAmount); + summary.put("homeProductCount",homeCount); summary.put("homeProductAmount",homeAmount); + summary.put("realSettleAmount",realAmount); + return ApiResponse.ok(Map.of("yearMonth",ym,"total",list.size(),"list",list,"summary",summary)); + } + @PostMapping("/settlements/docs/issue") @Transactional ApiResponse settlementIssue(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Object raw=d.get("ids"); + List ids=new ArrayList<>(); + if(raw instanceof List list) for(Object o:list) if(o!=null) ids.add(Long.valueOf(o.toString())); + if(ids.isEmpty()&&d.get("id")!=null) ids.add(Long.valueOf(d.get("id").toString())); + if(ids.isEmpty()) return ApiResponse.fail("발행할 항목을 선택하세요."); + int issued=0; + for(Long id:ids){ + SettlementMonth s=em.find(SettlementMonth.class,id); + if(s==null||!Objects.equals(s.getGroupId(),u.getGroupId())) continue; + s.setIssued(true); s.setStatus("발행"); issued++; + } + return ApiResponse.ok(Map.of("issued",issued)); + } + @PostMapping("/settlements/docs/delete") @Transactional ApiResponse settlementDocsDelete(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + String ym=d.get("yearMonth")==null?null:String.valueOf(d.get("yearMonth")); + Object raw=d.get("ids"); + int deleted=0; + if(raw instanceof List list && !list.isEmpty()){ + for(Object o:list){ + SettlementMonth s=em.find(SettlementMonth.class,Long.valueOf(o.toString())); + if(s==null||!Objects.equals(s.getGroupId(),u.getGroupId())) continue; + if(s.isIssued()){ s.setIssued(false); s.setStatus("READY"); deleted++; } + } + } else if(ym!=null&&!ym.isBlank()){ + List rows=em.createQuery("select s from SettlementMonth s where s.groupId=:g and s.yearMonth=:y and s.issued=true",SettlementMonth.class).setParameter("g",u.getGroupId()).setParameter("y",ym).getResultList(); + for(SettlementMonth s:rows){ s.setIssued(false); s.setStatus("READY"); deleted++; } + } else return ApiResponse.fail("삭제할 정산서를 선택하세요."); + return ApiResponse.ok(Map.of("deleted",deleted)); + } + @GetMapping(value="/settlements/docs/export", produces="text/csv;charset=UTF-8") ResponseEntity settlementDocsExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + @SuppressWarnings("unchecked") Map data=(Map) settlementDocs(u,f).data(); + @SuppressWarnings("unchecked") List> rows=(List>) data.get("list"); + StringBuilder csv=new StringBuilder("\uFEFF출고처,개통,개통금액,후정산,후정산금액,홈상품,홈상품금액,실정산금액,정산서발행,출고처문의,출고처동의,계산서상태,송금상태,비고\n"); + for(Map r:rows) csv.append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("openCount"))).append(',').append(csv(r.get("openAmount"))).append(',') + .append(csv(r.get("afterSettleCount"))).append(',').append(csv(r.get("afterSettleAmount"))).append(',') + .append(csv(r.get("homeProductCount"))).append(',').append(csv(r.get("homeProductAmount"))).append(',').append(csv(r.get("realSettleAmount"))).append(',') + .append(csv(Boolean.TRUE.equals(r.get("issued"))?"발행":"미발행")).append(',').append(csv(r.get("inquiry"))).append(',').append(csv(r.get("agreement"))).append(',') + .append(csv(r.get("invoiceStatus"))).append(',').append(csv(r.get("remittanceStatus"))).append(',').append(csv(r.get("memo"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=settlement-docs.csv").body(csv.toString()); + } + private Map settlementDocMap(SettlementMonth s,Map companies){ + Map r=new LinkedHashMap<>(); + r.put("id",s.getId()); + r.put("outcompanyId",s.getOutcompanyId()); + r.put("outcompanyName",s.getOutcompanyName()!=null&&!s.getOutcompanyName().isBlank()?s.getOutcompanyName():companies.getOrDefault(s.getOutcompanyId(),"-")); + r.put("yearMonth",s.getYearMonth()); r.put("outCategory",s.getOutCategory()); + r.put("openCount",s.getOpenCount()==null?0:s.getOpenCount()); + r.put("openAmount",s.getOpenAmount()==null?BigDecimal.ZERO:s.getOpenAmount()); + r.put("afterSettleCount",s.getAfterSettleCount()==null?0:s.getAfterSettleCount()); + r.put("afterSettleAmount",s.getAfterSettleAmount()==null?BigDecimal.ZERO:s.getAfterSettleAmount()); + r.put("homeProductCount",s.getHomeProductCount()==null?0:s.getHomeProductCount()); + r.put("homeProductAmount",s.getHomeProductAmount()==null?BigDecimal.ZERO:s.getHomeProductAmount()); + r.put("realSettleAmount",s.getRealSettleAmount()==null?BigDecimal.ZERO:s.getRealSettleAmount()); + r.put("issued",s.isIssued()); + r.put("inquiry",blank(s.getInquiry())?"-":s.getInquiry()); + r.put("agreement",blank(s.getAgreement())?"-":s.getAgreement()); + r.put("invoiceStatus",blank(s.getInvoiceStatus())?"-":s.getInvoiceStatus()); + r.put("remittanceStatus",blank(s.getRemittanceStatus())?"-":s.getRemittanceStatus()); + r.put("memo",s.getMemo()); r.put("status",s.getStatus()); + return r; + } + @PutMapping("/settlements") ApiResponse settlementPut(@RequestParam Long outcompanyId,@RequestParam String yearMonth,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ + List rows=em.createQuery("select e from SettlementMonth e where e.groupId=:g and e.outcompanyId=:o and e.yearMonth=:y",SettlementMonth.class).setParameter("g",u.getGroupId()).setParameter("o",outcompanyId).setParameter("y",yearMonth).getResultList(); + if(rows.isEmpty()){d.put("outcompanyId",outcompanyId);d.put("yearMonth",yearMonth);return post(SettlementMonth.class,d,u);} return put(SettlementMonth.class,rows.getFirst().getId(),d,u); + } + @GetMapping("/accounts/summary") ApiResponse accountSummary(@AuthenticationPrincipal JwtUser u,@RequestParam(required=false) String yearMonth){PageResponse p=crud.list(AccountEntry.class,u,yearMonth==null?Map.of():Map.of("yearMonth",yearMonth),0,1000);BigDecimal in=BigDecimal.ZERO,out=BigDecimal.ZERO;for(Object o:p.list()){AccountEntry e=(AccountEntry)o;if("IN".equals(e.getType()))in=in.add(nvl(e.getAmount()));else out=out.add(nvl(e.getAmount()));}return ApiResponse.ok(Map.of("in",in,"out",out,"balance",in.subtract(out)));} + @GetMapping({"/reports/stock","/reports/open","/reports/settle","/reports/sales","/reports/day","/reports/month"}) ApiResponse report(HttpServletRequest r,@AuthenticationPrincipal JwtUser u){String path=r.getRequestURI();Class c=path.contains("stock")?Stock.class:path.contains("open")?OpenRecord.class:path.contains("settle")?SettlementMonth.class:AccountEntry.class;return ApiResponse.ok(Map.of("report",path.substring(path.lastIndexOf('/')+1),"total",count(c,u,null)));} + @GetMapping("/settings/company") ApiResponse settingGet(@AuthenticationPrincipal JwtUser u){return list(Setting.class,u,Map.of("settingKey","company"),0,1);} + @PutMapping("/settings/company") ApiResponse settingPut(@AuthenticationPrincipal JwtUser u,@RequestBody Mapd){List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='company'",Setting.class).setParameter("g",u.getGroupId()).getResultList();d.put("settingKey","company");return rows.isEmpty()?post(Setting.class,d,u):put(Setting.class,rows.getFirst().getId(),d,u);} + @GetMapping("/settings/preference") ApiResponse preferenceGet(@AuthenticationPrincipal JwtUser u){ + return ApiResponse.ok(loadPreference(u.getGroupId())); + } + @PutMapping("/settings/preference") @Transactional ApiResponse preferencePut(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Map current=loadPreference(u.getGroupId()); + mergePreference(current,d); + savePreference(u.getGroupId(),current); + return ApiResponse.ok(current); + } + private Map loadPreference(String groupId){ + Map defaults=defaultPreference(); + List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='preference'",Setting.class).setParameter("g",groupId).getResultList(); + if(!rows.isEmpty() && !blank(rows.getFirst().getValue())){ + try{ + @SuppressWarnings("unchecked") Map parsed=new com.fasterxml.jackson.databind.ObjectMapper().readValue(rows.getFirst().getValue(), Map.class); + mergePreference(defaults,parsed); + }catch(Exception ignored){} + } else { + // fallback: legacy settings/company JSON + List companyRows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='company'",Setting.class).setParameter("g",groupId).getResultList(); + if(!companyRows.isEmpty() && !blank(companyRows.getFirst().getValue())){ + try{ + @SuppressWarnings("unchecked") Map legacy=new com.fasterxml.jackson.databind.ObjectMapper().readValue(companyRows.getFirst().getValue(), Map.class); + @SuppressWarnings("unchecked") Map company=(Map) defaults.get("company"); + if(legacy.get("name")!=null) company.put("name", legacy.get("name")); + if(legacy.get("phone")!=null) company.put("mobile", legacy.get("phone")); + if(legacy.get("managerName")!=null) company.put("managerName", legacy.get("managerName")); + if(legacy.get("businessName")!=null) company.put("businessName", legacy.get("businessName")); + if(legacy.get("businessNumber")!=null) company.put("businessNumber", legacy.get("businessNumber")); + if(legacy.get("ceo")!=null) company.put("ceo", legacy.get("ceo")); + if(legacy.get("bizType")!=null) company.put("bizType", legacy.get("bizType")); + if(legacy.get("bizItem")!=null) company.put("bizItem", legacy.get("bizItem")); + if(legacy.get("address")!=null) company.put("address", legacy.get("address")); + }catch(Exception ignored){} + } + } + return defaults; + } + private void savePreference(String groupId, Map data){ + String json=writeSettingJson(data); + List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='preference'",Setting.class).setParameter("g",groupId).getResultList(); + if(rows.isEmpty()){ + Setting s=new Setting(); s.setGroupId(groupId); s.setSettingKey("preference"); s.setValue(json); em.persist(s); + } else { + rows.getFirst().setValue(json); + } + // keep legacy company key in sync for older clients + @SuppressWarnings("unchecked") Map company=(Map) data.getOrDefault("company", Map.of()); + Map legacy=new LinkedHashMap<>(); + legacy.put("name", company.get("name")); + legacy.put("phone", company.get("mobile")); + legacy.put("managerName", company.get("managerName")); + legacy.put("businessName", company.get("businessName")); + legacy.put("businessNumber", company.get("businessNumber")); + legacy.put("ceo", company.get("ceo")); + legacy.put("bizType", company.get("bizType")); + legacy.put("bizItem", company.get("bizItem")); + legacy.put("address", company.get("address")); + String legacyJson=writeSettingJson(legacy); + List companyRows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='company'",Setting.class).setParameter("g",groupId).getResultList(); + if(companyRows.isEmpty()){ + Setting s=new Setting(); s.setGroupId(groupId); s.setSettingKey("company"); s.setValue(legacyJson); em.persist(s); + } else { + companyRows.getFirst().setValue(legacyJson); + } + } + @SuppressWarnings("unchecked") + private void mergePreference(Map target, Map patch){ + if(patch==null) return; + for(Map.Entry e:patch.entrySet()){ + Object val=e.getValue(); + if(val instanceof Map m && target.get(e.getKey()) instanceof Map){ + Map dest=new LinkedHashMap<>((Map) target.get(e.getKey())); + dest.putAll((Map) m); + target.put(e.getKey(), dest); + } else { + target.put(e.getKey(), val); + } + } + } + private Map defaultPreference(){ + Map company=new LinkedHashMap<>(); + company.put("name","에이전트 데모"); + company.put("managerName","김대표"); + company.put("mobile","010-0000-0000"); + company.put("businessName","에이전트 데모"); + company.put("businessNumber","123-45-67890"); + company.put("ceo","김유신"); + company.put("bizType","도소매"); + company.put("bizItem","휴대폰판매"); + company.put("address","대구시 수성구 수성4가 먼길"); + Map accessIps=new LinkedHashMap<>(); + accessIps.put("enabled", false); + accessIps.put("ips", List.of()); + Map masking=new LinkedHashMap<>(); + masking.put("enabled", true); + masking.put("name", true); + masking.put("phone", true); + masking.put("rrn", true); + masking.put("address", false); + Map stock=new LinkedHashMap<>(); + stock.put("duplicateSerialCheck", true); + stock.put("allowNegative", false); + stock.put("autoRecallDays", 0); + Map open=new LinkedHashMap<>(); + open.put("requireUsim", true); + open.put("requirePhone", true); + open.put("defaultOpenHow", ""); + open.put("autoSettle", false); + Map m=new LinkedHashMap<>(); + m.put("company", company); + m.put("accessIps", accessIps); + m.put("masking", masking); + m.put("outCategories", List.of("판매점","도매")); + m.put("stock", stock); + m.put("open", open); + return m; + } + @GetMapping("/staff") ApiResponse staffList(@AuthenticationPrincipal JwtUser u,@RequestParam Map f, + @RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="15") int size){ + return ApiResponse.ok(staffData(u,f,page,size)); + } + @GetMapping("/incompanies") ApiResponse incompanies(@AuthenticationPrincipal JwtUser u,@RequestParam Map f, + @RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="15") int size){ + return ApiResponse.ok(incompanyData(u,f,page,size)); + } + @PostMapping("/incompanies") @Transactional ApiResponse incompanyCreate(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + String name=d.get("name")==null?"":String.valueOf(d.get("name")).trim(); + if(name.isBlank()) return ApiResponse.fail("업체명을 입력하세요."); + Company c=new Company(); + c.setGroupId(u.getGroupId()); + c.setType("IN"); + applyIncompanyFields(c,d); + c.setName(name); + if(d.get("active")==null && d.get("status")==null) c.setActive(true); + em.persist(c); + return ApiResponse.ok(incompanyMap(c)); + } + @PutMapping("/incompanies/{id}") @Transactional ApiResponse incompanyUpdate(@PathVariable Long id,@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Company c=em.find(Company.class,id); + if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId()) || !"IN".equals(c.getType())) return ApiResponse.fail("입고처를 찾을 수 없습니다."); + if(d.get("name")!=null){ + String name=String.valueOf(d.get("name")).trim(); + if(name.isBlank()) return ApiResponse.fail("업체명을 입력하세요."); + c.setName(name); + } + applyIncompanyFields(c,d); + return ApiResponse.ok(incompanyMap(c)); + } + @PostMapping("/incompanies/status") @Transactional ApiResponse incompanyStatus(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Object raw=d.get("ids"); + if(!(raw instanceof List ids) || ids.isEmpty()) return ApiResponse.fail("항목을 선택하세요."); + boolean active=!"미사용".equals(String.valueOf(d.getOrDefault("status",""))) && !"false".equalsIgnoreCase(String.valueOf(d.getOrDefault("active",""))); + int updated=0; + for(Object idObj:ids){ + Company c=em.find(Company.class, Long.valueOf(idObj.toString())); + if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId()) || !"IN".equals(c.getType())) continue; + c.setActive(active); updated++; + } + return ApiResponse.ok(Map.of("updated", updated)); + } + @GetMapping(value="/incompanies/export", produces="text/csv;charset=UTF-8") ResponseEntity incompanyExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + @SuppressWarnings("unchecked") List> rows=(List>) incompanyData(u,f,0,10000).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF상태,업체명,통신사,전화,팩스,담당자,휴대폰,상호,사업자번호,등록일,비고\n"); + for(Map r:rows) csv.append(csv(r.get("status"))).append(',').append(csv(r.get("name"))).append(',') + .append(csv(r.get("telecom"))).append(',').append(csv(r.get("phone"))).append(',').append(csv(r.get("fax"))).append(',') + .append(csv(r.get("managerName"))).append(',').append(csv(r.get("mobile"))).append(',').append(csv(r.get("businessName"))).append(',') + .append(csv(r.get("businessNumber"))).append(',').append(csv(r.get("createdAt"))).append(',').append(csv(r.get("memo"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=incompanies.csv").body(csv.toString()); + } + private void applyIncompanyFields(Company c, Map d){ + if(d.get("telecom")!=null) c.setTelecom(blank(String.valueOf(d.get("telecom")))?null:String.valueOf(d.get("telecom")).trim()); + if(d.get("phone")!=null) c.setPhone(blank(String.valueOf(d.get("phone")))?null:String.valueOf(d.get("phone")).trim()); + if(d.get("fax")!=null) c.setFax(blank(String.valueOf(d.get("fax")))?null:String.valueOf(d.get("fax")).trim()); + if(d.get("managerName")!=null) c.setManagerName(blank(String.valueOf(d.get("managerName")))?null:String.valueOf(d.get("managerName")).trim()); + if(d.get("mobile")!=null) c.setMobile(blank(String.valueOf(d.get("mobile")))?null:String.valueOf(d.get("mobile")).trim()); + if(d.get("businessName")!=null) c.setBusinessName(blank(String.valueOf(d.get("businessName")))?null:String.valueOf(d.get("businessName")).trim()); + if(d.get("businessNumber")!=null) c.setBusinessNumber(blank(String.valueOf(d.get("businessNumber")))?null:String.valueOf(d.get("businessNumber")).trim()); + if(d.get("memo")!=null) c.setMemo(blank(String.valueOf(d.get("memo")))?null:String.valueOf(d.get("memo")).trim()); + if(d.get("status")!=null) c.setActive(!"미사용".equals(String.valueOf(d.get("status"))) && !"중지".equals(String.valueOf(d.get("status")))); + if(d.get("active")!=null) c.setActive(Boolean.parseBoolean(String.valueOf(d.get("active"))) || "true".equalsIgnoreCase(String.valueOf(d.get("active"))) || "사용".equals(String.valueOf(d.get("active")))); + } + private Map incompanyData(JwtUser u, Map f, int page, int size){ + List all=em.createQuery("select c from Company c where c.groupId=:g and c.type='IN'",Company.class) + .setParameter("g",u.getGroupId()).getResultList(); + String searchType=f.getOrDefault("searchType","업체명"); + String keyword=f.getOrDefault("keyword",""); + List filtered=all.stream().filter(c->{ + if(!blank(keyword)){ + String q=keyword.trim(); + return switch(searchType){ + case "통신사" -> like(c.getTelecom(), q); + case "담당자" -> like(c.getManagerName(), q); + case "상호" -> like(c.getBusinessName(), q); + case "사업자번호" -> like(c.getBusinessNumber(), q); + default -> like(c.getName(), q); + }; + } + return true; + }).sorted(incompanyComparator(f.get("sort"))).toList(); + int sz=Math.min(Math.max(size,1),200); + int fromIdx=Math.min(Math.max(page,0)*sz, filtered.size()); + int toIdx=Math.min(fromIdx+sz, filtered.size()); + List> list=filtered.subList(fromIdx,toIdx).stream().map(this::incompanyMap).toList(); + Map result=new LinkedHashMap<>(); + result.put("total", filtered.size()); + result.put("list", list); + return result; + } + private Comparator incompanyComparator(String sort){ + String field=sort==null?"name,asc":sort; boolean asc=field.endsWith(",asc"); + Comparator c=switch(field.split(",")[0]){ + case "telecom"->Comparator.comparing(Company::getTelecom,Comparator.nullsLast(String::compareTo)); + case "createdAt"->Comparator.comparing(Company::getCreatedAt,Comparator.nullsLast(Instant::compareTo)); + default->Comparator.comparing(Company::getName,Comparator.nullsLast(String::compareTo)); + }; + return (asc?c:c.reversed()).thenComparing(Company::getId); + } + private Map incompanyMap(Company c){ + Map r=new LinkedHashMap<>(); + r.put("id", c.getId()); + r.put("status", c.isActive()?"사용":"미사용"); + r.put("active", c.isActive()); + r.put("name", c.getName()); + r.put("telecom", c.getTelecom()); + r.put("phone", c.getPhone()); + r.put("fax", c.getFax()); + r.put("managerName", c.getManagerName()); + r.put("mobile", c.getMobile()); + r.put("businessName", c.getBusinessName()); + r.put("businessNumber", c.getBusinessNumber()); + r.put("createdAt", c.getCreatedAt()==null?null:c.getCreatedAt().atZone(ZoneId.systemDefault()).toLocalDate().toString()); + r.put("memo", c.getMemo()); + return r; + } + @GetMapping("/outcompanies") ApiResponse outcompanies(@AuthenticationPrincipal JwtUser u,@RequestParam Map f, + @RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="15") int size){ + return ApiResponse.ok(outcompanyData(u,f,page,size)); + } + @PostMapping("/outcompanies") @Transactional ApiResponse outcompanyCreate(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + String name=d.get("name")==null?"":String.valueOf(d.get("name")).trim(); + if(name.isBlank()) return ApiResponse.fail("출고처명을 입력하세요."); + Company c=new Company(); + c.setGroupId(u.getGroupId()); + c.setType(blank(String.valueOf(d.getOrDefault("type","")))?"OUT":String.valueOf(d.get("type"))); + if(!"OUT".equals(c.getType()) && !"SHOP".equals(c.getType())) c.setType("OUT"); + applyOutcompanyFields(c,d); + c.setName(name); + if(d.get("active")==null && d.get("status")==null) c.setActive(true); + em.persist(c); + return ApiResponse.ok(outcompanyMap(c)); + } + @PutMapping("/outcompanies/{id}") @Transactional ApiResponse outcompanyUpdate(@PathVariable Long id,@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Company c=em.find(Company.class,id); + if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId()) || !List.of("OUT","SHOP").contains(c.getType())) return ApiResponse.fail("출고처를 찾을 수 없습니다."); + if(d.get("name")!=null){ + String name=String.valueOf(d.get("name")).trim(); + if(name.isBlank()) return ApiResponse.fail("출고처명을 입력하세요."); + c.setName(name); + } + applyOutcompanyFields(c,d); + return ApiResponse.ok(outcompanyMap(c)); + } + @PostMapping("/outcompanies/status") @Transactional ApiResponse outcompanyStatus(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Object raw=d.get("ids"); + if(!(raw instanceof List ids) || ids.isEmpty()) return ApiResponse.fail("항목을 선택하세요."); + boolean active=!"미사용".equals(String.valueOf(d.getOrDefault("status",""))) && !"false".equalsIgnoreCase(String.valueOf(d.getOrDefault("active",""))); + int updated=0; + for(Object idObj:ids){ + Company c=em.find(Company.class, Long.valueOf(idObj.toString())); + if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId()) || !List.of("OUT","SHOP").contains(c.getType())) continue; + c.setActive(active); updated++; + } + return ApiResponse.ok(Map.of("updated", updated)); + } + @GetMapping(value="/outcompanies/export", produces="text/csv;charset=UTF-8") ResponseEntity outcompanyExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + @SuppressWarnings("unchecked") List> rows=(List>) outcompanyData(u,f,0,10000).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF상태,채널,업체명,P코드,영업담당,전화,담당자,휴대폰,상호,사업자번호,계좌,등록일,비고\n"); + for(Map r:rows) csv.append(csv(r.get("status"))).append(',').append(csv(r.get("channel"))).append(',') + .append(csv(r.get("name"))).append(',').append(csv(r.get("pcode"))).append(',').append(csv(r.get("salesperson"))).append(',') + .append(csv(r.get("phone"))).append(',').append(csv(r.get("managerName"))).append(',').append(csv(r.get("mobile"))).append(',') + .append(csv(r.get("businessName"))).append(',').append(csv(r.get("businessNumber"))).append(',').append(csv(r.get("bankAccount"))).append(',') + .append(csv(r.get("createdAt"))).append(',').append(csv(r.get("memo"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=outcompanies.csv").body(csv.toString()); + } + private void applyOutcompanyFields(Company c, Map d){ + if(d.get("channel")!=null) c.setChannel(blank(String.valueOf(d.get("channel")))?null:String.valueOf(d.get("channel")).trim()); + if(d.get("category")!=null) c.setCategory(blank(String.valueOf(d.get("category")))?null:String.valueOf(d.get("category")).trim()); + if(d.get("pcode")!=null) c.setPcode(blank(String.valueOf(d.get("pcode")))?null:String.valueOf(d.get("pcode")).trim()); + if(d.get("salesperson")!=null) c.setSalesperson(blank(String.valueOf(d.get("salesperson")))?null:String.valueOf(d.get("salesperson")).trim()); + if(d.get("phone")!=null) c.setPhone(blank(String.valueOf(d.get("phone")))?null:String.valueOf(d.get("phone")).trim()); + if(d.get("managerName")!=null) c.setManagerName(blank(String.valueOf(d.get("managerName")))?null:String.valueOf(d.get("managerName")).trim()); + if(d.get("mobile")!=null) c.setMobile(blank(String.valueOf(d.get("mobile")))?null:String.valueOf(d.get("mobile")).trim()); + if(d.get("businessName")!=null) c.setBusinessName(blank(String.valueOf(d.get("businessName")))?null:String.valueOf(d.get("businessName")).trim()); + if(d.get("businessNumber")!=null) c.setBusinessNumber(blank(String.valueOf(d.get("businessNumber")))?null:String.valueOf(d.get("businessNumber")).trim()); + if(d.get("bankAccount")!=null) c.setBankAccount(blank(String.valueOf(d.get("bankAccount")))?null:String.valueOf(d.get("bankAccount")).trim()); + if(d.get("memo")!=null) c.setMemo(blank(String.valueOf(d.get("memo")))?null:String.valueOf(d.get("memo")).trim()); + if(d.get("status")!=null) c.setActive(!"미사용".equals(String.valueOf(d.get("status"))) && !"중지".equals(String.valueOf(d.get("status")))); + if(d.get("active")!=null) c.setActive(Boolean.parseBoolean(String.valueOf(d.get("active"))) || "true".equalsIgnoreCase(String.valueOf(d.get("active"))) || "사용".equals(String.valueOf(d.get("active")))); + } + private Map outcompanyData(JwtUser u, Map f, int page, int size){ + List all=em.createQuery("select c from Company c where c.groupId=:g and c.type in ('OUT','SHOP')",Company.class) + .setParameter("g",u.getGroupId()).getResultList(); + String status=f.getOrDefault("status",""); + String channel=f.getOrDefault("channel",""); + String category=f.getOrDefault("category",""); + String searchType=f.getOrDefault("searchType","출고처명"); + String keyword=f.getOrDefault("keyword",""); + List filtered=all.stream().filter(c->{ + if("사용".equals(status) && !c.isActive()) return false; + if("미사용".equals(status) && c.isActive()) return false; + String ch=blank(c.getChannel())?"기타":c.getChannel(); + if(!eq(ch, channel)) return false; + String cat=blank(c.getCategory())?("SHOP".equals(c.getType())?"판매점":"출고처"):c.getCategory(); + if(!eq(cat, category)) return false; + if(!blank(keyword)){ + String q=keyword.trim(); + return switch(searchType){ + case "P코드" -> like(c.getPcode(), q); + case "담당자" -> like(c.getManagerName(), q); + case "영업담당" -> like(c.getSalesperson(), q); + case "상호" -> like(c.getBusinessName(), q); + case "사업자번호" -> like(c.getBusinessNumber(), q); + default -> like(c.getName(), q); + }; + } + return true; + }).sorted(outcompanyComparator(f.get("sort"))).toList(); + int sz=Math.min(Math.max(size,1),200); + int fromIdx=Math.min(Math.max(page,0)*sz, filtered.size()); + int toIdx=Math.min(fromIdx+sz, filtered.size()); + List> list=filtered.subList(fromIdx,toIdx).stream().map(this::outcompanyMap).toList(); + Map result=new LinkedHashMap<>(); + result.put("total", filtered.size()); + result.put("list", list); + return result; + } + private Comparator outcompanyComparator(String sort){ + String field=sort==null?"name,asc":sort; boolean asc=field.endsWith(",asc"); + Comparator c=switch(field.split(",")[0]){ + case "pcode"->Comparator.comparing(Company::getPcode,Comparator.nullsLast(String::compareTo)); + case "channel"->Comparator.comparing(x->blank(x.getChannel())?"기타":x.getChannel(),Comparator.nullsLast(String::compareTo)); + case "createdAt"->Comparator.comparing(Company::getCreatedAt,Comparator.nullsLast(Instant::compareTo)); + default->Comparator.comparing(Company::getName,Comparator.nullsLast(String::compareTo)); + }; + return (asc?c:c.reversed()).thenComparing(Company::getId); + } + private Map outcompanyMap(Company c){ + Map r=new LinkedHashMap<>(); + r.put("id", c.getId()); + r.put("status", c.isActive()?"사용":"미사용"); + r.put("active", c.isActive()); + r.put("channel", blank(c.getChannel())?"기타":c.getChannel()); + r.put("category", blank(c.getCategory())?("SHOP".equals(c.getType())?"판매점":"출고처"):c.getCategory()); + r.put("name", c.getName()); + r.put("pcode", c.getPcode()); + r.put("salesperson", c.getSalesperson()); + r.put("phone", c.getPhone()); + r.put("managerName", c.getManagerName()); + r.put("mobile", c.getMobile()); + r.put("businessName", c.getBusinessName()); + r.put("businessNumber", c.getBusinessNumber()); + r.put("bankAccount", c.getBankAccount()); + r.put("createdAt", c.getCreatedAt()==null?null:c.getCreatedAt().atZone(ZoneId.systemDefault()).toLocalDate().toString()); + r.put("memo", c.getMemo()); + r.put("type", c.getType()); + return r; + } + @GetMapping("/deliveries") ApiResponse deliveries(@AuthenticationPrincipal JwtUser u,@RequestParam Map f, + @RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="15") int size){ + return ApiResponse.ok(deliveryData(u,f,page,size)); + } + @PostMapping("/deliveries") @Transactional ApiResponse deliveryCreate(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + String name=d.get("name")==null?"":String.valueOf(d.get("name")).trim(); + if(name.isBlank()) return ApiResponse.fail("업체명을 입력하세요."); + Company c=new Company(); + c.setGroupId(u.getGroupId()); + c.setType("DELIVERY"); + c.setName(name); + if(d.get("phone")!=null) c.setPhone(blank(String.valueOf(d.get("phone")))?null:String.valueOf(d.get("phone")).trim()); + if(d.get("memo")!=null) c.setMemo(blank(String.valueOf(d.get("memo")))?null:String.valueOf(d.get("memo")).trim()); + c.setActive(!"미사용".equals(String.valueOf(d.getOrDefault("status","")))); + if(d.get("active")==null && d.get("status")==null) c.setActive(true); + em.persist(c); + return ApiResponse.ok(deliveryMap(c)); + } + @PutMapping("/deliveries/{id}") @Transactional ApiResponse deliveryUpdate(@PathVariable Long id,@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Company c=em.find(Company.class,id); + if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId()) || !"DELIVERY".equals(c.getType())) return ApiResponse.fail("배송처를 찾을 수 없습니다."); + if(d.get("name")!=null){ + String name=String.valueOf(d.get("name")).trim(); + if(name.isBlank()) return ApiResponse.fail("업체명을 입력하세요."); + c.setName(name); + } + if(d.get("phone")!=null) c.setPhone(blank(String.valueOf(d.get("phone")))?null:String.valueOf(d.get("phone")).trim()); + if(d.get("memo")!=null) c.setMemo(blank(String.valueOf(d.get("memo")))?null:String.valueOf(d.get("memo")).trim()); + if(d.get("status")!=null) c.setActive(!"미사용".equals(String.valueOf(d.get("status")))); + if(d.get("active")!=null) c.setActive(Boolean.parseBoolean(String.valueOf(d.get("active"))) || "true".equalsIgnoreCase(String.valueOf(d.get("active"))) || "사용".equals(String.valueOf(d.get("active")))); + return ApiResponse.ok(deliveryMap(c)); + } + @PostMapping("/deliveries/status") @Transactional ApiResponse deliveryStatus(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Object raw=d.get("ids"); + if(!(raw instanceof List ids) || ids.isEmpty()) return ApiResponse.fail("항목을 선택하세요."); + boolean active=!"미사용".equals(String.valueOf(d.getOrDefault("status",""))); + int updated=0; + for(Object idObj:ids){ + Company c=em.find(Company.class, Long.valueOf(idObj.toString())); + if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId()) || !"DELIVERY".equals(c.getType())) continue; + c.setActive(active); updated++; + } + return ApiResponse.ok(Map.of("updated", updated)); + } + @GetMapping(value="/deliveries/export", produces="text/csv;charset=UTF-8") ResponseEntity deliveryExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + @SuppressWarnings("unchecked") List> rows=(List>) deliveryData(u,f,0,10000).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF상태,업체명,연락처,비고,등록일\n"); + for(Map r:rows) csv.append(csv(r.get("status"))).append(',').append(csv(r.get("name"))).append(',') + .append(csv(r.get("phone"))).append(',').append(csv(r.get("memo"))).append(',').append(csv(r.get("createdAt"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=deliveries.csv").body(csv.toString()); + } + private Map deliveryData(JwtUser u, Map f, int page, int size){ + List all=em.createQuery("select c from Company c where c.groupId=:g and c.type='DELIVERY'",Company.class) + .setParameter("g",u.getGroupId()).getResultList(); + String searchType=f.getOrDefault("searchType","업체명"); + String keyword=f.getOrDefault("keyword",""); + List filtered=all.stream().filter(c->{ + if(!blank(keyword)){ + String q=keyword.trim(); + return switch(searchType){ + case "연락처" -> like(c.getPhone(), q); + case "비고" -> like(c.getMemo(), q); + default -> like(c.getName(), q); + }; + } + return true; + }).sorted(deliveryComparator(f.get("sort"))).toList(); + int sz=Math.min(Math.max(size,1),200); + int fromIdx=Math.min(Math.max(page,0)*sz, filtered.size()); + int toIdx=Math.min(fromIdx+sz, filtered.size()); + List> list=filtered.subList(fromIdx,toIdx).stream().map(this::deliveryMap).toList(); + Map result=new LinkedHashMap<>(); + result.put("total", filtered.size()); + result.put("list", list); + return result; + } + private Comparator deliveryComparator(String sort){ + String field=sort==null?"name,asc":sort; boolean asc=field.endsWith(",asc"); + Comparator c=switch(field.split(",")[0]){ + case "createdAt"->Comparator.comparing(Company::getCreatedAt,Comparator.nullsLast(Instant::compareTo)); + case "phone"->Comparator.comparing(Company::getPhone,Comparator.nullsLast(String::compareTo)); + default->Comparator.comparing(Company::getName,Comparator.nullsLast(String::compareTo)); + }; + return (asc?c:c.reversed()).thenComparing(Company::getId); + } + private Map deliveryMap(Company c){ + Map r=new LinkedHashMap<>(); + r.put("id", c.getId()); + r.put("status", c.isActive()?"사용":"미사용"); + r.put("active", c.isActive()); + r.put("name", c.getName()); + r.put("phone", c.getPhone()); + r.put("memo", c.getMemo()); + r.put("createdAt", c.getCreatedAt()==null?null:c.getCreatedAt().atZone(ZoneId.systemDefault()).toLocalDate().toString()); + return r; + } + @PostMapping("/staff") @Transactional ApiResponse staffCreate(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + String userid=d.get("userid")==null?"":String.valueOf(d.get("userid")).trim(); + String password=d.get("password")==null?"":String.valueOf(d.get("password")); + String name=d.get("name")==null?"":String.valueOf(d.get("name")).trim(); + if(userid.isBlank()) return ApiResponse.fail("아이디를 입력하세요."); + if(password.isBlank()) return ApiResponse.fail("비밀번호를 입력하세요."); + if(name.isBlank()) return ApiResponse.fail("이름을 입력하세요."); + if(!em.createQuery("select m from Member m where m.userid=:id",Member.class).setParameter("id",userid).getResultList().isEmpty()) + return ApiResponse.fail("이미 사용 중인 아이디입니다."); + Member m=new Member(); + m.setGroupId(u.getGroupId()); + m.setUserid(userid); + m.setPassword(encoder.encode(password)); + m.setPlainPassword(password); + m.setName(name); + m.setPhone(formatStaffPhone(String.valueOf(d.getOrDefault("phone","")))); + m.setRole("AGENT"); + m.setStaffPermission(blank(String.valueOf(d.getOrDefault("staffPermission","")))?"영업":String.valueOf(d.get("staffPermission"))); + m.setActive(!"중지".equals(String.valueOf(d.getOrDefault("status",""))) && !"false".equalsIgnoreCase(String.valueOf(d.getOrDefault("active","")))); + m.setLoginCount(0); + m.setAssignedOutNames(blank(String.valueOf(d.getOrDefault("assignedOutNames","")))?null:String.valueOf(d.get("assignedOutNames")).trim()); + m.setMemo(blank(String.valueOf(d.getOrDefault("memo","")))?null:String.valueOf(d.get("memo")).trim()); + em.persist(m); + return ApiResponse.ok(staffMap(m)); + } + @PutMapping("/staff/{id}") @Transactional ApiResponse staffUpdate(@PathVariable Long id,@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Member m=em.find(Member.class,id); + if(m==null || !Objects.equals(m.getGroupId(),u.getGroupId()) || !"AGENT".equals(m.getRole())) return ApiResponse.fail("직원을 찾을 수 없습니다."); + if(d.get("name")!=null){ + String name=String.valueOf(d.get("name")).trim(); + if(name.isBlank()) return ApiResponse.fail("이름을 입력하세요."); + m.setName(name); + } + if(d.get("phone")!=null) m.setPhone(formatStaffPhone(String.valueOf(d.get("phone")))); + if(d.get("staffPermission")!=null && !String.valueOf(d.get("staffPermission")).isBlank()) m.setStaffPermission(String.valueOf(d.get("staffPermission"))); + if(d.get("assignedOutNames")!=null) m.setAssignedOutNames(blank(String.valueOf(d.get("assignedOutNames")))?null:String.valueOf(d.get("assignedOutNames")).trim()); + if(d.get("memo")!=null) m.setMemo(blank(String.valueOf(d.get("memo")))?null:String.valueOf(d.get("memo")).trim()); + if(d.get("status")!=null) m.setActive(!"중지".equals(String.valueOf(d.get("status")))); + if(d.get("active")!=null) m.setActive(Boolean.parseBoolean(String.valueOf(d.get("active"))) || "true".equalsIgnoreCase(String.valueOf(d.get("active"))) || "사용".equals(String.valueOf(d.get("active")))); + if(d.get("password")!=null && !String.valueOf(d.get("password")).isBlank()){ + String password=String.valueOf(d.get("password")); + m.setPassword(encoder.encode(password)); + m.setPlainPassword(password); + } + return ApiResponse.ok(staffMap(m)); + } + @GetMapping(value="/staff/export", produces="text/csv;charset=UTF-8") ResponseEntity staffExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + @SuppressWarnings("unchecked") List> rows=(List>) staffData(u,f,0,10000).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF상태,권한,아이디,이름,휴대폰,담당출고처,접속수,최종접속,등록일,비고\n"); + for(Map r:rows) csv.append(csv(r.get("status"))).append(',').append(csv(r.get("staffPermission"))).append(',') + .append(csv(r.get("userid"))).append(',').append(csv(r.get("name"))).append(',').append(csv(r.get("phone"))).append(',') + .append(csv(r.get("assignedOutNames"))).append(',').append(csv(r.get("loginCount"))).append(',') + .append(csv(r.get("lastLoginAt"))).append(',').append(csv(r.get("createdAt"))).append(',').append(csv(r.get("memo"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=staff.csv").body(csv.toString()); + } + private Map staffData(JwtUser u, Map f, int page, int size){ + List all=em.createQuery("select m from Member m where m.groupId=:g and m.role='AGENT'",Member.class) + .setParameter("g",u.getGroupId()).getResultList(); + String status=f.getOrDefault("status",""); + String keyword=f.getOrDefault("keyword",""); + List filtered=all.stream().filter(m->{ + if("사용".equals(status) && !m.isActive()) return false; + if("중지".equals(status) && m.isActive()) return false; + if(!blank(keyword)){ + String q=keyword.trim().toLowerCase(); + boolean hit=(m.getName()!=null && m.getName().toLowerCase().contains(q)) + || (m.getUserid()!=null && m.getUserid().toLowerCase().contains(q)); + if(!hit) return false; + } + return true; + }).sorted(staffComparator(f.get("sort"))).toList(); + int sz=Math.min(Math.max(size,1),200); + int fromIdx=Math.min(Math.max(page,0)*sz, filtered.size()); + int toIdx=Math.min(fromIdx+sz, filtered.size()); + List> list=filtered.subList(fromIdx,toIdx).stream().map(this::staffMap).toList(); + Map result=new LinkedHashMap<>(); + result.put("total", filtered.size()); + result.put("list", list); + return result; + } + private Comparator staffComparator(String sort){ + String field=sort==null?"createdAt,asc":sort; boolean asc=field.endsWith(",asc"); + Comparator c=switch(field.split(",")[0]){ + case "staffPermission"->Comparator.comparing(m->blank(m.getStaffPermission())?"영업":m.getStaffPermission(),Comparator.nullsLast(String::compareTo)); + case "userid"->Comparator.comparing(Member::getUserid,Comparator.nullsLast(String::compareTo)); + case "name"->Comparator.comparing(Member::getName,Comparator.nullsLast(String::compareTo)); + case "loginCount"->Comparator.comparing(m->m.getLoginCount()==null?0:m.getLoginCount()); + case "lastLoginAt"->Comparator.comparing(Member::getLastLoginAt,Comparator.nullsLast(LocalDateTime::compareTo)); + default->Comparator.comparing(Member::getCreatedAt,Comparator.nullsLast(Instant::compareTo)); + }; + // 대표 first + Comparator ownerFirst=(a,b)->{ + boolean ao="대표".equals(a.getStaffPermission()); + boolean bo="대표".equals(b.getStaffPermission()); + if(ao==bo) return 0; + return ao?-1:1; + }; + return ownerFirst.thenComparing(asc?c:c.reversed()).thenComparing(Member::getId); + } + private Map staffMap(Member m){ + Map r=new LinkedHashMap<>(); + r.put("id", m.getId()); + r.put("status", m.isActive()?"사용":"중지"); + r.put("active", m.isActive()); + r.put("staffPermission", blank(m.getStaffPermission())?("demo".equals(m.getUserid())?"대표":"영업"):m.getStaffPermission()); + r.put("userid", m.getUserid()); + r.put("name", m.getName()); + r.put("phone", formatStaffPhone(m.getPhone())); + r.put("assignedOutNames", m.getAssignedOutNames()); + r.put("loginCount", m.getLoginCount()==null?0:m.getLoginCount()); + r.put("lastLoginAt", m.getLastLoginAt()==null?null:m.getLastLoginAt().format(java.time.format.DateTimeFormatter.ofPattern("MM-dd HH:mm"))); + r.put("createdAt", m.getCreatedAt()==null?null:m.getCreatedAt().atZone(ZoneId.systemDefault()).toLocalDate().toString()); + r.put("memo", m.getMemo()); + r.put("isOwner", "대표".equals(r.get("staffPermission")) || "demo".equals(m.getUserid())); + return r; + } + private String formatStaffPhone(String phone){ + if(phone==null || phone.isBlank()) return ""; + String p=phone.replaceAll("[^0-9]",""); + if(p.length()==11) return p.substring(0,3)+"-"+p.substring(3,7)+"-"+p.substring(7); + if(p.length()==10) return p.substring(0,3)+"-"+p.substring(3,6)+"-"+p.substring(6); + return phone; + } + @GetMapping("/settings/agentshop") ApiResponse agentShopSettingGet(@AuthenticationPrincipal JwtUser u){ + return ApiResponse.ok(loadAgentShopSetting(u.getGroupId())); + } + @PutMapping("/settings/agentshop") @Transactional ApiResponse agentShopSettingPut(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ + Map current=loadAgentShopSetting(u.getGroupId()); + current.putAll(d); + String json=writeSettingJson(current); + List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='agentshop'",Setting.class).setParameter("g",u.getGroupId()).getResultList(); + if(rows.isEmpty()){ + Setting s=new Setting(); s.setGroupId(u.getGroupId()); s.setSettingKey("agentshop"); s.setValue(json); em.persist(s); + } else { + rows.getFirst().setValue(json); + } + return ApiResponse.ok(current); + } + private Map loadAgentShopSetting(String groupId){ + Map defaults=defaultAgentShopSetting(); + List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='agentshop'",Setting.class).setParameter("g",groupId).getResultList(); + if(rows.isEmpty()||blank(rows.getFirst().getValue())) return defaults; + try{ + @SuppressWarnings("unchecked") Map parsed=new com.fasterxml.jackson.databind.ObjectMapper().readValue(rows.getFirst().getValue(), Map.class); + defaults.putAll(parsed); + }catch(Exception ignored){} + return defaults; + } + private Map defaultAgentShopSetting(){ + Map m=new LinkedHashMap<>(); + m.put("access",true); + m.put("policy",true); m.put("pds",true); m.put("returnPhone",true); m.put("indoc",true); + m.put("scan",true); m.put("scanReceiveSms",false); m.put("scanReceivePhones",""); + m.put("scanSendSms",false); m.put("scanSenderPhone","1600-3903"); + m.put("scanCompleteMsg","스캔신청서가 완료 처리되었습니다."); + m.put("scanHoldMsg","스캔신청서가 보류 처리되었습니다."); + m.put("stock",true); m.put("stockPeriod","always"); + m.put("open",true); m.put("settle",true); m.put("farebox",true); + m.put("shopUrl","https://shop.bizcare.co.kr"); + return m; + } + private String writeSettingJson(Map m){ + try{ return new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(m); } + catch(Exception e){ return "{}"; } + } + @DeleteMapping({"/scans/{id}","/indocs/{id}","/returns/{id}","/fareboxes/{id}","/accounts/{id}","/sms/{id}","/cs/{id}"}) ApiResponse commonDel(HttpServletRequest r,@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return del(type(r.getRequestURI()),id,u);} + @PutMapping({"/schedules/{id}","/accounts/{id}","/sms/{id}"}) ApiResponse commonPut2(HttpServletRequest r,@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return put(type(r.getRequestURI()),id,d,u);} + @GetMapping("/basic/phones") ApiResponse phones(@AuthenticationPrincipal JwtUser u,@RequestParam(required=false,defaultValue="") String q){return list(PhoneModel.class,u,q.isBlank()?Map.of():Map.of("modelName",q),0,100);} + @GetMapping("/basic/usims") ApiResponse usims(@AuthenticationPrincipal JwtUser u,@RequestParam(required=false,defaultValue="") String q){return list(Stock.class,u,Map.of("gubun","유심"),0,100);} + @GetMapping("/basic/plans") ApiResponse plans(@AuthenticationPrincipal JwtUser u,@RequestParam(required=false,defaultValue="") String telecom){return list(PlanMaster.class,u,telecom==null||telecom.isBlank()?Map.of():Map.of("telecom",telecom),0,100);} + @GetMapping("/basic/petnames") ApiResponse petnames(@AuthenticationPrincipal JwtUser u,@RequestParam(required=false,defaultValue="") String q){return list(Company.class,u,q.isBlank()?Map.of():Map.of("name",q),0,100);} + @GetMapping("/stocks/have-own") ApiResponse haveOwn(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + return ApiResponse.ok(haveOwnData(u, f)); + } + @GetMapping(value="/stocks/have-own/export", produces="text/csv;charset=UTF-8") ResponseEntity haveOwnExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + Map data=haveOwnData(u,f); + @SuppressWarnings("unchecked") List models=(List) data.get("models"); + @SuppressWarnings("unchecked") List> rows=(List>) data.get("rows"); + StringBuilder csv=new StringBuilder("\uFEFF채널,영업사원,출고처,전체"); + for(String m:models) csv.append(',').append(csv(m)); + csv.append('\n'); + for(Map r:rows) { + csv.append(csv(r.get("channel"))).append(',').append(csv(r.get("salesperson"))).append(',') + .append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("total"))); + @SuppressWarnings("unchecked") Map counts=(Map) r.get("counts"); + for(String m:models) csv.append(',').append(csv(counts.getOrDefault(m,0)==0?"-":counts.get(m))); + csv.append('\n'); + } + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=have-own.csv").body(csv.toString()); + } + private Map haveOwnData(JwtUser u, Map f) { + Map companies=companyNames(u); + boolean includeColor="true".equalsIgnoreCase(f.getOrDefault("includeColor","false")); + List stocks=em.createQuery("select s from Stock s where s.groupId=:g",Stock.class).setParameter("g",u.getGroupId()).getResultList(); + List held=stocks.stream().filter(s->{ + if(!Set.of("IN","OUT","RECOVERY").contains(s.getState()==null?"":s.getState())) return false; + if(!eq(s.getGubun(),f.get("gubun"))) return false; + if(!eqId(s.getIncompanyId(),f.get("incompanyId"))) return false; + if(!eqId(s.getOutcompanyId(),f.get("outcompanyId"))) return false; + return true; + }).toList(); + LinkedHashSet modelSet=new LinkedHashSet<>(); + Map> rowMap=new LinkedHashMap<>(); + for(Stock s:held) { + String channel=haveOwnChannel(s); + String salesperson=blank(s.getSalesperson())?( "IN".equals(s.getState())?"본점":"-"):s.getSalesperson(); + String outName; + Long outId=s.getOutcompanyId(); + if(outId!=null) outName=companies.getOrDefault(outId,"-"); + else if("IN".equals(s.getState())) outName="본점"; + else outName="-"; + String modelKey=blank(s.getModel())?"-":s.getModel(); + if(includeColor) { + String color=blank(s.getColor())?"-":s.getColor(); + modelKey=modelKey+" / "+color; + } + modelSet.add(modelKey); + String rowKey=channel+"|"+salesperson+"|"+(outId==null?"0":outId)+"|"+outName; + Map row=rowMap.computeIfAbsent(rowKey,k->{ + Map r=new LinkedHashMap<>(); + r.put("channel",channel); r.put("salesperson",salesperson); + r.put("outcompanyId",outId); r.put("outcompanyName",outName); + r.put("total",0); r.put("counts",new LinkedHashMap()); + return r; + }); + @SuppressWarnings("unchecked") Map counts=(Map) row.get("counts"); + counts.put(modelKey, counts.getOrDefault(modelKey,0)+1); + row.put("total", ((Integer)row.get("total"))+1); + } + List models=modelSet.stream().sorted().toList(); + List> rows=new ArrayList<>(rowMap.values()); + rows.sort(Comparator + .comparing((Map r)->String.valueOf(r.get("channel"))) + .thenComparing(r->String.valueOf(r.get("salesperson"))) + .thenComparing(r->String.valueOf(r.get("outcompanyName")))); + // row spans + for(int i=0;i result=new LinkedHashMap<>(); + result.put("asOf", LocalDate.now().toString()); + result.put("title", LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy년 MM월 dd일"))+" 현재보유재고"); + result.put("models", models); + result.put("rows", rows); + result.put("totalQty", held.size()); + return result; + } + private String haveOwnChannel(Stock s) { + if("도매".equals(s.getOutCategory())) return "도매"; + if("IN".equals(s.getState()) && s.getOutcompanyId()==null) return "본점"; + if("본사".equals(s.getSource()) || "판매점".equals(s.getOutCategory())) return "본점"; + return blank(s.getOutCategory())?"본점":s.getOutCategory(); + } + @GetMapping("/stocks/have-model") ApiResponse haveModel(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + return ApiResponse.ok(haveModelData(u, f)); + } + @GetMapping(value="/stocks/have-model/export", produces="text/csv;charset=UTF-8") ResponseEntity haveModelExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + Map data=haveModelData(u,f); + boolean totalsOnly="true".equalsIgnoreCase(f.getOrDefault("totalsOnly","false")); + @SuppressWarnings("unchecked") List holders=(List) data.get("holders"); + @SuppressWarnings("unchecked") List> rows=(List>) data.get("rows"); + StringBuilder csv=new StringBuilder("\uFEFF종류,모델명,색상,전체,불량,본점"); + if(!totalsOnly) for(String h:holders) csv.append(',').append(csv(h)); + csv.append('\n'); + for(Map r:rows) { + csv.append(csv(r.get("gubun"))).append(',').append(csv(r.get("model"))).append(',') + .append(csv(r.get("color"))).append(',').append(csv(dashZero(r.get("total")))).append(',') + .append(csv(dashZero(r.get("defect")))).append(',').append(csv(dashZero(r.get("hq")))); + if(!totalsOnly) { + @SuppressWarnings("unchecked") Map counts=(Map) r.get("counts"); + for(String h:holders) csv.append(',').append(csv(dashZero(counts.getOrDefault(h,0)))); + } + csv.append('\n'); + } + String filename=totalsOnly?"have-model-totals.csv":"have-model.csv"; + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename="+filename).body(csv.toString()); + } + private Object dashZero(Object v){ if(v==null) return "-"; if(v instanceof Number n && n.intValue()==0) return "-"; return v; } + private Map haveModelData(JwtUser u, Map f) { + Map companies=companyNames(u); + List stocks=em.createQuery("select s from Stock s where s.groupId=:g",Stock.class).setParameter("g",u.getGroupId()).getResultList(); + List held=stocks.stream().filter(s->{ + if(!Set.of("IN","OUT","RECOVERY").contains(s.getState()==null?"":s.getState())) return false; + if(!eqId(s.getIncompanyId(),f.get("incompanyId"))) return false; + if(!like(s.getModel(),f.get("model"))) return false; + return true; + }).toList(); + LinkedHashSet holderSet=new LinkedHashSet<>(); + Map> rowMap=new LinkedHashMap<>(); + for(Stock s:held) { + String gubun=blank(s.getGubun())?"-":s.getGubun(); + String model=blank(s.getModel())?"-":s.getModel(); + String color=blank(s.getColor())?"-":s.getColor(); + String rowKey=gubun+"|"+model+"|"+color; + Map row=rowMap.computeIfAbsent(rowKey,k->{ + Map r=new LinkedHashMap<>(); + r.put("gubun",gubun); r.put("model",model); r.put("color",color); + r.put("total",0); r.put("defect",0); r.put("hq",0); + r.put("counts",new LinkedHashMap()); + return r; + }); + row.put("total", ((Integer)row.get("total"))+1); + if("불량".equals(s.getCond())) row.put("defect", ((Integer)row.get("defect"))+1); + boolean isHq="IN".equals(s.getState()) && s.getOutcompanyId()==null; + if(isHq) { + row.put("hq", ((Integer)row.get("hq"))+1); + } else { + String holder; + if(s.getOutcompanyId()!=null) holder=companies.getOrDefault(s.getOutcompanyId(),"-"); + else if(!blank(s.getSalesperson())) holder=s.getSalesperson(); + else holder="-"; + holderSet.add(holder); + @SuppressWarnings("unchecked") Map counts=(Map) row.get("counts"); + counts.put(holder, counts.getOrDefault(holder,0)+1); + } + } + List holders=holderSet.stream().sorted().toList(); + List> rows=new ArrayList<>(rowMap.values()); + rows.sort(Comparator + .comparing((Map r)->String.valueOf(r.get("gubun"))) + .thenComparing(r->String.valueOf(r.get("model"))) + .thenComparing(r->String.valueOf(r.get("color")))); + for(int i=0;i result=new LinkedHashMap<>(); + result.put("asOf", LocalDate.now().toString()); + result.put("title", LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy년 MM월 dd일"))+" 현재보유재고"); + result.put("holders", holders); + result.put("rows", rows); + result.put("totalQty", held.size()); + return result; + } + @GetMapping("/stocks/turnover") ApiResponse turnover(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + return ApiResponse.ok(turnoverData(u, f)); + } + @GetMapping(value="/stocks/turnover/export", produces="text/csv;charset=UTF-8") ResponseEntity turnoverExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + Map data=turnoverData(u,f); + String mode=String.valueOf(data.getOrDefault("mode","outcompany")); + @SuppressWarnings("unchecked") List> rows=(List>) data.get("rows"); + StringBuilder csv=new StringBuilder("\uFEFF순위,"); + if("model".equals(mode)) csv.append("모델명,개통수,현재 재고수,회전율,회전일,개통예상\n"); + else csv.append("출고처,개통수,현재 재고수,회전율,회전일,개통예상,영업담당자\n"); + for(Map r:rows) { + csv.append(csv(r.get("rank"))).append(','); + if("model".equals(mode)) { + csv.append(csv(r.get("model"))).append(',').append(csv(r.get("openCount"))).append(',') + .append(csv(r.get("stockCount"))).append(',').append(csv(r.get("turnoverRate"))).append(',') + .append(csv(r.get("turnoverDays"))).append(',').append(csv(r.get("expectedOpen"))).append('\n'); + } else { + csv.append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("openCount"))).append(',') + .append(csv(r.get("stockCount"))).append(',').append(csv(r.get("turnoverRate"))).append(',') + .append(csv(r.get("turnoverDays"))).append(',').append(csv(r.get("expectedOpen"))).append(',') + .append(csv(r.get("salesperson"))).append('\n'); + } + } + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=turnover.csv").body(csv.toString()); + } + private Map turnoverData(JwtUser u, Map f) { + String mode=f.getOrDefault("mode","outcompany"); + Map companies=companyNames(u); + List stocks=em.createQuery("select s from Stock s where s.groupId=:g",Stock.class).setParameter("g",u.getGroupId()).getResultList() + .stream().filter(s->!"유심".equals(s.getGubun())).toList(); + List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g",OpenRecord.class).setParameter("g",u.getGroupId()).getResultList(); + Map agg=new LinkedHashMap<>(); + Map salesMap=new LinkedHashMap<>(); + for(OpenRecord o:opens) { + String key="model".equals(mode) + ? turnoverModelKey(o.getPmodel(), o.getPcolor()) + : (o.getOutcompanyId()==null?"본점":companies.getOrDefault(o.getOutcompanyId(),"-")); + int[] v=agg.computeIfAbsent(key,k->new int[]{0,0}); + v[0]++; + } + for(Stock s:stocks) { + if("OPEN".equals(s.getState())) { + String key="model".equals(mode) + ? turnoverModelKey(s.getModel(), s.getColor()) + : (s.getOutcompanyId()==null?"본점":companies.getOrDefault(s.getOutcompanyId(),"-")); + int[] v=agg.computeIfAbsent(key,k->new int[]{0,0}); + v[0]++; + if(!blank(s.getSalesperson())) salesMap.putIfAbsent(key, s.getSalesperson()); + } + if(Set.of("IN","OUT","RECOVERY").contains(s.getState()==null?"":s.getState())) { + String key="model".equals(mode) + ? turnoverModelKey(s.getModel(), s.getColor()) + : (s.getOutcompanyId()==null?"본점":companies.getOrDefault(s.getOutcompanyId(),"-")); + if(!"model".equals(mode) && "IN".equals(s.getState()) && s.getOutcompanyId()!=null) continue; + int[] v=agg.computeIfAbsent(key,k->new int[]{0,0}); + v[1]++; + if(!blank(s.getSalesperson())) salesMap.putIfAbsent(key, s.getSalesperson()); + } + } + List> rows=new ArrayList<>(); + for(var e:agg.entrySet()) { + int openCount=e.getValue()[0], stockCount=e.getValue()[1]; + if(openCount==0 && stockCount==0) continue; + double rate=stockCount>0 ? Math.round(openCount * 10000.0 / stockCount) / 100.0 : 0; + int days=openCount>0 ? (int)Math.round(stockCount * 10.0 / openCount) : 0; + Map row=new LinkedHashMap<>(); + if("model".equals(mode)) row.put("model", e.getKey()); + else row.put("outcompanyName", e.getKey()); + row.put("openCount", openCount); + row.put("stockCount", stockCount); + row.put("turnoverRate", rate); + row.put("turnoverDays", days); + // 10일 기준 개통수를 월(30일) 예상으로 환산 + row.put("expectedOpen", openCount == 0 ? 0 : openCount * 3); + row.put("salesperson", salesMap.getOrDefault(e.getKey(),"-")); + rows.add(row); + } + String sort=f.getOrDefault("sort","turnoverRate,desc"); + boolean asc=sort.endsWith(",asc"); + String field=sort.split(",")[0]; + rows.sort((a,b)->{ + int cmp=switch(field){ + case "outcompanyName"->String.valueOf(a.get("outcompanyName")).compareTo(String.valueOf(b.get("outcompanyName"))); + case "model"->String.valueOf(a.get("model")).compareTo(String.valueOf(b.get("model"))); + case "openCount"->Integer.compare((Integer)a.get("openCount"),(Integer)b.get("openCount")); + case "stockCount"->Integer.compare((Integer)a.get("stockCount"),(Integer)b.get("stockCount")); + case "turnoverDays"->Integer.compare((Integer)a.get("turnoverDays"),(Integer)b.get("turnoverDays")); + case "expectedOpen"->Integer.compare((Integer)a.get("expectedOpen"),(Integer)b.get("expectedOpen")); + case "salesperson"->String.valueOf(a.get("salesperson")).compareTo(String.valueOf(b.get("salesperson"))); + default->Double.compare(((Number)a.get("turnoverRate")).doubleValue(),((Number)b.get("turnoverRate")).doubleValue()); + }; + return asc?cmp:-cmp; + }); + for(int i=0;i result=new LinkedHashMap<>(); + result.put("mode", mode); + result.put("info", info); + result.put("rows", rows); + result.put("total", rows.size()); + return result; + } + private String turnoverModelKey(String model, String color) { + String m=blank(model)?"-":model.trim(); + if(blank(color)) return m; + return m+" "+color.trim(); + } + @GetMapping("/reports/mon-open-stats") ApiResponse monOpenStats(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + return ApiResponse.ok(monOpenStatsData(u,f)); + } + @GetMapping(value="/reports/mon-open-stats/export", produces="text/csv;charset=UTF-8") ResponseEntity monOpenStatsExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + Map data=monOpenStatsData(u,f); + String mode=String.valueOf(data.getOrDefault("mode","outcompany")); + String firstCol=switch(mode){ case "incompany"->"입고처"; case "model"->"모델명"; default->"출고처"; }; + @SuppressWarnings("unchecked") List> rows=(List>) data.get("rows"); + StringBuilder csv=new StringBuilder("\uFEFF").append(firstCol); + for(int m=1;m<=12;m++) csv.append(',').append(m).append("월"); + csv.append(",합계\n"); + for(Map r:rows){ + csv.append(csv(r.get("name"))); + @SuppressWarnings("unchecked") List months=(List) r.get("months"); + for(int m=0;m<12;m++) csv.append(',').append(csv(dashZero(months.get(m)))); + csv.append(',').append(csv(dashZero(r.get("total")))).append('\n'); + } + @SuppressWarnings("unchecked") Map totals=(Map) data.get("totals"); + if(totals!=null){ + csv.append(csv("합계")); + @SuppressWarnings("unchecked") List months=(List) totals.get("months"); + for(int m=0;m<12;m++) csv.append(',').append(csv(dashZero(months.get(m)))); + csv.append(',').append(csv(dashZero(totals.get("total")))).append('\n'); + } + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=mon-open-stats.csv").body(csv.toString()); + } + @GetMapping("/reports/open-charts") ApiResponse openCharts(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + return ApiResponse.ok(openChartsData(u,f)); + } + private Map openChartsData(JwtUser u, Map f){ + String mode=f.getOrDefault("mode","outcompany"); + return switch(mode){ + case "outcompany-annual"->openChartAnnual(u,f,"outcompany"); + case "sales-annual"->openChartSalesAnnual(u,f); + case "sales"->openChartPeriod(u,f,"sales"); + default->openChartPeriod(u,f,"outcompany"); + }; + } + private Map openChartPeriod(JwtUser u, Map f, String dim){ + LocalDate from=parseDate(f.get("from"), LocalDate.now().minusDays(29)); + LocalDate to=parseDate(f.get("to"), LocalDate.now()); + if(to.isBefore(from)){ LocalDate t=from; from=to; to=t; } + Map companies=companyNames(u); + Map counts=new LinkedHashMap<>(); + if("outcompany".equals(dim)){ + List outs=em.createQuery("select c from Company c where c.groupId=:g and c.type in ('OUT','SHOP') order by c.name",Company.class) + .setParameter("g",u.getGroupId()).getResultList(); + for(Company c:outs) counts.putIfAbsent(blank(c.getName())?"-":c.getName(),0); + } + List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g and o.opendate between :a and :b",OpenRecord.class) + .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); + for(OpenRecord o:opens){ + if(!eq(o.getTelecom(),f.get("telecom"))) continue; + String key; + if("sales".equals(dim)){ + if(blank(o.getSalesperson())) continue; + key=o.getSalesperson().trim(); + } else { + key=o.getOutcompanyId()==null?"본점":companies.getOrDefault(o.getOutcompanyId(),"-"); + if("-".equals(key)) continue; + } + counts.put(key, counts.getOrDefault(key,0)+1); + } + if("sales".equals(dim) && counts.isEmpty()){ + // keep empty chart categories from known salespeople on stocks + List names=em.createQuery("select distinct s.salesperson from Stock s where s.groupId=:g and s.salesperson is not null and s.salesperson<>''",String.class) + .setParameter("g",u.getGroupId()).getResultList(); + for(String n:names) counts.putIfAbsent(n,0); + } + String sort=f.getOrDefault("sort","name"); + List> entries=new ArrayList<>(counts.entrySet()); + if("count".equals(sort) || "value".equals(sort)) + entries.sort((a,b)->{ + int c=Integer.compare(b.getValue(),a.getValue()); + return c!=0?c:a.getKey().compareTo(b.getKey()); + }); + else entries.sort(Map.Entry.comparingByKey()); + List> items=new ArrayList<>(); + for(var e:entries){ + Map row=new LinkedHashMap<>(); + row.put("name", e.getKey()); + row.put("value", e.getValue()); + items.add(row); + } + Map result=new LinkedHashMap<>(); + result.put("mode", "sales".equals(dim)?"sales":"outcompany"); + result.put("chartType", "bar"); + result.put("title", "sales".equals(dim)?"영업사원별 개통":"출고처별 개통"); + result.put("subtitle", "기간 : "+from+" ~ "+to); + result.put("from", from.toString()); + result.put("to", to.toString()); + result.put("items", items); + return result; + } + private Map openChartAnnual(JwtUser u, Map f, String dim){ + int year=parseInt(f.get("year"), LocalDate.now().getYear()); + LocalDate from=LocalDate.of(year,1,1); + LocalDate to=LocalDate.of(year,12,31); + Map companies=companyNames(u); + Map seriesMap=new LinkedHashMap<>(); + if("outcompany".equals(dim)){ + List outs=em.createQuery("select c from Company c where c.groupId=:g and c.type in ('OUT','SHOP') order by c.name",Company.class) + .setParameter("g",u.getGroupId()).getResultList(); + for(Company c:outs) seriesMap.putIfAbsent(blank(c.getName())?"-":c.getName(), new int[12]); + } + List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g and o.opendate between :a and :b",OpenRecord.class) + .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); + for(OpenRecord o:opens){ + if(!eq(o.getTelecom(),f.get("telecom"))) continue; + String key=o.getOutcompanyId()==null?"본점":companies.getOrDefault(o.getOutcompanyId(),"-"); + if("-".equals(key)) continue; + int[] arr=seriesMap.computeIfAbsent(key,k->new int[12]); + arr[o.getOpendate().getMonthValue()-1]++; + } + List categories=new ArrayList<>(); + for(int m=1;m<=12;m++) categories.add(m+"월"); + List> series=new ArrayList<>(); + for(var e:seriesMap.entrySet()){ + Map s=new LinkedHashMap<>(); + s.put("name", e.getKey()); + List data=new ArrayList<>(12); + for(int v:e.getValue()) data.add(v); + s.put("data", data); + series.add(s); + } + series.sort(Comparator.comparing(s->String.valueOf(s.get("name")))); + Map result=new LinkedHashMap<>(); + result.put("mode", "outcompany-annual"); + result.put("chartType", "line"); + result.put("title", "출고처별 개통"); + result.put("subtitle", year+"년"); + result.put("year", year); + result.put("categories", categories); + result.put("series", series); + return result; + } + private Map openChartSalesAnnual(JwtUser u, Map f){ + int year=parseInt(f.get("year"), LocalDate.now().getYear()); + int month=parseInt(f.get("month"), LocalDate.now().getMonthValue()); + if(month<1||month>12) month=LocalDate.now().getMonthValue(); + LocalDate end=LocalDate.of(year, month, 1); + LocalDate start=end.minusMonths(11); + LocalDate from=start.withDayOfMonth(1); + LocalDate to=end.withDayOfMonth(end.lengthOfMonth()); + Map seriesMap=new LinkedHashMap<>(); + List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g and o.opendate between :a and :b",OpenRecord.class) + .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); + for(OpenRecord o:opens){ + if(!eq(o.getTelecom(),f.get("telecom"))) continue; + if(blank(o.getSalesperson())) continue; + String key=o.getSalesperson().trim(); + int[] arr=seriesMap.computeIfAbsent(key,k->new int[12]); + int idx=monthIndexInWindow(o.getOpendate(), start); + if(idx>=0) arr[idx]++; + } + if(seriesMap.isEmpty()){ + List names=em.createQuery("select distinct s.salesperson from Stock s where s.groupId=:g and s.salesperson is not null and s.salesperson<>''",String.class) + .setParameter("g",u.getGroupId()).getResultList(); + for(String n:names) seriesMap.putIfAbsent(n, new int[12]); + } + List categories=new ArrayList<>(); + for(int i=0;i<12;i++){ + LocalDate d=start.plusMonths(i); + categories.add(d.getMonthValue()+"월"); + } + int[] sum=new int[12]; + int nSeries=Math.max(seriesMap.size(),1); + List> series=new ArrayList<>(); + List keys=new ArrayList<>(seriesMap.keySet()); + keys.sort(String::compareTo); + for(String key:keys){ + int[] arr=seriesMap.get(key); + Map s=new LinkedHashMap<>(); + s.put("name", key); + s.put("type", "bar"); + List data=new ArrayList<>(12); + for(int i=0;i<12;i++){ data.add(arr[i]); sum[i]+=arr[i]; } + s.put("data", data); + series.add(s); + } + List avg=new ArrayList<>(12); + for(int i=0;i<12;i++) avg.add(Math.round(sum[i]*100.0/nSeries)/100.0); + Map avgSeries=new LinkedHashMap<>(); + avgSeries.put("name", "평균"); + avgSeries.put("type", "line"); + avgSeries.put("data", avg); + series.add(avgSeries); + Map result=new LinkedHashMap<>(); + result.put("mode", "sales-annual"); + result.put("chartType", "combo"); + result.put("title", "영업사원별 개통"); + result.put("subtitle", year+"년 "+String.format("%02d", month)+"월"); + result.put("year", year); + result.put("month", month); + result.put("categories", categories); + result.put("series", series); + return result; + } + private int monthIndexInWindow(LocalDate date, LocalDate windowStart){ + if(date==null) return -1; + int idx=(date.getYear()-windowStart.getYear())*12+(date.getMonthValue()-windowStart.getMonthValue()); + return (idx>=0 && idx<12)?idx:-1; + } + private LocalDate parseDate(String v, LocalDate fallback){ + if(blank(v)) return fallback; + try{ return LocalDate.parse(v); } catch(Exception e){ return fallback; } + } + private int parseInt(String v, int fallback){ + if(blank(v)) return fallback; + try{ return Integer.parseInt(v); } catch(Exception e){ return fallback; } + } + private Map monOpenStatsData(JwtUser u, Map f){ + String mode=f.getOrDefault("mode","outcompany"); + int year; + try{ year=Integer.parseInt(f.getOrDefault("year",String.valueOf(LocalDate.now().getYear()))); } + catch(Exception e){ year=LocalDate.now().getYear(); } + LocalDate from=LocalDate.of(year,1,1); + LocalDate to=LocalDate.of(year,12,31); + Map companies=companyNames(u); + Map agg=new LinkedHashMap<>(); + if("outcompany".equals(mode)){ + List outs=em.createQuery("select c from Company c where c.groupId=:g and c.type in ('OUT','SHOP') order by c.name",Company.class) + .setParameter("g",u.getGroupId()).getResultList(); + for(Company c:outs) agg.putIfAbsent(c.getName()==null?"-":c.getName(), new int[12]); + } else if("incompany".equals(mode)){ + List ins=em.createQuery("select c from Company c where c.groupId=:g and c.type='IN' order by c.name",Company.class) + .setParameter("g",u.getGroupId()).getResultList(); + for(Company c:ins) agg.putIfAbsent(c.getName()==null?"-":c.getName(), new int[12]); + } + List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g and o.opendate between :a and :b",OpenRecord.class) + .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); + for(OpenRecord o:opens){ + if(!eq(o.getTelecom(),f.get("telecom"))) continue; + if(!eq(o.getOutCategory(),f.get("outCategory"))) continue; + if(!eqId(o.getIncompanyId(),f.get("incompanyId"))) continue; + if(!eqId(o.getOutcompanyId(),f.get("outcompanyId"))) continue; + String key; + if("incompany".equals(mode)){ + if(o.getIncompanyId()==null) continue; + key=companies.getOrDefault(o.getIncompanyId(),"-"); + } else if("model".equals(mode)){ + if(blank(o.getPmodel())) continue; + key=o.getPmodel().trim(); + } else { + key=o.getOutcompanyId()==null?"본점":companies.getOrDefault(o.getOutcompanyId(),"-"); + } + if("-".equals(key)) continue; + int[] months=agg.computeIfAbsent(key,k->new int[12]); + int mi=o.getOpendate().getMonthValue()-1; + months[mi]++; + } + List> rows=new ArrayList<>(); + int[] totalMonths=new int[12]; + int grand=0; + List keys=new ArrayList<>(agg.keySet()); + keys.sort(String::compareTo); + long id=1; + for(String key:keys){ + int[] months=agg.get(key); + int rowTotal=0; + List monthList=new ArrayList<>(12); + for(int i=0;i<12;i++){ monthList.add(months[i]); rowTotal+=months[i]; totalMonths[i]+=months[i]; } + if("model".equals(mode) && rowTotal==0) continue; + grand+=rowTotal; + Map row=new LinkedHashMap<>(); + row.put("id", id++); + row.put("name", key); + row.put("months", monthList); + row.put("total", rowTotal); + rows.add(row); + } + List totalMonthList=new ArrayList<>(12); + for(int v:totalMonths) totalMonthList.add(v); + Map totals=new LinkedHashMap<>(); + totals.put("id", -1L); + totals.put("name", "합계"); + totals.put("months", totalMonthList); + totals.put("total", grand); + Map result=new LinkedHashMap<>(); + result.put("mode", mode); + result.put("year", year); + result.put("nameLabel", switch(mode){ case "incompany"->"입고처"; case "model"->"모델명"; default->"출고처"; }); + result.put("rows", rows); + result.put("totals", totals); + return result; + } + @GetMapping("/reports/open-stats") ApiResponse openStats(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + String mode=f.getOrDefault("mode","daily"); + if("monthly".equals(mode)) return ApiResponse.ok(openStatsMonthly(u,f)); + return ApiResponse.ok(openStatsDaily(u,f)); + } + @GetMapping(value="/reports/open-stats/export", produces="text/csv;charset=UTF-8") ResponseEntity openStatsExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + String mode=f.getOrDefault("mode","daily"); + Map data="monthly".equals(mode)?openStatsMonthly(u,f):openStatsDaily(u,f); + @SuppressWarnings("unchecked") List> rows=(List>) data.get("rows"); + String firstCol="monthly".equals(mode)?"월":"일자"; + StringBuilder csv=new StringBuilder("\uFEFF").append(firstCol).append(",신규,MNP,보상,기변,에이징,가개통,선불개통,합계,정산금액,후정산,후정산금액,철회,마진\n"); + for(Map r:rows) appendOpenStatCsv(csv,r,"monthly".equals(mode)?"label":"label"); + @SuppressWarnings("unchecked") Map totals=(Map) data.get("totals"); + if(totals!=null){ totals=new LinkedHashMap<>(totals); totals.put("label","합계"); appendOpenStatCsv(csv,totals,"label"); } + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=open-stats.csv").body(csv.toString()); + } + private void appendOpenStatCsv(StringBuilder csv,Map r,String labelKey){ + csv.append(csv(r.get(labelKey))).append(',').append(csv(r.get("newCount"))).append(',').append(csv(r.get("mnpCount"))).append(',') + .append(csv(r.get("rewardCount"))).append(',').append(csv(r.get("changeCount"))).append(',').append(csv(r.get("agingCount"))).append(',') + .append(csv(r.get("tempCount"))).append(',').append(csv(r.get("prepaidCount"))).append(',').append(csv(r.get("totalCount"))).append(',') + .append(csv(r.get("settleAmount"))).append(',').append(csv(r.get("afterCount"))).append(',').append(csv(r.get("afterAmount"))).append(',') + .append(csv(r.get("withdrawCount"))).append(',').append(csv(r.get("margin"))).append('\n'); + } + @GetMapping("/reports/home-stats") ApiResponse homeStats(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + String mode=f.getOrDefault("mode","daily"); + if("monthly".equals(mode)) return ApiResponse.ok(homeStatsMonthly(u,f)); + return ApiResponse.ok(homeStatsDaily(u,f)); + } + @GetMapping(value="/reports/home-stats/export", produces="text/csv;charset=UTF-8") ResponseEntity homeStatsExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + String mode=f.getOrDefault("mode","daily"); + Map data="monthly".equals(mode)?homeStatsMonthly(u,f):homeStatsDaily(u,f); + @SuppressWarnings("unchecked") List> rows=(List>) data.get("rows"); + String firstCol="monthly".equals(mode)?"월":"일자"; + StringBuilder csv=new StringBuilder("\uFEFF").append(firstCol).append(",인터넷,TV,집전화,인터넷전화,신용카드,홈IoT,동판,기타,후결합,재약정,소계,합계,정산금액\n"); + for(Map r:rows) appendHomeStatCsv(csv,r); + @SuppressWarnings("unchecked") Map totals=(Map) data.get("totals"); + if(totals!=null){ totals=new LinkedHashMap<>(totals); totals.put("label","합계"); appendHomeStatCsv(csv,totals); } + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=home-stats.csv").body(csv.toString()); + } + private void appendHomeStatCsv(StringBuilder csv,Map r){ + csv.append(csv(r.get("label"))).append(',').append(csv(r.get("internet"))).append(',').append(csv(r.get("tv"))).append(',') + .append(csv(r.get("homePhone"))).append(',').append(csv(r.get("voip"))).append(',').append(csv(r.get("creditCard"))).append(',') + .append(csv(r.get("homeIot"))).append(',').append(csv(r.get("dongpan"))).append(',').append(csv(r.get("etc"))).append(',') + .append(csv(r.get("postBind"))).append(',').append(csv(r.get("renew"))).append(',').append(csv(r.get("subtotal"))).append(',') + .append(csv(r.get("saleCount"))).append(',').append(csv(r.get("settleAmount"))).append('\n'); + } + private Map homeStatsDaily(JwtUser u,Map f){ + int year=Integer.parseInt(f.getOrDefault("year",String.valueOf(LocalDate.now().getYear()))); + int month=Integer.parseInt(f.getOrDefault("month",String.valueOf(LocalDate.now().getMonthValue()))); + LocalDate from=LocalDate.of(year,month,1); + LocalDate to=from.withDayOfMonth(from.lengthOfMonth()); + Map> byDay=new LinkedHashMap<>(); + for(int d=1;d<=to.getDayOfMonth();d++) byDay.put(d,emptyHomeStatRow(d+"일("+weekdayKo(from.withDayOfMonth(d))+")",d)); + for(HomeSale h:loadHomeSales(u,f,from,to)){ + Map target=byDay.get(h.getInstallDate().getDayOfMonth()); + if(target!=null) applyHomeSale(target,h); + } + List> rows=new ArrayList<>(byDay.values()); + Map out=new LinkedHashMap<>(); + out.put("mode","daily"); out.put("year",year); out.put("month",month); + out.put("title",year+"년 "+month+"월"); out.put("rows",rows); out.put("totals",sumHomeStatRows(rows)); + out.put("today",LocalDate.now().getYear()==year&&LocalDate.now().getMonthValue()==month?LocalDate.now().getDayOfMonth():null); + return out; + } + private Map homeStatsMonthly(JwtUser u,Map f){ + int year=Integer.parseInt(f.getOrDefault("year",String.valueOf(LocalDate.now().getYear()))); + LocalDate from=LocalDate.of(year,1,1); + LocalDate to=LocalDate.of(year,12,31); + Map> byMonth=new LinkedHashMap<>(); + for(int m=1;m<=12;m++) byMonth.put(m,emptyHomeStatRow(m+"월",m)); + for(HomeSale h:loadHomeSales(u,f,from,to)){ + Map target=byMonth.get(h.getInstallDate().getMonthValue()); + if(target!=null) applyHomeSale(target,h); + } + List> rows=new ArrayList<>(byMonth.values()); + Map out=new LinkedHashMap<>(); + out.put("mode","monthly"); out.put("year",year); + out.put("title",year+"년"); out.put("rows",rows); out.put("totals",sumHomeStatRows(rows)); + out.put("currentMonth",LocalDate.now().getYear()==year?LocalDate.now().getMonthValue():null); + return out; + } + private List loadHomeSales(JwtUser u,Map f,LocalDate from,LocalDate to){ + List all=em.createQuery("select h from HomeSale h where h.groupId=:g and h.installDate between :a and :b",HomeSale.class) + .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); + return all.stream().filter(h->{ + if(!"완료".equals(h.getStatus())&&!"판매완료".equals(h.getStatus())) return false; + if(!eq(h.getTelecom(),f.get("telecom"))) return false; + if(!eqId(h.getIncompanyId(),f.get("incompanyId"))) return false; + if(!eqId(h.getOutcompanyId(),f.get("outcompanyId"))) return false; + if(!eq(h.getOutCategory(),f.get("outCategory"))) return false; + return h.getInstallDate()!=null; + }).toList(); + } + private void applyHomeSale(Map target,HomeSale h){ + addInt(target,"saleCount",1); + addDec(target,"settleAmount",nvl(h.getSettleAmount())); + String products=h.getProducts()==null?"":h.getProducts(); + int productHits=0; + for(String raw:products.split("[,/|]")){ + String token=raw==null?"":raw.trim(); + if(token.isEmpty()) continue; + String bucket=homeProductBucket(token); + if(bucket==null) continue; + addInt(target,bucket,1); + productHits++; + } + if(productHits==0){ addInt(target,"etc",1); productHits=1; } + addInt(target,"subtotal",productHits); + } + private String homeProductBucket(String token){ + String t=token.replace(" ",""); + if(t.equalsIgnoreCase("인터넷")) return "internet"; + if(t.equalsIgnoreCase("TV")||t.equals("티비")) return "tv"; + if(t.equals("집전화")) return "homePhone"; + if(t.equals("인터넷전화")) return "voip"; + if(t.equals("신용카드")) return "creditCard"; + if(t.equalsIgnoreCase("홈IoT")||t.equalsIgnoreCase("홈IOT")||t.equals("홈아이오티")) return "homeIot"; + if(t.equals("동판")) return "dongpan"; + if(t.equals("후결합")) return "postBind"; + if(t.equals("재약정")) return "renew"; + return "etc"; + } + private Map emptyHomeStatRow(String label,int key){ + Map r=new LinkedHashMap<>(); + r.put("key",key); r.put("label",label); + r.put("internet",0); r.put("tv",0); r.put("homePhone",0); r.put("voip",0); r.put("creditCard",0); + r.put("homeIot",0); r.put("dongpan",0); r.put("etc",0); r.put("postBind",0); r.put("renew",0); + r.put("subtotal",0); r.put("saleCount",0); r.put("settleAmount",BigDecimal.ZERO); + return r; + } + private void mergeHomeStat(Map target,Map src){ + for(String k:List.of("internet","tv","homePhone","voip","creditCard","homeIot","dongpan","etc","postBind","renew","subtotal","saleCount")) + addInt(target,k,((Number)src.getOrDefault(k,0)).intValue()); + addDec(target,"settleAmount",nvl((BigDecimal)src.get("settleAmount"))); + } + private Map sumHomeStatRows(List> rows){ + Map t=emptyHomeStatRow("합계",0); + for(Map r:rows) mergeHomeStat(t,r); + return t; + } + @GetMapping("/reports/outcompany-stats") ApiResponse outcompanyStats(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + return ApiResponse.ok(outcompanyStatsData(u,f)); + } + @GetMapping(value="/reports/outcompany-stats/export", produces="text/csv;charset=UTF-8") ResponseEntity outcompanyStatsExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ + Map data=outcompanyStatsData(u,f); + @SuppressWarnings("unchecked") List> rows=(List>) data.get("rows"); + StringBuilder csv=new StringBuilder("\uFEFF지역,출고처,개통,개통금액,후정산,후정산금액,홈상품,홈상품금액,실정산금액,마진\n"); + for(Map r:rows){ + csv.append(csv(r.get("region"))).append(',').append(csv(r.get("name"))).append(',') + .append(csv(r.get("openCount"))).append(',').append(csv(r.get("openAmount"))).append(',') + .append(csv(r.get("afterCount"))).append(',').append(csv(r.get("afterAmount"))).append(',') + .append(csv(r.get("homeCount"))).append(',').append(csv(r.get("homeAmount"))).append(',') + .append(csv(r.get("realSettleAmount"))).append(',').append(csv(r.get("margin"))).append('\n'); + } + @SuppressWarnings("unchecked") Map totals=(Map) data.get("totals"); + if(totals!=null){ + csv.append(csv("합계")).append(',').append(csv("")).append(',') + .append(csv(totals.get("openCount"))).append(',').append(csv(totals.get("openAmount"))).append(',') + .append(csv(totals.get("afterCount"))).append(',').append(csv(totals.get("afterAmount"))).append(',') + .append(csv(totals.get("homeCount"))).append(',').append(csv(totals.get("homeAmount"))).append(',') + .append(csv(totals.get("realSettleAmount"))).append(',').append(csv(totals.get("margin"))).append('\n'); + } + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=outcompany-stats.csv").body(csv.toString()); + } + private Map outcompanyStatsData(JwtUser u,Map f){ + int year=Integer.parseInt(f.getOrDefault("year",String.valueOf(LocalDate.now().getYear()))); + int month=Integer.parseInt(f.getOrDefault("month",String.valueOf(LocalDate.now().getMonthValue()))); + LocalDate from=LocalDate.of(year,month,1); + LocalDate to=from.withDayOfMonth(from.lengthOfMonth()); + Map companies=companyNames(u); + Map> byOut=new LinkedHashMap<>(); + List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g and o.opendate between :a and :b",OpenRecord.class) + .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); + for(OpenRecord o:opens){ + if(o.getOutcompanyId()==null) continue; + if(!eq(o.getTelecom(),f.get("telecom"))) continue; + if(!eqId(o.getIncompanyId(),f.get("incompanyId"))) continue; + if(!eq(o.getOutCategory(),f.get("outCategory"))) continue; + Map row=outcompanyStatRow(byOut,o.getOutcompanyId(),companies); + addInt(row,"openCount",1); + addDec(row,"openAmount",nvl(o.getSellprice())); + addDec(row,"margin",nvl(o.getDealermargin1()).add(nvl(o.getDealermargin2()))); + } + List afters=em.createQuery("select b from AfterBalance b where b.groupId=:g and b.basedate between :a and :b",AfterBalance.class) + .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); + for(AfterBalance b:afters){ + if(b.getOutcompanyId()==null) continue; + if(!eq(b.getTelecom(),f.get("telecom"))) continue; + if(!eq(b.getOutCategory(),f.get("outCategory"))) continue; + Map row=outcompanyStatRow(byOut,b.getOutcompanyId(),companies); + addInt(row,"afterCount",1); + addDec(row,"afterAmount",nvl(b.getAmount())); + } + List homes=em.createQuery("select h from HomeSale h where h.groupId=:g and h.installDate between :a and :b",HomeSale.class) + .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); + for(HomeSale h:homes){ + if(h.getOutcompanyId()==null) continue; + if(!"완료".equals(h.getStatus())&&!"판매완료".equals(h.getStatus())) continue; + if(!eq(h.getTelecom(),f.get("telecom"))) continue; + if(!eqId(h.getIncompanyId(),f.get("incompanyId"))) continue; + if(!eq(h.getOutCategory(),f.get("outCategory"))) continue; + Map row=outcompanyStatRow(byOut,h.getOutcompanyId(),companies); + addInt(row,"homeCount",1); + addDec(row,"homeAmount",nvl(h.getSettleAmount())); + } + List> rows=new ArrayList<>(); + for(Map r:byOut.values()){ + BigDecimal real=nvl((BigDecimal)r.get("openAmount")).add(nvl((BigDecimal)r.get("afterAmount"))).add(nvl((BigDecimal)r.get("homeAmount"))); + r.put("realSettleAmount",real); + rows.add(r); + } + rows.sort(Comparator + .comparing((Map r)->String.valueOf(r.get("region")),Comparator.nullsLast(String::compareTo)) + .thenComparing(r->String.valueOf(r.get("name")),Comparator.nullsLast(String::compareTo))); + Map totals=emptyOutcompanyStatRow("","합계",0L); + for(Map r:rows){ + addInt(totals,"openCount",((Number)r.getOrDefault("openCount",0)).intValue()); + addInt(totals,"afterCount",((Number)r.getOrDefault("afterCount",0)).intValue()); + addInt(totals,"homeCount",((Number)r.getOrDefault("homeCount",0)).intValue()); + addDec(totals,"openAmount",nvl((BigDecimal)r.get("openAmount"))); + addDec(totals,"afterAmount",nvl((BigDecimal)r.get("afterAmount"))); + addDec(totals,"homeAmount",nvl((BigDecimal)r.get("homeAmount"))); + addDec(totals,"realSettleAmount",nvl((BigDecimal)r.get("realSettleAmount"))); + addDec(totals,"margin",nvl((BigDecimal)r.get("margin"))); + } + Map out=new LinkedHashMap<>(); + out.put("year",year); out.put("month",month); + out.put("title",year+"년 "+month+"월"); + out.put("total",rows.size()); out.put("rows",rows); out.put("totals",totals); + return out; + } + private Map outcompanyStatRow(Map> byOut,Long outId,Map companies){ + return byOut.computeIfAbsent(outId,id->{ + String full=companies.getOrDefault(id,"-"); + String[] parts=splitRegionName(full); + return emptyOutcompanyStatRow(parts[0],parts[1],id); + }); + } + private Map emptyOutcompanyStatRow(String region,String name,Long id){ + Map r=new LinkedHashMap<>(); + r.put("id",id); r.put("region",region); r.put("name",name); + r.put("openCount",0); r.put("openAmount",BigDecimal.ZERO); + r.put("afterCount",0); r.put("afterAmount",BigDecimal.ZERO); + r.put("homeCount",0); r.put("homeAmount",BigDecimal.ZERO); + r.put("realSettleAmount",BigDecimal.ZERO); r.put("margin",BigDecimal.ZERO); + return r; + } + private String[] splitRegionName(String full){ + if(full==null||full.isBlank()) return new String[]{"-","-"}; + String t=full.trim(); + int sp=t.indexOf(' '); + if(sp>0){ + String left=t.substring(0,sp).trim(); + String right=t.substring(sp+1).trim(); + if(!left.isEmpty()&&!right.isEmpty()&&(left.matches(".*(시|군|구)$")||List.of("대구","경산","서울","부산","인천","광주","대전","울산","세종","수원","성남","용인","창원","청주","전주","포항","제주").contains(left))) + return new String[]{left,right}; + } + return new String[]{"-",t}; + } + private Map openStatsDaily(JwtUser u,Map f){ + int year=Integer.parseInt(f.getOrDefault("year",String.valueOf(LocalDate.now().getYear()))); + int month=Integer.parseInt(f.getOrDefault("month",String.valueOf(LocalDate.now().getMonthValue()))); + LocalDate from=LocalDate.of(year,month,1); + LocalDate to=from.withDayOfMonth(from.lengthOfMonth()); + Map> byDay=new LinkedHashMap<>(); + for(int d=1;d<=to.getDayOfMonth();d++) byDay.put(d,emptyOpenStatRow(d+"일("+weekdayKo(from.withDayOfMonth(d))+")",d)); + accumulateOpenStats(u,f,from,to,(date,row)->{ + Map target=byDay.get(date.getDayOfMonth()); + if(target!=null) mergeOpenStat(target,row); + },(date,afterCount,afterAmount)->{ + Map target=byDay.get(date.getDayOfMonth()); + if(target!=null){ addInt(target,"afterCount",afterCount); addDec(target,"afterAmount",afterAmount); } + },(date,withdrawCount)->{ + Map target=byDay.get(date.getDayOfMonth()); + if(target!=null) addInt(target,"withdrawCount",withdrawCount); + }); + List> rows=new ArrayList<>(byDay.values()); + Map totals=sumOpenStatRows(rows); + Map out=new LinkedHashMap<>(); + out.put("mode","daily"); out.put("year",year); out.put("month",month); + out.put("title",year+"년 "+month+"월"); out.put("rows",rows); out.put("totals",totals); + out.put("today",LocalDate.now().getYear()==year&&LocalDate.now().getMonthValue()==month?LocalDate.now().getDayOfMonth():null); + return out; + } + private Map openStatsMonthly(JwtUser u,Map f){ + int year=Integer.parseInt(f.getOrDefault("year",String.valueOf(LocalDate.now().getYear()))); + LocalDate from=LocalDate.of(year,1,1); + LocalDate to=LocalDate.of(year,12,31); + Map> byMonth=new LinkedHashMap<>(); + for(int m=1;m<=12;m++) byMonth.put(m,emptyOpenStatRow(m+"월",m)); + accumulateOpenStats(u,f,from,to,(date,row)->{ + Map target=byMonth.get(date.getMonthValue()); + if(target!=null) mergeOpenStat(target,row); + },(date,afterCount,afterAmount)->{ + Map target=byMonth.get(date.getMonthValue()); + if(target!=null){ addInt(target,"afterCount",afterCount); addDec(target,"afterAmount",afterAmount); } + },(date,withdrawCount)->{ + Map target=byMonth.get(date.getMonthValue()); + if(target!=null) addInt(target,"withdrawCount",withdrawCount); + }); + List> rows=new ArrayList<>(byMonth.values()); + Map totals=sumOpenStatRows(rows); + Map out=new LinkedHashMap<>(); + out.put("mode","monthly"); out.put("year",year); + out.put("title",year+"년"); out.put("rows",rows); out.put("totals",totals); + out.put("currentMonth",LocalDate.now().getYear()==year?LocalDate.now().getMonthValue():null); + return out; + } + private interface OpenDayConsumer { void accept(LocalDate date,Map row); } + private interface AfterDayConsumer { void accept(LocalDate date,int count,BigDecimal amount); } + private interface WithdrawDayConsumer { void accept(LocalDate date,int count); } + private void accumulateOpenStats(JwtUser u,Map f,LocalDate from,LocalDate to, + OpenDayConsumer openFn,AfterDayConsumer afterFn,WithdrawDayConsumer withdrawFn){ + Map companies=companyNames(u); + List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g and o.opendate between :a and :b",OpenRecord.class) + .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); + for(OpenRecord o:opens){ + if(!matchesOpenStat(o,f,companies)) continue; + Map row=emptyOpenStatRow("",0); + String bucket=openStatBucket(o.getOpenhow()); + if(bucket!=null) addInt(row,bucket,1); + addInt(row,"totalCount",1); + addDec(row,"settleAmount",nvl(o.getSellprice())); + addDec(row,"margin",nvl(o.getDealermargin1()).add(nvl(o.getDealermargin2()))); + openFn.accept(o.getOpendate(),row); + } + List afters=em.createQuery("select b from AfterBalance b where b.groupId=:g and b.basedate between :a and :b",AfterBalance.class) + .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); + for(AfterBalance b:afters){ + if(b.getBasedate()==null) continue; + if(!eqId(b.getOutcompanyId(),f.get("outcompanyId"))) continue; + if(!eq(b.getTelecom(),f.get("telecom"))) continue; + if(!eq(b.getOutCategory(),f.get("outCategory"))) continue; + afterFn.accept(b.getBasedate(),1,nvl(b.getAmount())); + } + List withdraws=em.createQuery("select w from OpenWithdrawal w where w.groupId=:g and w.withdrawDate between :a and :b",OpenWithdrawal.class) + .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); + for(OpenWithdrawal w:withdraws){ + if(w.getWithdrawDate()==null) continue; + if(!eqId(w.getOutcompanyId(),f.get("outcompanyId"))) continue; + withdrawFn.accept(w.getWithdrawDate(),1); + } + } + private boolean matchesOpenStat(OpenRecord o,Map f,Map companies){ + if(!eq(o.getTelecom(),f.get("telecom"))) return false; + if(!eqId(o.getIncompanyId(),f.get("incompanyId"))) return false; + if(!eqId(o.getOutcompanyId(),f.get("outcompanyId"))) return false; + if(!eq(o.getOutCategory(),f.get("outCategory"))) return false; + return true; + } + private String openStatBucket(String openhow){ + if(blank(openhow)) return null; + return switch(openhow){ + case "신규"->"newCount"; + case "번호이동","MNP"->"mnpCount"; + case "보상"->"rewardCount"; + case "기변"->"changeCount"; + case "에이징","메이징"->"agingCount"; + case "가개통"->"tempCount"; + case "선불개통"->"prepaidCount"; + default->null; + }; + } + private Map emptyOpenStatRow(String label,int key){ + Map r=new LinkedHashMap<>(); + r.put("key",key); r.put("label",label); + r.put("newCount",0); r.put("mnpCount",0); r.put("rewardCount",0); r.put("changeCount",0); + r.put("agingCount",0); r.put("tempCount",0); r.put("prepaidCount",0); r.put("totalCount",0); + r.put("settleAmount",BigDecimal.ZERO); r.put("afterCount",0); r.put("afterAmount",BigDecimal.ZERO); + r.put("withdrawCount",0); r.put("margin",BigDecimal.ZERO); + return r; + } + private void mergeOpenStat(Map target,Map src){ + for(String k:List.of("newCount","mnpCount","rewardCount","changeCount","agingCount","tempCount","prepaidCount","totalCount","afterCount","withdrawCount")) + addInt(target,k,((Number)src.getOrDefault(k,0)).intValue()); + for(String k:List.of("settleAmount","afterAmount","margin")) + addDec(target,k,nvl((BigDecimal)src.get(k))); + } + private Map sumOpenStatRows(List> rows){ + Map t=emptyOpenStatRow("합계",0); + for(Map r:rows) mergeOpenStat(t,r); + return t; + } + private void addInt(Map m,String k,int v){ m.put(k,((Number)m.getOrDefault(k,0)).intValue()+v); } + private void addDec(Map m,String k,BigDecimal v){ m.put(k,nvl((BigDecimal)m.get(k)).add(nvl(v))); } + private String weekdayKo(LocalDate d){ + return switch(d.getDayOfWeek()){ + case MONDAY->"월"; case TUESDAY->"화"; case WEDNESDAY->"수"; case THURSDAY->"목"; + case FRIDAY->"금"; case SATURDAY->"토"; case SUNDAY->"일"; + }; + } + @GetMapping("/stocks/report-day") ApiResponse reportDay(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + if(blank(f.get("year")) || blank(f.get("month"))) return ApiResponse.fail("년월을 선택하세요."); + return ApiResponse.ok(reportDayData(u, f)); + } + @GetMapping(value="/stocks/report-day/export", produces="text/csv;charset=UTF-8") ResponseEntity reportDayExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + if(blank(f.get("year")) || blank(f.get("month"))) return ResponseEntity.badRequest().body("년월을 선택하세요."); + Map data=reportDayData(u,f); + @SuppressWarnings("unchecked") List> rows=(List>) data.get("rows"); + StringBuilder csv=new StringBuilder("\uFEFF일자,입고,출고,회수,반품,분실,개통,판매,합계\n"); + for(Map r:rows) { + csv.append(csv(r.get("day"))).append(',').append(csv(r.get("inCount"))).append(',') + .append(csv(r.get("outCount"))).append(',').append(csv(r.get("recoveryCount"))).append(',') + .append(csv(r.get("returnCount"))).append(',').append(csv(r.get("lossCount"))).append(',') + .append(csv(r.get("openCount"))).append(',').append(csv(r.get("sellCount"))).append(',') + .append(csv(r.get("total"))).append('\n'); + } + @SuppressWarnings("unchecked") Map totals=(Map) data.get("totals"); + if(totals!=null) { + csv.append(csv("합계")).append(',').append(csv(totals.get("inCount"))).append(',') + .append(csv(totals.get("outCount"))).append(',').append(csv(totals.get("recoveryCount"))).append(',') + .append(csv(totals.get("returnCount"))).append(',').append(csv(totals.get("lossCount"))).append(',') + .append(csv(totals.get("openCount"))).append(',').append(csv(totals.get("sellCount"))).append(',') + .append(csv(totals.get("total"))).append('\n'); + } + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=report-day.csv").body(csv.toString()); + } + private Map reportDayData(JwtUser u, Map f) { + int year=Integer.parseInt(f.get("year")); + int month=Integer.parseInt(f.get("month")); + LocalDate from=LocalDate.of(year, month, 1); + LocalDate to=from.withDayOfMonth(from.lengthOfMonth()); + LocalDate minAllowed=LocalDate.now().minusYears(1).withDayOfMonth(1); + if(from.isBefore(minAllowed)) from=minAllowed; + Map companies=companyNames(u); + Long inFilter=blank(f.get("incompanyId"))?null:Long.valueOf(f.get("incompanyId")); + List stocks=em.createQuery("select s from Stock s where s.groupId=:g",Stock.class).setParameter("g",u.getGroupId()).getResultList(); + Map days=new LinkedHashMap<>(); // [in,out,recovery,return,loss,open,sell] + for(int d=1;d<=to.getDayOfMonth();d++) days.put(d, new int[]{0,0,0,0,0,0,0}); + for(Stock s:stocks) { + if(inFilter!=null && !Objects.equals(s.getIncompanyId(), inFilter)) continue; + bumpDay(days, s.getIndate(), from, to, 0); + bumpDay(days, s.getOutdate(), from, to, 1); + if("RECOVERY".equals(s.getState())) bumpDay(days, s.getProcessDate()!=null?s.getProcessDate():s.getIndate(), from, to, 2); + bumpDay(days, s.getReturndate(), from, to, 3); + bumpDay(days, s.getLossdate(), from, to, 4); + bumpDay(days, s.getSelldate(), from, to, 6); + if("RETURN".equals(s.getState()) && s.getReturndate()==null) bumpDay(days, s.getProcessDate(), from, to, 3); + if("LOSS".equals(s.getState()) && s.getLossdate()==null) bumpDay(days, s.getProcessDate(), from, to, 4); + if("SELL".equals(s.getState()) && s.getSelldate()==null) bumpDay(days, s.getProcessDate(), from, to, 6); + if("OUT".equals(s.getState()) && s.getOutdate()==null) bumpDay(days, s.getProcessDate(), from, to, 1); + if("OPEN".equals(s.getState())) bumpDay(days, s.getProcessDate()!=null?s.getProcessDate():s.getIndate(), from, to, 5); + } + // OpenRecord는 stock OPEN과 중복될 수 있어 stock 기준만 사용 + List> rows=new ArrayList<>(); + int[] sum=new int[7]; + for(var e:days.entrySet()) { + int[] c=e.getValue(); + int total=c[0]+c[1]+c[2]+c[3]+c[4]+c[5]+c[6]; + if(total==0) continue; + for(int i=0;i<7;i++) sum[i]+=c[i]; + Map row=new LinkedHashMap<>(); + row.put("day", String.format("%04d-%02d-%02d", year, month, e.getKey())); + row.put("inCount", c[0]); row.put("outCount", c[1]); row.put("recoveryCount", c[2]); + row.put("returnCount", c[3]); row.put("lossCount", c[4]); row.put("openCount", c[5]); + row.put("sellCount", c[6]); row.put("total", total); + rows.add(row); + } + Map totals=new LinkedHashMap<>(); + totals.put("inCount", sum[0]); totals.put("outCount", sum[1]); totals.put("recoveryCount", sum[2]); + totals.put("returnCount", sum[3]); totals.put("lossCount", sum[4]); totals.put("openCount", sum[5]); + totals.put("sellCount", sum[6]); totals.put("total", sum[0]+sum[1]+sum[2]+sum[3]+sum[4]+sum[5]+sum[6]); + Map result=new LinkedHashMap<>(); + result.put("year", year); result.put("month", month); + result.put("title", year+"년 "+month+"월 일별이력통계"); + result.put("rows", rows); result.put("totals", totals); + result.put("incompanyName", inFilter==null?null:companies.get(inFilter)); + return result; + } + private void bumpDay(Map days, LocalDate date, LocalDate from, LocalDate to, int idx) { + if(date==null || date.isBefore(from) || date.isAfter(to)) return; + int[] c=days.get(date.getDayOfMonth()); + if(c!=null) c[idx]++; + } + @GetMapping("/stocks/history") ApiResponse stockHistory(@AuthenticationPrincipal JwtUser u, @RequestParam Map f, + @RequestParam(defaultValue="0") int page, @RequestParam(defaultValue="15") int size) { + if(blank(f.get("dateFrom")) || blank(f.get("dateTo"))) return ApiResponse.fail("날짜를 선택하세요."); + return ApiResponse.ok(stockHistoryData(u, f, page, size)); + } + @PostMapping("/stocks/history/delete") @Transactional ApiResponse stockHistoryDelete(@AuthenticationPrincipal JwtUser u, @RequestBody Map d) { + Object raw=d.get("ids"); + if(!(raw instanceof List ids) || ids.isEmpty()) return ApiResponse.fail("삭제할 이력을 선택하세요."); + int deleted=0; + for(Object idObj:ids) { + StockHistory h=em.find(StockHistory.class, Long.valueOf(idObj.toString())); + if(h==null || !Objects.equals(h.getGroupId(),u.getGroupId())) continue; + em.remove(h); deleted++; + } + return ApiResponse.ok(Map.of("deleted", deleted)); + } + @GetMapping(value="/stocks/history/export", produces="text/csv;charset=UTF-8") ResponseEntity stockHistoryExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + if(blank(f.get("dateFrom")) || blank(f.get("dateTo"))) return ResponseEntity.badRequest().body("날짜를 선택하세요."); + @SuppressWarnings("unchecked") List> rows=(List>) stockHistoryData(u,f,0,10000).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF처리일,현황,종류,일련번호,모델명,색상,상태,입고처,출고처,처리자,비고\n"); + for(Map r:rows) csv.append(csv(r.get("processDate"))).append(',').append(csv(r.get("statusLabel"))).append(',') + .append(csv(r.get("gubun"))).append(',').append(csv(r.get("serial"))).append(',').append(csv(r.get("model"))).append(',') + .append(csv(r.get("color"))).append(',').append(csv(r.get("cond"))).append(',').append(csv(r.get("incompanyName"))).append(',') + .append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("processor"))).append(',').append(csv(r.get("memo"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=stock-history.csv").body(csv.toString()); + } + private Map stockHistoryData(JwtUser u, Map f, int page, int size) { + Map companies=companyNames(u); + LocalDate from=LocalDate.parse(f.get("dateFrom")); + LocalDate to=LocalDate.parse(f.get("dateTo")); + List all=em.createQuery("select h from StockHistory h where h.groupId=:g",StockHistory.class).setParameter("g",u.getGroupId()).getResultList(); + String searchType=f.getOrDefault("searchType","일련번호"); + String keyword=f.get("keyword"); + List filtered=all.stream().filter(h->{ + if(h.getProcessDate()==null || h.getProcessDate().isBefore(from) || h.getProcessDate().isAfter(to)) return false; + if(keyword!=null && !keyword.isBlank()) { + boolean ok=switch(searchType){ + case "모델명"->like(h.getModel(),keyword); + case "색상"->like(h.getColor(),keyword); + case "펫네임"->like(h.getSalesperson(),keyword); + default->like(h.getSerial(),keyword); + }; + if(!ok) return false; + } + return true; + }).sorted((a,b)->{ + int cmp=Comparator.nullsLast(LocalDate::compareTo).compare(b.getProcessDate(), a.getProcessDate()); + return cmp!=0?cmp:Long.compare(b.getId(), a.getId()); + }).toList(); + int fromIdx=Math.min(Math.max(page,0)*Math.max(size,1), filtered.size()); + int toIdx=Math.min(fromIdx+Math.min(Math.max(size,1),200), filtered.size()); + List> list=filtered.subList(fromIdx,toIdx).stream().map(h->{ + Map r=new LinkedHashMap<>(); + r.put("id", h.getId()); r.put("stockId", h.getStockId()); + r.put("processDate", h.getProcessDate()); r.put("statusLabel", h.getStatusLabel()); + r.put("state", h.getState()); r.put("gubun", h.getGubun()); + r.put("serial", h.getSerial()); r.put("model", h.getModel()); r.put("color", h.getColor()); + r.put("cond", h.getCond()); r.put("memo", h.getMemo()); r.put("processor", h.getProcessor()); + r.put("salesperson", h.getSalesperson()); + r.put("incompanyName", companies.getOrDefault(h.getIncompanyId(),"-")); + r.put("outcompanyName", companies.getOrDefault(h.getOutcompanyId(),"-")); + return r; + }).toList(); + return Map.of("total", filtered.size(), "list", list); + } + private void writeStockHistory(Stock s, JwtUser u) { + StockHistory h=new StockHistory(); + h.setGroupId(u.getGroupId()); + h.setStockId(s.getId()); + h.setIncompanyId(s.getIncompanyId()); + h.setOutcompanyId(s.getOutcompanyId()); + h.setProcessDate(s.getProcessDate()!=null?s.getProcessDate():LocalDate.now()); + h.setGubun(s.getGubun()); h.setState(s.getState()); h.setStatusLabel(statusLabel(s.getState())); + h.setSerial(s.getSerial()); h.setModel(s.getModel()); h.setColor(s.getColor()); + h.setCond(s.getCond()); h.setMemo(s.getMemo()); + h.setProcessor(s.getProcessor()!=null?s.getProcessor():memberName(u)); + h.setSalesperson(s.getSalesperson()); + if("OUT".equals(s.getState()) && s.getOutcompanyId()!=null){ + Company oc=em.find(Company.class, s.getOutcompanyId()); + if(oc!=null){ + String label=oc.getName(); + String cat=s.getOutCategory()!=null&&!s.getOutCategory().isBlank()?s.getOutCategory():oc.getCategory(); + if(cat!=null&&!cat.isBlank()) label=label+" ("+cat+")"; + h.setCompanyLabel(label); + } + } else if("IN".equals(s.getState()) && s.getIncompanyId()!=null){ + Company ic=em.find(Company.class, s.getIncompanyId()); + if(ic!=null) h.setCompanyLabel(ic.getName()); + } + em.persist(h); + } + @GetMapping("/stocks/abooks") ApiResponse abooks(@AuthenticationPrincipal JwtUser u,@RequestParam Map f, + @RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="15") int size) { + return ApiResponse.ok(abookData(u,f,page,size)); + } + @PostMapping("/stocks/abooks") @Transactional ApiResponse abookCreate(@AuthenticationPrincipal JwtUser u,@RequestBody Map d) { + UsimLedger row=new UsimLedger(); + row.setGroupId(u.getGroupId()); + Long outId=null; + if(d.get("outcompanyId")!=null && !d.get("outcompanyId").toString().isBlank()) { + outId=Long.valueOf(d.get("outcompanyId").toString()); + } else { + String name=d.get("outcompanyName")==null?"":d.get("outcompanyName").toString().trim(); + if(name.isBlank()) return ApiResponse.fail("출고처명을 입력해주세요."); + List found=em.createQuery("select c from Company c where c.groupId=:g and c.name=:n",Company.class) + .setParameter("g",u.getGroupId()).setParameter("n",name).getResultList(); + if(!found.isEmpty()) outId=found.getFirst().getId(); + else { + Company c=new Company(); c.setGroupId(u.getGroupId()); c.setType("OUT"); c.setName(name); c.setActive(true); + em.persist(c); em.flush(); outId=c.getId(); + } + } + if(d.get("telecom")==null || d.get("telecom").toString().isBlank()) return ApiResponse.fail("통신사를 선택하세요."); + if(d.get("qty")==null || d.get("qty").toString().isBlank()) return ApiResponse.fail("수량을 입력하세요."); + if(d.get("amount")==null || d.get("amount").toString().isBlank()) return ApiResponse.fail("거래금액을 입력하세요."); + row.setOutcompanyId(outId); + row.setTradeDate(d.get("tradeDate")==null||d.get("tradeDate").toString().isBlank()?LocalDate.now():LocalDate.parse(d.get("tradeDate").toString())); + row.setTelecom(String.valueOf(d.get("telecom"))); + row.setQty(Integer.valueOf(d.get("qty").toString().replace(",",""))); + row.setAmount(new BigDecimal(d.get("amount").toString().replace(",",""))); + row.setDetail(d.get("detail")==null||String.valueOf(d.get("detail")).isBlank()?null:String.valueOf(d.get("detail"))); + row.setRegistrant(d.get("registrant")==null||String.valueOf(d.get("registrant")).isBlank()?u.getUserid():String.valueOf(d.get("registrant"))); + row.setStatus("정상거래"); + em.persist(row); + return ApiResponse.ok(abookMap(row, companyNames(u))); + } + @PostMapping("/stocks/abooks/delete") @Transactional ApiResponse abookDelete(@AuthenticationPrincipal JwtUser u,@RequestBody Map d) { + Object raw=d.get("ids"); + if(!(raw instanceof List ids) || ids.isEmpty()) return ApiResponse.fail("삭제할 항목을 선택하세요."); + int deleted=0; + for(Object idObj:ids) { + UsimLedger row=em.find(UsimLedger.class, Long.valueOf(idObj.toString())); + if(row==null || !Objects.equals(row.getGroupId(),u.getGroupId())) continue; + em.remove(row); deleted++; + } + return ApiResponse.ok(Map.of("deleted",deleted)); + } + @GetMapping(value="/stocks/abooks/export", produces="text/csv;charset=UTF-8") ResponseEntity abookExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f) { + @SuppressWarnings("unchecked") List> rows=(List>) abookData(u,f,0,10000).get("list"); + StringBuilder csv=new StringBuilder("\uFEFF거래일,출고처,통신사,수량,거래금액,거래명세,등록자\n"); + for(Map r:rows) csv.append(csv(r.get("tradeDate"))).append(',').append(csv(r.get("outcompanyName"))).append(',') + .append(csv(r.get("telecom"))).append(',').append(csv(r.get("qty"))).append(',').append(csv(r.get("amount"))).append(',') + .append(csv(r.get("detail"))).append(',').append(csv(r.get("registrant"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=abooks.csv").body(csv.toString()); + } + @GetMapping(value="/stocks/abooks/summary-export", produces="text/csv;charset=UTF-8") ResponseEntity abookSummaryExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f) { + @SuppressWarnings("unchecked") List> rows=(List>) abookData(u,f,0,10000).get("summary"); + StringBuilder csv=new StringBuilder("\uFEFF출고처명,거래,수량,금액\n"); + for(Map r:rows) csv.append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("trades"))).append(',') + .append(csv(r.get("qty"))).append(',').append(csv(r.get("amount"))).append('\n'); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=abooks-summary.csv").body(csv.toString()); + } + private Map companyNames(JwtUser u){ + Map companies=new HashMap<>(); + for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); + return companies; + } + private Map abookData(JwtUser u,Map f,int page,int size) { + Map companies=companyNames(u); + List all=em.createQuery("select e from UsimLedger e where e.groupId=:g",UsimLedger.class).setParameter("g",u.getGroupId()).getResultList(); + String status=f.getOrDefault("status","정상거래"); + Long outFilter=blank(f.get("outcompanyId"))?null:Long.valueOf(f.get("outcompanyId")); + LocalDate from=blank(f.get("dateFrom"))?null:LocalDate.parse(f.get("dateFrom")); + LocalDate to=blank(f.get("dateTo"))?null:LocalDate.parse(f.get("dateTo")); + List filtered=all.stream().filter(e->{ + if(!"전체".equals(status) && !status.equals(e.getStatus())) return false; + if(outFilter!=null && !Objects.equals(e.getOutcompanyId(),outFilter)) return false; + if(from!=null && (e.getTradeDate()==null || e.getTradeDate().isBefore(from))) return false; + return to==null || (e.getTradeDate()!=null && !e.getTradeDate().isAfter(to)); + }).sorted(abookComparator(f.get("sort"))).toList(); + Map qtyMap=new LinkedHashMap<>(); + Map amountMap=new LinkedHashMap<>(); + Map tradeMap=new LinkedHashMap<>(); + for(UsimLedger e:filtered) { + Long key=e.getOutcompanyId()==null?-1L:e.getOutcompanyId(); + tradeMap.put(key, tradeMap.getOrDefault(key,0)+1); + qtyMap.put(key, new int[]{qtyMap.getOrDefault(key,new int[]{0})[0]+(e.getQty()==null?0:e.getQty())}); + amountMap.put(key, amountMap.getOrDefault(key,BigDecimal.ZERO).add(e.getAmount()==null?BigDecimal.ZERO:e.getAmount())); + } + boolean summaryAsc=f.getOrDefault("summarySort","amount,desc").endsWith(",asc"); + String summaryField=f.getOrDefault("summarySort","amount,desc").split(",")[0]; + List> summary=tradeMap.keySet().stream().map(key->{ + Map row=new LinkedHashMap<>(); + row.put("outcompanyId", key<0?null:key); + row.put("outcompanyName", key<0?"-":companies.getOrDefault(key,"-")); + row.put("trades", tradeMap.get(key)); + row.put("qty", qtyMap.get(key)[0]); + row.put("amount", amountMap.get(key)); + return row; + }).sorted((a,b)->{ + int cmp=switch(summaryField){ + case "outcompanyName"->String.valueOf(a.get("outcompanyName")).compareTo(String.valueOf(b.get("outcompanyName"))); + case "trades"->Integer.compare((Integer)a.get("trades"),(Integer)b.get("trades")); + case "qty"->Integer.compare((Integer)a.get("qty"),(Integer)b.get("qty")); + default->((BigDecimal)a.get("amount")).compareTo((BigDecimal)b.get("amount")); + }; + return summaryAsc?cmp:-cmp; + }).toList(); + int tradeTotal=filtered.size(); + int qtyTotal=filtered.stream().mapToInt(e->e.getQty()==null?0:e.getQty()).sum(); + BigDecimal amountTotal=filtered.stream().map(e->e.getAmount()==null?BigDecimal.ZERO:e.getAmount()).reduce(BigDecimal.ZERO,BigDecimal::add); + int fromIdx=Math.min(Math.max(page,0)*Math.max(size,1),filtered.size()); + int toIdx=Math.min(fromIdx+Math.min(Math.max(size,1),200),filtered.size()); + List> list=filtered.subList(fromIdx,toIdx).stream().map(e->abookMap(e,companies)).toList(); + Map totals=new LinkedHashMap<>(); + totals.put("trades",tradeTotal); totals.put("qty",qtyTotal); totals.put("amount",amountTotal); + return Map.of("total",tradeTotal,"list",list,"summary",summary,"totals",totals); + } + private Comparator abookComparator(String sort){ + String field=sort==null?"tradeDate,desc":sort; boolean asc=field.endsWith(",asc"); + Comparator c=switch(field.split(",")[0]){ + case "telecom"->Comparator.comparing(UsimLedger::getTelecom,Comparator.nullsLast(String::compareTo)); + case "qty"->Comparator.comparing(UsimLedger::getQty,Comparator.nullsLast(Integer::compareTo)); + case "amount"->Comparator.comparing(UsimLedger::getAmount,Comparator.nullsLast(BigDecimal::compareTo)); + case "registrant"->Comparator.comparing(UsimLedger::getRegistrant,Comparator.nullsLast(String::compareTo)); + default->Comparator.comparing(UsimLedger::getTradeDate,Comparator.nullsLast(LocalDate::compareTo)); + }; + return (asc?c:c.reversed()).thenComparing(UsimLedger::getId); + } + private Map abookMap(UsimLedger e,Map companies){ + Map r=new LinkedHashMap<>(); + r.put("id",e.getId()); r.put("outcompanyId",e.getOutcompanyId()); + r.put("outcompanyName",companies.getOrDefault(e.getOutcompanyId(),"-")); + r.put("tradeDate",e.getTradeDate()); r.put("telecom",e.getTelecom()); + r.put("qty",e.getQty()); r.put("amount",e.getAmount()); r.put("detail",e.getDetail()); + r.put("registrant",e.getRegistrant()); r.put("status",e.getStatus()); + return r; + } + @SuppressWarnings("unchecked") private Class type(String uri){if(uri.contains("scans"))return (Class)ScanDoc.class;if(uri.contains("indocs"))return (Class)IncompleteDoc.class;if(uri.contains("returns"))return (Class)UnreturnedPhone.class;if(uri.contains("fareboxes"))return (Class)Farebox.class;if(uri.contains("accounts"))return (Class)AccountEntry.class;if(uri.contains("schedules"))return (Class)ScheduleItem.class;if(uri.contains("templates"))return (Class)SmsTemplate.class;if(uri.contains("sms"))return (Class)SmsMessage.class;if(uri.contains("alims"))return (Class)Alim.class;return (Class)CsInquiry.class;} + private BigDecimal nvl(BigDecimal v){return v==null?BigDecimal.ZERO:v;} +} diff --git a/backend/src/main/java/com/bizcare/agent/CareHomesController.java b/backend/src/main/java/com/bizcare/agent/CareHomesController.java new file mode 100644 index 0000000..3fa6271 --- /dev/null +++ b/backend/src/main/java/com/bizcare/agent/CareHomesController.java @@ -0,0 +1,3690 @@ +package com.bizcare.agent; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.*; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/agent/homes") +@RequiredArgsConstructor +class CareHomesController { + private final CrudService crud; + private final EntityManager em; + private final PasswordEncoder encoder; + private final ObjectMapper mapper; + + // ---- Dashboard ---- + @GetMapping("/dashboard") + ApiResponse dashboard(@AuthenticationPrincipal JwtUser u, + @RequestParam(defaultValue = "0") int offset, + @RequestParam(defaultValue = "day") String period) { + return ApiResponse.ok(buildHomesDashboard(u, offset, period)); + } + + @GetMapping("/dashboard/postits") + ApiResponse homesPostitsGet(@AuthenticationPrincipal JwtUser u) { + return ApiResponse.ok(Map.of("list", loadHomesPostits(u.getGroupId()))); + } + + @PutMapping("/dashboard/postits/{slot}") + @Transactional + ApiResponse homesPostitPut(@PathVariable int slot, @AuthenticationPrincipal JwtUser u, + @RequestBody Map d) { + if (slot < 1 || slot > 8) return ApiResponse.fail("슬롯 번호가 올바르지 않습니다."); + List> list = loadHomesPostits(u.getGroupId()); + Map note = list.get(slot - 1); + if (d.get("contents") != null) note.put("contents", String.valueOf(d.get("contents"))); + if (d.get("authorName") != null) note.put("authorName", String.valueOf(d.get("authorName"))); + note.put("noteDate", LocalDate.now(ZoneId.of("Asia/Seoul")).format(java.time.format.DateTimeFormatter.ofPattern("yy-MM-dd"))); + note.put("title", "포스트잇 #" + slot); + saveHomesPostits(u.getGroupId(), list); + return ApiResponse.ok(note); + } + + private Map buildHomesDashboard(JwtUser u, int offset, String period) { + String g = u.getGroupId(); + ZoneId zone = ZoneId.of("Asia/Seoul"); + LocalDate today = LocalDate.now(zone); + YearMonth ym = YearMonth.from(today).minusMonths(Math.max(0, offset)); + LocalDate from = ym.atDay(1); + LocalDate to = ym.atEndOfMonth(); + String label = ym.getYear() + "년 " + String.format("%02d", ym.getMonthValue()) + "월"; + + long contracts = count(HomesContract.class, g); + long customers = count(HomesCustomer.class, g); + long pendingToday = em.createQuery( + "select count(p) from HomesPending p where p.groupId=:g and p.pendingDate=:d", Long.class) + .setParameter("g", g).setParameter("d", today).getSingleResult(); + long pendingOpen = em.createQuery( + "select count(p) from HomesPending p where p.groupId=:g and (p.status is null or p.status not in ('해결','완료','취소'))", Long.class) + .setParameter("g", g).getSingleResult(); + long bookingOpen = em.createQuery( + "select count(b) from HomesBooking b where b.groupId=:g and b.status='접수'", Long.class) + .setParameter("g", g).getSingleResult(); + long bookingTotal = count(HomesBooking.class, g); + BigDecimal abookMonth = em.createQuery( + "select coalesce(sum(a.amount),0) from HomesAbook a where a.groupId=:g and a.entryDate between :f and :t and a.entryType in ('입금','수입','카드입금')", BigDecimal.class) + .setParameter("g", g).setParameter("f", from).setParameter("t", to).getSingleResult(); + BigDecimal settleMonth = em.createQuery( + "select coalesce(sum(c.settleAmount),0) from HomesContract c where c.groupId=:g and c.contractDate between :f and :t", BigDecimal.class) + .setParameter("g", g).setParameter("f", from).setParameter("t", to).getSingleResult(); + + List monthContracts = em.createQuery( + "select c from HomesContract c where c.groupId=:g and c.contractDate between :f and :t", HomesContract.class) + .setParameter("g", g).setParameter("f", from).setParameter("t", to).getResultList(); + List internetMonth = monthContracts.stream().filter(c -> !"HOME".equals(c.getKind())).toList(); + List homeMonth = monthContracts.stream().filter(c -> "HOME".equals(c.getKind())).toList(); + + Map internet = contractPanel("인터넷접수", label, offset, internetMonth, from, to, period, false); + Map home = contractPanel("홈렌탈접수", label, offset, homeMonth, from, to, period, true); + + Map internetCats = categoryCounts(internetMonth, false); + Map homeCats = categoryCounts(homeMonth, true); + + List notices = em.createQuery( + "select n from HomesNotice n where n.groupId=:g order by n.id desc", HomesNotice.class) + .setParameter("g", g).setMaxResults(5).getResultList(); + List questions = em.createQuery( + "select q from HomesQuestion q where q.groupId=:g order by q.id desc", HomesQuestion.class) + .setParameter("g", g).setMaxResults(5).getResultList(); + + Map data = new LinkedHashMap<>(); + data.put("pending", Map.of( + "open", pendingOpen, "today", pendingToday, "total", Math.max(pendingOpen, pendingToday), + "spark", List.of(2, 4, 3, 5, 2, 6, 4, 3, 5, 4, 3, 4))); + data.put("booking", Map.of( + "open", bookingOpen, "total", Math.max(bookingTotal, bookingOpen), + "spark", List.of(1, 2, 2, 3, 1, 4, 2, 3, 2, 1, 3, 2))); + data.put("customer", Map.of( + "total", customers, "monthNew", customers > 0 ? Math.min(customers, 3) : 0, + "spark", List.of(3, 5, 4, 6, 5, 7, 4, 6, 5, 8, 6, 7))); + data.put("abook", Map.of( + "monthAmount", abookMonth, "settleMonth", settleMonth, + "spark", List.of(4, 3, 5, 2, 6, 4, 5, 3, 4, 5, 3, 4))); + data.put("contractTotal", contracts); + data.put("customerTotal", customers); + data.put("postits", loadHomesPostits(g).stream().limit(6).toList()); + data.put("internet", internet); + data.put("home", home); + data.put("internetStatus", Map.of("total", internetCats.values().stream().mapToLong(Long::longValue).sum(), "items", internetCats)); + data.put("homeStatus", Map.of("total", homeCats.values().stream().mapToLong(Long::longValue).sum(), "items", homeCats)); + data.put("notices", notices.stream().map(this::noticeMap).toList()); + data.put("questions", questions.stream().map(q -> { + Map m = new LinkedHashMap<>(); + m.put("id", q.getId()); + m.put("title", q.getTitle()); + m.put("createdDate", q.getCreatedAt() == null ? null : LocalDate.ofInstant(q.getCreatedAt(), zone).toString()); + return m; + }).toList()); + data.put("csPhone", "0000-0000"); + return data; + } + + private Map contractPanel(String title, String label, int offset, List list, + LocalDate from, LocalDate to, String period, boolean home) { + long apply = list.stream().filter(c -> "신청".equals(c.getStatus())).count(); + long accept = list.stream().filter(c -> "접수".equals(c.getStatus())).count(); + long done = list.stream().filter(c -> "완료".equals(c.getStatus())).count(); + long cancel = list.stream().filter(c -> "철회".equals(c.getStatus()) || "취소".equals(c.getStatus())).count(); + BigDecimal settle = list.stream().map(c -> c.getSettleAmount() == null ? BigDecimal.ZERO : c.getSettleAmount()) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + List> daily = new ArrayList<>(); + int days = to.getDayOfMonth(); + Map byDay = new LinkedHashMap<>(); + for (int d = 1; d <= days; d++) byDay.put(d, 0L); + for (HomesContract c : list) { + if (c.getContractDate() == null) continue; + int d = c.getContractDate().getDayOfMonth(); + byDay.put(d, byDay.getOrDefault(d, 0L) + 1); + } + for (Map.Entry e : byDay.entrySet()) { + daily.add(Map.of("day", e.getKey(), "value", e.getValue())); + } + // decorative demo when empty + if (list.isEmpty()) { + int[] demo = home + ? new int[]{0, 0, 1, 0, 2, 1, 0, 1, 0, 0, 1, 2, 0, 1, 0, 0, 1, 0, 2, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1} + : new int[]{0, 1, 0, 2, 1, 0, 0, 1, 0, 2, 0, 1, 0, 0, 1, 2, 0, 1, 0, 0, 1, 0, 2, 1, 0, 0, 1, 0, 0, 0, 1}; + daily.clear(); + for (int d = 1; d <= days; d++) { + daily.add(Map.of("day", d, "value", demo[Math.min(d - 1, demo.length - 1)])); + } + } + + Map m = new LinkedHashMap<>(); + m.put("title", title); + m.put("label", label); + m.put("offset", offset); + m.put("period", period); + m.put("apply", apply); + m.put("accept", accept); + m.put("done", done); + m.put("cancel", cancel); + m.put("settleAmount", settle); + m.put("count", list.size()); + m.put("daily", daily); + return m; + } + + private Map categoryCounts(List list, boolean home) { + String[] keys = home + ? new String[]{"정수기", "공기청정기", "비데", "매트리스", "가전", "기타"} + : new String[]{"인터넷", "TV", "전화", "와이파이", "스마트홈", "기타"}; + Map map = new LinkedHashMap<>(); + for (String k : keys) map.put(k, 0L); + for (HomesContract c : list) { + String products = c.getProducts(); + if (products == null || products.isBlank()) { + map.put("기타", map.get("기타") + 1); + continue; + } + boolean matched = false; + for (String k : keys) { + if (!"기타".equals(k) && products.contains(k)) { + map.put(k, map.get(k) + 1); + matched = true; + } + } + if (!matched) map.put("기타", map.get("기타") + 1); + } + // demo filler when all zero + long sum = map.values().stream().mapToLong(Long::longValue).sum(); + if (sum == 0) { + if (home) { + map.put("정수기", 2L); map.put("공기청정기", 1L); map.put("비데", 1L); + map.put("매트리스", 1L); map.put("가전", 0L); map.put("기타", 1L); + } else { + map.put("인터넷", 3L); map.put("TV", 2L); map.put("전화", 1L); + map.put("와이파이", 1L); map.put("스마트홈", 0L); map.put("기타", 1L); + } + } + return map; + } + + private List> loadHomesPostits(String groupId) { + List> defaults = defaultHomesPostits(); + List rows = em.createQuery( + "select e from Setting e where e.groupId=:g and e.settingKey='postits'", Setting.class) + .setParameter("g", groupId).getResultList(); + if (rows.isEmpty() || rows.getFirst().getValue() == null || rows.getFirst().getValue().isBlank()) return defaults; + try { + @SuppressWarnings("unchecked") + List> parsed = mapper.readValue(rows.getFirst().getValue(), List.class); + for (int i = 0; i < Math.min(8, parsed.size()); i++) { + Map d = defaults.get(i); + d.putAll(parsed.get(i)); + d.put("slot", i + 1); + d.put("title", "포스트잇 #" + (i + 1)); + } + } catch (Exception ignored) {} + return defaults; + } + + private void saveHomesPostits(String groupId, List> list) { + String json; + try { json = mapper.writeValueAsString(list); } catch (Exception e) { json = "[]"; } + List rows = em.createQuery( + "select e from Setting e where e.groupId=:g and e.settingKey='postits'", Setting.class) + .setParameter("g", groupId).getResultList(); + if (rows.isEmpty()) { + Setting s = new Setting(); + s.setGroupId(groupId); + s.setSettingKey("postits"); + s.setValue(json); + em.persist(s); + } else { + rows.getFirst().setValue(json); + } + } + + private List> defaultHomesPostits() { + Object[][] samples = { + {"신규 인터넷 접수 확인", "홍길동", "26-07-08"}, + {"홈렌탈 설치 일정 조율", "홍길동", "26-07-07"}, + {"약속 미결 처리 요청", "김대표", "26-07-05"}, + {"정산서 마감 체크", "홍길동", "26-06-28"}, + {"고객 분류 정리", "홍총괄", "26-06-20"}, + {"예약 접수 현황 점검", "홍길동", "26-06-12"}, + {"고객센터 휴무 안내", "김대표", "26-05-29"}, + {"월간 리포트 작성", "홍총괄", "26-05-15"}, + }; + List> list = new ArrayList<>(); + for (int i = 0; i < 8; i++) { + Map m = new LinkedHashMap<>(); + m.put("slot", i + 1); + m.put("title", "포스트잇 #" + (i + 1)); + m.put("contents", samples[i][0]); + m.put("authorName", samples[i][1]); + m.put("noteDate", samples[i][2]); + list.add(m); + } + return list; + } + + // ---- Products ---- + @GetMapping("/products") + ApiResponse products(@AuthenticationPrincipal JwtUser u, @RequestParam Map f, + @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "15") int size) { + List all = listAll(HomesProduct.class, u); + String kind = blankToNull(f.get("kind")); + String category = blankToNull(f.get("category")); + if ("ALL".equalsIgnoreCase(category)) category = null; + String company = blankToNull(f.get("company")); + String q = qOf(f); + String cat = category; + List base = all.stream().filter(p -> kind == null || kind.equals(p.getKind())).toList(); + List filtered = base.stream().filter(p -> { + if (cat != null && !cat.equals(p.getCategory())) return false; + if (company != null && !company.equals(p.getCompany())) return false; + if (q != null) { + String hay = (nullToEmpty(p.getName()) + " " + nullToEmpty(p.getModel()) + " " + nullToEmpty(p.getCompany())).toLowerCase(); + if (!hay.contains(q.toLowerCase())) return false; + } + return true; + }).toList(); + Map counts = new LinkedHashMap<>(); + counts.put("ALL", (long) base.size()); + base.stream().collect(Collectors.groupingBy(p -> nullToEmpty(p.getCategory()), Collectors.counting())) + .forEach(counts::put); + return pageOk(filtered, page, size, this::productMap, counts); + } + + @GetMapping("/products/{id}") ApiResponse productGet(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + return ApiResponse.ok(productMap(crud.get(HomesProduct.class, id, u))); + } + @PostMapping("/products") @Transactional ApiResponse productPost(@RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + d.putIfAbsent("policyCount", 0); + HomesProduct p = crud.create(HomesProduct.class, d, u); + writeProductLog(u, p, "등록"); + return ApiResponse.ok(productMap(p)); + } + + @PostMapping("/products/sync") @Transactional + ApiResponse productSync(@AuthenticationPrincipal JwtUser u, @RequestBody(required = false) Map body) { + String kind = body != null && body.get("kind") != null ? String.valueOf(body.get("kind")) : "INTERNET"; + List existing = listAll(HomesProduct.class, u).stream() + .filter(p -> kind.equals(p.getKind())).toList(); + Set keys = existing.stream() + .map(p -> p.getCompany() + "|" + p.getCategory() + "|" + p.getName() + "|" + nullToEmpty(p.getModel())) + .collect(Collectors.toSet()); + int added = 0; + for (Object[] row : baseProductCatalog()) { + if (!kind.equals(String.valueOf(row[0]))) continue; + String model = row[4] == null ? "" : String.valueOf(row[4]); + String key = row[2] + "|" + row[1] + "|" + row[3] + "|" + model; + if (keys.contains(key)) continue; + HomesProduct p = new HomesProduct(); + p.setGroupId(u.getGroupId()); + p.setKind(String.valueOf(row[0])); + p.setCategory(String.valueOf(row[1])); + p.setCompany(String.valueOf(row[2])); + p.setName(String.valueOf(row[3])); + p.setModel(model.isBlank() ? null : model); + p.setMonths((Integer) row[5]); + p.setPolicyAmount(row[6] == null ? null : new BigDecimal(String.valueOf(row[6]))); + p.setPolicyCount(row[7] == null ? 0 : (Integer) row[7]); + em.persist(p); + added++; + } + String kindLabel = "HOME".equals(kind) ? "홈렌탈상품" : "인터넷상품"; + writeLog(u, "PRODUCT", "기본상품-동기화", kindLabel + " 기본상품 " + added + "건을 동기화하였습니다."); + return ApiResponse.ok(Map.of("added", added)); + } + + @PutMapping("/products/{id}") @Transactional ApiResponse productPut(@PathVariable Long id, @RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + HomesProduct p = crud.update(HomesProduct.class, id, d, u); + writeProductLog(u, p, "수정"); + return ApiResponse.ok(productMap(p)); + } + @DeleteMapping("/products/{id}") @Transactional ApiResponse productDel(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + HomesProduct p = crud.get(HomesProduct.class, id, u); + em.createQuery("delete from HomesProductPolicy pol where pol.productId=:id and pol.groupId=:g") + .setParameter("id", id).setParameter("g", u.getGroupId()).executeUpdate(); + writeProductLog(u, p, "삭제"); + crud.delete(HomesProduct.class, id, u); + return ApiResponse.ok(null); + } + + @GetMapping("/products/{id}/policies") + ApiResponse productPolicies(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + HomesProduct p = crud.get(HomesProduct.class, id, u); + List list = em.createQuery( + "select pol from HomesProductPolicy pol where pol.groupId=:g and pol.productId=:id order by pol.id", + HomesProductPolicy.class) + .setParameter("g", u.getGroupId()).setParameter("id", id).getResultList(); + return ApiResponse.ok(Map.of("product", productMap(p), "list", list.stream().map(this::productPolicyMap).toList())); + } + + @PostMapping("/products/{id}/policies") @Transactional + ApiResponse productPolicyPost(@PathVariable Long id, @RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + HomesProduct p = crud.get(HomesProduct.class, id, u); + d.put("productId", id); + HomesProductPolicy pol = crud.create(HomesProductPolicy.class, d, u); + refreshProductPolicyCount(p); + String kindLabel = productKindLabel(p); + String policyName = d.get("name") == null ? p.getName() : String.valueOf(d.get("name")); + writeLog(u, "PRODUCT", kindLabel + "-정책-등록", kindLabel + " 정책 [" + policyName + "] 등록하였습니다."); + return ApiResponse.ok(productPolicyMap(pol)); + } + + @DeleteMapping("/products/policies/{policyId}") @Transactional + ApiResponse productPolicyDel(@PathVariable Long policyId, @AuthenticationPrincipal JwtUser u) { + HomesProductPolicy pol = crud.get(HomesProductPolicy.class, policyId, u); + Long productId = pol.getProductId(); + crud.delete(HomesProductPolicy.class, policyId, u); + HomesProduct p = em.find(HomesProduct.class, productId); + if (p != null && Objects.equals(p.getGroupId(), u.getGroupId())) refreshProductPolicyCount(p); + return ApiResponse.ok(null); + } + + private void refreshProductPolicyCount(HomesProduct p) { + long c = em.createQuery("select count(pol) from HomesProductPolicy pol where pol.productId=:id", Long.class) + .setParameter("id", p.getId()).getSingleResult(); + p.setPolicyCount((int) c); + if (c > 0) { + BigDecimal amt = em.createQuery( + "select pol.amount from HomesProductPolicy pol where pol.productId=:id order by pol.id", BigDecimal.class) + .setParameter("id", p.getId()).setMaxResults(1).getResultList().stream().findFirst().orElse(p.getPolicyAmount()); + p.setPolicyAmount(amt); + } + } + + private static Object[][] baseProductCatalog() { + return new Object[][]{ + // kind, category, company, name, model, months, policyAmount, policyCount + {"INTERNET", "인터넷", "KT", "안심 인터넷 베이직 와이드", null, 36, "80000", 3}, + {"INTERNET", "인터넷", "KT", "인터넷 슬림", null, 36, "50000", 1}, + {"INTERNET", "인터넷", "KT", "기가인터넷 500M", null, 36, "80000", 0}, + {"INTERNET", "인터넷", "KT", "기가인터넷 1G", null, 36, "90000", 2}, + {"INTERNET", "인터넷", "KT", "인터넷 베이직 와이파이", null, 36, "60000", 1}, + {"INTERNET", "인터넷", "KT", "인터넷 에센스", null, 36, "70000", 0}, + {"INTERNET", "인터넷", "KT", "인터넷 베이직", null, 36, "55000", 0}, + {"INTERNET", "인터넷", "KT", "인터넷 프리미엄", null, 36, "100000", 3}, + {"INTERNET", "인터넷", "SKB", "B tv 인터넷", null, 36, "75000", 1}, + {"INTERNET", "인터넷", "LGU+", "U+ 인터넷", null, 36, "70000", 0}, + {"INTERNET", "TV", "KT", "올레tv 베이직", null, 36, "30000", 1}, + {"INTERNET", "TV", "KT", "올레tv 슬림", null, 36, "25000", 0}, + {"INTERNET", "TV", "KT", "지니 TV 프리미엄", null, 36, "40000", 2}, + {"INTERNET", "전화", "KT", "집전화 기본", null, 24, "10000", 0}, + {"INTERNET", "전화", "KT", "집전화 스마트", null, 24, "15000", 1}, + {"INTERNET", "와이파이", "KT", "와이파이 공유기", null, 24, "15000", 0}, + {"INTERNET", "와이파이", "SKB", "와이파이 메시", null, 24, "20000", 1}, + {"INTERNET", "스마트홈", "LGU+", "스마트홈 패키지", null, 36, "20000", 0}, + {"INTERNET", "스마트홈", "KT", "GiGA IoT 홈캠", null, 36, "25000", 1}, + {"INTERNET", "기타", "KT", "기타 부가상품", null, 12, "5000", 0}, + {"HOME", "정수기", "청호나이스", "450 (냉온정)", "WP-40C9500M", 60, "120000", 1}, + {"HOME", "정수기", "청호나이스", "550 (얼음냉온정)", "WI-55S9500M", 60, "150000", 0}, + {"HOME", "정수기", "청호나이스", "Compact (정)", "WP-CP1000", 60, "90000", 0}, + {"HOME", "정수기", "청호나이스", "700 (얼음냉온정)", "WI-70S9500M", 60, "180000", 1}, + {"HOME", "정수기", "코웨이", "아이콘 정수기", "CHP-7210N", 60, "130000", 0}, + {"HOME", "정수기", "코웨이", "냉온정수기", "CHP-5610L", 36, "110000", 1}, + {"HOME", "정수기", "SK매직", "스스로살균 정수기", "WPU-A110C", 60, "100000", 0}, + {"HOME", "정수기", "LG전자", "퓨리케어 오브제", "WD503AS", 60, "140000", 0}, + {"HOME", "공기청정기", "LG전자", "퓨리케어 360", "AS180DWFA", 36, "90000", 0}, + {"HOME", "공기청정기", "코웨이", "에어메가", "AP-2021A", 36, "85000", 1}, + {"HOME", "공기청정기", "삼성", "비스포크 큐브", "AX60A", 36, "95000", 0}, + {"HOME", "비데", "SK매직", "비데 기본형", "BIDET-01", 36, "50000", 0}, + {"HOME", "비데", "코웨이", "스스로케어 비데", "BAU-20", 36, "60000", 1}, + {"HOME", "비데", "청호나이스", "방수비데", "BA-17", 0, "45000", 0}, + {"HOME", "매트리스", "코웨이", "매트리스 퀸", "MT-Q", 60, "150000", 0}, + {"HOME", "매트리스", "코웨이", "매트리스 킹", "MT-K", 60, "180000", 1}, + {"HOME", "매트리스", "시몬스", "뷰티레스트", "BR-Q", 0, "200000", 0}, + {"HOME", "가전", "삼성", "김치냉장고", "RQ33A74", 36, "80000", 0}, + {"HOME", "가전", "LG전자", "디오스 냉장고", "M872GBB", 36, "100000", 0}, + {"HOME", "가전", "위닉스", "제습기", "DXAE100-KEK", 24, "40000", 1}, + {"HOME", "기타", "기타", "렌탈 기타상품", "ETC-01", 12, "30000", 0}, + }; + } + + // ---- Contracts ---- + @GetMapping("/contracts") + ApiResponse contracts(@AuthenticationPrincipal JwtUser u, @RequestParam Map f, + @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "15") int size) { + List all = listAll(HomesContract.class, u); + String kind = blankToNull(f.get("kind")); + if ("ALL".equalsIgnoreCase(kind)) kind = null; + String status = blankToNull(f.get("status")); + if ("ALL".equalsIgnoreCase(status)) status = null; + LocalDate from = parseDate(f.get("dateFrom")); + LocalDate to = parseDate(f.get("dateTo")); + String agency = blankToNull(f.get("agency")); + String employee = blankToNull(f.get("employee")); + String searchType = blankToNull(f.get("searchType")); + String q = qOf(f); + String k = kind; String st = status; + List base = all.stream().filter(c -> k == null || k.equals(c.getKind())).toList(); + List filtered = base.stream().filter(c -> { + if (st != null && !st.equals(c.getStatus())) return false; + if (from != null && (c.getContractDate() == null || c.getContractDate().isBefore(from))) return false; + if (to != null && (c.getContractDate() == null || c.getContractDate().isAfter(to))) return false; + if (agency != null && !agency.equals(c.getAgency())) return false; + if (employee != null && !employee.equals(c.getEmployee())) return false; + if (q != null) { + String hay; + if ("거래처".equals(searchType)) hay = nullToEmpty(c.getAgency()); + else if ("직원".equals(searchType)) hay = nullToEmpty(c.getEmployee()); + else if ("연락처".equals(searchType)) hay = nullToEmpty(c.getPhone()); + else if ("가입상품".equals(searchType)) hay = nullToEmpty(c.getProducts()); + else hay = nullToEmpty(c.getCustomerName()) + " " + nullToEmpty(c.getPhone()) + " " + nullToEmpty(c.getProducts()); + if (!hay.toLowerCase().contains(q.toLowerCase())) return false; + } + return true; + }).toList(); + Map counts = new LinkedHashMap<>(); + counts.put("ALL", (long) base.size()); + for (String s : List.of("접수", "신청", "완료", "철회")) { + counts.put(s, base.stream().filter(c -> s.equals(c.getStatus())).count()); + } + Map data = new LinkedHashMap<>(); + int fromIdx = Math.max(0, page) * Math.max(1, size); + data.put("total", filtered.size()); + data.put("list", filtered.stream().skip(fromIdx).limit(Math.min(Math.max(size, 1), 200)).map(this::contractMap).toList()); + data.put("counts", counts); + data.put("agencies", base.stream().map(HomesContract::getAgency).filter(Objects::nonNull).filter(s -> !s.isBlank()).distinct().sorted().toList()); + data.put("employees", base.stream().map(HomesContract::getEmployee).filter(Objects::nonNull).filter(s -> !s.isBlank()).distinct().sorted().toList()); + BigDecimal settleSum = filtered.stream().map(HomesContract::getSettleAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal marginSum = filtered.stream().map(HomesContract::getMargin).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); + data.put("settleSum", settleSum); + data.put("marginSum", marginSum); + return ApiResponse.ok(data); + } + + @GetMapping("/contracts/adjust") + ApiResponse contractsAdjust(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + LocalDate from = parseDate(f.get("dateFrom")); + LocalDate to = parseDate(f.get("dateTo")); + if (from == null || to == null) return ApiResponse.fail("계약일을 선택하세요."); + if (to.isBefore(from)) return ApiResponse.fail("날짜 범위가 올바르지 않습니다."); + if (java.time.temporal.ChronoUnit.DAYS.between(from, to) > 30) return ApiResponse.fail("최대 31일까지 조회할 수 있습니다."); + String kind = blankToNull(f.get("kind")); + String category = blankToNull(f.get("category")); + String product = blankToNull(f.get("product")); + String months = blankToNull(f.get("months")); + String status = blankToNull(f.get("status")); + String employee = blankToNull(f.get("employee")); + List list = listAll(HomesContract.class, u).stream().filter(c -> { + if (c.getContractDate() == null || c.getContractDate().isBefore(from) || c.getContractDate().isAfter(to)) return false; + if (kind != null && !kind.equals(c.getKind())) return false; + if (status != null && !status.equals(c.getStatus())) return false; + if (employee != null && !employee.equals(c.getEmployee())) return false; + if (months != null) { + Integer m = c.getMonths(); + if (m == null || !months.equals(String.valueOf(m))) return false; + } + String products = nullToEmpty(c.getProducts()); + if (product != null && !products.toLowerCase().contains(product.toLowerCase())) return false; + if (category != null) { + if (!products.contains(category) && !nullToEmpty(c.getMemo()).contains(category)) return false; + } + if (products.matches("(?is).*\\bx\\s*([2-9]|\\d{2,})\\b.*")) return false; + return true; + }).toList(); + Set employees = listAll(HomesContract.class, u).stream() + .map(HomesContract::getEmployee).filter(Objects::nonNull).filter(s -> !s.isBlank()).collect(Collectors.toCollection(TreeSet::new)); + Set products = listAll(HomesContract.class, u).stream() + .map(HomesContract::getProducts).filter(Objects::nonNull).flatMap(p -> Arrays.stream(p.split("[,/]"))) + .map(String::trim).filter(s -> !s.isBlank()).collect(Collectors.toCollection(TreeSet::new)); + Map data = new LinkedHashMap<>(); + data.put("total", list.size()); + data.put("list", list.stream().map(this::contractMap).toList()); + data.put("employees", employees); + data.put("products", products); + return ApiResponse.ok(data); + } + + @PostMapping("/contracts/adjust/batch") @Transactional + ApiResponse contractsAdjustBatch(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + Object raw = body.get("items"); + if (!(raw instanceof List items) || items.isEmpty()) return ApiResponse.fail("변경할 항목이 없습니다."); + int updated = 0; + for (Object item : items) { + if (!(item instanceof Map m)) continue; + Object idObj = m.get("id"); + if (idObj == null) continue; + HomesContract c = em.find(HomesContract.class, Long.valueOf(String.valueOf(idObj))); + if (c == null || !Objects.equals(c.getGroupId(), u.getGroupId())) continue; + if (m.containsKey("policySum") && m.get("policySum") != null) c.setPolicySum(new BigDecimal(String.valueOf(m.get("policySum")))); + if (m.containsKey("settleAmount") && m.get("settleAmount") != null) c.setSettleAmount(new BigDecimal(String.valueOf(m.get("settleAmount")))); + if (m.containsKey("margin") && m.get("margin") != null) c.setMargin(new BigDecimal(String.valueOf(m.get("margin")))); + writeLog(u, "CONTRACT", + ("HOME".equals(c.getKind()) ? "홈렌탈접수" : "인터넷접수") + "-정책", + ("HOME".equals(c.getKind()) ? "홈렌탈접수" : "인터넷접수") + " [" + nullToEmpty(c.getCustomerName()) + "] 정책금액을 변경하였습니다."); + updated++; + } + return ApiResponse.ok(Map.of("updated", updated)); + } + + @GetMapping("/contracts/{id}") ApiResponse contractGet(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + return ApiResponse.ok(contractMap(crud.get(HomesContract.class, id, u))); + } + @PostMapping("/contracts") @Transactional ApiResponse contractPost(@RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + d.putIfAbsent("status", "접수"); + HomesContract c = crud.create(HomesContract.class, d, u); + writeContractLog(u, c, "등록", null); + return ApiResponse.ok(contractMap(c)); + } + @PutMapping("/contracts/{id}") @Transactional ApiResponse contractPut(@PathVariable Long id, @RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + HomesContract c = crud.update(HomesContract.class, id, d, u); + writeContractLog(u, c, "수정", null); + return ApiResponse.ok(contractMap(c)); + } + @DeleteMapping("/contracts/{id}") @Transactional ApiResponse contractDel(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + HomesContract c = crud.get(HomesContract.class, id, u); + writeContractLog(u, c, "삭제", null); + crud.delete(HomesContract.class, id, u); + return ApiResponse.ok(null); + } + @PostMapping("/contracts/delete") @Transactional + ApiResponse contractsBulkDelete(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + Object raw = body.get("ids"); + if (!(raw instanceof List list) || list.isEmpty()) return ApiResponse.fail("삭제할 계약을 선택하세요."); + int deleted = 0; + for (Object o : list) { + Long id = Long.valueOf(String.valueOf(o)); + HomesContract c = em.find(HomesContract.class, id); + if (c == null || !Objects.equals(c.getGroupId(), u.getGroupId())) continue; + writeContractLog(u, c, "삭제", null); + em.remove(c); + deleted++; + } + return ApiResponse.ok(Map.of("deleted", deleted)); + } + @PostMapping("/contracts/status") @Transactional + ApiResponse contractsBulkStatus(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + Object raw = body.get("ids"); + String status = body.get("status") == null ? null : String.valueOf(body.get("status")); + if (!(raw instanceof List list) || list.isEmpty()) return ApiResponse.fail("계약을 선택하세요."); + if (status == null || status.isBlank()) return ApiResponse.fail("상태를 선택하세요."); + int updated = 0; + for (Object o : list) { + Long id = Long.valueOf(String.valueOf(o)); + HomesContract c = em.find(HomesContract.class, id); + if (c == null || !Objects.equals(c.getGroupId(), u.getGroupId())) continue; + c.setStatus(status); + writeContractLog(u, c, "상태", status); + updated++; + } + return ApiResponse.ok(Map.of("updated", updated)); + } + + // ---- Balances ---- + @GetMapping("/balances") + ApiResponse balances(@AuthenticationPrincipal JwtUser u, @RequestParam Map f, + @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { + List filtered = filterBalances(u, f); + List all = listAll(HomesBalance.class, u); + Map counts = new LinkedHashMap<>(); + counts.put("ALL", (long) all.size()); + counts.put("정산차감", all.stream().filter(b -> "정산차감".equals(normalizeBalanceType(b.getBalanceType()))).count()); + counts.put("정산추가", all.stream().filter(b -> "정산추가".equals(normalizeBalanceType(b.getBalanceType()))).count()); + Set employees = all.stream().filter(b -> "직원".equals(resolveTargetType(b))) + .map(this::resolveTargetName).filter(s -> !s.isBlank()).collect(Collectors.toCollection(TreeSet::new)); + Set agencies = all.stream().filter(b -> "하부점".equals(resolveTargetType(b))) + .map(this::resolveTargetName).filter(s -> !s.isBlank()).collect(Collectors.toCollection(TreeSet::new)); + Set reasons = all.stream().map(HomesBalance::getReason).filter(Objects::nonNull).filter(s -> !s.isBlank()) + .collect(Collectors.toCollection(TreeSet::new)); + // 옵션 보강: 멤버/거래처 + listAll(HomesAgency.class, u).stream().map(HomesAgency::getName).filter(Objects::nonNull).forEach(agencies::add); + em.createQuery("select m from Member m where m.groupId=:g and m.role='AGENT'", Member.class) + .setParameter("g", u.getGroupId()).getResultList().stream() + .map(Member::getName).filter(Objects::nonNull).filter(s -> !s.isBlank()).forEach(employees::add); + int fromIdx = Math.max(0, page) * Math.max(1, size); + List> list = filtered.stream().skip(fromIdx).limit(Math.max(size, 1)).map(this::balanceMap).toList(); + Map data = new LinkedHashMap<>(); + data.put("total", filtered.size()); + data.put("list", list); + data.put("counts", counts); + data.put("employees", employees); + data.put("agencies", agencies); + data.put("reasons", reasons); + return ApiResponse.ok(data); + } + @PostMapping("/balances") @Transactional ApiResponse balancePost(@RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + normalizeBalancePayload(d); + return ApiResponse.ok(balanceMap(crud.create(HomesBalance.class, d, u))); + } + @PutMapping("/balances/{id}") @Transactional ApiResponse balancePut(@PathVariable Long id, @RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + normalizeBalancePayload(d); + return ApiResponse.ok(balanceMap(crud.update(HomesBalance.class, id, d, u))); + } + @DeleteMapping("/balances/{id}") @Transactional ApiResponse balanceDel(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + crud.delete(HomesBalance.class, id, u); return ApiResponse.ok(null); + } + @PostMapping("/balances/delete") @Transactional + ApiResponse balancesBulkDelete(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + Object raw = body.get("ids"); + if (!(raw instanceof List list) || list.isEmpty()) return ApiResponse.fail("삭제할 항목을 선택하세요."); + int deleted = 0; + for (Object o : list) { + Long id = Long.valueOf(String.valueOf(o)); + HomesBalance b = em.find(HomesBalance.class, id); + if (b == null || !Objects.equals(b.getGroupId(), u.getGroupId())) continue; + em.remove(b); + deleted++; + } + return ApiResponse.ok(Map.of("deleted", deleted)); + } + @GetMapping(value = "/balances/export", produces = "text/csv;charset=UTF-8") + org.springframework.http.ResponseEntity balancesExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + List filtered = filterBalances(u, f); + StringBuilder csv = new StringBuilder("\uFEFF후정산일,대상,대상명,구분,사유,정산금액,비고\n"); + for (HomesBalance b : filtered) { + Map row = balanceMap(b); + csv.append(csvCell(row.get("baseDate"))).append(',') + .append(csvCell(row.get("targetType"))).append(',') + .append(csvCell(row.get("targetName"))).append(',') + .append(csvCell(row.get("balanceType"))).append(',') + .append(csvCell(row.get("reason"))).append(',') + .append(csvCell(row.get("amount"))).append(',') + .append(csvCell(row.get("memo"))).append('\n'); + } + return org.springframework.http.ResponseEntity.ok() + .header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=contract-balances.csv") + .body(csv.toString()); + } + + private List filterBalances(JwtUser u, Map f) { + List all = listAll(HomesBalance.class, u); + String tab = blankToNull(f.get("tab")); + if (tab == null) tab = blankToNull(f.get("balanceType")); + if ("ALL".equalsIgnoreCase(tab)) tab = null; + LocalDate from = parseDate(f.get("dateFrom")); + LocalDate to = parseDate(f.get("dateTo")); + String targetType = blankToNull(f.get("targetType")); + String targetName = blankToNull(f.get("targetName")); + String employee = blankToNull(f.get("employee")); + String agency = blankToNull(f.get("agency")); + String reason = blankToNull(f.get("reason")); + String q = qOf(f); + String t = tab; + return all.stream().filter(b -> { + String bt = normalizeBalanceType(b.getBalanceType()); + if (t != null && !t.equals(bt)) return false; + if (from != null && (b.getBaseDate() == null || b.getBaseDate().isBefore(from))) return false; + if (to != null && (b.getBaseDate() == null || b.getBaseDate().isAfter(to))) return false; + String tt = resolveTargetType(b); + String tn = resolveTargetName(b); + if (targetType != null && !targetType.equals(tt)) return false; + if (targetName != null && !targetName.equals(tn)) return false; + if (employee != null && (!"직원".equals(tt) || !employee.equals(tn))) return false; + if (agency != null && (!"하부점".equals(tt) || !agency.equals(tn))) return false; + if (reason != null && !reason.equals(nullToEmpty(b.getReason()))) return false; + if (q != null) { + String hay = (tn + " " + nullToEmpty(b.getReason()) + " " + nullToEmpty(b.getMemo()) + + " " + nullToEmpty(b.getCustomerName()) + " " + nullToEmpty(b.getAgency())).toLowerCase(); + if (!hay.contains(q.toLowerCase())) return false; + } + return true; + }).sorted(Comparator.comparing(HomesBalance::getBaseDate, Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing(HomesBalance::getId, Comparator.reverseOrder())).toList(); + } + + private void normalizeBalancePayload(Map d) { + if (d.get("balanceType") != null) d.put("balanceType", normalizeBalanceType(String.valueOf(d.get("balanceType")))); + if (d.get("targetType") != null) { + String tt = String.valueOf(d.get("targetType")); + d.put("targetType", tt); + if ("하부점".equals(tt) && d.get("targetName") != null) d.put("agency", d.get("targetName")); + if ("직원".equals(tt) && d.get("targetName") != null) d.put("customerName", d.get("targetName")); + } + if (d.get("amount") != null) { + BigDecimal amt = new BigDecimal(String.valueOf(d.get("amount"))); + String bt = normalizeBalanceType(d.get("balanceType") == null ? null : String.valueOf(d.get("balanceType"))); + if ("정산차감".equals(bt) && amt.signum() > 0) amt = amt.negate(); + if ("정산추가".equals(bt) && amt.signum() < 0) amt = amt.abs(); + d.put("amount", amt); + } + } + + private static String normalizeBalanceType(String t) { + if (t == null || t.isBlank()) return t; + if ("차감".equals(t) || "정산차감".equals(t)) return "정산차감"; + if ("추가".equals(t) || "정산추가".equals(t)) return "정산추가"; + return t; + } + + private String resolveTargetType(HomesBalance b) { + if (b.getTargetType() != null && !b.getTargetType().isBlank()) return b.getTargetType(); + if (b.getAgency() != null && !b.getAgency().isBlank() && (b.getCustomerName() == null || b.getCustomerName().isBlank())) return "하부점"; + return "직원"; + } + + private String resolveTargetName(HomesBalance b) { + if (b.getTargetName() != null && !b.getTargetName().isBlank()) return b.getTargetName(); + if ("하부점".equals(resolveTargetType(b))) return nullToEmpty(b.getAgency()); + if (b.getCustomerName() != null && !b.getCustomerName().isBlank()) return b.getCustomerName(); + return nullToEmpty(b.getAgency()); + } + + private static String csvCell(Object value) { + String s = String.valueOf(value == null ? "" : value); + return "\"" + s.replace("\"", "\"\"") + "\""; + } + + // ---- Invoices ---- + @GetMapping("/invoices") + ApiResponse invoices(@AuthenticationPrincipal JwtUser u, @RequestParam Map f, + @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "15") int size) { + List all = listAll(HomesInvoice.class, u); + String scope = blankToNull(f.get("scope")); + String ym = blankToNull(f.get("yearMonth")); + String q = qOf(f); + List filtered = all.stream().filter(i -> { + if (scope != null && !scope.equals(i.getScope())) return false; + if (ym != null && !ym.equals(i.getYearMonth())) return false; + if ("MY".equals(scope)) { + YearMonth limit = YearMonth.now().minusMonths(11); + if (i.getYearMonth() != null) { + try { + if (YearMonth.parse(i.getYearMonth()).isBefore(limit)) return false; + } catch (Exception ignored) {} + } + } + if (q != null) { + String hay = (nullToEmpty(i.getEmployee()) + " " + nullToEmpty(i.getAgency()) + " " + nullToEmpty(i.getYearMonth())).toLowerCase(); + if (!hay.contains(q.toLowerCase())) return false; + } + return true; + }).toList(); + return pageOk(filtered, page, size, this::invoiceMap, null); + } + + @GetMapping("/invoices/my") + ApiResponse invoicesMy(@AuthenticationPrincipal JwtUser u) { + String myName = resolveMemberName(u); + YearMonth limit = YearMonth.now().minusMonths(11); + List list = listAll(HomesInvoice.class, u).stream() + .filter(i -> Boolean.TRUE.equals(i.getIssued()) || "발행".equals(i.getStatus()) || "확정".equals(i.getStatus())) + .filter(i -> { + if (i.getYearMonth() == null || i.getYearMonth().isBlank()) return false; + try { return !YearMonth.parse(i.getYearMonth()).isBefore(limit); } + catch (Exception e) { return false; } + }) + .filter(i -> myName == null || myName.isBlank() || myName.equals(i.getEmployee())) + .sorted(Comparator.comparing(HomesInvoice::getYearMonth, Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing(HomesInvoice::getId, Comparator.reverseOrder())) + .toList(); + Map data = new LinkedHashMap<>(); + data.put("total", list.size()); + data.put("employee", myName); + data.put("list", list.stream().map(this::invoiceMap).toList()); + return ApiResponse.ok(data); + } + + @GetMapping(value = "/invoices/{id}/export", produces = "text/csv;charset=UTF-8") + org.springframework.http.ResponseEntity invoiceExport(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + HomesInvoice inv = crud.get(HomesInvoice.class, id, u); + Map row = invoiceMap(inv); + StringBuilder csv = new StringBuilder("\uFEFF기준월,직원,인터넷,인터넷금액,홈렌탈,홈렌탈금액,후정산,후정산금액,정산금액,상태\n"); + csv.append(csvCell(row.get("yearMonth"))).append(',') + .append(csvCell(row.get("employee"))).append(',') + .append(csvCell(row.get("internetCount"))).append(',') + .append(csvCell(row.get("internetAmount"))).append(',') + .append(csvCell(row.get("homeCount"))).append(',') + .append(csvCell(row.get("homeAmount"))).append(',') + .append(csvCell(row.get("balanceCount"))).append(',') + .append(csvCell(row.get("balanceAmount"))).append(',') + .append(csvCell(row.get("settleAmount"))).append(',') + .append(csvCell(row.get("status"))).append('\n'); + return org.springframework.http.ResponseEntity.ok() + .header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=invoice-" + nullToEmpty(inv.getYearMonth()) + ".csv") + .body(csv.toString()); + } + + private String resolveMemberName(JwtUser u) { + List members = em.createQuery("select m from Member m where m.groupId=:g and m.userid=:uid", Member.class) + .setParameter("g", u.getGroupId()).setParameter("uid", u.getUserid()).getResultList(); + if (members.isEmpty()) return null; + return members.getFirst().getName(); + } + + @GetMapping("/invoices/publish") + ApiResponse invoicesPublish(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + String ym = blankToNull(f.get("yearMonth")); + if (ym == null) ym = YearMonth.now().toString(); + final String yearMonthKey = ym; + YearMonth yearMonth; + try { yearMonth = YearMonth.parse(yearMonthKey); } + catch (Exception e) { return ApiResponse.fail("정산월이 올바르지 않습니다."); } + LocalDate from = yearMonth.atDay(1); + LocalDate to = yearMonth.atEndOfMonth(); + + // 완료 계약만 집계 + Map> byEmp = listAll(HomesContract.class, u).stream() + .filter(c -> "완료".equals(c.getStatus())) + .filter(c -> c.getContractDate() != null && !c.getContractDate().isBefore(from) && !c.getContractDate().isAfter(to)) + .filter(c -> c.getEmployee() != null && !c.getEmployee().isBlank()) + .collect(Collectors.groupingBy(HomesContract::getEmployee, LinkedHashMap::new, Collectors.toList())); + + Map> balByEmp = listAll(HomesBalance.class, u).stream() + .filter(b -> b.getBaseDate() != null && !b.getBaseDate().isBefore(from) && !b.getBaseDate().isAfter(to)) + .collect(Collectors.groupingBy(b -> { + String tn = resolveTargetName(b); + return tn.isBlank() ? "미지정" : tn; + }, LinkedHashMap::new, Collectors.toList())); + + Map issuedMap = listAll(HomesInvoice.class, u).stream() + .filter(i -> "PUBLISH".equals(i.getScope()) && yearMonthKey.equals(i.getYearMonth())) + .filter(i -> i.getEmployee() != null) + .collect(Collectors.toMap(HomesInvoice::getEmployee, i -> i, (a, b) -> a, LinkedHashMap::new)); + + Set names = new TreeSet<>(); + names.addAll(byEmp.keySet()); + // 후정산만 있는 직원도 포함 + for (String n : balByEmp.keySet()) { + if (byEmp.containsKey(n) || issuedMap.containsKey(n)) names.add(n); + } + names.addAll(issuedMap.keySet()); + + List> list = new ArrayList<>(); + for (String emp : names) { + List contracts = byEmp.getOrDefault(emp, List.of()); + List bals = balByEmp.getOrDefault(emp, List.of()); + int internetCount = (int) contracts.stream().filter(c -> "INTERNET".equals(c.getKind())).count(); + int homeCount = (int) contracts.stream().filter(c -> "HOME".equals(c.getKind())).count(); + BigDecimal internetAmount = contracts.stream().filter(c -> "INTERNET".equals(c.getKind())) + .map(c -> bd(c.getSettleAmount())).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal homeAmount = contracts.stream().filter(c -> "HOME".equals(c.getKind())) + .map(c -> bd(c.getSettleAmount())).reduce(BigDecimal.ZERO, BigDecimal::add); + int balanceCount = bals.size(); + BigDecimal balanceAmount = bals.stream().map(b -> bd(b.getAmount())).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal settleAmount = internetAmount.add(homeAmount).add(balanceAmount); + if (internetCount + homeCount + balanceCount == 0 && !issuedMap.containsKey(emp)) continue; + + HomesInvoice inv = issuedMap.get(emp); + Map row = new LinkedHashMap<>(); + row.put("employee", emp); + row.put("yearMonth", yearMonthKey); + row.put("internetCount", internetCount); + row.put("internetAmount", internetAmount); + row.put("homeCount", homeCount); + row.put("homeAmount", homeAmount); + row.put("balanceCount", balanceCount); + row.put("balanceAmount", balanceAmount); + row.put("settleAmount", settleAmount); + row.put("contractCount", internetCount + homeCount); + if (inv != null) { + row.put("id", inv.getId()); + row.put("issued", Boolean.TRUE.equals(inv.getIssued()) || "발행".equals(inv.getStatus()) || "확정".equals(inv.getStatus())); + row.put("status", inv.getStatus()); + row.put("consent", inv.getConsent() == null || inv.getConsent().isBlank() ? "-" : inv.getConsent()); + row.put("taxInvoice", inv.getTaxInvoice() == null || inv.getTaxInvoice().isBlank() ? "-" : inv.getTaxInvoice()); + row.put("withdrawStatus", inv.getWithdrawStatus() == null || inv.getWithdrawStatus().isBlank() ? "-" : inv.getWithdrawStatus()); + BigDecimal snap = inv.getSettleAmount() != null ? inv.getSettleAmount() : bd(inv.getAmount()); + row.put("mismatched", snap.compareTo(settleAmount) != 0); + } else { + row.put("id", null); + row.put("issued", false); + row.put("status", "미발행"); + row.put("consent", "-"); + row.put("taxInvoice", "-"); + row.put("withdrawStatus", "-"); + row.put("mismatched", false); + } + list.add(row); + } + + Map data = new LinkedHashMap<>(); + data.put("yearMonth", yearMonthKey); + data.put("total", list.size()); + data.put("list", list); + return ApiResponse.ok(data); + } + + @PostMapping("/invoices/publish/issue") @Transactional + ApiResponse invoicesPublishIssue(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + String ym = body.get("yearMonth") == null ? null : String.valueOf(body.get("yearMonth")); + String employee = body.get("employee") == null ? null : String.valueOf(body.get("employee")); + if (ym == null || ym.isBlank() || employee == null || employee.isBlank()) { + return ApiResponse.fail("정산월과 직원을 확인하세요."); + } + // 최신 집계 재계산 + Map f = Map.of("yearMonth", ym); + ApiResponse pub = invoicesPublish(u, f); + @SuppressWarnings("unchecked") + Map data = (Map) pub.data(); + @SuppressWarnings("unchecked") + List> rows = data == null ? List.of() : (List>) data.getOrDefault("list", List.of()); + Map src = rows.stream().filter(r -> employee.equals(String.valueOf(r.get("employee")))).findFirst().orElse(null); + if (src == null) return ApiResponse.fail("발행할 정산 자료가 없습니다. (완료 계약만 발행 가능)"); + + HomesInvoice inv = listAll(HomesInvoice.class, u).stream() + .filter(i -> "PUBLISH".equals(i.getScope()) && ym.equals(i.getYearMonth()) && employee.equals(i.getEmployee())) + .findFirst().orElse(null); + boolean created = false; + if (inv == null) { + inv = new HomesInvoice(); + inv.setGroupId(u.getGroupId()); + inv.setScope("PUBLISH"); + inv.setYearMonth(ym); + inv.setEmployee(employee); + created = true; + } + boolean reissue = !created && (Boolean.TRUE.equals(inv.getIssued()) || "발행".equals(inv.getStatus()) || "확정".equals(inv.getStatus())); + inv.setInternetCount(((Number) src.getOrDefault("internetCount", 0)).intValue()); + inv.setHomeCount(((Number) src.getOrDefault("homeCount", 0)).intValue()); + inv.setBalanceCount(((Number) src.getOrDefault("balanceCount", 0)).intValue()); + inv.setInternetAmount(bd(src.get("internetAmount"))); + inv.setHomeAmount(bd(src.get("homeAmount"))); + inv.setBalanceAmount(bd(src.get("balanceAmount"))); + inv.setSettleAmount(bd(src.get("settleAmount"))); + inv.setContractCount(((Number) src.getOrDefault("contractCount", 0)).intValue()); + inv.setAmount(bd(src.get("settleAmount"))); + inv.setIssued(true); + inv.setStatus("발행"); + if (inv.getConsent() == null || inv.getConsent().isBlank()) inv.setConsent("대기"); + if (inv.getTaxInvoice() == null || inv.getTaxInvoice().isBlank()) inv.setTaxInvoice("미발행"); + if (inv.getWithdrawStatus() == null || inv.getWithdrawStatus().isBlank()) inv.setWithdrawStatus("대기"); + if (created) em.persist(inv); + writeLog(u, "INVOICE", reissue ? "정산서-재발행" : "정산서-발행", + "정산서 [" + employee + "] " + (reissue ? "재발행" : "발행") + "하였습니다."); + return ApiResponse.ok(invoiceMap(inv)); + } + + @GetMapping(value = "/invoices/publish/export", produces = "text/csv;charset=UTF-8") + org.springframework.http.ResponseEntity invoicesPublishExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + ApiResponse res = invoicesPublish(u, f); + @SuppressWarnings("unchecked") + Map data = (Map) res.data(); + @SuppressWarnings("unchecked") + List> list = data == null ? List.of() : (List>) data.getOrDefault("list", List.of()); + StringBuilder csv = new StringBuilder("\uFEFF직원,인터넷,인터넷금액,홈렌탈,홈렌탈금액,후정산,후정산금액,정산금액,정산서발행,동의,계산서,출금상태\n"); + for (Map row : list) { + csv.append(csvCell(row.get("employee"))).append(',') + .append(csvCell(row.get("internetCount"))).append(',') + .append(csvCell(row.get("internetAmount"))).append(',') + .append(csvCell(row.get("homeCount"))).append(',') + .append(csvCell(row.get("homeAmount"))).append(',') + .append(csvCell(row.get("balanceCount"))).append(',') + .append(csvCell(row.get("balanceAmount"))).append(',') + .append(csvCell(row.get("settleAmount"))).append(',') + .append(csvCell(Boolean.TRUE.equals(row.get("issued")) ? "발행" : "미발행")).append(',') + .append(csvCell(row.get("consent"))).append(',') + .append(csvCell(row.get("taxInvoice"))).append(',') + .append(csvCell(row.get("withdrawStatus"))).append('\n'); + } + return org.springframework.http.ResponseEntity.ok() + .header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=invoice-publish.csv") + .body(csv.toString()); + } + + @PostMapping("/invoices") @Transactional ApiResponse invoicePost(@RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + HomesInvoice inv = crud.create(HomesInvoice.class, d, u); + writeLog(u, "INVOICE", "정산서-등록", + "정산서 [" + nullToEmpty(inv.getEmployee()) + "] 등록하였습니다."); + return ApiResponse.ok(invoiceMap(inv)); + } + @PutMapping("/invoices/{id}") @Transactional ApiResponse invoicePut(@PathVariable Long id, @RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + HomesInvoice inv = crud.update(HomesInvoice.class, id, d, u); + writeLog(u, "INVOICE", "정산서-수정", + "정산서 [" + nullToEmpty(inv.getEmployee()) + "] 수정하였습니다."); + return ApiResponse.ok(invoiceMap(inv)); + } + @DeleteMapping("/invoices/{id}") @Transactional ApiResponse invoiceDel(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + HomesInvoice inv = crud.get(HomesInvoice.class, id, u); + writeLog(u, "INVOICE", "정산서-삭제", + "정산서 [" + nullToEmpty(inv.getEmployee()) + "] 삭제하였습니다."); + crud.delete(HomesInvoice.class, id, u); return ApiResponse.ok(null); + } + + // ---- Pendings ---- + @GetMapping("/pendings") + ApiResponse pendings(@AuthenticationPrincipal JwtUser u, @RequestParam Map f, + @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { + List all = listAll(HomesPending.class, u); + String type = blankToNull(f.get("pendingType")); + if (type == null) type = blankToNull(f.get("tab")); + if ("ALL".equalsIgnoreCase(type)) type = null; + String status = blankToNull(f.get("status")); + if ("ALL".equalsIgnoreCase(status)) status = null; + String receiveEmployee = blankToNull(f.get("receiveEmployee")); + String processEmployee = blankToNull(f.get("processEmployee")); + String phoneTail = blankToNull(f.get("phoneTail")); + LocalDate from = parseDate(f.get("dateFrom")); + LocalDate to = parseDate(f.get("dateTo")); + String q = qOf(f); + String t = type; + String st = status; + List filtered = all.stream().filter(p -> { + if (t != null && !t.equals(p.getPendingType())) return false; + if (st != null && !normalizePendingStatus(p.getStatus()).equals(st)) return false; + String recv = resolveReceiveEmployee(p); + String proc = nullToEmpty(p.getProcessEmployee()); + if (receiveEmployee != null && !receiveEmployee.equals(recv)) return false; + if (processEmployee != null && !processEmployee.equals(proc)) return false; + if (phoneTail != null) { + String digits = nullToEmpty(p.getPhone()).replaceAll("\\D", ""); + if (!digits.endsWith(phoneTail)) return false; + } + if (from != null && (p.getPendingDate() == null || p.getPendingDate().isBefore(from))) return false; + if (to != null && (p.getPendingDate() == null || p.getPendingDate().isAfter(to))) return false; + if (q != null) { + String hay = (nullToEmpty(p.getCustomerName()) + " " + nullToEmpty(p.getContents()) + " " + recv + " " + proc).toLowerCase(); + if (!hay.contains(q.toLowerCase())) return false; + } + return true; + }).sorted(Comparator.comparing(HomesPending::getPendingDate, Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing(HomesPending::getId, Comparator.reverseOrder())).toList(); + + Map counts = new LinkedHashMap<>(); + counts.put("ALL", (long) all.size()); + List typeList = pendingTypeList(u); + for (String s : typeList) { + counts.put(s, all.stream().filter(p -> s.equals(p.getPendingType())).count()); + } + Set receiveEmployees = all.stream().map(this::resolveReceiveEmployee).filter(s -> !s.isBlank()) + .collect(Collectors.toCollection(TreeSet::new)); + Set processEmployees = all.stream().map(HomesPending::getProcessEmployee).filter(Objects::nonNull).filter(s -> !s.isBlank()) + .collect(Collectors.toCollection(TreeSet::new)); + em.createQuery("select m from Member m where m.groupId=:g and m.role='AGENT'", Member.class) + .setParameter("g", u.getGroupId()).getResultList().stream() + .map(Member::getName).filter(Objects::nonNull).filter(s -> !s.isBlank()) + .forEach(n -> { receiveEmployees.add(n); processEmployees.add(n); }); + + Set statuses = all.stream().map(p -> normalizePendingStatus(p.getStatus())).filter(s -> !s.isBlank()) + .collect(Collectors.toCollection(TreeSet::new)); + statuses.add("접수"); statuses.add("해결"); + + Map data = new LinkedHashMap<>(); + int fromIdx = Math.max(0, page) * Math.max(1, size); + data.put("total", filtered.size()); + data.put("list", filtered.stream().skip(fromIdx).limit(Math.max(size, 1)).map(this::pendingMap).toList()); + data.put("counts", counts); + data.put("types", typeList); + data.put("statuses", statuses); + data.put("receiveEmployees", receiveEmployees); + data.put("processEmployees", processEmployees); + return ApiResponse.ok(data); + } + + @GetMapping("/pendings/types") + ApiResponse pendingTypesGet(@AuthenticationPrincipal JwtUser u) { + return ApiResponse.ok(Map.of("types", pendingTypeList(u))); + } + + @PutMapping("/pendings/types") @Transactional + ApiResponse pendingTypesPut(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + Object raw = body.get("types"); + List types = new ArrayList<>(); + if (raw instanceof List list) { + for (Object o : list) { + if (o == null) continue; + String s = String.valueOf(o).trim(); + if (!s.isBlank() && !types.contains(s)) types.add(s); + } + } + if (types.isEmpty()) types = new ArrayList<>(List.of("일반", "약속1", "약속2")); + saveJsonSetting(u.getGroupId(), "homes.pendingTypes", Map.of("types", types)); + return ApiResponse.ok(Map.of("types", types)); + } + + @PostMapping("/pendings") @Transactional ApiResponse pendingPost(@RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + normalizePendingPayload(d, u); + HomesPending p = crud.create(HomesPending.class, d, u); + writeLog(u, "PENDING", "약속-등록", "약속 [" + nullToEmpty(p.getPendingType()) + "] 등록하였습니다."); + return ApiResponse.ok(pendingMap(p)); + } + @PutMapping("/pendings/{id}") @Transactional ApiResponse pendingPut(@PathVariable Long id, @RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + normalizePendingPayload(d, u); + HomesPending p = crud.update(HomesPending.class, id, d, u); + writeLog(u, "PENDING", "약속-수정", "약속 [" + nullToEmpty(p.getPendingType()) + "] 수정하였습니다."); + return ApiResponse.ok(pendingMap(p)); + } + @DeleteMapping("/pendings/{id}") @Transactional ApiResponse pendingDel(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + HomesPending p = crud.get(HomesPending.class, id, u); + writeLog(u, "PENDING", "약속-삭제", "약속 [" + nullToEmpty(p.getPendingType()) + "] 삭제하였습니다."); + crud.delete(HomesPending.class, id, u); return ApiResponse.ok(null); + } + + @PostMapping("/pendings/process") @Transactional + ApiResponse pendingsProcess(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + Object raw = body.get("ids"); + if (!(raw instanceof List list) || list.isEmpty()) return ApiResponse.fail("처리할 약속을 선택하세요."); + String processEmployee = body.get("processEmployee") == null ? resolveMemberName(u) : String.valueOf(body.get("processEmployee")); + int updated = 0; + for (Object o : list) { + Long id = Long.valueOf(String.valueOf(o)); + HomesPending p = em.find(HomesPending.class, id); + if (p == null || !Objects.equals(p.getGroupId(), u.getGroupId())) continue; + p.setStatus("해결"); + p.setProcessEmployee(processEmployee); + p.setProcessDate(LocalDate.now()); + writeLog(u, "PENDING", "약속-해결", "약속 [" + nullToEmpty(p.getPendingType()) + "] 해결완료하였습니다."); + updated++; + } + return ApiResponse.ok(Map.of("updated", updated)); + } + + @PostMapping("/pendings/delete") @Transactional + ApiResponse pendingsBulkDelete(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + Object raw = body.get("ids"); + if (!(raw instanceof List list) || list.isEmpty()) return ApiResponse.fail("삭제할 약속을 선택하세요."); + int deleted = 0; + for (Object o : list) { + Long id = Long.valueOf(String.valueOf(o)); + HomesPending p = em.find(HomesPending.class, id); + if (p == null || !Objects.equals(p.getGroupId(), u.getGroupId())) continue; + writeLog(u, "PENDING", "약속-삭제", "약속 [" + nullToEmpty(p.getPendingType()) + "] 삭제하였습니다."); + em.remove(p); + deleted++; + } + return ApiResponse.ok(Map.of("deleted", deleted)); + } + + @GetMapping(value = "/pendings/export", produces = "text/csv;charset=UTF-8") + org.springframework.http.ResponseEntity pendingsExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + ApiResponse res = pendings(u, f, 0, 10000); + @SuppressWarnings("unchecked") + Map data = (Map) res.data(); + @SuppressWarnings("unchecked") + List> list = data == null ? List.of() : (List>) data.getOrDefault("list", List.of()); + StringBuilder csv = new StringBuilder("\uFEFF상태,일자,과목,내용,고객명,연락처,접수직원,처리일,처리직원,비고\n"); + for (Map row : list) { + csv.append(csvCell(row.get("status"))).append(',') + .append(csvCell(row.get("pendingDate"))).append(',') + .append(csvCell(row.get("pendingType"))).append(',') + .append(csvCell(row.get("contents"))).append(',') + .append(csvCell(row.get("customerName"))).append(',') + .append(csvCell(row.get("phone"))).append(',') + .append(csvCell(row.get("receiveEmployee"))).append(',') + .append(csvCell(row.get("processDate"))).append(',') + .append(csvCell(row.get("processEmployee"))).append(',') + .append(csvCell(row.get("memo"))).append('\n'); + } + return org.springframework.http.ResponseEntity.ok() + .header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=pendings.csv") + .body(csv.toString()); + } + + private List pendingTypeList(JwtUser u) { + Map saved = loadJsonSetting(u.getGroupId(), "homes.pendingTypes", Map.of()); + Object raw = saved.get("types"); + List types = new ArrayList<>(); + if (raw instanceof List list) { + for (Object o : list) { + if (o == null) continue; + String s = String.valueOf(o).trim(); + if (!s.isBlank() && !types.contains(s)) types.add(s); + } + } + if (types.isEmpty()) types.addAll(List.of("일반", "약속1", "약속2")); + return types; + } + + private void normalizePendingPayload(Map d, JwtUser u) { + if (d.get("status") != null) d.put("status", normalizePendingStatus(String.valueOf(d.get("status")))); + if (d.get("receiveEmployee") != null) { + d.put("employee", d.get("receiveEmployee")); + } else if (d.get("employee") == null) { + String name = resolveMemberName(u); + if (name != null) { + d.put("receiveEmployee", name); + d.put("employee", name); + } + } + if (d.containsKey("phone1") || d.containsKey("phone2") || d.containsKey("phone3")) { + String p1 = d.get("phone1") == null ? "" : String.valueOf(d.get("phone1")).trim(); + String p2 = d.get("phone2") == null ? "" : String.valueOf(d.get("phone2")).trim(); + String p3 = d.get("phone3") == null ? "" : String.valueOf(d.get("phone3")).trim(); + List parts = new ArrayList<>(); + if (!p1.isBlank()) parts.add(p1); + if (!p2.isBlank()) parts.add(p2); + if (!p3.isBlank()) parts.add(p3); + if (!parts.isEmpty()) d.put("phone", String.join("-", parts)); + } + } + + private static String normalizePendingStatus(String s) { + if (s == null || s.isBlank()) return "접수"; + if ("예정".equals(s) || "접수".equals(s)) return "접수"; + if ("완료".equals(s) || "해결".equals(s)) return "해결"; + if ("보류".equals(s)) return "보류"; + if ("취소".equals(s)) return "취소"; + return s; + } + + private String resolveReceiveEmployee(HomesPending p) { + if (p.getReceiveEmployee() != null && !p.getReceiveEmployee().isBlank()) return p.getReceiveEmployee(); + return nullToEmpty(p.getEmployee()); + } + + // ---- Bookings ---- + private static final List BOOKING_INTERNET_OPTIONS = List.of("인터넷", "TV", "전화", "와이파이", "스마트홈", "기타"); + private static final List BOOKING_RENTAL_OPTIONS = List.of("정수기", "공기청정기", "비데", "매트리스", "가전", "기타"); + + @GetMapping("/bookings") + ApiResponse bookings(@AuthenticationPrincipal JwtUser u, @RequestParam Map f, + @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "15") int size) { + List all = listAll(HomesBooking.class, u); + String status = blankToNull(f.get("status")); + if ("ALL".equalsIgnoreCase(status)) status = null; + if (status == null) status = blankToNull(f.get("tab")); + if ("ALL".equalsIgnoreCase(status)) status = null; + LocalDate from = parseDate(f.get("dateFrom")); + LocalDate to = parseDate(f.get("dateTo")); + String employee = blankToNull(f.get("employee")); + if (employee == null) employee = blankToNull(f.get("receiveEmployee")); + String internetProduct = blankToNull(f.get("internetProduct")); + String rentalProduct = blankToNull(f.get("rentalProduct")); + String phoneTail = blankToNull(f.get("phoneTail")); + String q = qOf(f); + String st = status; + String emp = employee; + List filtered = all.stream().filter(b -> { + if (st != null && !st.equals(b.getStatus())) return false; + if (from != null && (b.getBookingDate() == null || b.getBookingDate().isBefore(from))) return false; + if (to != null && (b.getBookingDate() == null || b.getBookingDate().isAfter(to))) return false; + if (emp != null && !emp.equals(b.getEmployee())) return false; + if (internetProduct != null && !containsProductToken(resolveInternetProducts(b), internetProduct)) return false; + if (rentalProduct != null && !containsProductToken(resolveRentalProducts(b), rentalProduct)) return false; + if (phoneTail != null) { + String digits = nullToEmpty(b.getPhone()).replaceAll("\\D", ""); + if (digits.length() < 4 || !digits.endsWith(phoneTail)) return false; + } + if (q != null) { + String hay = (nullToEmpty(b.getCustomerName()) + " " + nullToEmpty(b.getPhone()) + " " + + nullToEmpty(b.getProducts()) + " " + nullToEmpty(b.getInternetProducts()) + " " + + nullToEmpty(b.getRentalProducts()) + " " + nullToEmpty(b.getAddress()) + " " + + nullToEmpty(b.getMemo())).toLowerCase(); + if (!hay.contains(q.toLowerCase())) return false; + } + return true; + }).toList(); + Map counts = new LinkedHashMap<>(); + counts.put("ALL", (long) all.size()); + for (String s : List.of("접수", "완료", "보류")) { + counts.put(s, all.stream().filter(b -> s.equals(b.getStatus())).count()); + } + List sorted = filtered.stream() + .sorted(Comparator.comparing(HomesBooking::getBookingDate, Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing(HomesBooking::getId, Comparator.reverseOrder())) + .toList(); + ApiResponse res = pageOk(sorted, page, size, this::bookingMap, counts); + @SuppressWarnings("unchecked") + Map body = (Map) res.data(); + if (body != null) { + body.put("employees", all.stream().map(HomesBooking::getEmployee).filter(Objects::nonNull) + .map(String::trim).filter(s -> !s.isBlank()).distinct().sorted().toList()); + body.put("internetOptions", BOOKING_INTERNET_OPTIONS); + body.put("rentalOptions", BOOKING_RENTAL_OPTIONS); + } + return res; + } + + @PostMapping("/bookings") @Transactional ApiResponse bookingPost(@RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + normalizeBookingPayload(d, u); + HomesBooking b = crud.create(HomesBooking.class, d, u); + ensureBookingCustomer(b, u); + writeLog(u, "BOOKING", "예약-등록", "예약 [" + nullToEmpty(b.getCustomerName()) + "] 등록하였습니다."); + return ApiResponse.ok(bookingMap(b)); + } + @PutMapping("/bookings/{id}") @Transactional ApiResponse bookingPut(@PathVariable Long id, @RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + HomesBooking before = crud.get(HomesBooking.class, id, u); + String prevStatus = before.getStatus(); + normalizeBookingPayload(d, u); + HomesBooking b = crud.update(HomesBooking.class, id, d, u); + String nextStatus = b.getStatus(); + if (nextStatus != null && !nextStatus.equals(prevStatus)) { + writeLog(u, "BOOKING", "예약-상태", + "예약 [" + nullToEmpty(b.getCustomerName()) + "] " + nextStatus + "(으)로 상태변경하였습니다."); + } else { + writeLog(u, "BOOKING", "예약-수정", "예약 [" + nullToEmpty(b.getCustomerName()) + "] 수정하였습니다."); + } + return ApiResponse.ok(bookingMap(b)); + } + @DeleteMapping("/bookings/{id}") @Transactional ApiResponse bookingDel(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + HomesBooking b = crud.get(HomesBooking.class, id, u); + writeLog(u, "BOOKING", "예약-삭제", "예약 [" + nullToEmpty(b.getCustomerName()) + "] 삭제하였습니다."); + crud.delete(HomesBooking.class, id, u); return ApiResponse.ok(null); + } + + @PostMapping("/bookings/delete") @Transactional + ApiResponse bookingsBulkDelete(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + Object raw = body.get("ids"); + if (!(raw instanceof List list) || list.isEmpty()) return ApiResponse.fail("삭제할 예약을 선택하세요."); + int deleted = 0; + for (Object o : list) { + Long id = Long.valueOf(String.valueOf(o)); + HomesBooking b = em.find(HomesBooking.class, id); + if (b == null || !Objects.equals(b.getGroupId(), u.getGroupId())) continue; + writeLog(u, "BOOKING", "예약-삭제", "예약 [" + nullToEmpty(b.getCustomerName()) + "] 삭제하였습니다."); + em.remove(b); + deleted++; + } + return ApiResponse.ok(Map.of("deleted", deleted)); + } + + @GetMapping(value = "/bookings/export", produces = "text/csv;charset=UTF-8") + org.springframework.http.ResponseEntity bookingsExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + ApiResponse res = bookings(u, f, 0, 10000); + @SuppressWarnings("unchecked") + Map data = (Map) res.data(); + @SuppressWarnings("unchecked") + List> list = data == null ? List.of() : (List>) data.getOrDefault("list", List.of()); + StringBuilder csv = new StringBuilder("\uFEFF접수일,상태,고객명,연락처,인터넷상품,홈렌탈상품,접수직원,주소,비고\n"); + for (Map row : list) { + csv.append(csvCell(row.get("bookingDate"))).append(',') + .append(csvCell(row.get("status"))).append(',') + .append(csvCell(row.get("customerName"))).append(',') + .append(csvCell(row.get("phone"))).append(',') + .append(csvCell(row.get("internetProducts"))).append(',') + .append(csvCell(row.get("rentalProducts"))).append(',') + .append(csvCell(row.get("employee"))).append(',') + .append(csvCell(row.get("address"))).append(',') + .append(csvCell(row.get("memo"))).append('\n'); + } + return org.springframework.http.ResponseEntity.ok() + .header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=bookings.csv") + .body(csv.toString()); + } + + // ---- Customers ---- + private static final List DEFAULT_CUSTOMER_GRADES = List.of("단골", "단골1", "단골2", "단골3", "단골4"); + + @GetMapping("/customers") + ApiResponse customers(@AuthenticationPrincipal JwtUser u, @RequestParam Map f, + @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "15") int size) { + List all = listAll(HomesCustomer.class, u); + String grade = blankToNull(f.get("grade")); + if (grade == null) grade = blankToNull(f.get("tab")); + if ("ALL".equalsIgnoreCase(grade)) grade = null; + String employee = blankToNull(f.get("employee")); + if (employee == null) employee = blankToNull(f.get("receiveEmployee")); + String customerType = blankToNull(f.get("customerType")); + String history = blankToNull(f.get("history")); + String searchType = blankToNull(f.get("searchType")); + String phoneTail = blankToNull(f.get("phoneTail")); + LocalDate from = parseDate(f.get("dateFrom")); + LocalDate to = parseDate(f.get("dateTo")); + String q = qOf(f); + String gr = grade; + String emp = employee; + Map contractCounts = countByCustomerPhone(u, HomesContract.class); + Map bookingCounts = countByCustomerPhone(u, HomesBooking.class); + + List filtered = all.stream().filter(c -> { + if (gr != null && !gr.equals(normalizeCustomerGrade(c.getGrade()))) return false; + if (emp != null && !emp.equals(c.getEmployee())) return false; + if (customerType != null && !customerType.equals(nullToEmpty(c.getCustomerType()).isBlank() ? "개인" : c.getCustomerType())) return false; + LocalDate reg = resolveCustomerRegisteredDate(c); + if (from != null && (reg == null || reg.isBefore(from))) return false; + if (to != null && (reg == null || reg.isAfter(to))) return false; + String digits = nullToEmpty(c.getPhone()).replaceAll("\\D", ""); + long contracts = contractCounts.getOrDefault(digits, 0L); + long bookings = bookingCounts.getOrDefault(digits, 0L); + if ("계약".equals(history) && contracts <= 0) return false; + if ("예약".equals(history) && bookings <= 0) return false; + if ("없음".equals(history) && (contracts > 0 || bookings > 0)) return false; + if (phoneTail != null) { + if (digits.length() < 4 || !digits.endsWith(phoneTail)) return false; + } + if (q != null) { + String needle = q.toLowerCase(); + if ("name".equals(searchType) || "고객명".equals(searchType)) { + if (!nullToEmpty(c.getName()).toLowerCase().contains(needle)) return false; + } else if ("phone".equals(searchType) || "연락처".equals(searchType) || "연락처(뒷4자리)".equals(searchType)) { + if (!digits.contains(q.replaceAll("\\D", "")) && !nullToEmpty(c.getPhone()).toLowerCase().contains(needle)) return false; + } else if ("address".equals(searchType) || "주소".equals(searchType)) { + if (!nullToEmpty(c.getAddress()).toLowerCase().contains(needle) + && !nullToEmpty(c.getAddressDetail()).toLowerCase().contains(needle)) return false; + } else { + String hay = (nullToEmpty(c.getName()) + " " + nullToEmpty(c.getPhone()) + " " + + nullToEmpty(c.getPhoneAlt()) + " " + nullToEmpty(c.getAddress()) + " " + + nullToEmpty(c.getMemo())).toLowerCase(); + if (!hay.contains(needle)) return false; + } + } + return true; + }).sorted(Comparator.comparing(this::resolveCustomerRegisteredDate, Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing(HomesCustomer::getId, Comparator.reverseOrder())).toList(); + + List grades = customerGradeList(u); + Map counts = new LinkedHashMap<>(); + counts.put("ALL", (long) all.size()); + for (String s : grades) { + counts.put(s, all.stream().filter(c -> s.equals(normalizeCustomerGrade(c.getGrade()))).count()); + } + ApiResponse res = pageOk(filtered, page, size, c -> customerMap(c, contractCounts, bookingCounts), counts); + @SuppressWarnings("unchecked") + Map body = (Map) res.data(); + if (body != null) { + body.put("grades", grades); + body.put("employees", all.stream().map(HomesCustomer::getEmployee).filter(Objects::nonNull) + .map(String::trim).filter(s -> !s.isBlank()).distinct().sorted().toList()); + } + return res; + } + + @GetMapping("/customers/grades") + ApiResponse customerGradesGet(@AuthenticationPrincipal JwtUser u) { + return ApiResponse.ok(Map.of("grades", customerGradeList(u))); + } + + @PutMapping("/customers/grades") @Transactional + ApiResponse customerGradesPut(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + Object raw = body.get("grades"); + List grades = new ArrayList<>(); + if (raw instanceof List list) { + for (Object o : list) { + if (o == null) continue; + String s = String.valueOf(o).trim(); + if (!s.isBlank() && !grades.contains(s) && !"분류없음".equals(s) && !"ALL".equalsIgnoreCase(s)) grades.add(s); + } + } + if (grades.isEmpty()) grades = new ArrayList<>(DEFAULT_CUSTOMER_GRADES); + saveJsonSetting(u.getGroupId(), "homes.customerGrades", Map.of("grades", grades)); + return ApiResponse.ok(Map.of("grades", grades)); + } + + @PostMapping("/customers") @Transactional ApiResponse customerPost(@RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + normalizeCustomerPayload(d, u); + HomesCustomer c = crud.create(HomesCustomer.class, d, u); + writeLog(u, "CUSTOMER", "고객-등록", "고객 [" + nullToEmpty(c.getName()) + "] 등록하였습니다."); + return ApiResponse.ok(customerMap(c, Map.of(), Map.of())); + } + @PutMapping("/customers/{id}") @Transactional ApiResponse customerPut(@PathVariable Long id, @RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + normalizeCustomerPayload(d, u); + HomesCustomer c = crud.update(HomesCustomer.class, id, d, u); + writeLog(u, "CUSTOMER", "고객-수정", "고객 [" + nullToEmpty(c.getName()) + "] 수정하였습니다."); + return ApiResponse.ok(customerMap(c, Map.of(), Map.of())); + } + @DeleteMapping("/customers/{id}") @Transactional ApiResponse customerDel(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + HomesCustomer c = crud.get(HomesCustomer.class, id, u); + writeLog(u, "CUSTOMER", "고객-삭제", "고객 [" + nullToEmpty(c.getName()) + "] 삭제하였습니다."); + crud.delete(HomesCustomer.class, id, u); return ApiResponse.ok(null); + } + + @PostMapping("/customers/delete") @Transactional + ApiResponse customersBulkDelete(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + Object raw = body.get("ids"); + if (!(raw instanceof List list) || list.isEmpty()) return ApiResponse.fail("삭제할 고객을 선택하세요."); + int deleted = 0; + for (Object o : list) { + Long id = Long.valueOf(String.valueOf(o)); + HomesCustomer c = em.find(HomesCustomer.class, id); + if (c == null || !Objects.equals(c.getGroupId(), u.getGroupId())) continue; + writeLog(u, "CUSTOMER", "고객-삭제", "고객 [" + nullToEmpty(c.getName()) + "] 삭제하였습니다."); + em.remove(c); + deleted++; + } + return ApiResponse.ok(Map.of("deleted", deleted)); + } + + @PostMapping("/customers/grade") @Transactional + ApiResponse customersChangeGrade(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + Object raw = body.get("ids"); + if (!(raw instanceof List list) || list.isEmpty()) return ApiResponse.fail("분류를 변경할 고객을 선택하세요."); + String grade = body.get("grade") == null ? "분류없음" : String.valueOf(body.get("grade")).trim(); + if (grade.isBlank()) grade = "분류없음"; + int updated = 0; + for (Object o : list) { + Long id = Long.valueOf(String.valueOf(o)); + HomesCustomer c = em.find(HomesCustomer.class, id); + if (c == null || !Objects.equals(c.getGroupId(), u.getGroupId())) continue; + c.setGrade(grade); + writeLog(u, "CUSTOMER", "고객-분류", "고객 [" + nullToEmpty(c.getName()) + "] 분류수정하였습니다."); + updated++; + } + return ApiResponse.ok(Map.of("updated", updated)); + } + + @GetMapping(value = "/customers/export", produces = "text/csv;charset=UTF-8") + org.springframework.http.ResponseEntity customersExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + ApiResponse res = customers(u, f, 0, 10000); + @SuppressWarnings("unchecked") + Map data = (Map) res.data(); + @SuppressWarnings("unchecked") + List> list = data == null ? List.of() : (List>) data.getOrDefault("list", List.of()); + StringBuilder csv = new StringBuilder("\uFEFF등록일,분류,구분,고객명,연락처,보조연락처,주소,메모,계약,예약,등록직원\n"); + for (Map row : list) { + csv.append(csvCell(row.get("registeredDate"))).append(',') + .append(csvCell(row.get("grade"))).append(',') + .append(csvCell(row.get("customerType"))).append(',') + .append(csvCell(row.get("name"))).append(',') + .append(csvCell(row.get("phone"))).append(',') + .append(csvCell(row.get("phoneAlt"))).append(',') + .append(csvCell(row.get("address"))).append(',') + .append(csvCell(row.get("memo"))).append(',') + .append(csvCell(row.get("contractCount"))).append(',') + .append(csvCell(row.get("bookingCount"))).append(',') + .append(csvCell(row.get("employee"))).append('\n'); + } + return org.springframework.http.ResponseEntity.ok() + .header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=customers.csv") + .body(csv.toString()); + } + + // ---- Abooks ---- + private static final List DEFAULT_ABOOK_CATEGORIES = List.of("일반", "123"); + private static final List ABOOK_ENTRY_TYPES = List.of("입금", "출금", "카드입금", "카드출금"); + + @GetMapping("/abooks") + ApiResponse abooks(@AuthenticationPrincipal JwtUser u, @RequestParam Map f, + @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "15") int size) { + List all = listAll(HomesAbook.class, u); + String entryType = blankToNull(f.get("entryType")); + if (entryType == null) entryType = blankToNull(f.get("tab")); + if ("ALL".equalsIgnoreCase(entryType)) entryType = null; + String category = blankToNull(f.get("category")); + if (category == null) category = blankToNull(f.get("subject")); + String employee = blankToNull(f.get("employee")); + String item = blankToNull(f.get("item")); + String yearMonth = blankToNull(f.get("yearMonth")); + Integer day = f.get("day") == null || f.get("day").isBlank() || "ALL".equalsIgnoreCase(f.get("day")) + ? null : parseInt(f.get("day"), 0); + if (day != null && day <= 0) day = null; + LocalDate from = parseDate(f.get("dateFrom")); + LocalDate to = parseDate(f.get("dateTo")); + if (yearMonth != null && from == null && to == null) { + try { + YearMonth ym = YearMonth.parse(yearMonth); + from = ym.atDay(1); + to = ym.atEndOfMonth(); + } catch (Exception ignored) {} + } + String q = qOf(f); + String et = entryType; + String cat = category; + LocalDate fromF = from; + LocalDate toF = to; + Integer dayF = day; + + List filtered = all.stream().filter(a -> { + if (et != null && !et.equals(normalizeAbookEntryType(a.getEntryType()))) return false; + if (cat != null && !cat.equals(a.getCategory())) return false; + if (employee != null && !employee.equals(a.getEmployee())) return false; + if (item != null && !item.equals(nullToEmpty(a.getItem()).isBlank() ? a.getAgency() : a.getItem())) return false; + if (fromF != null && (a.getEntryDate() == null || a.getEntryDate().isBefore(fromF))) return false; + if (toF != null && (a.getEntryDate() == null || a.getEntryDate().isAfter(toF))) return false; + if (dayF != null && (a.getEntryDate() == null || a.getEntryDate().getDayOfMonth() != dayF)) return false; + if (q != null) { + String hay = (nullToEmpty(a.getCategory()) + " " + nullToEmpty(a.getItem()) + " " + + nullToEmpty(a.getAgency()) + " " + nullToEmpty(a.getMemo()) + " " + + nullToEmpty(a.getEmployee())).toLowerCase(); + if (!hay.contains(q.toLowerCase())) return false; + } + return true; + }).sorted(Comparator.comparing(HomesAbook::getEntryDate, Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing(HomesAbook::getId, Comparator.reverseOrder())).toList(); + + List categories = abookCategoryList(u); + Map counts = new LinkedHashMap<>(); + // detailed tabs by entry type (within date range / month, ignoring entryType tab) + List baseForCounts = all.stream().filter(a -> { + if (fromF != null && (a.getEntryDate() == null || a.getEntryDate().isBefore(fromF))) return false; + if (toF != null && (a.getEntryDate() == null || a.getEntryDate().isAfter(toF))) return false; + if (dayF != null && (a.getEntryDate() == null || a.getEntryDate().getDayOfMonth() != dayF)) return false; + return true; + }).toList(); + counts.put("ALL", (long) baseForCounts.size()); + for (String t : ABOOK_ENTRY_TYPES) { + counts.put(t, baseForCounts.stream().filter(a -> t.equals(normalizeAbookEntryType(a.getEntryType()))).count()); + } + Map categoryCounts = new LinkedHashMap<>(); + categoryCounts.put("ALL", (long) baseForCounts.size()); + for (String c : categories) { + categoryCounts.put(c, baseForCounts.stream().filter(a -> c.equals(a.getCategory())).count()); + } + + // cash summary for month view + LocalDate carryCutoff = fromF; + if (carryCutoff == null) { + try { + if (yearMonth != null) carryCutoff = YearMonth.parse(yearMonth).atDay(1); + } catch (Exception ignored) {} + } + if (carryCutoff == null) carryCutoff = LocalDate.now().withDayOfMonth(1); + BigDecimal carryOver = BigDecimal.ZERO; + BigDecimal deposit = BigDecimal.ZERO; + BigDecimal withdraw = BigDecimal.ZERO; + for (HomesAbook a : all) { + if (a.getEntryDate() == null || a.getAmount() == null) continue; + String type = normalizeAbookEntryType(a.getEntryType()); + boolean inPeriod = (fromF == null || !a.getEntryDate().isBefore(fromF)) + && (toF == null || !a.getEntryDate().isAfter(toF)) + && (dayF == null || a.getEntryDate().getDayOfMonth() == dayF) + && (cat == null || cat.equals(a.getCategory())); + if (a.getEntryDate().isBefore(carryCutoff) && ("입금".equals(type) || "출금".equals(type))) { + if ("입금".equals(type)) carryOver = carryOver.add(a.getAmount()); + else carryOver = carryOver.subtract(a.getAmount()); + } + if (inPeriod) { + if ("입금".equals(type)) deposit = deposit.add(a.getAmount()); + else if ("출금".equals(type)) withdraw = withdraw.add(a.getAmount()); + } + } + BigDecimal cashTotal = carryOver.add(deposit).subtract(withdraw); + + ApiResponse res = pageOk(filtered, page, size, this::abookMap, counts); + @SuppressWarnings("unchecked") + Map body = (Map) res.data(); + if (body != null) { + body.put("categories", categories); + body.put("categoryCounts", categoryCounts); + body.put("employees", all.stream().map(HomesAbook::getEmployee).filter(Objects::nonNull) + .map(String::trim).filter(s -> !s.isBlank()).distinct().sorted().toList()); + body.put("items", all.stream().map(a -> { + String it = a.getItem(); + if (it == null || it.isBlank()) it = a.getAgency(); + return it; + }).filter(Objects::nonNull).map(String::trim).filter(s -> !s.isBlank()).distinct().sorted().toList()); + body.put("summary", Map.of( + "carryOver", carryOver, + "deposit", deposit, + "withdraw", withdraw, + "total", cashTotal + )); + } + return res; + } + + @GetMapping("/abooks/categories") + ApiResponse abookCategoriesGet(@AuthenticationPrincipal JwtUser u) { + return ApiResponse.ok(Map.of("categories", abookCategoryList(u))); + } + + @PutMapping("/abooks/categories") @Transactional + ApiResponse abookCategoriesPut(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + Object raw = body.get("categories"); + List categories = new ArrayList<>(); + if (raw instanceof List list) { + for (Object o : list) { + if (o == null) continue; + String s = String.valueOf(o).trim(); + if (!s.isBlank() && !categories.contains(s)) categories.add(s); + } + } + if (categories.isEmpty()) categories = new ArrayList<>(DEFAULT_ABOOK_CATEGORIES); + saveJsonSetting(u.getGroupId(), "homes.abookCategories", Map.of("categories", categories)); + return ApiResponse.ok(Map.of("categories", categories)); + } + + @PostMapping("/abooks") @Transactional ApiResponse abookPost(@RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + normalizeAbookPayload(d, u); + HomesAbook a = crud.create(HomesAbook.class, d, u); + writeLog(u, "ABOOKS", "간편장부-등록", abookLogContent(a, "등록하였습니다.")); + return ApiResponse.ok(abookMap(a)); + } + @PutMapping("/abooks/{id}") @Transactional ApiResponse abookPut(@PathVariable Long id, @RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + normalizeAbookPayload(d, u); + HomesAbook a = crud.update(HomesAbook.class, id, d, u); + writeLog(u, "ABOOKS", "간편장부-수정", abookLogContent(a, "수정하였습니다.")); + return ApiResponse.ok(abookMap(a)); + } + @DeleteMapping("/abooks/{id}") @Transactional ApiResponse abookDel(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + HomesAbook a = crud.get(HomesAbook.class, id, u); + writeLog(u, "ABOOKS", "간편장부-삭제", abookLogContent(a, "삭제하였습니다.")); + crud.delete(HomesAbook.class, id, u); return ApiResponse.ok(null); + } + + @PostMapping("/abooks/delete") @Transactional + ApiResponse abooksBulkDelete(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + Object raw = body.get("ids"); + if (!(raw instanceof List list) || list.isEmpty()) return ApiResponse.fail("삭제할 장부를 선택하세요."); + int deleted = 0; + for (Object o : list) { + Long id = Long.valueOf(String.valueOf(o)); + HomesAbook a = em.find(HomesAbook.class, id); + if (a == null || !Objects.equals(a.getGroupId(), u.getGroupId())) continue; + writeLog(u, "ABOOKS", "간편장부-삭제", abookLogContent(a, "삭제하였습니다.")); + em.remove(a); + deleted++; + } + return ApiResponse.ok(Map.of("deleted", deleted)); + } + + @GetMapping(value = "/abooks/export", produces = "text/csv;charset=UTF-8") + org.springframework.http.ResponseEntity abooksExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + ApiResponse res = abooks(u, f, 0, 10000); + @SuppressWarnings("unchecked") + Map data = (Map) res.data(); + @SuppressWarnings("unchecked") + List> list = data == null ? List.of() : (List>) data.getOrDefault("list", List.of()); + StringBuilder csv = new StringBuilder("\uFEFF거래일,구분,과목,품목,금액,비고,직원\n"); + for (Map row : list) { + csv.append(csvCell(row.get("entryDate"))).append(',') + .append(csvCell(row.get("entryType"))).append(',') + .append(csvCell(row.get("category"))).append(',') + .append(csvCell(row.get("item"))).append(',') + .append(csvCell(row.get("amount"))).append(',') + .append(csvCell(row.get("memo"))).append(',') + .append(csvCell(row.get("employee"))).append('\n'); + } + return org.springframework.http.ResponseEntity.ok() + .header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=abooks.csv") + .body(csv.toString()); + } + + // ---- Agencies / Partners / Members ---- + @GetMapping("/agencies") ApiResponse agencies(@AuthenticationPrincipal JwtUser u, @RequestParam Map f, + @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "15") int size) { + String q = qOf(f); + String status = blankToNull(f.get("status")); + if ("ALL".equalsIgnoreCase(status)) status = null; + String st = status; + List all = listAll(HomesAgency.class, u); + List filtered = all.stream().filter(a -> { + if ("사용".equals(st) && !a.isActive()) return false; + if ("정지".equals(st) && a.isActive()) return false; + if (q != null) { + String hay = (nullToEmpty(a.getName()) + " " + nullToEmpty(a.getManagerName()) + + " " + nullToEmpty(a.getPhone()) + " " + nullToEmpty(a.getMobile())).toLowerCase(); + if (!hay.contains(q.toLowerCase())) return false; + } + return true; + }).sorted(Comparator.comparing(HomesAgency::getName, Comparator.nullsLast(String::compareTo)) + .thenComparing(HomesAgency::getId, Comparator.reverseOrder())).toList(); + Map counts = new LinkedHashMap<>(); + counts.put("ALL", (long) all.size()); + counts.put("사용", all.stream().filter(HomesAgency::isActive).count()); + counts.put("정지", all.stream().filter(a -> !a.isActive()).count()); + return pageOk(filtered, page, size, this::agencyMap, counts); + } + @PostMapping("/agencies") @Transactional ApiResponse agencyPost(@RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + normalizeAgencyPayload(d); + HomesAgency a = crud.create(HomesAgency.class, d, u); + if (a.getRegisteredDate() == null) a.setRegisteredDate(LocalDate.now()); + writeLog(u, "AGENCY", "거래처-등록", "거래처 [" + nullToEmpty(a.getName()) + "] 등록하였습니다."); + return ApiResponse.ok(agencyMap(a)); + } + @PutMapping("/agencies/{id}") @Transactional ApiResponse agencyPut(@PathVariable Long id, @RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + normalizeAgencyPayload(d); + HomesAgency a = crud.update(HomesAgency.class, id, d, u); + writeLog(u, "AGENCY", "거래처-수정", "거래처 [" + nullToEmpty(a.getName()) + "] 수정하였습니다."); + return ApiResponse.ok(agencyMap(a)); + } + @DeleteMapping("/agencies/{id}") @Transactional ApiResponse agencyDel(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + HomesAgency a = crud.get(HomesAgency.class, id, u); + writeLog(u, "AGENCY", "거래처-삭제", "거래처 [" + nullToEmpty(a.getName()) + "] 삭제하였습니다."); + crud.delete(HomesAgency.class, id, u); return ApiResponse.ok(null); + } + + @PostMapping("/agencies/status") @Transactional + ApiResponse agenciesStatus(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + Object raw = body.get("ids"); + if (!(raw instanceof List list) || list.isEmpty()) return ApiResponse.fail("거래처를 선택하세요."); + boolean active = !"정지".equals(String.valueOf(body.getOrDefault("status", "사용"))); + int updated = 0; + for (Object o : list) { + Long id = Long.valueOf(String.valueOf(o)); + HomesAgency a = em.find(HomesAgency.class, id); + if (a == null || !Objects.equals(a.getGroupId(), u.getGroupId())) continue; + a.setActive(active); + writeLog(u, "AGENCY", "거래처-상태", + "거래처 [" + nullToEmpty(a.getName()) + "] " + (active ? "사용" : "정지") + "(으)로 변경하였습니다."); + updated++; + } + return ApiResponse.ok(Map.of("updated", updated)); + } + + @GetMapping(value = "/agencies/export", produces = "text/csv;charset=UTF-8") + org.springframework.http.ResponseEntity agenciesExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + ApiResponse res = agencies(u, f, 0, 10000); + @SuppressWarnings("unchecked") + Map data = (Map) res.data(); + @SuppressWarnings("unchecked") + List> list = data == null ? List.of() : (List>) data.getOrDefault("list", List.of()); + StringBuilder csv = new StringBuilder("\uFEFF상태,업체명,전화,팩스,담당자,휴대폰,등록일,비고\n"); + for (Map row : list) { + csv.append(csvCell(row.get("status"))).append(',') + .append(csvCell(row.get("name"))).append(',') + .append(csvCell(row.get("phone"))).append(',') + .append(csvCell(row.get("fax"))).append(',') + .append(csvCell(row.get("managerName"))).append(',') + .append(csvCell(row.get("mobile"))).append(',') + .append(csvCell(row.get("registeredDate"))).append(',') + .append(csvCell(row.get("memo"))).append('\n'); + } + return org.springframework.http.ResponseEntity.ok() + .header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=agencies.csv") + .body(csv.toString()); + } + + @GetMapping("/partners") ApiResponse partners(@AuthenticationPrincipal JwtUser u, @RequestParam Map f, + @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "15") int size) { + String level = blankToNull(f.get("partnerLevel")); + String status = blankToNull(f.get("status")); + if ("ALL".equalsIgnoreCase(status)) status = null; + String q = qOf(f); + String st = status; + List filtered = listAll(HomesPartner.class, u).stream().filter(p -> { + if (level != null && !level.equals(p.getPartnerLevel())) return false; + if (st != null && !st.equals(nullToEmpty(p.getStatus()).isBlank() ? "대기" : p.getStatus())) return false; + if (q != null) { + String hay = (nullToEmpty(p.getName()) + " " + nullToEmpty(p.getUserid()) + + " " + nullToEmpty(p.getManagerName()) + " " + nullToEmpty(p.getParentName())).toLowerCase(); + if (!hay.contains(q.toLowerCase())) return false; + } + return true; + }).sorted(Comparator.comparing(HomesPartner::getId, Comparator.reverseOrder())).toList(); + return pageOk(filtered, page, size, this::partnerMap, null); + } + @PostMapping("/partners") @Transactional ApiResponse partnerPost(@RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + normalizePartnerPayload(d); + HomesPartner p = crud.create(HomesPartner.class, d, u); + if (p.getRegisteredDate() == null) p.setRegisteredDate(LocalDate.now()); + if (p.getStatus() == null || p.getStatus().isBlank()) p.setStatus("대기"); + writeLog(u, "PARTNER", "상부점".equals(levelLabel(p)) ? "상부점-등록" : "하부점-등록", + levelLabel(p) + " [" + nullToEmpty(p.getName()) + "] 등록하였습니다."); + return ApiResponse.ok(partnerMap(p)); + } + @PutMapping("/partners/{id}") @Transactional ApiResponse partnerPut(@PathVariable Long id, @RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + normalizePartnerPayload(d); + HomesPartner p = crud.update(HomesPartner.class, id, d, u); + writeLog(u, "PARTNER", "상부점".equals(levelLabel(p)) ? "상부점-수정" : "하부점-수정", + levelLabel(p) + " [" + nullToEmpty(p.getName()) + "] 수정하였습니다."); + return ApiResponse.ok(partnerMap(p)); + } + @DeleteMapping("/partners/{id}") @Transactional ApiResponse partnerDel(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + HomesPartner p = crud.get(HomesPartner.class, id, u); + writeLog(u, "PARTNER", "상부점".equals(levelLabel(p)) ? "상부점-삭제" : "하부점-삭제", + levelLabel(p) + " [" + nullToEmpty(p.getName()) + "] 삭제하였습니다."); + crud.delete(HomesPartner.class, id, u); + return ApiResponse.ok(null); + } + + private String levelLabel(HomesPartner p) { + return "2".equals(p.getPartnerLevel()) ? "하부점" : "상부점"; + } + + private void normalizePartnerPayload(Map d) { + if (d.get("partnerLevel") == null || String.valueOf(d.get("partnerLevel")).isBlank()) { + d.put("partnerLevel", "1"); + } + if (d.get("status") == null || String.valueOf(d.get("status")).isBlank()) { + d.put("status", "대기"); + } + if (d.get("registeredDate") == null || String.valueOf(d.get("registeredDate")).isBlank()) { + d.put("registeredDate", LocalDate.now().toString()); + } + } + + @GetMapping("/members") ApiResponse members(@AuthenticationPrincipal JwtUser u, @RequestParam Map f, + @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "15") int size) { + String q = qOf(f); + if (q == null) q = blankToNull(f.get("keyword")); + String status = blankToNull(f.get("status")); + if ("ALL".equalsIgnoreCase(status)) status = null; + String st = status; + String qLower = q == null ? null : q.toLowerCase(); + List all = em.createQuery("select m from Member m where m.groupId=:g and m.role='AGENT'", Member.class) + .setParameter("g", u.getGroupId()).getResultList(); + List filtered = all.stream().filter(m -> { + if ("사용".equals(st) && !m.isActive()) return false; + if (("중지".equals(st) || "정지".equals(st)) && m.isActive()) return false; + if (qLower != null) { + String hay = (nullToEmpty(m.getUserid()) + " " + nullToEmpty(m.getName())).toLowerCase(); + if (!hay.contains(qLower)) return false; + } + return true; + }).sorted(memberComparator(f.get("sort"))).toList(); + Map counts = new LinkedHashMap<>(); + counts.put("ALL", (long) all.size()); + counts.put("사용", all.stream().filter(Member::isActive).count()); + counts.put("중지", all.stream().filter(m -> !m.isActive()).count()); + return pageOk(filtered, page, size, this::memberMap, counts); + } + @PostMapping("/members") @Transactional ApiResponse memberPost(@RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + String userid = d.get("userid") == null ? "" : String.valueOf(d.get("userid")).trim(); + if (userid.isBlank()) return ApiResponse.fail("아이디를 입력하세요."); + if (!em.createQuery("select m from Member m where m.userid=:id", Member.class).setParameter("id", userid).getResultList().isEmpty()) { + return ApiResponse.fail("이미 사용 중인 아이디입니다."); + } + Member m = new Member(); + m.setGroupId(u.getGroupId()); + m.setUserid(userid); + m.setName(String.valueOf(d.getOrDefault("name", "")).trim()); + m.setPhone(blankToNull(d.get("phone") == null ? null : String.valueOf(d.get("phone")))); + m.setAddress(blankToNull(d.get("address") == null ? null : String.valueOf(d.get("address")))); + m.setAccessLimit(blankToNull(d.get("accessLimit") == null ? null : String.valueOf(d.get("accessLimit")))); + m.setMemo(blankToNull(d.get("memo") == null ? null : String.valueOf(d.get("memo")))); + m.setStaffPermission(String.valueOf(d.getOrDefault("staffPermission", "사원"))); + m.setRole("AGENT"); + m.setActive(!"중지".equals(String.valueOf(d.getOrDefault("status", "사용"))) && !"정지".equals(String.valueOf(d.getOrDefault("status", "")))); + m.setLoginCount(0); + m.setMenuFlags(d.get("menuFlags") == null ? null : String.valueOf(d.get("menuFlags"))); + String pw = d.get("password") == null || String.valueOf(d.get("password")).isBlank() ? "demo123" : String.valueOf(d.get("password")); + m.setPassword(encoder.encode(pw)); + m.setPlainPassword(pw); + em.persist(m); + writeLog(u, "MEMBER", "직원-등록", "직원 [" + nullToEmpty(m.getName()) + "] 등록하였습니다."); + return ApiResponse.ok(memberMap(m)); + } + @PutMapping("/members/{id}") @Transactional ApiResponse memberPut(@PathVariable Long id, @RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + Member m = crud.get(Member.class, id, u); + if (d.get("name") != null) m.setName(String.valueOf(d.get("name")).trim()); + if (d.get("phone") != null) m.setPhone(blankToNull(String.valueOf(d.get("phone")))); + if (d.get("address") != null) m.setAddress(blankToNull(String.valueOf(d.get("address")))); + if (d.get("accessLimit") != null) m.setAccessLimit(blankToNull(String.valueOf(d.get("accessLimit")))); + if (d.get("memo") != null) m.setMemo(blankToNull(String.valueOf(d.get("memo")))); + if (d.get("staffPermission") != null && !String.valueOf(d.get("staffPermission")).isBlank()) { + m.setStaffPermission(String.valueOf(d.get("staffPermission"))); + } + if (d.get("status") != null) { + String st = String.valueOf(d.get("status")); + m.setActive(!"중지".equals(st) && !"정지".equals(st) && !"false".equalsIgnoreCase(st)); + } + if (d.containsKey("menuFlags")) { + m.setMenuFlags(d.get("menuFlags") == null || String.valueOf(d.get("menuFlags")).isBlank() + ? null : String.valueOf(d.get("menuFlags"))); + } + if (d.get("password") != null && !String.valueOf(d.get("password")).isBlank()) { + String pw = String.valueOf(d.get("password")); + m.setPassword(encoder.encode(pw)); + m.setPlainPassword(pw); + } + writeLog(u, "MEMBER", "직원-수정", "직원 [" + nullToEmpty(m.getName()) + "] 수정하였습니다."); + return ApiResponse.ok(memberMap(m)); + } + @DeleteMapping("/members/{id}") @Transactional ApiResponse memberDel(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + Member m = crud.get(Member.class, id, u); + if ("demo".equals(m.getUserid()) || "대표".equals(m.getStaffPermission())) { + return ApiResponse.fail("대표 계정은 삭제할 수 없습니다."); + } + writeLog(u, "MEMBER", "직원-삭제", "직원 [" + nullToEmpty(m.getName()) + "] 삭제하였습니다."); + em.remove(m); + return ApiResponse.ok(null); + } + + @PostMapping("/members/status") @Transactional + ApiResponse membersStatus(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + Object raw = body.get("ids"); + if (!(raw instanceof List list) || list.isEmpty()) return ApiResponse.fail("직원을 선택하세요."); + String status = String.valueOf(body.getOrDefault("status", "사용")); + boolean active = !"중지".equals(status) && !"정지".equals(status); + int updated = 0; + for (Object o : list) { + Long id = Long.valueOf(String.valueOf(o)); + Member m = em.find(Member.class, id); + if (m == null || !Objects.equals(m.getGroupId(), u.getGroupId()) || !"AGENT".equals(m.getRole())) continue; + if ("demo".equals(m.getUserid()) || "대표".equals(m.getStaffPermission())) continue; + m.setActive(active); + writeLog(u, "MEMBER", "직원-상태", + "직원 [" + nullToEmpty(m.getName()) + "] " + (active ? "사용" : "중지") + "(으)로 변경하였습니다."); + updated++; + } + return ApiResponse.ok(Map.of("updated", updated)); + } + + @GetMapping(value = "/members/export", produces = "text/csv;charset=UTF-8") + org.springframework.http.ResponseEntity membersExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + ApiResponse res = members(u, f, 0, 10000); + @SuppressWarnings("unchecked") + Map data = (Map) res.data(); + @SuppressWarnings("unchecked") + List> list = data == null ? List.of() : (List>) data.getOrDefault("list", List.of()); + StringBuilder csv = new StringBuilder("\uFEFF상태,권한,아이디,이름,휴대폰,주소,접속제한,접속수,최종접속,등록일,비고\n"); + for (Map row : list) { + csv.append(csvCell(row.get("status"))).append(',') + .append(csvCell(row.get("staffPermission"))).append(',') + .append(csvCell(row.get("userid"))).append(',') + .append(csvCell(row.get("name"))).append(',') + .append(csvCell(row.get("phone"))).append(',') + .append(csvCell(row.get("address"))).append(',') + .append(csvCell(row.get("accessLimit"))).append(',') + .append(csvCell(row.get("loginCount"))).append(',') + .append(csvCell(row.get("lastLoginAt"))).append(',') + .append(csvCell(row.get("registeredDate"))).append(',') + .append(csvCell(row.get("memo"))).append('\n'); + } + return org.springframework.http.ResponseEntity.ok() + .header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=members.csv") + .body(csv.toString()); + } + + // ---- Preference / Levels ---- + @GetMapping("/preference") ApiResponse preferenceGet(@AuthenticationPrincipal JwtUser u) { + return ApiResponse.ok(loadHomesPreference(u.getGroupId())); + } + @PutMapping("/preference") @Transactional ApiResponse preferencePut(@RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + Map current = loadHomesPreference(u.getGroupId()); + mergePreference(current, d); + saveJsonSetting(u.getGroupId(), "homes_preference", current); + writeLog(u, "SETTING", "기본설정-저장", "기본설정을 저장하였습니다."); + return ApiResponse.ok(current); + } + /** 상단 계정 팝오버용 프로필 요약 */ + @GetMapping("/account-profile") + ApiResponse accountProfile(@AuthenticationPrincipal JwtUser u) { + List users = em.createQuery("select m from Member m where m.userid=:id", Member.class) + .setParameter("id", u.getUserid()).getResultList(); + Member member = users.isEmpty() ? null : users.getFirst(); + String name = member == null || member.getName() == null || member.getName().isBlank() + ? u.getUserid() : member.getName(); + String perm = member == null ? null : member.getStaffPermission(); + if (perm == null || perm.isBlank()) { + perm = "demo".equals(u.getUserid()) ? "대표" : "사원"; + } + long memberCount = em.createQuery( + "select count(m) from Member m where m.groupId=:g and m.role='AGENT' and m.active=true", Long.class) + .setParameter("g", u.getGroupId()).getSingleResult(); + + Map sub = loadJsonSetting(u.getGroupId(), "homes_subscription", defaultSubscription()); + LocalDate expire = LocalDate.parse(String.valueOf(sub.getOrDefault("expireDate", "2030-12-31"))); + long remainDays = java.time.temporal.ChronoUnit.DAYS.between(LocalDate.now(ZoneId.of("Asia/Seoul")), expire); + + String orgLabel = String.valueOf(sub.getOrDefault("orgLabel", "")); + if (orgLabel.isBlank()) { + orgLabel = "demo".equals(u.getGroupId()) ? "데모" : u.getGroupId(); + } + + Map data = new LinkedHashMap<>(); + data.put("name", name); + data.put("userid", u.getUserid()); + data.put("staffPermission", perm); + data.put("orgLabel", orgLabel); + data.put("memberCount", memberCount); + data.put("expireDate", expire.toString()); + data.put("remainDays", remainDays); + return ApiResponse.ok(data); + } + + @PutMapping("/password") + @Transactional + ApiResponse changePassword(@AuthenticationPrincipal JwtUser u, @RequestBody Map body) { + String current = body.get("currentPassword") == null ? "" : String.valueOf(body.get("currentPassword")); + String next = body.get("newPassword") == null ? "" : String.valueOf(body.get("newPassword")).trim(); + if (current.isBlank()) return ApiResponse.fail("현재 비밀번호를 입력하세요."); + if (!next.matches("^[A-Za-z0-9]{4,20}$")) return ApiResponse.fail("새 비밀번호는 영문, 숫자 4자~20자입니다."); + List users = em.createQuery("select m from Member m where m.userid=:id", Member.class) + .setParameter("id", u.getUserid()).getResultList(); + if (users.isEmpty()) return ApiResponse.fail("사용자를 찾을 수 없습니다."); + Member m = users.getFirst(); + if (!encoder.matches(current, m.getPassword())) return ApiResponse.fail("현재 비밀번호가 올바르지 않습니다."); + m.setPassword(encoder.encode(next)); + m.setPlainPassword(next); + writeLog(u, "SETTING", "비밀번호변경", "비밀번호를 변경하였습니다."); + return ApiResponse.ok(Map.of("ok", true)); + } + + private Map defaultSubscription() { + Map m = new LinkedHashMap<>(); + m.put("orgLabel", "데모"); + m.put("expireDate", "2030-12-31"); + return m; + } + + @GetMapping("/levels") ApiResponse levelsGet(@AuthenticationPrincipal JwtUser u) { + return ApiResponse.ok(loadHomesLevels(u.getGroupId())); + } + + /** 현재 로그인 사용자에게 허용된 좌측 메뉴 섹션 id 목록 */ + @GetMapping("/my-menus") + ApiResponse myMenus(@AuthenticationPrincipal JwtUser u) { + List users = em.createQuery("select m from Member m where m.userid=:id", Member.class) + .setParameter("id", u.getUserid()).getResultList(); + Member member = users.isEmpty() ? null : users.getFirst(); + String perm = member == null ? null : member.getStaffPermission(); + if (perm == null || perm.isBlank()) { + perm = "demo".equals(u.getUserid()) ? "대표" : "사원"; + } + String menuFlagsRaw = member == null ? null : member.getMenuFlags(); + Map data = new LinkedHashMap<>(); + data.put("staffPermission", perm); + data.put("menus", resolveAllowedMenus(u.getGroupId(), u.getUserid(), perm, menuFlagsRaw)); + return ApiResponse.ok(data); + } + + @PutMapping("/levels") @Transactional ApiResponse levelsPut(@RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + if (u == null) return ApiResponse.fail("로그인이 필요합니다."); + try { + Map current = loadHomesLevels(u.getGroupId()); + if (d.get("modes") instanceof Map modes) { + @SuppressWarnings("unchecked") + Map dest = new LinkedHashMap<>((Map) current.get("modes")); + dest.putAll((Map) modes); + current.put("modes", dest); + } + if (d.get("custom") instanceof Map custom) { + @SuppressWarnings("unchecked") + Map dest = new LinkedHashMap<>((Map) current.get("custom")); + for (Map.Entry e : custom.entrySet()) { + dest.put(String.valueOf(e.getKey()), e.getValue()); + } + current.put("custom", dest); + } + // persist without embedding defaults (recomputed on get) + Map toSave = new LinkedHashMap<>(); + toSave.put("modes", current.get("modes")); + toSave.put("custom", current.get("custom")); + saveJsonSetting(u.getGroupId(), "homes_levels", toSave); + writeLog(u, "SETTING", "권한설정-저장", "권한설정을 저장하였습니다."); + return ApiResponse.ok(loadHomesLevels(u.getGroupId())); + } catch (Exception e) { + return ApiResponse.fail("권한설정 저장에 실패했습니다: " + e.getMessage()); + } + } + + // ---- Notices / Questions / BBS / Logs ---- + @GetMapping("/notices") ApiResponse notices(@AuthenticationPrincipal JwtUser u, @RequestParam Map f, + @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "15") int size) { + String searchType = blankToNull(f.get("searchType")); + String q = qOf(f); + List filtered = listAll(HomesNotice.class, u).stream().filter(n -> { + if (q == null) return true; + String hay = switch (searchType == null ? "제목" : searchType) { + case "내용" -> nullToEmpty(n.getContents()); + case "작성자" -> nullToEmpty(n.getAuthorName()); + default -> nullToEmpty(n.getTitle()); + }; + return hay.toLowerCase().contains(q.toLowerCase()); + }).sorted(noticeComparator(f.get("sort"))).toList(); + int from = Math.max(0, page) * Math.max(1, size); + List> list = new ArrayList<>(); + int i = 0; + for (HomesNotice n : filtered.stream().skip(from).limit(Math.min(Math.max(size, 1), 200)).toList()) { + Map row = noticeMap(n); + // 번호: 최신순 목록에서의 순번 (전체 기준) + row.put("no", filtered.size() - from - i); + list.add(row); + i++; + } + Map data = new LinkedHashMap<>(); + data.put("total", filtered.size()); + data.put("list", list); + return ApiResponse.ok(data); + } + @GetMapping("/notices/{id}") @Transactional ApiResponse noticeGet(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + HomesNotice n = crud.get(HomesNotice.class, id, u); + n.setViews((n.getViews() == null ? 0 : n.getViews()) + 1); + return ApiResponse.ok(noticeMap(n)); + } + @PostMapping("/notices") @Transactional ApiResponse noticePost(@RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + d.putIfAbsent("authorName", u.getUsername()); + d.putIfAbsent("views", 0); + return ApiResponse.ok(noticeMap(crud.create(HomesNotice.class, d, u))); + } + @PutMapping("/notices/{id}") @Transactional ApiResponse noticePut(@PathVariable Long id, @RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + return ApiResponse.ok(noticeMap(crud.update(HomesNotice.class, id, d, u))); + } + @DeleteMapping("/notices/{id}") @Transactional ApiResponse noticeDel(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + crud.delete(HomesNotice.class, id, u); return ApiResponse.ok(null); + } + + private Comparator noticeComparator(String sort) { + String field = sort == null || sort.isBlank() ? "id,desc" : sort; + boolean asc = field.endsWith(",asc"); + String key = field.split(",")[0]; + Comparator c = switch (key) { + case "views", "조회" -> Comparator.comparing(n -> n.getViews() == null ? 0 : n.getViews()); + case "createdDate", "createdAt", "등록일" -> Comparator.comparing(HomesNotice::getCreatedAt, Comparator.nullsLast(Instant::compareTo)); + case "no", "id", "번호" -> Comparator.comparing(HomesNotice::getId, Comparator.nullsLast(Long::compareTo)); + default -> Comparator.comparing(HomesNotice::getId, Comparator.nullsLast(Long::compareTo)); + }; + return (asc ? c : c.reversed()).thenComparing(HomesNotice::getId, Comparator.reverseOrder()); + } + + @GetMapping("/questions") ApiResponse questions(@AuthenticationPrincipal JwtUser u, @RequestParam Map f, + @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "15") int size) { + String status = blankToNull(f.get("status")); + if ("ALL".equalsIgnoreCase(status)) status = null; + String searchType = blankToNull(f.get("searchType")); + String q = qOf(f); + String st = status; + List all = listAll(HomesQuestion.class, u); + List filtered = all.stream().filter(x -> { + if (st != null && !st.equals(x.getStatus())) return false; + if (q != null) { + String hay = switch (searchType == null ? "제목" : searchType) { + case "작성자" -> nullToEmpty(x.getAuthorName()); + case "내용" -> nullToEmpty(x.getContent()); + case "답변" -> nullToEmpty(x.getReply()) + " " + nullToEmpty(x.getStatus()); + default -> nullToEmpty(x.getTitle()); + }; + if (!hay.toLowerCase().contains(q.toLowerCase())) return false; + } + return true; + }).sorted(questionComparator(f.get("sort"))).toList(); + int from = Math.max(0, page) * Math.max(1, size); + List> list = new ArrayList<>(); + int i = 0; + for (HomesQuestion x : filtered.stream().skip(from).limit(Math.min(Math.max(size, 1), 200)).toList()) { + Map row = questionMap(x); + row.put("no", filtered.size() - from - i); + list.add(row); + i++; + } + Map data = new LinkedHashMap<>(); + data.put("total", filtered.size()); + data.put("list", list); + Map counts = new LinkedHashMap<>(); + counts.put("ALL", (long) all.size()); + counts.put("접수", all.stream().filter(x -> "접수".equals(x.getStatus())).count()); + counts.put("답변완료", all.stream().filter(x -> "답변완료".equals(x.getStatus())).count()); + data.put("counts", counts); + return ApiResponse.ok(data); + } + @GetMapping("/questions/{id}") ApiResponse questionGet(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + return ApiResponse.ok(questionMap(crud.get(HomesQuestion.class, id, u))); + } + @PostMapping("/questions") @Transactional ApiResponse questionPost(@RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + d.putIfAbsent("status", "접수"); + if (d.get("authorName") == null || String.valueOf(d.get("authorName")).isBlank()) { + d.put("authorName", u.getUsername()); + } + HomesQuestion q = crud.create(HomesQuestion.class, d, u); + writeLog(u, "CS", "사용문의-등록", "사용문의 [" + nullToEmpty(q.getTitle()) + "] 등록하였습니다."); + return ApiResponse.ok(questionMap(q)); + } + @PutMapping("/questions/{id}") @Transactional ApiResponse questionPut(@PathVariable Long id, @RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + if (d.get("reply") != null && !String.valueOf(d.get("reply")).isBlank()) { + d.putIfAbsent("status", "답변완료"); + } + HomesQuestion q = crud.update(HomesQuestion.class, id, d, u); + writeLog(u, "CS", "사용문의-수정", "사용문의 [" + nullToEmpty(q.getTitle()) + "] 수정하였습니다."); + return ApiResponse.ok(questionMap(q)); + } + @DeleteMapping("/questions/{id}") @Transactional ApiResponse questionDel(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + HomesQuestion q = crud.get(HomesQuestion.class, id, u); + writeLog(u, "CS", "사용문의-삭제", "사용문의 [" + nullToEmpty(q.getTitle()) + "] 삭제하였습니다."); + crud.delete(HomesQuestion.class, id, u); + return ApiResponse.ok(null); + } + + private Comparator questionComparator(String sort) { + String field = sort == null || sort.isBlank() ? "id,desc" : sort; + boolean asc = field.endsWith(",asc"); + String key = field.split(",")[0]; + Comparator c = switch (key) { + case "answer", "답변", "status" -> Comparator.comparing(HomesQuestion::getStatus, Comparator.nullsLast(String::compareTo)); + case "authorName", "작성자" -> Comparator.comparing(HomesQuestion::getAuthorName, Comparator.nullsLast(String::compareTo)); + case "createdDate", "createdAt", "등록일" -> Comparator.comparing(HomesQuestion::getCreatedAt, Comparator.nullsLast(Instant::compareTo)); + case "title", "제목" -> Comparator.comparing(HomesQuestion::getTitle, Comparator.nullsLast(String::compareTo)); + default -> Comparator.comparing(HomesQuestion::getId, Comparator.nullsLast(Long::compareTo)); + }; + return (asc ? c : c.reversed()).thenComparing(HomesQuestion::getId, Comparator.reverseOrder()); + } + + @GetMapping("/bbs") ApiResponse bbs(@AuthenticationPrincipal JwtUser u, @RequestParam Map f, + @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "15") int size) { + String searchType = blankToNull(f.get("searchType")); + String q = qOf(f); + List filtered = listAll(HomesBbs.class, u).stream().filter(b -> { + if (q == null) return true; + String hay = switch (searchType == null ? "제목" : searchType) { + case "직원", "작성자" -> nullToEmpty(b.getAuthorName()); + case "내용" -> nullToEmpty(b.getContents()); + default -> nullToEmpty(b.getTitle()); + }; + return hay.toLowerCase().contains(q.toLowerCase()); + }).sorted(bbsComparator(f.get("sort"))).toList(); + int from = Math.max(0, page) * Math.max(1, size); + List> list = new ArrayList<>(); + int i = 0; + for (HomesBbs b : filtered.stream().skip(from).limit(Math.min(Math.max(size, 1), 200)).toList()) { + Map row = bbsMap(b); + row.put("no", filtered.size() - from - i); + list.add(row); + i++; + } + Map data = new LinkedHashMap<>(); + data.put("total", filtered.size()); + data.put("list", list); + return ApiResponse.ok(data); + } + @GetMapping("/bbs/{id}") @Transactional ApiResponse bbsGet(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + HomesBbs b = crud.get(HomesBbs.class, id, u); + b.setViews((b.getViews() == null ? 0 : b.getViews()) + 1); + return ApiResponse.ok(bbsMap(b)); + } + @PostMapping("/bbs") @Transactional ApiResponse bbsPost(@RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + d.putIfAbsent("authorName", u.getUsername()); + d.putIfAbsent("views", 0); + HomesBbs b = crud.create(HomesBbs.class, d, u); + writeLog(u, "BBS", "사내게시판-등록", "게시물 [" + nullToEmpty(b.getTitle()) + "] 등록하였습니다."); + return ApiResponse.ok(bbsMap(b)); + } + @PutMapping("/bbs/{id}") @Transactional ApiResponse bbsPut(@PathVariable Long id, @RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + HomesBbs b = crud.update(HomesBbs.class, id, d, u); + writeLog(u, "BBS", "사내게시판-수정", "게시물 [" + nullToEmpty(b.getTitle()) + "] 수정하였습니다."); + return ApiResponse.ok(bbsMap(b)); + } + @DeleteMapping("/bbs/{id}") @Transactional ApiResponse bbsDel(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + HomesBbs b = crud.get(HomesBbs.class, id, u); + writeLog(u, "BBS", "사내게시판-삭제", "게시물 [" + nullToEmpty(b.getTitle()) + "] 삭제하였습니다."); + crud.delete(HomesBbs.class, id, u); + return ApiResponse.ok(null); + } + + private Comparator bbsComparator(String sort) { + String field = sort == null || sort.isBlank() ? "id,desc" : sort; + boolean asc = field.endsWith(",asc"); + String key = field.split(",")[0]; + Comparator c = switch (key) { + case "views", "조회" -> Comparator.comparing(b -> b.getViews() == null ? 0 : b.getViews()); + case "authorName", "직원", "작성자" -> Comparator.comparing(HomesBbs::getAuthorName, Comparator.nullsLast(String::compareTo)); + case "createdDate", "createdAt", "등록일" -> Comparator.comparing(HomesBbs::getCreatedAt, Comparator.nullsLast(Instant::compareTo)); + case "title", "제목" -> Comparator.comparing(HomesBbs::getTitle, Comparator.nullsLast(String::compareTo)); + default -> Comparator.comparing(HomesBbs::getId, Comparator.nullsLast(Long::compareTo)); + }; + return (asc ? c : c.reversed()).thenComparing(HomesBbs::getId, Comparator.reverseOrder()); + } + + @GetMapping("/logs") ApiResponse logs(@AuthenticationPrincipal JwtUser u, @RequestParam Map f, + @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { + String domain = blankToNull(f.get("domain")); + String action = blankToNull(f.get("action")); + String staff = blankToNull(f.get("staff")); + LocalDate from = parseDate(f.get("dateFrom")); + LocalDate to = parseDate(f.get("dateTo")); + String q = qOf(f); + List base = listAll(HomesLog.class, u).stream() + .filter(l -> domain == null || domain.equals(l.getDomain())) + .toList(); + List filtered = base.stream().filter(l -> { + if (action != null && !action.equals(l.getAction())) return false; + if (staff != null && !staff.equals(l.getStaff())) return false; + if (from != null && (l.getProcessedAt() == null || l.getProcessedAt().toLocalDate().isBefore(from))) return false; + if (to != null && (l.getProcessedAt() == null || l.getProcessedAt().toLocalDate().isAfter(to))) return false; + if (q != null && !nullToEmpty(l.getContent()).toLowerCase().contains(q.toLowerCase())) return false; + return true; + }).toList(); + Map pageData = new LinkedHashMap<>(); + int fromIdx = Math.max(0, page) * Math.max(1, size); + pageData.put("total", filtered.size()); + pageData.put("list", filtered.stream().skip(fromIdx).limit(Math.min(Math.max(size, 1), 200)).map(this::logMap).toList()); + pageData.put("actions", base.stream().map(HomesLog::getAction).filter(Objects::nonNull).distinct().sorted().toList()); + pageData.put("staffs", base.stream().map(HomesLog::getStaff).filter(Objects::nonNull).distinct().sorted().toList()); + return ApiResponse.ok(pageData); + } + @PostMapping("/logs") @Transactional ApiResponse logPost(@RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + if (d.get("processedAt") == null) d.put("processedAt", LocalDateTime.now().toString()); + if (d.get("staff") == null) d.put("staff", u.getUsername()); + return ApiResponse.ok(logMap(crud.create(HomesLog.class, d, u))); + } + @PutMapping("/logs/{id}") @Transactional ApiResponse logPut(@PathVariable Long id, @RequestBody Map d, @AuthenticationPrincipal JwtUser u) { + return ApiResponse.ok(logMap(crud.update(HomesLog.class, id, d, u))); + } + @DeleteMapping("/logs/{id}") @Transactional ApiResponse logDel(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + crud.delete(HomesLog.class, id, u); return ApiResponse.ok(null); + } + @PostMapping("/logs/delete") @Transactional + ApiResponse logsBulkDelete(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + Object raw = body.get("ids"); + if (!(raw instanceof List list) || list.isEmpty()) return ApiResponse.fail("삭제할 이력을 선택하세요."); + int deleted = 0; + for (Object o : list) { + Long id = Long.valueOf(String.valueOf(o)); + HomesLog log = em.find(HomesLog.class, id); + if (log == null || !Objects.equals(log.getGroupId(), u.getGroupId())) continue; + em.remove(log); + deleted++; + } + return ApiResponse.ok(Map.of("deleted", deleted)); + } + + // ---- Reports ---- + @GetMapping("/reports/{type}") + ApiResponse report(@PathVariable String type, @AuthenticationPrincipal JwtUser u, @RequestParam Map f) { + int year = parseInt(f.get("year"), LocalDate.now().getYear()); + Integer month = f.get("month") == null || f.get("month").isBlank() ? null : parseInt(f.get("month"), LocalDate.now().getMonthValue()); + List contracts = listAll(HomesContract.class, u).stream().filter(c -> { + if (c.getContractDate() == null || c.getContractDate().getYear() != year) return false; + return month == null || c.getContractDate().getMonthValue() == month; + }).toList(); + List> rows = new ArrayList<>(); + String title = year + "년" + (month == null ? "" : " " + month + "월") + " "; + switch (type) { + case "daily" -> { + LocalDate date = parseDate(f.get("date")); + if (date == null) date = LocalDate.now(); + return ApiResponse.ok(buildDailyReport(u, date)); + } + case "monthly" -> { + return ApiResponse.ok(buildMonthlyReport(u, year)); + } + case "internet" -> { + String mode = blankToNull(f.get("mode")); + if (mode == null) mode = "daily"; + int m = month == null ? LocalDate.now().getMonthValue() : month; + return ApiResponse.ok(buildKindPeriodReport(u, "INTERNET", "인터넷통계", mode, year, m)); + } + case "home" -> { + String mode = blankToNull(f.get("mode")); + if (mode == null) mode = "daily"; + int m = month == null ? LocalDate.now().getMonthValue() : month; + return ApiResponse.ok(buildKindPeriodReport(u, "HOME", "홈렌탈통계", mode, year, m)); + } + case "member" -> { + int m = month == null ? LocalDate.now().getMonthValue() : month; + return ApiResponse.ok(buildGroupedPeriodReport(u, "member", "직원별통계", year, m)); + } + case "agency" -> { + int m = month == null ? LocalDate.now().getMonthValue() : month; + return ApiResponse.ok(buildGroupedPeriodReport(u, "agency", "거래처별통계", year, m)); + } + case "incentive" -> { + title += "인센티브계산"; + Map pref = loadJsonSetting(u.getGroupId(), "homes_preference", defaultPreference()); + BigDecimal rate = bd(pref.getOrDefault("incentiveRate", 5)); + int key = 1; + for (Map base : groupByLabel(contracts, c -> nullToEmpty(c.getEmployee()).isBlank() ? "미지정" : c.getEmployee())) { + BigDecimal settle = bd(base.get("settleAmount")); + Map row = new LinkedHashMap<>(base); + row.put("key", key++); + row.put("rate", rate); + row.put("incentive", settle.multiply(rate).divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP)); + rows.add(row); + } + } + default -> title += "통계"; + } + Map data = new LinkedHashMap<>(); + data.put("title", title); + data.put("year", year); + data.put("month", month); + data.put("rows", rows); + return ApiResponse.ok(data); + } + + // ---- helpers ---- + private Map buildDailyReport(JwtUser u, LocalDate date) { + List dayContracts = listAll(HomesContract.class, u).stream() + .filter(c -> date.equals(c.getContractDate())).toList(); + List internet = dayContracts.stream().filter(c -> "INTERNET".equals(c.getKind())).toList(); + List home = dayContracts.stream().filter(c -> "HOME".equals(c.getKind())).toList(); + List pendings = listAll(HomesPending.class, u).stream() + .filter(p -> date.equals(p.getPendingDate())).toList(); + List bookings = listAll(HomesBooking.class, u).stream() + .filter(b -> date.equals(b.getBookingDate())).toList(); + + Map summary = new LinkedHashMap<>(); + summary.put("internetCount", internet.size()); + summary.put("homeCount", home.size()); + summary.put("pendingCount", pendings.size()); + summary.put("bookingCount", bookings.size()); + + Map data = new LinkedHashMap<>(); + data.put("date", date.toString()); + data.put("title", date.getYear() + "년 " + date.getMonthValue() + "월 " + date.getDayOfMonth() + "일 일일리포트"); + data.put("summary", summary); + data.put("internet", internet.stream().map(this::dailyContractItem).toList()); + data.put("home", home.stream().map(this::dailyContractItem).toList()); + data.put("pendings", pendings.stream().map(p -> { + Map m = new LinkedHashMap<>(); + m.put("id", p.getId()); + m.put("status", normalizePendingStatus(p.getStatus())); + m.put("pendingType", p.getPendingType()); + m.put("contents", p.getContents()); + m.put("customerName", p.getCustomerName()); + m.put("phone", p.getPhone()); + m.put("employee", resolveReceiveEmployee(p)); + return m; + }).toList()); + data.put("bookings", bookings.stream().map(b -> { + Map m = new LinkedHashMap<>(); + m.put("id", b.getId()); + m.put("status", b.getStatus()); + m.put("customerName", b.getCustomerName()); + m.put("phone", b.getPhone()); + m.put("internetProducts", b.getInternetProducts()); + m.put("rentalProducts", b.getRentalProducts()); + m.put("products", b.getProducts()); + m.put("employee", b.getEmployee()); + return m; + }).toList()); + return data; + } + + private Map dailyContractItem(HomesContract c) { + Map m = new LinkedHashMap<>(); + m.put("id", c.getId()); + m.put("status", c.getStatus()); + m.put("customerName", c.getCustomerName()); + m.put("phone", c.getPhone()); + m.put("products", c.getProducts()); + m.put("employee", c.getEmployee()); + m.put("agency", c.getAgency()); + m.put("settleAmount", c.getSettleAmount()); + m.put("margin", c.getMargin()); + return m; + } + + private Map buildMonthlyReport(JwtUser u, int year) { + List yearContracts = listAll(HomesContract.class, u).stream() + .filter(c -> c.getContractDate() != null && c.getContractDate().getYear() == year) + .toList(); + Map> byMon = yearContracts.stream() + .collect(Collectors.groupingBy(c -> c.getContractDate().getMonthValue(), TreeMap::new, Collectors.toList())); + Map pref = loadJsonSetting(u.getGroupId(), "homes_preference", defaultPreference()); + BigDecimal rebateRate = bd(pref.getOrDefault("incentiveRate", 5)); + + List> rows = new ArrayList<>(); + for (int m = 1; m <= 12; m++) { + List list = byMon.getOrDefault(m, List.of()); + Map row = reportRow(m, m + "월", list); + BigDecimal discount = list.stream().map(this::contractDiscount).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal settle = bd(row.get("settleAmount")); + BigDecimal rebate = settle.multiply(rebateRate).divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP); + row.put("month", m); + row.put("discountSum", discount); + row.put("employeeRebate", rebate); + row.put("rebateRate", rebateRate); + rows.add(row); + } + + Map data = new LinkedHashMap<>(); + data.put("year", year); + data.put("title", year + "년 월간리포트"); + data.put("rows", rows); + return data; + } + + /** 인터넷/홈렌탈 일별·월별 통계 (철회 제외) */ + private Map buildKindPeriodReport(JwtUser u, String kind, String titleLabel, String mode, int year, int month) { + boolean daily = !"monthly".equalsIgnoreCase(mode); + List base = listAll(HomesContract.class, u).stream() + .filter(c -> kind.equals(c.getKind())) + .filter(c -> !"철회".equals(c.getStatus())) + .filter(c -> c.getContractDate() != null && c.getContractDate().getYear() == year) + .filter(c -> !daily || c.getContractDate().getMonthValue() == month) + .toList(); + + List> rows = new ArrayList<>(); + if (daily) { + YearMonth ym = YearMonth.of(year, month); + Map> byDay = base.stream() + .collect(Collectors.groupingBy(c -> c.getContractDate().getDayOfMonth(), TreeMap::new, Collectors.toList())); + for (int d = 1; d <= ym.lengthOfMonth(); d++) { + List list = byDay.getOrDefault(d, List.of()); + if (list.isEmpty()) continue; + Map row = reportRow(d, d + "일", list); + row.put("day", d); + rows.add(row); + } + } else { + Map> byMon = base.stream() + .collect(Collectors.groupingBy(c -> c.getContractDate().getMonthValue(), TreeMap::new, Collectors.toList())); + for (int m = 1; m <= 12; m++) { + List list = byMon.getOrDefault(m, List.of()); + if (list.isEmpty()) continue; + Map row = reportRow(m, m + "월", list); + row.put("month", m); + rows.add(row); + } + } + + Map totals = reportRow(0, "합계", base); + Map data = new LinkedHashMap<>(); + data.put("kind", kind); + data.put("mode", daily ? "daily" : "monthly"); + data.put("year", year); + data.put("month", daily ? month : null); + data.put("title", titleLabel); + data.put("rows", rows); + data.put("totals", totals); + data.put("excludeWithdrawn", true); + return data; + } + + /** 직원별/거래처별 월간 통계 (철회 제외) */ + private Map buildGroupedPeriodReport(JwtUser u, String groupBy, String titleLabel, int year, int month) { + List base = listAll(HomesContract.class, u).stream() + .filter(c -> !"철회".equals(c.getStatus())) + .filter(c -> c.getContractDate() != null + && c.getContractDate().getYear() == year + && c.getContractDate().getMonthValue() == month) + .toList(); + Function labelFn = "agency".equals(groupBy) + ? (c -> nullToEmpty(c.getAgency()).isBlank() ? "미지정" : c.getAgency()) + : (c -> nullToEmpty(c.getEmployee()).isBlank() ? "미지정" : c.getEmployee()); + List> rows = groupByLabel(base, labelFn); + Map totals = reportRow(0, "합계", base); + Map data = new LinkedHashMap<>(); + data.put("groupBy", groupBy); + data.put("year", year); + data.put("month", month); + data.put("title", titleLabel); + data.put("rows", rows); + data.put("totals", totals); + data.put("excludeWithdrawn", true); + return data; + } + + /** 할인합계: feeReduction 숫자값 또는 정책합-정산금액 */ + private BigDecimal contractDiscount(HomesContract c) { + String fr = c.getFeeReduction(); + if (fr != null && !fr.isBlank()) { + try { + String digits = fr.replaceAll("[^0-9.\\-]", ""); + if (!digits.isBlank()) return new BigDecimal(digits).abs(); + } catch (Exception ignored) {} + } + BigDecimal policy = bd(c.getPolicySum()); + BigDecimal settle = bd(c.getSettleAmount()); + BigDecimal diff = policy.subtract(settle); + return diff.compareTo(BigDecimal.ZERO) > 0 ? diff : BigDecimal.ZERO; + } + + private Map reportRow(int key, String label, List list) { + Map row = new LinkedHashMap<>(); + row.put("key", key); + row.put("label", label); + row.put("internetCount", list.stream().filter(c -> "INTERNET".equals(c.getKind())).count()); + row.put("homeCount", list.stream().filter(c -> "HOME".equals(c.getKind())).count()); + row.put("count", list.size()); + row.put("settleAmount", list.stream().map(c -> bd(c.getSettleAmount())).reduce(BigDecimal.ZERO, BigDecimal::add)); + row.put("margin", list.stream().map(c -> bd(c.getMargin())).reduce(BigDecimal.ZERO, BigDecimal::add)); + row.put("policySum", list.stream().map(c -> bd(c.getPolicySum())).reduce(BigDecimal.ZERO, BigDecimal::add)); + return row; + } + + private List> groupByLabel(List list, Function labelFn) { + Map> grouped = list.stream().collect(Collectors.groupingBy(labelFn, LinkedHashMap::new, Collectors.toList())); + List> rows = new ArrayList<>(); + int key = 1; + for (var e : grouped.entrySet()) rows.add(reportRow(key++, e.getKey(), e.getValue())); + return rows; + } + + private void writeContractLog(JwtUser u, HomesContract c, String verb, String status) { + String kindLabel = c != null && "HOME".equals(c.getKind()) ? "홈렌탈접수" : "인터넷접수"; + String name = c == null || c.getCustomerName() == null ? "" : c.getCustomerName(); + if ("상태".equals(verb)) { + writeLog(u, "CONTRACT", kindLabel + "-상태", + kindLabel + " [" + name + "] " + status + "(으)로 변경하였습니다."); + } else { + writeLog(u, "CONTRACT", kindLabel + "-" + verb, + kindLabel + " [" + name + "] " + verb + "하였습니다."); + } + } + + private void writeProductLog(JwtUser u, HomesProduct p, String verb) { + String kindLabel = productKindLabel(p); + writeLog(u, "PRODUCT", kindLabel + "-" + verb, kindLabel + " [" + p.getName() + "] " + verb + "하였습니다."); + } + + private static String productKindLabel(HomesProduct p) { + return p != null && "HOME".equals(p.getKind()) ? "홈렌탈상품" : "인터넷상품"; + } + + private void writeLog(JwtUser u, String domain, String action, String content) { + HomesLog log = new HomesLog(); + log.setGroupId(u.getGroupId()); + log.setDomain(domain); + log.setAction(action); + log.setContent(content); + String staff = resolveMemberName(u); + log.setStaff(staff == null || staff.isBlank() ? u.getUserid() : staff); + log.setProcessedAt(LocalDateTime.now()); + em.persist(log); + } + + private List listAll(Class type, JwtUser u) { + return em.createQuery("select e from " + type.getSimpleName() + " e where e.groupId=:g order by e.id desc", type) + .setParameter("g", u.getGroupId()).getResultList(); + } + + private long count(Class type, String g) { + return em.createQuery("select count(e) from " + type.getSimpleName() + " e where e.groupId=:g", Long.class) + .setParameter("g", g).getSingleResult(); + } + + private ApiResponse pageOk(List filtered, int page, int size, Function> map, Map counts) { + int fromIdx = Math.max(0, page) * Math.max(1, size); + List> list = filtered.stream().skip(fromIdx).limit(Math.min(Math.max(size, 1), 200)).map(map).toList(); + Map data = new LinkedHashMap<>(); + data.put("total", filtered.size()); + data.put("list", list); + if (counts != null) data.put("counts", counts); + return ApiResponse.ok(data); + } + + @SuppressWarnings("unchecked") + private Map loadJsonSetting(String groupId, String key, Map def) { + List rows = em.createQuery("select s from Setting s where s.groupId=:g and s.settingKey=:k", Setting.class) + .setParameter("g", groupId).setParameter("k", key).getResultList(); + if (rows.isEmpty() || rows.getFirst().getValue() == null || rows.getFirst().getValue().isBlank()) return def; + try { return mapper.readValue(rows.getFirst().getValue(), Map.class); } + catch (Exception e) { return def; } + } + + private void saveJsonSetting(String groupId, String key, Map value) { + List rows = em.createQuery("select s from Setting s where s.groupId=:g and s.settingKey=:k", Setting.class) + .setParameter("g", groupId).setParameter("k", key).getResultList(); + try { + String json = mapper.writeValueAsString(value); + if (rows.isEmpty()) { + Setting s = new Setting(); s.setGroupId(groupId); s.setSettingKey(key); s.setValue(json); em.persist(s); + } else { + rows.getFirst().setValue(json); + } + } catch (Exception e) { throw new IllegalArgumentException("설정 저장 실패"); } + } + + private Map loadHomesPreference(String groupId) { + Map result = defaultPreference(); + List rows = em.createQuery("select s from Setting s where s.groupId=:g and s.settingKey=:k", Setting.class) + .setParameter("g", groupId).setParameter("k", "homes_preference").getResultList(); + if (!rows.isEmpty() && rows.getFirst().getValue() != null && !rows.getFirst().getValue().isBlank()) { + try { + @SuppressWarnings("unchecked") + Map loaded = mapper.readValue(rows.getFirst().getValue(), Map.class); + mergePreference(result, loaded); + } catch (Exception ignored) { /* keep defaults */ } + } + // migrate legacy flat keys → manager + if (!(result.get("manager") instanceof Map)) { + Map manager = new LinkedHashMap<>(); + manager.put("name", "홍길순"); + manager.put("phone", "010-1234-5678"); + result.put("manager", manager); + } + @SuppressWarnings("unchecked") + Map manager = (Map) result.get("manager"); + if ((manager.get("name") == null || String.valueOf(manager.get("name")).isBlank()) && result.get("ceo") != null) { + manager.put("name", result.get("ceo")); + } + if ((manager.get("phone") == null || String.valueOf(manager.get("phone")).isBlank()) && result.get("phone") != null) { + manager.put("phone", result.get("phone")); + } + return result; + } + + @SuppressWarnings("unchecked") + private void mergePreference(Map target, Map patch) { + if (patch == null) return; + for (Map.Entry e : patch.entrySet()) { + Object val = e.getValue(); + if (val instanceof Map m && target.get(e.getKey()) instanceof Map) { + Map dest = new LinkedHashMap<>((Map) target.get(e.getKey())); + dest.putAll((Map) m); + target.put(e.getKey(), dest); + } else { + target.put(e.getKey(), val); + } + } + } + + private Map defaultPreference() { + Map manager = new LinkedHashMap<>(); + manager.put("name", "홍길순"); + manager.put("phone", "010-1234-5678"); + + Map loginBlock = new LinkedHashMap<>(); + loginBlock.put("mode", "none"); // none | block_staff + + Map accessIps = new LinkedHashMap<>(); + accessIps.put("enabled", false); + accessIps.put("ips", List.of()); + + Map contract = new LinkedHashMap<>(); + contract.put("autoRegisterCustomer", true); + contract.put("defaultMonthsInternet", 36); + contract.put("defaultMonthsHome", 60); + contract.put("requireAgency", false); + contract.put("autoCalcSettle", true); + contract.put("allowWithdraw", true); + + Map m = new LinkedHashMap<>(); + m.put("manager", manager); + m.put("loginBlock", loginBlock); + m.put("accessIps", accessIps); + m.put("contract", contract); + m.put("incentiveRate", 5); + m.put("autoInvoice", true); + return m; + } + + private static final Map MENU_TITLE_TO_ID; + static { + Map m = new LinkedHashMap<>(); + m.put("상품관리", "product"); + m.put("계약관리", "contract"); + m.put("접수관리", "reception"); + m.put("정산관리", "invoice"); + m.put("일정관리", "pending"); + m.put("예약관리", "booking"); + m.put("고객관리", "customer"); + m.put("장부관리", "abooks"); + m.put("통계관리", "report"); + m.put("환경설정", "setting"); + MENU_TITLE_TO_ID = Collections.unmodifiableMap(m); + } + + private static final List ALWAYS_MENU_IDS = List.of("home", "cs", "bbs", "sitemap"); + + @SuppressWarnings("unchecked") + private List resolveAllowedMenus(String groupId, String userid, String staffPermission, String menuFlagsRaw) { + LinkedHashSet allowed = new LinkedHashSet<>(); + boolean isOwner = "대표".equals(staffPermission) || "demo".equals(userid); + + Map exposure = parseMenuFlagsMap(menuFlagsRaw); + + if (isOwner) { + for (String id : List.of("home", "product", "contract", "reception", "invoice", "pending", "booking", + "customer", "abooks", "report", "setting", "cs", "bbs", "sitemap")) { + if (Boolean.FALSE.equals(exposure.get(id))) continue; + allowed.add(id); + } + return new ArrayList<>(allowed); + } + + // 권한설정(메뉴설정) 매트릭스 + String matrixRole = staffPermission; + if (!List.of("총괄", "팀장", "사원", "영업", "딜러", "개발자").contains(matrixRole)) { + matrixRole = "사원"; + } + Map levels = loadHomesLevels(groupId); + Object modesObj = levels.get("modes"); + Object modeRaw = modesObj instanceof Map modesMap ? modesMap.get("menu") : null; + String mode = String.valueOf(modeRaw == null ? "default" : modeRaw); + Map defaults = (Map) levels.get("defaults"); + Map custom = (Map) levels.get("custom"); + Map menuMatrixSrc = "custom".equals(mode) + ? (Map) custom.getOrDefault("menu", defaults.get("menu")) + : (Map) defaults.get("menu"); + Object roleFlagsObj = menuMatrixSrc == null ? null : menuMatrixSrc.get(matrixRole); + @SuppressWarnings("unchecked") + Map roleFlags = roleFlagsObj instanceof Map m + ? (Map) m + : Map.of(); + + for (Map.Entry e : MENU_TITLE_TO_ID.entrySet()) { + String title = e.getKey(); + String id = e.getValue(); + Object flag = roleFlags.get(title); + if (flag == null && "일정관리".equals(title)) { + flag = roleFlags.containsKey("약속관리") ? roleFlags.get("약속관리") : roleFlags.get("약속업무"); + } + if (flag == null && title.endsWith("관리")) { + String legacyTitle = title.substring(0, title.length() - 2) + "업무"; + flag = roleFlags.get(legacyTitle); + } + boolean levelOk = Boolean.TRUE.equals(flag) + || (flag != null && "true".equalsIgnoreCase(String.valueOf(flag))); + if (!levelOk) continue; + if (Boolean.FALSE.equals(exposure.get(id))) continue; + allowed.add(id); + } + + // 대시보드/고객센터/게시판/전체보기 — 권한매트릭스 외, 직원 노출설정만 적용 + for (String id : ALWAYS_MENU_IDS) { + if (Boolean.FALSE.equals(exposure.get(id))) continue; + allowed.add(id); + } + + if (allowed.isEmpty()) allowed.add("home"); + return new ArrayList<>(allowed); + } + + private Map parseMenuFlagsMap(String raw) { + Map flags = new LinkedHashMap<>(); + for (String id : List.of("home", "product", "contract", "reception", "invoice", "pending", "booking", + "customer", "abooks", "report", "cs", "bbs", "setting", "sitemap")) { + flags.put(id, true); + } + if (raw == null || raw.isBlank()) return flags; + try { + @SuppressWarnings("unchecked") + Map parsed = mapper.readValue(raw, Map.class); + for (Map.Entry e : parsed.entrySet()) { + Object v = e.getValue(); + boolean on = v instanceof Boolean b ? b : !"false".equalsIgnoreCase(String.valueOf(v)); + flags.put(e.getKey(), on); + } + } catch (Exception ignored) { /* keep defaults */ } + return flags; + } + + private Map loadHomesLevels(String groupId) { + Map defaults = defaultLevelMatrices(); + Map modes = new LinkedHashMap<>(); + Map custom = new LinkedHashMap<>(); + for (String tab : LEVEL_TABS) { + modes.put(tab, "default"); + custom.put(tab, deepCopyMatrix(defaults.get(tab))); + } + List rows = em.createQuery("select s from Setting s where s.groupId=:g and s.settingKey=:k", Setting.class) + .setParameter("g", groupId).setParameter("k", "homes_levels").getResultList(); + if (!rows.isEmpty() && rows.getFirst().getValue() != null && !rows.getFirst().getValue().isBlank()) { + try { + @SuppressWarnings("unchecked") + Map loaded = mapper.readValue(rows.getFirst().getValue(), Map.class); + // legacy flat role→menu map + if (loaded.containsKey("총괄") && !loaded.containsKey("modes") && !loaded.containsKey("custom")) { + custom.put("menu", loaded); + modes.put("menu", "custom"); + } else { + if (loaded.get("modes") instanceof Map m) { + for (Map.Entry e : m.entrySet()) modes.put(String.valueOf(e.getKey()), e.getValue()); + } + if (loaded.get("custom") instanceof Map c) { + for (Map.Entry e : c.entrySet()) custom.put(String.valueOf(e.getKey()), e.getValue()); + } + } + } catch (Exception ignored) { /* keep defaults */ } + } + // 메뉴 매트릭스 구키(ㅇㅇ업무) → ㅇㅇ관리 호환 + 신규 메뉴(접수관리) 보강 + custom.put("menu", migrateMenuMatrixKeys(custom.get("menu"))); + defaults.put("menu", migrateMenuMatrixKeys(defaults.get("menu"))); + for (String tab : LEVEL_TABS) { + Object defMat = defaults.get(tab); + custom.put(tab, syncMatrixRows(custom.get(tab), defMat, LEVEL_ROWS.get(tab))); + defaults.put(tab, syncMatrixRows(defMat, defMat, LEVEL_ROWS.get(tab))); + if (!modes.containsKey(tab) || modes.get(tab) == null) modes.put(tab, "default"); + } + + Map result = new LinkedHashMap<>(); + result.put("roles", List.of("총괄", "팀장", "사원", "영업", "딜러", "개발자")); + result.put("tabs", LEVEL_TAB_META); + result.put("rows", copyLevelRows()); + result.put("modes", modes); + result.put("custom", custom); + result.put("defaults", defaults); + return result; + } + + /** 저장된 매트릭스에 누락된 행(신규 메뉴/액션)을 기본값으로 채운다. */ + @SuppressWarnings("unchecked") + private Object syncMatrixRows(Object src, Object fallback, List rowLabels) { + Map base = deepCopyMatrix(src != null ? src : fallback); + if (rowLabels == null || rowLabels.isEmpty()) return base; + Map fb = deepCopyMatrix(fallback); + for (String role : List.of("총괄", "팀장", "사원", "영업", "딜러", "개발자")) { + Map flags = base.get(role) instanceof Map m + ? new LinkedHashMap<>((Map) m) + : new LinkedHashMap<>(); + Map fbFlags = fb.get(role) instanceof Map m + ? (Map) m + : Map.of(); + for (String label : rowLabels) { + if (!flags.containsKey(label)) { + Object v = fbFlags.get(label); + flags.put(label, v instanceof Boolean ? v : Boolean.FALSE); + } + } + base.put(role, flags); + } + return base; + } + + private static Map> copyLevelRows() { + Map> out = new LinkedHashMap<>(); + for (Map.Entry> e : LEVEL_ROWS.entrySet()) { + out.put(e.getKey(), List.copyOf(e.getValue())); + } + return out; + } + + /** 권한설정 메뉴행: ㅇㅇ업무 → ㅇㅇ관리 */ + @SuppressWarnings("unchecked") + private Object migrateMenuMatrixKeys(Object src) { + if (!(src instanceof Map roles)) return src; + Map out = new LinkedHashMap<>(); + for (Map.Entry roleEntry : roles.entrySet()) { + Object flagsObj = roleEntry.getValue(); + if (!(flagsObj instanceof Map flags)) { + out.put(String.valueOf(roleEntry.getKey()), flagsObj); + continue; + } + Map migrated = new LinkedHashMap<>(); + for (Map.Entry flag : flags.entrySet()) { + String key = String.valueOf(flag.getKey()); + if ("약속업무".equals(key) || "약속관리".equals(key)) { + key = "일정관리"; + } else if (key.endsWith("업무") && key.length() > 2) { + key = key.substring(0, key.length() - 2) + "관리"; + } + // 동일 키 중복 시 true 우선 + Object prev = migrated.get(key); + Object next = flag.getValue(); + if (prev == null) migrated.put(key, next); + else if (Boolean.TRUE.equals(next) || "true".equalsIgnoreCase(String.valueOf(next))) migrated.put(key, true); + } + out.put(String.valueOf(roleEntry.getKey()), migrated); + } + return out; + } + + private static final List LEVEL_TABS = List.of( + "menu", "product", "contract", "reception", "invoice", "pending", "booking", "customer", "abooks", "report"); + + private static final List> LEVEL_TAB_META = List.of( + Map.of("key", "menu", "label", "메뉴설정"), + Map.of("key", "product", "label", "상품설정"), + Map.of("key", "contract", "label", "계약설정"), + Map.of("key", "reception", "label", "접수설정"), + Map.of("key", "invoice", "label", "정산설정"), + Map.of("key", "pending", "label", "일정설정"), + Map.of("key", "booking", "label", "예약설정"), + Map.of("key", "customer", "label", "고객설정"), + Map.of("key", "abooks", "label", "장부설정"), + Map.of("key", "report", "label", "통계설정") + ); + + private static final Map> LEVEL_ROWS; + static { + Map> rows = new LinkedHashMap<>(); + rows.put("menu", List.of("상품관리", "계약관리", "접수관리", "정산관리", "일정관리", "예약관리", "고객관리", "장부관리", "통계관리", "환경설정")); + rows.put("product", List.of("목록조회", "상품등록", "상품수정", "상품삭제", "정책관리", "엑셀다운로드")); + rows.put("contract", List.of("목록조회", "계약등록", "계약수정", "상태변경", "엑셀다운로드")); + rows.put("reception", List.of("목록조회", "접수등록", "접수수정", "진행변경", "엑셀다운로드")); + rows.put("invoice", List.of("목록조회", "정산서발행", "정산서수정", "엑셀다운로드")); + rows.put("pending", List.of("목록조회", "일정등록", "약속수정", "약속삭제", "캘린더조회")); + rows.put("booking", List.of("목록조회", "예약등록", "예약수정", "예약삭제")); + rows.put("customer", List.of("목록조회", "고객등록", "고객수정", "고객삭제", "분류설정")); + rows.put("abooks", List.of("목록조회", "장부등록", "장부수정", "장부삭제", "엑셀다운로드")); + rows.put("report", List.of("일일리포트", "월간리포트", "상품통계", "직원통계", "거래처통계", "인센티브")); + LEVEL_ROWS = Collections.unmodifiableMap(rows); + } + + @SuppressWarnings("unchecked") + private Map deepCopyMatrix(Object src) { + if (!(src instanceof Map m)) return new LinkedHashMap<>(); + Map out = new LinkedHashMap<>(); + for (Map.Entry e : m.entrySet()) { + if (e.getValue() instanceof Map inner) { + out.put(String.valueOf(e.getKey()), new LinkedHashMap<>((Map) inner)); + } else { + out.put(String.valueOf(e.getKey()), e.getValue()); + } + } + return out; + } + + private Map defaultLevelMatrices() { + Map all = new LinkedHashMap<>(); + all.put("menu", defaultMenuMatrix()); + all.put("product", defaultActionMatrix(LEVEL_ROWS.get("product"), true, true, true, false, false)); + all.put("contract", defaultActionMatrix(LEVEL_ROWS.get("contract"), true, true, true, true, true)); + all.put("reception", defaultActionMatrix(LEVEL_ROWS.get("reception"), true, true, true, true, true)); + all.put("invoice", defaultActionMatrix(LEVEL_ROWS.get("invoice"), true, true, true, true, true)); + all.put("pending", defaultActionMatrix(LEVEL_ROWS.get("pending"), true, true, true, true, true)); + all.put("booking", defaultActionMatrix(LEVEL_ROWS.get("booking"), true, true, true, true, true)); + all.put("customer", defaultActionMatrix(LEVEL_ROWS.get("customer"), true, true, true, false, false)); + all.put("abooks", defaultActionMatrix(LEVEL_ROWS.get("abooks"), true, true, true, false, false)); + all.put("report", defaultActionMatrix(LEVEL_ROWS.get("report"), true, true, false, false, false)); + return all; + } + + /** Screenshot menu defaults */ + private Map defaultMenuMatrix() { + List menus = LEVEL_ROWS.get("menu"); + Map m = new LinkedHashMap<>(); + for (String role : List.of("총괄", "팀장", "사원", "영업", "딜러", "개발자")) { + Map flags = new LinkedHashMap<>(); + for (String menu : menus) { + boolean allow = switch (role) { + case "총괄", "팀장", "개발자" -> true; + case "사원" -> List.of("계약관리", "접수관리", "정산관리", "일정관리", "예약관리", "고객관리", "장부관리").contains(menu); + case "영업", "딜러" -> List.of("계약관리", "접수관리", "정산관리", "일정관리", "예약관리").contains(menu); + default -> false; + }; + flags.put(menu, allow); + } + m.put(role, flags); + } + return m; + } + + private Map defaultActionMatrix(List actions, + boolean 총괄, boolean 팀장, boolean 사원, boolean 영업, boolean 딜러) { + Map m = new LinkedHashMap<>(); + Map roleAllow = new LinkedHashMap<>(); + roleAllow.put("총괄", 총괄); + roleAllow.put("팀장", 팀장); + roleAllow.put("사원", 사원); + roleAllow.put("영업", 영업); + roleAllow.put("딜러", 딜러); + roleAllow.put("개발자", 총괄); // 개발자 = 총괄과 동일한 액션 권한 + for (String role : List.of("총괄", "팀장", "사원", "영업", "딜러", "개발자")) { + Map flags = new LinkedHashMap<>(); + boolean base = Boolean.TRUE.equals(roleAllow.get(role)); + for (String action : actions) { + boolean allow = base; + if (base && "사원".equals(role) && action.contains("삭제")) allow = false; + if (base && ("영업".equals(role) || "딜러".equals(role)) + && (action.contains("삭제") || action.contains("발행") || action.contains("정책"))) allow = false; + flags.put(action, allow); + } + m.put(role, flags); + } + return m; + } + + /** @deprecated kept for any leftover callers; use defaultLevelMatrices */ + private Map defaultLevels() { + return defaultMenuMatrix(); + } + + private Map productMap(HomesProduct p) { + Map m = new LinkedHashMap<>(); + m.put("id", p.getId()); m.put("kind", p.getKind()); m.put("category", p.getCategory()); + m.put("company", p.getCompany()); m.put("name", p.getName()); m.put("model", p.getModel()); + m.put("months", p.getMonths()); m.put("policyAmount", p.getPolicyAmount()); + m.put("policyCount", p.getPolicyCount() == null ? 0 : p.getPolicyCount()); + m.put("memo", p.getMemo()); + return m; + } + + private Map productPolicyMap(HomesProductPolicy p) { + Map m = new LinkedHashMap<>(); + m.put("id", p.getId()); m.put("productId", p.getProductId()); + m.put("name", p.getName()); m.put("amount", p.getAmount()); + return m; + } + private Map contractMap(HomesContract c) { + Map m = new LinkedHashMap<>(); + m.put("id", c.getId()); m.put("kind", c.getKind()); m.put("status", c.getStatus()); + m.put("customerType", c.getCustomerType()); m.put("customerName", c.getCustomerName()); + m.put("phone", c.getPhone()); m.put("address", c.getAddress()); m.put("products", c.getProducts()); + m.put("agency", c.getAgency()); m.put("employee", c.getEmployee()); m.put("payMethod", c.getPayMethod()); + m.put("contractDate", c.getContractDate()); m.put("installDate", c.getInstallDate()); + m.put("months", c.getMonths()); m.put("policySum", c.getPolicySum()); + m.put("settleAmount", c.getSettleAmount()); m.put("margin", c.getMargin()); m.put("memo", c.getMemo()); + m.put("email", c.getEmail()); m.put("gender", c.getGender()); m.put("nationality", c.getNationality()); + m.put("customerGrade", c.getCustomerGrade()); m.put("birthDate", c.getBirthDate()); + m.put("zipcode", c.getZipcode()); m.put("addressDetail", c.getAddressDetail()); + m.put("installPhone", c.getInstallPhone()); m.put("installZip", c.getInstallZip()); + m.put("installAddress", c.getInstallAddress()); m.put("installAddressDetail", c.getInstallAddressDetail()); + m.put("feeReduction", c.getFeeReduction()); m.put("invoiceType", c.getInvoiceType()); + m.put("sameContact", c.getSameContact()); + return m; + } + private Map balanceMap(HomesBalance b) { + Map m = new LinkedHashMap<>(); + String balanceType = normalizeBalanceType(b.getBalanceType()); + String targetType = resolveTargetType(b); + String targetName = resolveTargetName(b); + BigDecimal amount = b.getAmount() == null ? BigDecimal.ZERO : b.getAmount(); + if ("정산차감".equals(balanceType) && amount.signum() > 0) amount = amount.negate(); + m.put("id", b.getId()); + m.put("balanceType", balanceType); + m.put("targetType", targetType); + m.put("targetName", targetName); + m.put("baseDate", b.getBaseDate()); + m.put("customerName", b.getCustomerName()); + m.put("phone", b.getPhone()); + m.put("products", b.getProducts()); + m.put("amount", amount); + m.put("reason", b.getReason()); + m.put("agency", b.getAgency()); + m.put("memo", b.getMemo()); + m.put("contractId", b.getContractId()); + return m; + } + private Map invoiceMap(HomesInvoice i) { + Map m = new LinkedHashMap<>(); + m.put("id", i.getId()); m.put("scope", i.getScope()); m.put("yearMonth", i.getYearMonth()); + m.put("employee", i.getEmployee()); m.put("agency", i.getAgency()); m.put("contractCount", i.getContractCount()); + m.put("amount", i.getAmount()); m.put("status", i.getStatus()); m.put("memo", i.getMemo()); + m.put("internetCount", i.getInternetCount()); m.put("internetAmount", i.getInternetAmount()); + m.put("homeCount", i.getHomeCount()); m.put("homeAmount", i.getHomeAmount()); + m.put("balanceCount", i.getBalanceCount()); m.put("balanceAmount", i.getBalanceAmount()); + m.put("settleAmount", i.getSettleAmount() != null ? i.getSettleAmount() : i.getAmount()); + m.put("issued", Boolean.TRUE.equals(i.getIssued()) || "발행".equals(i.getStatus()) || "확정".equals(i.getStatus())); + m.put("consent", i.getConsent()); m.put("taxInvoice", i.getTaxInvoice()); m.put("withdrawStatus", i.getWithdrawStatus()); + return m; + } + private Map pendingMap(HomesPending p) { + Map m = new LinkedHashMap<>(); + m.put("id", p.getId()); + m.put("pendingType", p.getPendingType()); + m.put("pendingDate", p.getPendingDate()); + m.put("pendingTime", p.getPendingTime()); + m.put("customerName", p.getCustomerName()); + m.put("phone", p.getPhone()); + m.put("contents", p.getContents()); + m.put("employee", p.getEmployee()); + m.put("receiveEmployee", resolveReceiveEmployee(p)); + m.put("processEmployee", p.getProcessEmployee()); + m.put("processDate", p.getProcessDate()); + m.put("status", normalizePendingStatus(p.getStatus())); + m.put("memo", p.getMemo()); + return m; + } + private Map bookingMap(HomesBooking b) { + String internet = resolveInternetProducts(b); + String rental = resolveRentalProducts(b); + Map m = new LinkedHashMap<>(); + m.put("id", b.getId()); + m.put("status", b.getStatus()); + m.put("bookingDate", b.getBookingDate()); + m.put("customerName", b.getCustomerName()); + m.put("phone", b.getPhone()); + m.put("products", b.getProducts()); + m.put("internetProducts", internet); + m.put("rentalProducts", rental); + m.put("internetNote", b.getInternetNote()); + m.put("rentalNote", b.getRentalNote()); + m.put("zipcode", b.getZipcode()); + m.put("address", b.getAddress()); + m.put("addressDetail", b.getAddressDetail()); + m.put("employee", b.getEmployee()); + m.put("receiveEmployee", b.getEmployee()); + m.put("memo", b.getMemo()); + m.put("fileName", b.getFileName()); + return m; + } + + private void normalizeBookingPayload(Map d, JwtUser u) { + if (d.get("receiveEmployee") != null) d.put("employee", d.get("receiveEmployee")); + else if (d.get("employee") == null || String.valueOf(d.get("employee")).isBlank()) { + String name = resolveMemberName(u); + if (name != null) d.put("employee", name); + } + if (d.get("status") == null || String.valueOf(d.get("status")).isBlank()) d.put("status", "접수"); + if (d.containsKey("phone1") || d.containsKey("phone2") || d.containsKey("phone3")) { + String p1 = d.get("phone1") == null ? "" : String.valueOf(d.get("phone1")).trim(); + String p2 = d.get("phone2") == null ? "" : String.valueOf(d.get("phone2")).trim(); + String p3 = d.get("phone3") == null ? "" : String.valueOf(d.get("phone3")).trim(); + List parts = new ArrayList<>(); + if (!p1.isBlank()) parts.add(p1); + if (!p2.isBlank()) parts.add(p2); + if (!p3.isBlank()) parts.add(p3); + if (!parts.isEmpty()) d.put("phone", String.join("-", parts)); + } + String internet = joinProductTokens(d.get("internetProducts")); + String rental = joinProductTokens(d.get("rentalProducts")); + if (internet != null) d.put("internetProducts", internet); + if (rental != null) d.put("rentalProducts", rental); + List all = new ArrayList<>(); + if (internet != null && !internet.isBlank()) all.add(internet); + if (rental != null && !rental.isBlank()) all.add(rental); + if (!all.isEmpty()) d.put("products", String.join(", ", all)); + d.remove("phone1"); d.remove("phone2"); d.remove("phone3"); d.remove("receiveEmployee"); + } + + private void ensureBookingCustomer(HomesBooking b, JwtUser u) { + String name = nullToEmpty(b.getCustomerName()).trim(); + String phone = nullToEmpty(b.getPhone()).trim(); + if (name.isBlank() || phone.isBlank()) return; + String digits = phone.replaceAll("\\D", ""); + List existing = em.createQuery( + "select c from HomesCustomer c where c.groupId=:g", HomesCustomer.class) + .setParameter("g", u.getGroupId()).getResultList().stream() + .filter(c -> digits.equals(nullToEmpty(c.getPhone()).replaceAll("\\D", ""))) + .toList(); + if (!existing.isEmpty()) return; + HomesCustomer c = new HomesCustomer(); + c.setGroupId(u.getGroupId()); + c.setName(name); + c.setPhone(phone); + c.setAddress(b.getAddress()); + c.setZipcode(b.getZipcode()); + c.setAddressDetail(b.getAddressDetail()); + c.setEmployee(b.getEmployee()); + c.setGrade("분류없음"); + c.setCustomerType("개인"); + c.setRegisteredDate(LocalDate.now()); + em.persist(c); + writeLog(u, "CUSTOMER", "고객-등록", "고객 [" + name + "] 예약과 함께 등록하였습니다."); + } + + private static String joinProductTokens(Object raw) { + if (raw == null) return null; + if (raw instanceof List list) { + List parts = new ArrayList<>(); + for (Object o : list) { + if (o == null) continue; + String s = String.valueOf(o).trim(); + if (!s.isBlank() && !parts.contains(s)) parts.add(s); + } + return String.join(", ", parts); + } + String s = String.valueOf(raw).trim(); + return s.isBlank() ? null : s; + } + + private static boolean containsProductToken(String haystack, String needle) { + if (haystack == null || needle == null) return false; + for (String part : haystack.split("[,,/|]")) { + if (needle.equals(part.trim())) return true; + } + return haystack.contains(needle); + } + + private static String resolveInternetProducts(HomesBooking b) { + if (b.getInternetProducts() != null && !b.getInternetProducts().isBlank()) return b.getInternetProducts(); + return extractKnownProducts(b.getProducts(), BOOKING_INTERNET_OPTIONS); + } + + private static String resolveRentalProducts(HomesBooking b) { + if (b.getRentalProducts() != null && !b.getRentalProducts().isBlank()) return b.getRentalProducts(); + return extractKnownProducts(b.getProducts(), BOOKING_RENTAL_OPTIONS); + } + + private static String extractKnownProducts(String products, List options) { + if (products == null || products.isBlank()) return ""; + List found = new ArrayList<>(); + for (String opt : options) { + if (products.contains(opt) && !found.contains(opt)) found.add(opt); + } + return String.join(", ", found); + } + private Map customerMap(HomesCustomer c) { + return customerMap(c, Map.of(), Map.of()); + } + + private Map customerMap(HomesCustomer c, Map contractCounts, Map bookingCounts) { + String digits = nullToEmpty(c.getPhone()).replaceAll("\\D", ""); + Map m = new LinkedHashMap<>(); + m.put("id", c.getId()); + m.put("grade", normalizeCustomerGrade(c.getGrade())); + m.put("customerType", nullToEmpty(c.getCustomerType()).isBlank() ? "개인" : c.getCustomerType()); + m.put("name", c.getName()); + m.put("phone", c.getPhone()); + m.put("phoneAlt", c.getPhoneAlt()); + m.put("zipcode", c.getZipcode()); + m.put("address", c.getAddress()); + m.put("addressDetail", c.getAddressDetail()); + m.put("agency", c.getAgency()); + m.put("employee", c.getEmployee()); + m.put("memo", c.getMemo()); + m.put("email", c.getEmail()); + m.put("gender", c.getGender()); + m.put("nationality", c.getNationality()); + m.put("birthYear", c.getBirthYear()); + m.put("birthMonth", c.getBirthMonth()); + m.put("birthDay", c.getBirthDay()); + m.put("registeredDate", resolveCustomerRegisteredDate(c)); + m.put("contractCount", contractCounts.getOrDefault(digits, 0L)); + m.put("bookingCount", bookingCounts.getOrDefault(digits, 0L)); + return m; + } + + private List customerGradeList(JwtUser u) { + Map saved = loadJsonSetting(u.getGroupId(), "homes.customerGrades", Map.of()); + Object raw = saved.get("grades"); + List grades = new ArrayList<>(); + if (raw instanceof List list) { + for (Object o : list) { + if (o == null) continue; + String s = String.valueOf(o).trim(); + if (!s.isBlank() && !grades.contains(s)) grades.add(s); + } + } + if (grades.isEmpty()) grades.addAll(DEFAULT_CUSTOMER_GRADES); + return grades; + } + + private void normalizeCustomerPayload(Map d, JwtUser u) { + if (d.get("employee") == null || String.valueOf(d.get("employee")).isBlank()) { + String name = resolveMemberName(u); + if (name != null) d.put("employee", name); + } + if (d.get("customerType") == null || String.valueOf(d.get("customerType")).isBlank()) d.put("customerType", "개인"); + if (d.get("grade") == null || String.valueOf(d.get("grade")).isBlank()) d.put("grade", "분류없음"); + if (d.containsKey("phone1") || d.containsKey("phone2") || d.containsKey("phone3")) { + d.put("phone", joinPhoneParts(d.get("phone1"), d.get("phone2"), d.get("phone3"))); + } + if (d.containsKey("altPhone1") || d.containsKey("altPhone2") || d.containsKey("altPhone3")) { + String alt = joinPhoneParts(d.get("altPhone1"), d.get("altPhone2"), d.get("altPhone3")); + if (alt != null) d.put("phoneAlt", alt); + } + if (d.get("registeredDate") == null || String.valueOf(d.get("registeredDate")).isBlank()) { + d.put("registeredDate", LocalDate.now().toString()); + } + d.remove("phone1"); d.remove("phone2"); d.remove("phone3"); + d.remove("altPhone1"); d.remove("altPhone2"); d.remove("altPhone3"); + } + + private static String joinPhoneParts(Object p1, Object p2, Object p3) { + String a = p1 == null ? "" : String.valueOf(p1).trim(); + String b = p2 == null ? "" : String.valueOf(p2).trim(); + String c = p3 == null ? "" : String.valueOf(p3).trim(); + List parts = new ArrayList<>(); + if (!a.isBlank()) parts.add(a); + if (!b.isBlank()) parts.add(b); + if (!c.isBlank()) parts.add(c); + return parts.isEmpty() ? null : String.join("-", parts); + } + + private static String normalizeCustomerGrade(String grade) { + if (grade == null || grade.isBlank() || "일반".equals(grade)) return "분류없음"; + return grade; + } + + private LocalDate resolveCustomerRegisteredDate(HomesCustomer c) { + if (c.getRegisteredDate() != null) return c.getRegisteredDate(); + if (c.getCreatedAt() != null) return c.getCreatedAt().atZone(ZoneId.systemDefault()).toLocalDate(); + return null; + } + + private Map countByCustomerPhone(JwtUser u, Class type) { + Map map = new HashMap<>(); + if (type == HomesContract.class) { + for (HomesContract c : listAll(HomesContract.class, u)) { + String digits = nullToEmpty(c.getPhone()).replaceAll("\\D", ""); + if (digits.isBlank()) continue; + map.merge(digits, 1L, Long::sum); + } + } else if (type == HomesBooking.class) { + for (HomesBooking b : listAll(HomesBooking.class, u)) { + String digits = nullToEmpty(b.getPhone()).replaceAll("\\D", ""); + if (digits.isBlank()) continue; + map.merge(digits, 1L, Long::sum); + } + } + return map; + } + private Map abookMap(HomesAbook a) { + String type = normalizeAbookEntryType(a.getEntryType()); + String item = a.getItem(); + if (item == null || item.isBlank()) item = a.getAgency(); + String category = a.getCategory(); + if (category == null || category.isBlank()) category = "일반"; + Map m = new LinkedHashMap<>(); + m.put("id", a.getId()); + m.put("entryDate", a.getEntryDate()); + m.put("entryType", type); + m.put("category", category); + m.put("item", item); + m.put("amount", a.getAmount()); + m.put("deposit", "입금".equals(type) ? a.getAmount() : null); + m.put("withdraw", "출금".equals(type) ? a.getAmount() : null); + m.put("cardDeposit", "카드입금".equals(type) ? a.getAmount() : null); + m.put("cardWithdraw", "카드출금".equals(type) ? a.getAmount() : null); + m.put("agency", a.getAgency()); + m.put("employee", a.getEmployee()); + m.put("memo", a.getMemo()); + return m; + } + + private List abookCategoryList(JwtUser u) { + Map saved = loadJsonSetting(u.getGroupId(), "homes.abookCategories", Map.of()); + Object raw = saved.get("categories"); + List categories = new ArrayList<>(); + if (raw instanceof List list) { + for (Object o : list) { + if (o == null) continue; + String s = String.valueOf(o).trim(); + if (!s.isBlank() && !categories.contains(s)) categories.add(s); + } + } + if (categories.isEmpty()) categories.addAll(DEFAULT_ABOOK_CATEGORIES); + return categories; + } + + private void normalizeAbookPayload(Map d, JwtUser u) { + if (d.get("entryType") != null) d.put("entryType", normalizeAbookEntryType(String.valueOf(d.get("entryType")))); + if (d.get("employee") == null || String.valueOf(d.get("employee")).isBlank()) { + String name = resolveMemberName(u); + if (name != null) d.put("employee", name); + } + if (d.get("category") == null || String.valueOf(d.get("category")).isBlank()) d.put("category", "일반"); + if ((d.get("item") == null || String.valueOf(d.get("item")).isBlank()) && d.get("agency") != null) { + d.put("item", d.get("agency")); + } + } + + private static String normalizeAbookEntryType(String type) { + if (type == null || type.isBlank()) return "입금"; + return switch (type) { + case "수입" -> "입금"; + case "지출" -> "출금"; + default -> type; + }; + } + + private static String abookLogContent(HomesAbook a, String verb) { + String type = normalizeAbookEntryType(a.getEntryType()); + String category = nullToEmpty(a.getCategory()).isBlank() ? "일반" : a.getCategory(); + return "간편장부 [" + type + "/" + category + "] " + verb; + } + private Map agencyMap(HomesAgency a) { + Map m = new LinkedHashMap<>(); + m.put("id", a.getId()); + m.put("name", a.getName()); + m.put("managerName", a.getManagerName()); + m.put("phone", a.getPhone()); + m.put("fax", a.getFax()); + m.put("mobile", a.getMobile()); + m.put("agencyType", a.getAgencyType()); + m.put("address", a.getAddress()); + m.put("memo", a.getMemo()); + m.put("active", a.isActive()); + m.put("status", a.isActive() ? "사용" : "정지"); + LocalDate reg = a.getRegisteredDate(); + if (reg == null && a.getCreatedAt() != null) { + reg = a.getCreatedAt().atZone(ZoneId.of("Asia/Seoul")).toLocalDate(); + } + m.put("registeredDate", reg); + return m; + } + + private void normalizeAgencyPayload(Map d) { + if (d.containsKey("status")) { + String st = String.valueOf(d.get("status")); + d.put("active", !"정지".equals(st) && !"false".equalsIgnoreCase(st) && !"N".equalsIgnoreCase(st)); + d.remove("status"); + } + if (d.get("registeredDate") == null || String.valueOf(d.get("registeredDate")).isBlank()) { + d.put("registeredDate", LocalDate.now().toString()); + } + if (d.get("active") == null) d.put("active", true); + } + private Map partnerMap(HomesPartner p) { + Map m = new LinkedHashMap<>(); + m.put("id", p.getId()); + m.put("partnerLevel", p.getPartnerLevel()); + m.put("name", p.getName()); + m.put("userid", p.getUserid()); + m.put("parentName", p.getParentName()); + m.put("managerName", p.getManagerName()); + m.put("phone", p.getPhone()); + m.put("address", p.getAddress()); + m.put("memo", p.getMemo()); + String status = p.getStatus(); + if (status == null || status.isBlank()) status = "대기"; + m.put("status", status); + LocalDate reg = p.getRegisteredDate(); + if (reg == null && p.getCreatedAt() != null) { + reg = LocalDate.ofInstant(p.getCreatedAt(), ZoneId.of("Asia/Seoul")); + } + m.put("registeredDate", reg); + return m; + } + private Map memberMap(Member m) { + Map map = new LinkedHashMap<>(); + map.put("id", m.getId()); + map.put("userid", m.getUserid()); + map.put("name", m.getName()); + map.put("phone", m.getPhone()); + map.put("address", m.getAddress()); + map.put("accessLimit", m.getAccessLimit()); + map.put("staffPermission", m.getStaffPermission()); + map.put("active", m.isActive()); + map.put("status", m.isActive() ? "사용" : "중지"); + map.put("role", m.getRole()); + map.put("loginCount", m.getLoginCount() == null ? 0 : m.getLoginCount()); + map.put("lastLoginAt", m.getLastLoginAt() == null + ? "" + : m.getLastLoginAt().format(java.time.format.DateTimeFormatter.ofPattern("MM-dd HH:mm"))); + map.put("lastLoginAtFull", m.getLastLoginAt() == null + ? null + : m.getLastLoginAt().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))); + LocalDate reg = m.getCreatedAt() == null ? null : LocalDate.ofInstant(m.getCreatedAt(), ZoneId.of("Asia/Seoul")); + map.put("registeredDate", reg == null ? null : reg.toString()); + map.put("memo", m.getMemo()); + map.put("menuFlags", m.getMenuFlags()); + map.put("isOwner", "대표".equals(m.getStaffPermission()) || "demo".equals(m.getUserid())); + return map; + } + + private Comparator memberComparator(String sort) { + String field = sort == null || sort.isBlank() ? "id,asc" : sort; + boolean asc = !field.endsWith(",desc"); + String key = field.split(",")[0]; + Comparator c = switch (key) { + case "staffPermission", "권한" -> Comparator.comparing(Member::getStaffPermission, Comparator.nullsLast(String::compareTo)); + case "userid", "아이디" -> Comparator.comparing(Member::getUserid, Comparator.nullsLast(String::compareTo)); + case "name", "이름" -> Comparator.comparing(Member::getName, Comparator.nullsLast(String::compareTo)); + case "loginCount", "접속수" -> Comparator.comparing(m -> m.getLoginCount() == null ? 0 : m.getLoginCount()); + case "lastLoginAt", "최종접속" -> Comparator.comparing(Member::getLastLoginAt, Comparator.nullsLast(LocalDateTime::compareTo)); + case "registeredDate", "createdAt", "등록일" -> Comparator.comparing(Member::getCreatedAt, Comparator.nullsLast(Instant::compareTo)); + default -> Comparator.comparing(Member::getId, Comparator.nullsLast(Long::compareTo)); + }; + return (asc ? c : c.reversed()).thenComparing(Member::getId); + } + private Map noticeMap(HomesNotice n) { + Map m = new LinkedHashMap<>(); + m.put("id", n.getId()); m.put("title", n.getTitle()); m.put("authorName", n.getAuthorName()); + m.put("contents", n.getContents()); m.put("views", n.getViews()); + m.put("createdDate", n.getCreatedAt() == null ? null : LocalDate.ofInstant(n.getCreatedAt(), ZoneId.of("Asia/Seoul")).toString()); + return m; + } + private Map questionMap(HomesQuestion q) { + Map m = new LinkedHashMap<>(); + m.put("id", q.getId()); + m.put("title", q.getTitle()); + m.put("authorName", q.getAuthorName()); + m.put("content", q.getContent()); + m.put("reply", q.getReply()); + m.put("status", q.getStatus()); + boolean answered = "답변완료".equals(q.getStatus()) || (q.getReply() != null && !q.getReply().isBlank()); + m.put("answer", answered ? "답변완료" : ""); + m.put("createdDate", q.getCreatedAt() == null ? null : LocalDate.ofInstant(q.getCreatedAt(), ZoneId.of("Asia/Seoul")).toString()); + return m; + } + private Map bbsMap(HomesBbs b) { + Map m = new LinkedHashMap<>(); + m.put("id", b.getId()); m.put("title", b.getTitle()); m.put("authorName", b.getAuthorName()); + m.put("contents", b.getContents()); m.put("views", b.getViews()); + m.put("createdDate", b.getCreatedAt() == null ? null : LocalDate.ofInstant(b.getCreatedAt(), ZoneId.of("Asia/Seoul")).toString()); + return m; + } + private Map logMap(HomesLog l) { + Map m = new LinkedHashMap<>(); + m.put("id", l.getId()); m.put("domain", l.getDomain()); m.put("action", l.getAction()); + m.put("content", l.getContent()); m.put("staff", l.getStaff()); + m.put("processedAt", l.getProcessedAt() == null ? null + : l.getProcessedAt().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))); + return m; + } + + private static String qOf(Map f) { + String q = blankToNull(f.get("q")); + if (q == null) q = blankToNull(f.get("keyword")); + if (q == null) q = blankToNull(f.get("name")); + return q; + } + private static LocalDate parseDate(String v) { + if (v == null || v.isBlank()) return null; + try { return LocalDate.parse(v); } catch (Exception e) { return null; } + } + private static int parseInt(String v, int def) { + try { return Integer.parseInt(v); } catch (Exception e) { return def; } + } + private static String blankToNull(String v) { return v == null || v.isBlank() ? null : v; } + private static String nullToEmpty(String v) { return v == null ? "" : v; } + private static BigDecimal bd(Object v) { + if (v == null) return BigDecimal.ZERO; + if (v instanceof BigDecimal b) return b; + try { return new BigDecimal(String.valueOf(v)); } catch (Exception e) { return BigDecimal.ZERO; } + } +} diff --git a/backend/src/main/java/com/bizcare/agent/CareReceptionController.java b/backend/src/main/java/com/bizcare/agent/CareReceptionController.java new file mode 100644 index 0000000..b044221 --- /dev/null +++ b/backend/src/main/java/com/bizcare/agent/CareReceptionController.java @@ -0,0 +1,1241 @@ +package com.bizcare.agent; + +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.math.BigDecimal; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 접수관리 — billing_react /order(전체 리스트) 기능을 Care Homes 에 이식. + */ +@RestController +@RequestMapping("/api/agent/homes") +@RequiredArgsConstructor +class CareReceptionController { + private final EntityManager em; + private final FileStorageService fileStorage; + + static final List PROGRESS_STATUSES = List.of( + "1.접수완료", "2.고객통화", "3.서류검토", "4.개통완료", "5.사은품지급", "6.정산완료", "9.취소"); + static final List SETTLE_STATUSES = List.of("정산대기", "정산완료", "회수대기", "회수완료"); + static final List GIFT_STATUSES = List.of("미지급", "지급대기", "지급완료", "반송"); + static final List CARRIERS = List.of("KT", "SKB", "LGU+", "SKT", "기타"); + static final List REGIONS = List.of( + "서울", "경기", "인천", "강원", "충북", "충남", "대전", "전북", "전남", "광주", + "경북", "대구", "경남", "울산", "부산", "제주"); + + @GetMapping("/receptions") + ApiResponse list(@AuthenticationPrincipal JwtUser u, + @RequestParam Map f, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "50") int size) { + List filtered = filterReceptions(u, f); + Map counts = new LinkedHashMap<>(); + counts.put("ALL", (long) filtered.size()); + for (String st : PROGRESS_STATUSES) { + counts.put(st, filtered.stream().filter(r -> st.equals(r.getProgressStatus())).count()); + } + int fromIdx = Math.max(0, page) * Math.max(1, size); + int limit = Math.min(Math.max(size, 1), 200); + List> list = filtered.stream().skip(fromIdx).limit(limit) + .map(this::toMap).toList(); + Map data = new LinkedHashMap<>(); + data.put("total", filtered.size()); + data.put("list", list); + data.put("counts", counts); + data.put("progressStatuses", PROGRESS_STATUSES); + data.put("settleStatuses", SETTLE_STATUSES); + data.put("giftStatuses", GIFT_STATUSES); + data.put("carriers", distinct(filtered, HomesReception::getCarrier, CARRIERS)); + data.put("branches", distinct(filtered, HomesReception::getBranch, List.of())); + data.put("counselors", distinct(filtered, HomesReception::getCounselor, List.of())); + data.put("processors", distinct(filtered, HomesReception::getProcessor, List.of())); + data.put("regions", REGIONS); + data.put("giftCategories", distinct(filtered, HomesReception::getGiftCategory, List.of())); + data.put("productCategories", distinct(filtered, HomesReception::getProductCategory, List.of())); + return ApiResponse.ok(data); + } + + @GetMapping(value = "/receptions/excel", produces = "text/csv; charset=UTF-8") + org.springframework.http.ResponseEntity excel(@AuthenticationPrincipal JwtUser u, + @RequestParam Map f) { + List filtered = filterReceptions(u, f); + StringBuilder csv = new StringBuilder("\uFEFF"); + csv.append("접수번호,접수일,상담자,지점,통신사,사은품분류,상품분류,상품명,고객명,연락처,지역,진행상황,주문번호,입고정보,처리자,세부진행,지급구분,금액,정산상태,개통일,사은품일,사은품진행,지급처,약정,개통번호,메모\n"); + for (HomesReception r : filtered) { + csv.append(csvCell(r.getId())).append(',') + .append(csvCell(fmtDt(r.getOrderDate()))).append(',') + .append(csvCell(r.getCounselor())).append(',') + .append(csvCell(r.getBranch())).append(',') + .append(csvCell(r.getCarrier())).append(',') + .append(csvCell(r.getGiftCategory())).append(',') + .append(csvCell(r.getProductCategory())).append(',') + .append(csvCell(r.getProductName())).append(',') + .append(csvCell(r.getCustomerName())).append(',') + .append(csvCell(r.getPhone())).append(',') + .append(csvCell(r.getRegion())).append(',') + .append(csvCell(r.getProgressStatus())).append(',') + .append(csvCell(r.getOrderNumber())).append(',') + .append(csvCell(r.getInboundInfo())).append(',') + .append(csvCell(r.getProcessor())).append(',') + .append(csvCell(r.getDetailProgress())).append(',') + .append(csvCell(r.getPaymentType())).append(',') + .append(csvCell(r.getAmount())).append(',') + .append(csvCell(r.getSettleStatus())).append(',') + .append(csvCell(r.getOpenDate())).append(',') + .append(csvCell(r.getGiftDate())).append(',') + .append(csvCell(r.getGiftStatus())).append(',') + .append(csvCell(r.getGiftPlace())).append(',') + .append(csvCell(r.getAgreementName())).append(',') + .append(csvCell(r.getContractNumber())).append(',') + .append(csvCell(r.getMemo())).append('\n'); + } + return org.springframework.http.ResponseEntity.ok() + .header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=receptions.csv") + .body(csv.toString()); + } + + @GetMapping("/receptions/{id}") + ApiResponse get(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + HomesReception r = findOwned(id, u); + if (r == null) return ApiResponse.fail("접수를 찾을 수 없습니다."); + return ApiResponse.ok(toProgressDetail(r, u)); + } + + /** 업무 진행내역 저장 — billing_react POST /order/{id}/progress */ + @PostMapping("/receptions/{id}/progress") + @Transactional + ApiResponse progress(@PathVariable Long id, @RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + HomesReception r = findOwned(id, u); + if (r == null) return ApiResponse.fail("접수를 찾을 수 없습니다."); + + if (body.containsKey("postIt")) r.setPostIt(str(body.get("postIt"))); + if (body.containsKey("postAdd")) r.setPostAdd(str(body.get("postAdd"))); + if (body.containsKey("contractNumber")) r.setContractNumber(str(body.get("contractNumber"))); + if (body.containsKey("installSchedule")) r.setInstallSchedule(str(body.get("installSchedule"))); + if (body.containsKey("customerLoginId") || body.containsKey("customerid")) { + Object v = body.containsKey("customerLoginId") ? body.get("customerLoginId") : body.get("customerid"); + r.setCustomerLoginId(str(v)); + } + if (body.containsKey("newTel")) r.setNewTel(str(body.get("newTel"))); + if (body.containsKey("multiLine")) r.setMultiLine(Boolean.TRUE.equals(body.get("multiLine")) || "true".equalsIgnoreCase(String.valueOf(body.get("multiLine")))); + if (body.containsKey("addPayer")) r.setAddPayer(Boolean.TRUE.equals(body.get("addPayer")) || "true".equalsIgnoreCase(String.valueOf(body.get("addPayer")))); + if (body.containsKey("record") || body.containsKey("recordFlag")) { + Object v = body.containsKey("recordFlag") ? body.get("recordFlag") : body.get("record"); + r.setRecordFlag(Boolean.TRUE.equals(v) || "true".equalsIgnoreCase(String.valueOf(v))); + } + if (body.containsKey("adminMemo")) r.setAdminMemo(str(body.get("adminMemo"))); + if (body.containsKey("openDate") || body.containsKey("opended")) { + Object v = body.containsKey("openDate") ? body.get("openDate") : body.get("opended"); + r.setOpenDate(parseDate(str(v))); + } + if (body.containsKey("cancelDate") || body.containsKey("canceled")) { + Object v = body.containsKey("cancelDate") ? body.get("cancelDate") : body.get("canceled"); + r.setCancelDate(parseDate(str(v))); + } + if (body.containsKey("giftPlace") || body.containsKey("place")) { + Object v = body.containsKey("giftPlace") ? body.get("giftPlace") : body.get("place"); + r.setGiftPlace(str(v)); + } + if (body.containsKey("giftRequested") || body.containsKey("requested")) { + Object v = body.containsKey("giftRequested") ? body.get("giftRequested") : body.get("requested"); + r.setGiftRequested(parseDate(str(v))); + } + if (body.containsKey("giftSended") || body.containsKey("sended")) { + Object v = body.containsKey("giftSended") ? body.get("giftSended") : body.get("sended"); + r.setGiftSended(parseDate(str(v))); + } + if (Boolean.TRUE.equals(body.get("changeOrderDate")) && body.get("orderDate") != null) { + r.setOrderDate(parseDateTime(str(body.get("orderDate")))); + } + + String newStatus = str(body.get("status")); + if (blank(newStatus)) newStatus = str(body.get("progressStatus")); + String progressMemo = str(body.get("memo")); + if (blank(progressMemo)) progressMemo = str(body.get("progressLog")); + boolean adminOnly = Boolean.TRUE.equals(body.get("adminOnly")) || "true".equalsIgnoreCase(String.valueOf(body.get("adminOnly"))); + + boolean statusChanged = false; + if (!blank(newStatus) && !Objects.equals(newStatus, r.getProgressStatus())) { + r.setProgressStatus(newStatus); + statusChanged = true; + if ("4.개통완료".equals(newStatus) && blank(r.getGiftStatus())) { + r.setGiftStatus("지급대기"); + } + if ("4.개통완료".equals(newStatus) && r.getOpenDate() == null) { + r.setOpenDate(LocalDate.now(ZoneId.of("Asia/Seoul"))); + } + } + + String giftStatus = str(body.get("giftStatus")); + boolean giftChanged = false; + if (!blank(giftStatus) && !Objects.equals(giftStatus, r.getGiftStatus())) { + r.setGiftStatus(giftStatus); + giftChanged = true; + appendGiftLog(r, giftStatus, str(body.get("giftProgressLog")), u); + if ("지급완료".equals(giftStatus) && "4.개통완료".equals(r.getProgressStatus())) { + r.setProgressStatus("6.정산완료"); + r.setSettleStatus("정산완료"); + statusChanged = true; + } + } else if (!blank(str(body.get("giftProgressLog")))) { + appendGiftLog(r, r.getGiftStatus(), str(body.get("giftProgressLog")), u); + } + + if (statusChanged || !blank(progressMemo)) { + appendLog(r, blank(newStatus) ? r.getProgressStatus() : newStatus, progressMemo, u, adminOnly); + } + + if (body.containsKey("processor") && !blank(str(body.get("processor")))) { + r.setProcessor(str(body.get("processor"))); + } else if (statusChanged) { + r.setProcessor(resolveMemberName(u)); + } + + writeLog(u, "RECEPTION", "접수-진행", "접수 #" + r.getId() + " [" + nullToEmpty(r.getCustomerName()) + "] 진행내역 저장"); + return ApiResponse.ok(toProgressDetail(r, u)); + } + + @DeleteMapping("/receptions/progress-log/{logId}") + @Transactional + ApiResponse deleteProgressLog(@PathVariable Long logId, @AuthenticationPrincipal JwtUser u) { + HomesReceptionProgressLog log = em.find(HomesReceptionProgressLog.class, logId); + if (log == null || !Objects.equals(log.getGroupId(), u.getGroupId())) return ApiResponse.fail("로그를 찾을 수 없습니다."); + em.remove(log); + return ApiResponse.ok(null); + } + + @DeleteMapping("/receptions/gift-progress-log/{logId}") + @Transactional + ApiResponse deleteGiftProgressLog(@PathVariable Long logId, @AuthenticationPrincipal JwtUser u) { + HomesReceptionGiftProgressLog log = em.find(HomesReceptionGiftProgressLog.class, logId); + if (log == null || !Objects.equals(log.getGroupId(), u.getGroupId())) return ApiResponse.fail("로그를 찾을 수 없습니다."); + em.remove(log); + return ApiResponse.ok(null); + } + + @PostMapping("/receptions") + @Transactional + ApiResponse create(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + HomesReception r = new HomesReception(); + r.setGroupId(u.getGroupId()); + apply(r, body); + if (r.getOrderDate() == null) r.setOrderDate(LocalDateTime.now(ZoneId.of("Asia/Seoul"))); + if (blank(r.getProgressStatus())) r.setProgressStatus("1.접수완료"); + if (blank(r.getSettleStatus())) r.setSettleStatus("정산대기"); + if (blank(r.getGiftStatus())) r.setGiftStatus("미지급"); + if (blank(r.getOrderNumber())) r.setOrderNumber(genOrderNumber()); + em.persist(r); + em.flush(); + appendLog(r, r.getProgressStatus(), "접수 등록", u); + writeLog(u, "RECEPTION", "접수-등록", "접수 [" + nullToEmpty(r.getCustomerName()) + "] 등록하였습니다."); + return ApiResponse.ok(toMap(r)); + } + + @PutMapping("/receptions/{id}") + @Transactional + ApiResponse update(@PathVariable Long id, @RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + HomesReception r = findOwned(id, u); + if (r == null) return ApiResponse.fail("접수를 찾을 수 없습니다."); + String prev = r.getProgressStatus(); + apply(r, body); + if (!Objects.equals(prev, r.getProgressStatus())) { + appendLog(r, r.getProgressStatus(), str(body.get("progressMemo")), u); + } + writeLog(u, "RECEPTION", "접수-수정", "접수 [" + nullToEmpty(r.getCustomerName()) + "] 수정하였습니다."); + return ApiResponse.ok(toMap(r)); + } + + @DeleteMapping("/receptions/{id}") + @Transactional + ApiResponse delete(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) { + HomesReception r = findOwned(id, u); + if (r == null) return ApiResponse.fail("접수를 찾을 수 없습니다."); + writeLog(u, "RECEPTION", "접수-삭제", "접수 [" + nullToEmpty(r.getCustomerName()) + "] 삭제하였습니다."); + em.remove(r); + return ApiResponse.ok(null); + } + + @PostMapping("/receptions/delete") + @Transactional + ApiResponse bulkDelete(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + List ids = toIds(body.get("ids")); + int n = 0; + for (Long id : ids) { + HomesReception r = findOwned(id, u); + if (r == null) continue; + writeLog(u, "RECEPTION", "접수-삭제", "접수 [" + nullToEmpty(r.getCustomerName()) + "] 삭제하였습니다."); + em.remove(r); + n++; + } + return ApiResponse.ok(Map.of("count", n)); + } + + @PostMapping("/receptions/bulk-progress") + @Transactional + ApiResponse bulkProgress(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + List ids = toIds(body.get("ids")); + if (ids.isEmpty()) return ApiResponse.fail("리스트를 선택하세요."); + String status = str(body.get("status")); + if (blank(status)) return ApiResponse.fail("진행상황을 선택하세요."); + String memo = "[일괄처리] " + nullToEmpty(str(body.get("memo"))); + boolean adminOnly = Boolean.TRUE.equals(body.get("adminOnly")); + String processor = firstNonBlank(str(body.get("centerId")), str(body.get("processor")), str(body.get("centerName"))); + LocalDate opened = parseDate(str(body.containsKey("opended") ? body.get("opended") + : body.containsKey("opened") ? body.get("opened") : body.get("openDate"))); + LocalDate canceled = parseDate(str(body.containsKey("canceled") ? body.get("canceled") : body.get("cancelDate"))); + + List rows = new ArrayList<>(); + String baseStatus = null; + for (Long id : ids) { + HomesReception r = findOwned(id, u); + if (r == null) return ApiResponse.fail("접수 리스트가 올바르지 않습니다."); + if (baseStatus != null && !Objects.equals(baseStatus, r.getProgressStatus())) { + return ApiResponse.fail("진행상황이 다른 접수가 포함되어 있습니다."); + } + baseStatus = r.getProgressStatus(); + rows.add(r); + } + + int n = 0; + for (HomesReception r : rows) { + r.setProgressStatus(status); + if (!blank(processor)) r.setProcessor(processor); + if (opened != null) r.setOpenDate(opened); + if (canceled != null) r.setCancelDate(canceled); + if ("4.개통완료".equals(status) && blank(r.getGiftStatus())) r.setGiftStatus("지급대기"); + appendLog(r, status, memo, u, adminOnly); + n++; + } + writeLog(u, "RECEPTION", "접수-일괄진행", "접수 " + n + "건 진행상황을 [" + status + "]로 변경하였습니다."); + return ApiResponse.ok(Map.of("count", n)); + } + + @PostMapping("/receptions/bulk-gift") + @Transactional + ApiResponse bulkGift(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + List ids = toIds(body.get("ids")); + if (ids.isEmpty()) return ApiResponse.fail("리스트를 선택하세요."); + String status = str(body.get("status")); + if (blank(status)) return ApiResponse.fail("사은품진행상황을 선택하세요."); + String place = str(body.get("place")); + String giftMemo = "[일괄처리] " + nullToEmpty(str( + body.containsKey("giftProgressLog") ? body.get("giftProgressLog") : body.get("memo"))); + String deliveryCompany = str(body.get("deliveryCompany")); + String trackingNumber = str(body.get("trackingNumber")); + LocalDate requested = parseDate(str(body.containsKey("requested") ? body.get("requested") : body.get("giftRequested"))); + LocalDate sended = parseDate(str(body.containsKey("sended") ? body.get("sended") + : body.containsKey("giftSended") ? body.get("giftSended") : body.get("giftDate"))); + + List rows = new ArrayList<>(); + String basePlace = null; + for (Long id : ids) { + HomesReception r = findOwned(id, u); + if (r == null) return ApiResponse.fail("선택한 접수가 올바르지 않습니다."); + String p = r.getGiftPlace(); + if (basePlace == null) { + basePlace = p; + } else { + boolean baseSelf = "b02".equals(basePlace) || "영업점".equals(basePlace); + boolean curSelf = "b02".equals(p) || "영업점".equals(p); + if (baseSelf != curSelf) return ApiResponse.fail("사은품 지급처가 다른 접수가 포함되어 있습니다."); + } + rows.add(r); + } + + int n = 0; + for (HomesReception r : rows) { + r.setGiftStatus(status); + if (!blank(place)) r.setGiftPlace(place); + if (!blank(deliveryCompany)) r.setDeliveryCompany(deliveryCompany); + if (!blank(trackingNumber)) r.setTrackingNumber(trackingNumber); + if (requested != null) r.setGiftRequested(requested); + if (sended != null) { + r.setGiftSended(sended); + r.setGiftDate(sended); + } + appendGiftLog(r, status, giftMemo, u); + n++; + } + writeLog(u, "RECEPTION", "사은품-일괄", "접수 " + n + "건 사은품진행을 [" + status + "]로 변경하였습니다."); + return ApiResponse.ok(Map.of("count", n)); + } + + @PostMapping("/receptions/bulk-center-progress") + @Transactional + ApiResponse bulkCenterProgress(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + List ids = toIds(body.get("ids")); + if (ids.isEmpty()) return ApiResponse.fail("리스트를 선택하세요."); + + List rows = new ArrayList<>(); + for (Long id : ids) { + HomesReception r = findOwned(id, u); + if (r == null) return ApiResponse.fail("접수 리스트가 올바르지 않습니다."); + // 입력처 정산이 비어있거나 미지정인 건만 대상 + if (!blank(r.getSettleStatus()) && !"정산대기".equals(r.getSettleStatus()) + && !"미지정".equals(r.getSettleStatus())) { + return ApiResponse.fail("접수 리스트가 올바르지 않습니다."); + } + rows.add(r); + } + + int n = 0; + for (HomesReception r : rows) { + r.setSettleStatus("정산대기"); + appendLog(r, r.getProgressStatus(), "[일괄변경] 입력처 정산대기", u, false); + n++; + } + writeLog(u, "RECEPTION", "입력처-일괄", "접수 " + n + "건을 입력처 정산대기로 변경하였습니다."); + return ApiResponse.ok(Map.of("count", n)); + } + + /** 신규신청 통신사 목록 (billing_react GET /isp?onlyOn=true) */ + @GetMapping("/reception-isps") + ApiResponse isps(@AuthenticationPrincipal JwtUser u) { + List list = em.createQuery( + "select e from HomesReceptionIsp e where e.groupId=:g and (e.active is null or e.active=true) order by e.seq asc, e.id asc", + HomesReceptionIsp.class) + .setParameter("g", u.getGroupId()).getResultList(); + return ApiResponse.ok(list.stream().map(i -> { + Map m = new LinkedHashMap<>(); + m.put("id", i.getId()); + m.put("name", i.getName()); + m.put("telecom", i.getTelecom()); + m.put("seq", i.getSeq()); + m.put("isSktb", Boolean.TRUE.equals(i.getIsSktb())); + return m; + }).toList()); + } + + @GetMapping("/reception-isps/{ispId}/products") + ApiResponse ispProducts(@PathVariable Long ispId, @AuthenticationPrincipal JwtUser u) { + HomesReceptionIsp isp = findIsp(ispId, u); + if (isp == null) return ApiResponse.fail("통신사를 찾을 수 없습니다."); + List list = em.createQuery( + "select e from HomesReceptionIspProduct e where e.groupId=:g and e.ispId=:isp and (e.active is null or e.active=true) order by e.id", + HomesReceptionIspProduct.class) + .setParameter("g", u.getGroupId()).setParameter("isp", ispId).getResultList(); + return ApiResponse.ok(list.stream().map(p -> Map.of( + "id", p.getId(), + "type", nullToEmpty(p.getProductType()), + "name", nullToEmpty(p.getName()) + )).toList()); + } + + @GetMapping("/reception-isps/{ispId}/agreements") + ApiResponse ispAgreements(@PathVariable Long ispId, @AuthenticationPrincipal JwtUser u) { + HomesReceptionIsp isp = findIsp(ispId, u); + if (isp == null) return ApiResponse.fail("통신사를 찾을 수 없습니다."); + List list = em.createQuery( + "select e from HomesReceptionIspAgreement e where e.groupId=:g and e.ispId=:isp and (e.active is null or e.active=true) order by e.id", + HomesReceptionIspAgreement.class) + .setParameter("g", u.getGroupId()).setParameter("isp", ispId).getResultList(); + return ApiResponse.ok(list.stream().map(a -> Map.of( + "id", a.getId(), + "name", nullToEmpty(a.getName()) + )).toList()); + } + + /** + * 신규신청 등록 — billing_react POST /order 를 Care Homes reception 으로 매핑. + * body: { ispId, counselor, branch, customer:{type,name,phone,region,address,ssid,postIt}, payment:{method,name,...}, + * product:{productType,productId,agreementId,price,installDate,discount}, gift:{type,place,cashAmount,goods,giftDate}, memo } + */ + @PostMapping("/receptions/apply") + @Transactional + ApiResponse apply(@RequestBody Map body, @AuthenticationPrincipal JwtUser u) { + Long ispId = toLong(body.get("ispId")); + HomesReceptionIsp isp = ispId == null ? null : findIsp(ispId, u); + if (isp == null) return ApiResponse.fail("통신사를 선택하세요."); + + @SuppressWarnings("unchecked") + Map customer = body.get("customer") instanceof Map m ? (Map) m : Map.of(); + @SuppressWarnings("unchecked") + Map payment = body.get("payment") instanceof Map m ? (Map) m : Map.of(); + @SuppressWarnings("unchecked") + Map product = body.get("product") instanceof Map m ? (Map) m : Map.of(); + @SuppressWarnings("unchecked") + Map gift = body.get("gift") instanceof Map m ? (Map) m : Map.of(); + + String customerName = str(customer.get("name")); + if (blank(customerName)) return ApiResponse.fail("고객명을 입력하세요."); + Long productId = toLong(product.get("productId")); + if (productId == null) return ApiResponse.fail("상품을 선택하세요."); + + HomesReceptionIspProduct prod = em.find(HomesReceptionIspProduct.class, productId); + if (prod == null || !Objects.equals(prod.getGroupId(), u.getGroupId()) || !Objects.equals(prod.getIspId(), ispId)) { + return ApiResponse.fail("상품이 올바르지 않습니다."); + } + String agreementName = null; + Long agreementId = toLong(product.get("agreementId")); + if (agreementId != null) { + HomesReceptionIspAgreement ag = em.find(HomesReceptionIspAgreement.class, agreementId); + if (ag != null && Objects.equals(ag.getGroupId(), u.getGroupId())) agreementName = ag.getName(); + } + + String productTypeLabel = switch (nullToEmpty(prod.getProductType())) { + case "e02" -> "전화"; + case "e03" -> "TV"; + case "e04" -> "렌탈"; + default -> "인터넷"; + }; + String giftType = str(gift.get("type")); + String giftCategory = switch (nullToEmpty(giftType)) { + case "a02" -> "현금"; + case "a03" -> "상품"; + case "a04" -> "현금+상품"; + case "a01" -> "없음"; + default -> blank(giftType) ? "상품권" : giftType; + }; + String giftPlaceCode = str(gift.get("place")); + String payMethod = str(payment.get("method")); + String paymentType = switch (nullToEmpty(payMethod)) { + case "card" -> "카드"; + case "paper" -> "지로"; + case "telephone" -> "전화납부"; + default -> "계좌"; + }; + + HomesReception r = new HomesReception(); + r.setGroupId(u.getGroupId()); + r.setOrderDate(LocalDateTime.now(ZoneId.of("Asia/Seoul"))); + r.setIspId(isp.getId()); + r.setIspName(isp.getName()); + r.setCarrier(isp.getTelecom()); + r.setCounselor(blank(str(body.get("counselor"))) ? resolveMemberName(u) : str(body.get("counselor"))); + r.setBranch(str(body.get("branch"))); + r.setBranchGroup(str(body.get("branchGroup"))); + r.setCustomerType(blank(str(customer.get("type"))) ? "public" : str(customer.get("type"))); + r.setCustomerName(customerName); + r.setPhone(str(customer.get("phone"))); + r.setTel(str(customer.get("tel"))); + r.setEmail(str(customer.get("email"))); + r.setZipcode(str(customer.get("zipcode"))); + r.setAddress(str(customer.get("address"))); + r.setAddressDetail(str(customer.get("addressDetail"))); + r.setRegion(str(customer.get("region"))); + r.setIdentifyNumber(str(customer.get("ssid"))); + r.setPostIt(str(customer.get("postIt"))); + r.setPostAdd(str(customer.get("postAdd"))); + r.setProductType(prod.getProductType()); + r.setProductCategory(productTypeLabel); + r.setProductName(prod.getName()); + r.setAgreementName(agreementName); + r.setAmount(toDecimal(product.get("price"))); + r.setOpenDate(parseDate(str(product.get("installDate")))); + r.setGiftCategory(giftCategory); + r.setGiftPlace(giftPlaceCode); + r.setGiftDate(parseDate(str(gift.get("giftDate")))); + r.setGiftStatus(blank(giftType) || "a01".equals(giftType) ? "미지급" : "지급대기"); + r.setGiftCashAmount(toDecimal(gift.get("cashAmount"))); + r.setGiftGoods(str(gift.get("goods"))); + r.setGiftGoodsPrice(toDecimal(gift.get("goodsPrice"))); + r.setGiftBank(str(gift.get("accountBank"))); + r.setGiftAccountNumber(str(gift.get("accountNumber"))); + r.setGiftAccountName(str(gift.get("accountName"))); + r.setGiftSsid(str(gift.get("ssid"))); + r.setPaymentMethod(blank(payMethod) ? "transfer" : payMethod); + r.setPaymentType(paymentType); + r.setPaymentName(str(payment.get("name"))); + r.setPaymentSsid(str(payment.get("ssid"))); + r.setPaymentTel(str(payment.get("tel"))); + r.setPaymentBank(str(payment.get("bank"))); + r.setPaymentAccountNumber(str(payment.get("accountNumber"))); + r.setPaymentCardCompany(str(payment.get("cardCompany"))); + r.setPaymentCardYear(str(payment.get("cardYear"))); + r.setPaymentCardMonth(str(payment.get("cardMonth"))); + r.setPaymentCardNumber(str(payment.get("cardNumber"))); + r.setProgressStatus("1.접수완료"); + r.setSettleStatus("정산대기"); + r.setOrderNumber(genOrderNumber()); + r.setProcessor(r.getCounselor()); + r.setDetailProgress("신규신청"); + r.setMemo(str(body.get("memo"))); + em.persist(r); + em.flush(); + appendLog(r, r.getProgressStatus(), "신규신청 [" + isp.getName() + "]", u, false); + writeLog(u, "RECEPTION", "신규신청", "신규신청 [" + isp.getName() + "] " + customerName + " 등록하였습니다."); + return ApiResponse.ok(toProgressDetail(r, u)); + } + + // ---- 첨부파일 (일반 file1~5) ---- + @PostMapping("/receptions/{id}/file/{slot}") + @Transactional + ApiResponse uploadFile(@PathVariable Long id, @PathVariable int slot, + @RequestParam("file") MultipartFile file, + @AuthenticationPrincipal JwtUser u) { + return uploadAttachment(id, slot, false, file, u); + } + + @GetMapping("/receptions/{id}/file/{slot}") + ResponseEntity downloadFile(@PathVariable Long id, @PathVariable int slot, + @AuthenticationPrincipal JwtUser u) { + return downloadAttachment(id, slot, false, u); + } + + @DeleteMapping("/receptions/{id}/file/{slot}") + @Transactional + ApiResponse deleteFile(@PathVariable Long id, @PathVariable int slot, + @AuthenticationPrincipal JwtUser u) { + return deleteAttachment(id, slot, false, u); + } + + // ---- 관리자 첨부파일 (admin_file1~3) ---- + @PostMapping("/receptions/{id}/admin-file/{slot}") + @Transactional + ApiResponse uploadAdminFile(@PathVariable Long id, @PathVariable int slot, + @RequestParam("file") MultipartFile file, + @AuthenticationPrincipal JwtUser u) { + return uploadAttachment(id, slot, true, file, u); + } + + @GetMapping("/receptions/{id}/admin-file/{slot}") + ResponseEntity downloadAdminFile(@PathVariable Long id, @PathVariable int slot, + @AuthenticationPrincipal JwtUser u) { + return downloadAttachment(id, slot, true, u); + } + + @DeleteMapping("/receptions/{id}/admin-file/{slot}") + @Transactional + ApiResponse deleteAdminFile(@PathVariable Long id, @PathVariable int slot, + @AuthenticationPrincipal JwtUser u) { + return deleteAttachment(id, slot, true, u); + } + + private ApiResponse uploadAttachment(Long id, int slot, boolean admin, MultipartFile file, JwtUser u) { + HomesReception r = findOwned(id, u); + int max = admin ? 3 : 5; + if (slot < 1 || slot > max) return ApiResponse.fail("첨부 슬롯이 올바르지 않습니다."); + if (file == null || file.isEmpty()) return ApiResponse.fail("업로드할 파일을 선택하세요."); + String old = getFile(r, slot, admin); + if (old != null && !old.isBlank()) fileStorage.delete(old); + String path = fileStorage.store("reception/" + id, file); + setFile(r, slot, admin, path); + writeLog(u, "RECEPTION", admin ? "관리자첨부" : "첨부", + "접수 #" + id + " " + (admin ? "관리자첨부" : "첨부") + slot + " 업로드"); + Map data = new LinkedHashMap<>(); + data.put("id", r.getId()); + data.put("slot", slot); + data.put("admin", admin); + data.put("path", path); + data.put("name", FileStorageService.displayName(path)); + return ApiResponse.ok(data); + } + + private ResponseEntity downloadAttachment(Long id, int slot, boolean admin, JwtUser u) { + HomesReception r = findOwned(id, u); + String rel = getFile(r, slot, admin); + if (rel == null || rel.isBlank()) { + return ResponseEntity.notFound().build(); + } + Resource resource = fileStorage.loadAsResource(rel); + String filename = URLEncoder.encode(FileStorageService.displayName(rel), StandardCharsets.UTF_8) + .replace("+", "%20"); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + filename) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(resource); + } + + private ApiResponse deleteAttachment(Long id, int slot, boolean admin, JwtUser u) { + HomesReception r = findOwned(id, u); + int max = admin ? 3 : 5; + if (slot < 1 || slot > max) return ApiResponse.fail("첨부 슬롯이 올바르지 않습니다."); + String old = getFile(r, slot, admin); + if (old != null && !old.isBlank()) fileStorage.delete(old); + setFile(r, slot, admin, null); + writeLog(u, "RECEPTION", admin ? "관리자첨부삭제" : "첨부삭제", + "접수 #" + id + " " + (admin ? "관리자첨부" : "첨부") + slot + " 삭제"); + return ApiResponse.ok(Map.of("id", id, "slot", slot, "admin", admin)); + } + + private static String getFile(HomesReception r, int slot, boolean admin) { + if (admin) { + return switch (slot) { + case 1 -> r.getAdminFile1(); + case 2 -> r.getAdminFile2(); + case 3 -> r.getAdminFile3(); + default -> null; + }; + } + return switch (slot) { + case 1 -> r.getFile1(); + case 2 -> r.getFile2(); + case 3 -> r.getFile3(); + case 4 -> r.getFile4(); + case 5 -> r.getFile5(); + default -> null; + }; + } + + private static void setFile(HomesReception r, int slot, boolean admin, String path) { + if (admin) { + switch (slot) { + case 1 -> r.setAdminFile1(path); + case 2 -> r.setAdminFile2(path); + case 3 -> r.setAdminFile3(path); + default -> { + } + } + return; + } + switch (slot) { + case 1 -> r.setFile1(path); + case 2 -> r.setFile2(path); + case 3 -> r.setFile3(path); + case 4 -> r.setFile4(path); + case 5 -> r.setFile5(path); + default -> { + } + } + } + + private HomesReceptionIsp findIsp(Long id, JwtUser u) { + HomesReceptionIsp isp = em.find(HomesReceptionIsp.class, id); + if (isp == null || !Objects.equals(isp.getGroupId(), u.getGroupId())) return null; + return isp; + } + + private static Long toLong(Object v) { + if (v == null || String.valueOf(v).isBlank()) return null; + try { return Long.valueOf(String.valueOf(v)); } catch (Exception e) { return null; } + } + + private List filterReceptions(JwtUser u, Map f) { + List all = em.createQuery( + "select e from HomesReception e where e.groupId=:g order by e.orderDate desc, e.id desc", + HomesReception.class) + .setParameter("g", u.getGroupId()).getResultList(); + + LocalDate from = parseDate(first(f, "dateFrom", "orderDateStart")); + LocalDate to = parseDate(first(f, "dateTo", "orderDateEnd")); + LocalDate openFrom = parseDate(f.get("openDateFrom")); + LocalDate openTo = parseDate(f.get("openDateTo")); + LocalDate opened = parseDate(first(f, "opended", "opened", "openDate")); + LocalDate giftSended = parseDate(first(f, "giftSended", "giftDateExact")); + String progress = blankToNull(first(f, "progressStatus", "tab")); + if ("ALL".equalsIgnoreCase(progress)) progress = null; + String settle = blankToNull(first(f, "settleStatus", "centerStatus")); + String giftStatus = blankToNull(f.get("giftStatus")); + String carrier = blankToNull(f.get("carrier")); + String branch = blankToNull(f.get("branch")); + String counselor = blankToNull(f.get("counselor")); + String processor = blankToNull(f.get("processor")); + String region = blankToNull(first(f, "region", "customerSido")); + String giftCategory = blankToNull(first(f, "giftCategory", "giftType")); + String productCategory = blankToNull(first(f, "productCategory", "productType")); + String customerName = blankToNull(f.get("customerName")); + String phone = blankToNull(f.get("customerPhone") != null ? f.get("customerPhone") : f.get("phone")); + String orderNumber = blankToNull(f.get("orderNumber")); + String q = blankToNull(first(f, "q", "keyword")); + String postIt = blankToNull(f.get("postIt")); + String agreementName = blankToNull(f.get("agreementName")); + String customerSsid = blankToNull(first(f, "customerSsid", "identifyNumber")); + String customerEmail = blankToNull(first(f, "customerEmail", "email")); + String productName = blankToNull(f.get("productName")); + String ispName = blankToNull(f.get("ispName")); + String contractNumber = blankToNull(f.get("contractNumber")); + String giftPlace = blankToNull(f.get("giftPlace")); + String branchEmployee = blankToNull(first(f, "branchEmployee")); + String branchName = blankToNull(f.get("branchName")); + String sorting = blankToNull(f.get("sorting")); + + String progressF = progress; + List result = all.stream().filter(r -> { + LocalDate od = r.getOrderDate() == null ? null : r.getOrderDate().toLocalDate(); + if (from != null && (od == null || od.isBefore(from))) return false; + if (to != null && (od == null || od.isAfter(to))) return false; + if (openFrom != null && (r.getOpenDate() == null || r.getOpenDate().isBefore(openFrom))) return false; + if (openTo != null && (r.getOpenDate() == null || r.getOpenDate().isAfter(openTo))) return false; + if (opened != null && (r.getOpenDate() == null || !r.getOpenDate().equals(opened))) return false; + if (giftSended != null) { + LocalDate gd = r.getGiftSended() != null ? r.getGiftSended() : r.getGiftDate(); + if (gd == null || !gd.equals(giftSended)) return false; + } + if (progressF != null && !progressF.equals(r.getProgressStatus())) return false; + if (settle != null && !settle.equals(r.getSettleStatus())) return false; + if (giftStatus != null && !giftStatus.equals(r.getGiftStatus())) return false; + if (carrier != null && !carrier.equals(r.getCarrier()) && !carrier.equals(r.getIspName())) return false; + if (ispName != null) { + String hay = (nullToEmpty(r.getIspName()) + " " + nullToEmpty(r.getCarrier())).toLowerCase(); + if (!hay.contains(ispName.toLowerCase())) return false; + } + if (branch != null && !branch.equals(r.getBranch())) return false; + if (branchName != null && !nullToEmpty(r.getBranch()).contains(branchName)) return false; + if (counselor != null && !counselor.equals(r.getCounselor())) return false; + if (branchEmployee != null && !nullToEmpty(r.getCounselor()).contains(branchEmployee)) return false; + if (processor != null && !processor.equals(r.getProcessor())) return false; + if (region != null && !region.equals(r.getRegion())) return false; + if (giftCategory != null && !nullToEmpty(r.getGiftCategory()).contains(giftCategory)) return false; + if (productCategory != null + && !productCategory.equals(r.getProductCategory()) + && !productCategory.equals(r.getProductType())) return false; + if (giftPlace != null && !giftPlace.equals(r.getGiftPlace())) return false; + if (customerName != null && !nullToEmpty(r.getCustomerName()).contains(customerName)) return false; + if (phone != null) { + String digits = nullToEmpty(r.getPhone()).replaceAll("\\D", ""); + if (!digits.contains(phone.replaceAll("\\D", "")) && !nullToEmpty(r.getPhone()).contains(phone)) return false; + } + if (orderNumber != null && !nullToEmpty(r.getOrderNumber()).contains(orderNumber)) return false; + if (postIt != null && !nullToEmpty(r.getPostIt()).contains(postIt) + && !nullToEmpty(r.getPostAdd()).contains(postIt)) return false; + if (agreementName != null && !nullToEmpty(r.getAgreementName()).contains(agreementName)) return false; + if (customerSsid != null && !nullToEmpty(r.getIdentifyNumber()).contains(customerSsid)) return false; + if (customerEmail != null && !nullToEmpty(r.getEmail()).contains(customerEmail)) return false; + if (productName != null && !nullToEmpty(r.getProductName()).contains(productName)) return false; + if (contractNumber != null && !nullToEmpty(r.getContractNumber()).contains(contractNumber)) return false; + if (q != null) { + String hay = (nullToEmpty(r.getCustomerName()) + " " + nullToEmpty(r.getPhone()) + " " + + nullToEmpty(r.getProductName()) + " " + nullToEmpty(r.getOrderNumber()) + " " + + nullToEmpty(r.getCounselor()) + " " + nullToEmpty(r.getBranch()) + " " + + nullToEmpty(r.getMemo()) + " " + nullToEmpty(r.getPostIt()) + " " + + nullToEmpty(r.getIdentifyNumber())).toLowerCase(); + if (!hay.contains(q.toLowerCase())) return false; + } + return true; + }).collect(Collectors.toCollection(ArrayList::new)); + + if ("opended".equals(sorting) || "opened".equals(sorting)) { + result.sort(Comparator + .comparing(HomesReception::getOpenDate, Comparator.nullsLast(Comparator.naturalOrder())) + .reversed()); + } + return result; + } + + private void apply(HomesReception r, Map d) { + if (d.containsKey("orderDate")) r.setOrderDate(parseDateTime(str(d.get("orderDate")))); + if (d.containsKey("counselor")) r.setCounselor(str(d.get("counselor"))); + if (d.containsKey("branch")) r.setBranch(str(d.get("branch"))); + if (d.containsKey("branchGroup")) r.setBranchGroup(str(d.get("branchGroup"))); + if (d.containsKey("carrier")) r.setCarrier(str(d.get("carrier"))); + if (d.containsKey("giftCategory")) r.setGiftCategory(str(d.get("giftCategory"))); + if (d.containsKey("productCategory")) r.setProductCategory(str(d.get("productCategory"))); + if (d.containsKey("productName")) r.setProductName(str(d.get("productName"))); + if (d.containsKey("customerName")) r.setCustomerName(str(d.get("customerName"))); + if (d.containsKey("phone")) r.setPhone(str(d.get("phone"))); + if (d.containsKey("region")) r.setRegion(str(d.get("region"))); + if (d.containsKey("progressStatus")) r.setProgressStatus(str(d.get("progressStatus"))); + if (d.containsKey("orderNumber")) r.setOrderNumber(str(d.get("orderNumber"))); + if (d.containsKey("ispId")) r.setIspId(toLong(d.get("ispId"))); + if (d.containsKey("ispName")) r.setIspName(str(d.get("ispName"))); + if (d.containsKey("inboundInfo")) r.setInboundInfo(str(d.get("inboundInfo"))); + if (d.containsKey("processor")) r.setProcessor(str(d.get("processor"))); + if (d.containsKey("detailProgress")) r.setDetailProgress(str(d.get("detailProgress"))); + if (d.containsKey("paymentType")) r.setPaymentType(str(d.get("paymentType"))); + if (d.containsKey("amount")) r.setAmount(toDecimal(d.get("amount"))); + if (d.containsKey("settleStatus")) r.setSettleStatus(str(d.get("settleStatus"))); + if (d.containsKey("openDate")) r.setOpenDate(parseDate(str(d.get("openDate")))); + if (d.containsKey("giftDate")) r.setGiftDate(parseDate(str(d.get("giftDate")))); + if (d.containsKey("giftStatus")) r.setGiftStatus(str(d.get("giftStatus"))); + if (d.containsKey("giftPlace")) r.setGiftPlace(str(d.get("giftPlace"))); + if (d.containsKey("agreementName")) r.setAgreementName(str(d.get("agreementName"))); + if (d.containsKey("contractNumber")) r.setContractNumber(str(d.get("contractNumber"))); + if (d.containsKey("identifyNumber")) r.setIdentifyNumber(str(d.get("identifyNumber"))); + if (d.containsKey("postIt")) r.setPostIt(str(d.get("postIt"))); + if (d.containsKey("postAdd")) r.setPostAdd(str(d.get("postAdd"))); + if (d.containsKey("memo")) r.setMemo(str(d.get("memo"))); + if (d.containsKey("adminMemo")) r.setAdminMemo(str(d.get("adminMemo"))); + if (d.containsKey("customerType")) r.setCustomerType(str(d.get("customerType"))); + if (d.containsKey("tel")) r.setTel(str(d.get("tel"))); + if (d.containsKey("email")) r.setEmail(str(d.get("email"))); + if (d.containsKey("zipcode")) r.setZipcode(str(d.get("zipcode"))); + if (d.containsKey("address")) r.setAddress(str(d.get("address"))); + if (d.containsKey("addressDetail")) r.setAddressDetail(str(d.get("addressDetail"))); + if (d.containsKey("paymentMethod")) r.setPaymentMethod(str(d.get("paymentMethod"))); + if (d.containsKey("productType")) r.setProductType(str(d.get("productType"))); + if (d.containsKey("installSchedule")) r.setInstallSchedule(str(d.get("installSchedule"))); + if (d.containsKey("customerLoginId")) r.setCustomerLoginId(str(d.get("customerLoginId"))); + if (d.containsKey("newTel")) r.setNewTel(str(d.get("newTel"))); + if (d.containsKey("cancelDate")) r.setCancelDate(parseDate(str(d.get("cancelDate")))); + if (d.containsKey("branchPhone")) r.setBranchPhone(str(d.get("branchPhone"))); + } + + private Map toProgressDetail(HomesReception r, JwtUser u) { + Map data = new LinkedHashMap<>(); + Map reception = toMap(r); + data.put("reception", reception); + data.put("order", reception); // billing_react 호환 별칭 + data.put("id", r.getId()); + + Map customer = new LinkedHashMap<>(); + customer.put("type", blank(r.getCustomerType()) ? "public" : r.getCustomerType()); + customer.put("name", r.getCustomerName()); + customer.put("postIt", r.getPostIt()); + customer.put("postAdd", r.getPostAdd()); + customer.put("phone", r.getPhone()); + customer.put("tel", r.getTel()); + customer.put("email", r.getEmail()); + customer.put("zipcode", r.getZipcode()); + customer.put("address", r.getAddress()); + customer.put("addressDetail", r.getAddressDetail()); + customer.put("sido", r.getRegion()); + customer.put("ssid", r.getIdentifyNumber()); + data.put("customer", customer); + data.put("customerPublic", Map.of("ssid", nullToEmpty(r.getIdentifyNumber()))); + + Map payment = new LinkedHashMap<>(); + payment.put("method", blank(r.getPaymentMethod()) ? paymentMethodFromType(r.getPaymentType()) : r.getPaymentMethod()); + payment.put("name", r.getPaymentName()); + payment.put("ssid", r.getPaymentSsid()); + payment.put("tel", r.getPaymentTel()); + payment.put("bank", r.getPaymentBank()); + payment.put("accountNumber", r.getPaymentAccountNumber()); + payment.put("cardCompany", r.getPaymentCardCompany()); + payment.put("cardYear", r.getPaymentCardYear()); + payment.put("cardMonth", r.getPaymentCardMonth()); + payment.put("cardNumber", r.getPaymentCardNumber()); + data.put("payment", payment); + if ("card".equals(payment.get("method"))) data.put("paymentCard", payment); + else if ("transfer".equals(payment.get("method"))) data.put("paymentTransfer", payment); + + Map product = new LinkedHashMap<>(); + product.put("productType", blank(r.getProductType()) ? productTypeFromCategory(r.getProductCategory()) : r.getProductType()); + product.put("name", r.getProductName()); + product.put("productName", r.getProductName()); + product.put("price", r.getAmount() == null ? 0 : r.getAmount()); + data.put("product", product); + data.put("productName", r.getProductName()); + data.put("agreementName", r.getAgreementName()); + data.put("ispName", blank(r.getIspName()) ? r.getCarrier() : r.getIspName()); + data.put("eventNames", List.of()); + + Map gift = new LinkedHashMap<>(); + gift.put("status", r.getGiftStatus()); + gift.put("place", r.getGiftPlace()); + gift.put("giftDate", r.getGiftDate() == null ? null : r.getGiftDate().toString()); + gift.put("requested", r.getGiftRequested() == null ? null : r.getGiftRequested().toString()); + gift.put("sended", r.getGiftSended() == null ? null : r.getGiftSended().toString()); + gift.put("accountBank", r.getGiftBank()); + gift.put("accountNumber", r.getGiftAccountNumber()); + gift.put("accountName", r.getGiftAccountName()); + gift.put("ssid", r.getGiftSsid()); + gift.put("goods", r.getGiftGoods()); + gift.put("cashAmount", r.getGiftCashAmount()); + gift.put("goodsPrice", r.getGiftGoodsPrice()); + gift.put("category", r.getGiftCategory()); + data.put("gift", gift); + + Map etc = new LinkedHashMap<>(); + etc.put("memo", r.getMemo()); + etc.put("adminMemo", r.getAdminMemo()); + etc.put("file1", r.getFile1()); + etc.put("file2", r.getFile2()); + etc.put("file3", r.getFile3()); + etc.put("file4", r.getFile4()); + etc.put("file5", r.getFile5()); + etc.put("adminFile1", r.getAdminFile1()); + etc.put("adminFile2", r.getAdminFile2()); + etc.put("adminFile3", r.getAdminFile3()); + data.put("etc", etc); + + data.put("employeeName", r.getCounselor()); + data.put("branchOfficeName", blank(r.getBranch()) ? "본지점" : r.getBranch()); + data.put("branchPhone", blank(r.getBranchPhone()) ? "" : r.getBranchPhone()); + data.put("branchTel", ""); + data.put("progressLogs", progressLogs(r.getId(), u.getGroupId())); + data.put("giftProgressLogs", giftProgressLogs(r.getId(), u.getGroupId())); + data.put("logs", data.get("progressLogs")); + data.put("progressStatuses", PROGRESS_STATUSES); + data.put("giftStatuses", GIFT_STATUSES); + data.put("settleStatuses", SETTLE_STATUSES); + Map statusOptions = new LinkedHashMap<>(); + for (String s : PROGRESS_STATUSES) statusOptions.put(s, s); + data.put("statusOptions", statusOptions); + Map giftStatusOptions = new LinkedHashMap<>(); + for (String s : GIFT_STATUSES) giftStatusOptions.put(s, s); + data.put("giftStatusOptions", giftStatusOptions); + // flat alias for list edit form compatibility + data.putAll(reception); + return data; + } + + private static String paymentMethodFromType(String paymentType) { + if ("카드".equals(paymentType)) return "card"; + if ("지로".equals(paymentType)) return "paper"; + if ("전화납부".equals(paymentType)) return "telephone"; + return "transfer"; + } + + private static String productTypeFromCategory(String cat) { + if ("전화".equals(cat)) return "e02"; + if ("TV".equals(cat)) return "e03"; + if ("렌탈".equals(cat)) return "e04"; + return "e01"; + } + + private Map toMap(HomesReception r) { + Map m = new LinkedHashMap<>(); + m.put("id", r.getId()); + m.put("orderDate", fmtDt(r.getOrderDate())); + m.put("today", r.getOrderDate() != null + && r.getOrderDate().toLocalDate().equals(LocalDate.now(ZoneId.of("Asia/Seoul")))); + m.put("counselor", r.getCounselor()); + m.put("branch", r.getBranch()); + m.put("branchGroup", r.getBranchGroup()); + m.put("branchPhone", r.getBranchPhone()); + m.put("carrier", r.getCarrier()); + m.put("giftCategory", r.getGiftCategory()); + m.put("productCategory", r.getProductCategory()); + m.put("productType", r.getProductType()); + m.put("productName", r.getProductName()); + m.put("customerType", r.getCustomerType()); + m.put("customerName", r.getCustomerName()); + m.put("phone", r.getPhone()); + m.put("tel", r.getTel()); + m.put("email", r.getEmail()); + m.put("zipcode", r.getZipcode()); + m.put("address", r.getAddress()); + m.put("addressDetail", r.getAddressDetail()); + m.put("region", r.getRegion()); + m.put("progressStatus", r.getProgressStatus()); + m.put("orderNumber", r.getOrderNumber()); + m.put("ispId", r.getIspId()); + m.put("ispName", r.getIspName()); + m.put("inboundInfo", r.getInboundInfo()); + m.put("processor", r.getProcessor()); + m.put("detailProgress", r.getDetailProgress()); + m.put("paymentType", r.getPaymentType()); + m.put("paymentMethod", r.getPaymentMethod()); + m.put("amount", r.getAmount()); + m.put("settleStatus", r.getSettleStatus()); + m.put("openDate", r.getOpenDate() == null ? null : r.getOpenDate().toString()); + m.put("cancelDate", r.getCancelDate() == null ? null : r.getCancelDate().toString()); + m.put("giftDate", r.getGiftDate() == null ? null : r.getGiftDate().toString()); + m.put("giftStatus", r.getGiftStatus()); + m.put("giftPlace", r.getGiftPlace()); + m.put("giftRequested", r.getGiftRequested() == null ? null : r.getGiftRequested().toString()); + m.put("giftSended", r.getGiftSended() == null ? null : r.getGiftSended().toString()); + m.put("agreementName", r.getAgreementName()); + m.put("contractNumber", r.getContractNumber()); + m.put("installSchedule", r.getInstallSchedule()); + m.put("customerLoginId", r.getCustomerLoginId()); + m.put("newTel", r.getNewTel()); + m.put("multiLine", Boolean.TRUE.equals(r.getMultiLine())); + m.put("addPayer", Boolean.TRUE.equals(r.getAddPayer())); + m.put("record", Boolean.TRUE.equals(r.getRecordFlag())); + m.put("identifyNumber", r.getIdentifyNumber()); + m.put("postIt", r.getPostIt()); + m.put("postAdd", r.getPostAdd()); + m.put("memo", r.getMemo()); + m.put("adminMemo", r.getAdminMemo()); + // billing_react OrderListRow 호환 별칭 + m.put("employeeName", r.getCounselor()); + m.put("sido", r.getRegion()); + m.put("opened", r.getOpenDate() == null ? null : r.getOpenDate().toString()); + m.put("canceled", r.getCancelDate() == null ? null : r.getCancelDate().toString()); + m.put("status", r.getProgressStatus()); + m.put("giftType", r.getGiftCategory()); + m.put("giftSended", r.getGiftSended() == null + ? (r.getGiftDate() == null ? null : r.getGiftDate().toString()) + : r.getGiftSended().toString()); + m.put("branchOfficeName", r.getBranch()); + m.put("branchGroupName", r.getBranchGroup()); + if (blank(r.getIspName())) m.put("ispName", r.getCarrier()); + BigDecimal giftAmt = BigDecimal.ZERO; + if (r.getGiftCashAmount() != null) giftAmt = giftAmt.add(r.getGiftCashAmount()); + if (r.getGiftGoodsPrice() != null) giftAmt = giftAmt.add(r.getGiftGoodsPrice()); + if (giftAmt.signum() == 0 && r.getAmount() != null + && r.getGiftStatus() != null && !"미지급".equals(r.getGiftStatus()) && !"없음".equals(r.getGiftCategory())) { + giftAmt = r.getAmount(); + } + m.put("giftAmount", giftAmt.signum() == 0 ? null : giftAmt); + m.put("centerName", blank(r.getProcessor()) ? "본사" : r.getProcessor()); + m.put("centerStatus", r.getSettleStatus()); + return m; + } + + private List> progressLogs(Long receptionId, String groupId) { + return em.createQuery( + "select l from HomesReceptionProgressLog l where l.groupId=:g and l.receptionId=:rid order by l.loggedAt desc, l.id desc", + HomesReceptionProgressLog.class) + .setParameter("g", groupId).setParameter("rid", receptionId) + .getResultList().stream().map(l -> { + Map m = new LinkedHashMap<>(); + m.put("id", l.getId()); + m.put("status", l.getStatus()); + m.put("memo", l.getMemo()); + m.put("authorName", l.getAuthorName()); + m.put("writer", l.getAuthorName()); + m.put("loggedAt", fmtDt(l.getLoggedAt())); + m.put("createdAt", fmtDt(l.getLoggedAt())); + m.put("adminOnly", Boolean.TRUE.equals(l.getAdminOnly())); + return m; + }).toList(); + } + + private List> giftProgressLogs(Long receptionId, String groupId) { + return em.createQuery( + "select l from HomesReceptionGiftProgressLog l where l.groupId=:g and l.receptionId=:rid order by l.loggedAt desc, l.id desc", + HomesReceptionGiftProgressLog.class) + .setParameter("g", groupId).setParameter("rid", receptionId) + .getResultList().stream().map(l -> { + Map m = new LinkedHashMap<>(); + m.put("id", l.getId()); + m.put("status", l.getStatus()); + m.put("memo", l.getMemo()); + m.put("authorName", l.getAuthorName()); + m.put("writer", l.getAuthorName()); + m.put("loggedAt", fmtDt(l.getLoggedAt())); + m.put("createdAt", fmtDt(l.getLoggedAt())); + return m; + }).toList(); + } + + private void appendLog(HomesReception r, String status, String memo, JwtUser u) { + appendLog(r, status, memo, u, false); + } + + private void appendLog(HomesReception r, String status, String memo, JwtUser u, boolean adminOnly) { + HomesReceptionProgressLog log = new HomesReceptionProgressLog(); + log.setGroupId(u.getGroupId()); + log.setReceptionId(r.getId()); + log.setStatus(status); + log.setMemo(memo); + log.setAuthorName(resolveMemberName(u)); + log.setLoggedAt(LocalDateTime.now(ZoneId.of("Asia/Seoul"))); + log.setAdminOnly(adminOnly); + em.persist(log); + } + + private void appendGiftLog(HomesReception r, String status, String memo, JwtUser u) { + HomesReceptionGiftProgressLog log = new HomesReceptionGiftProgressLog(); + log.setGroupId(u.getGroupId()); + log.setReceptionId(r.getId()); + log.setStatus(status); + log.setMemo(memo); + log.setAuthorName(resolveMemberName(u)); + log.setLoggedAt(LocalDateTime.now(ZoneId.of("Asia/Seoul"))); + em.persist(log); + } + + private HomesReception findOwned(Long id, JwtUser u) { + HomesReception r = em.find(HomesReception.class, id); + if (r == null || !Objects.equals(r.getGroupId(), u.getGroupId())) return null; + return r; + } + + private void writeLog(JwtUser u, String domain, String action, String content) { + HomesLog log = new HomesLog(); + log.setGroupId(u.getGroupId()); + log.setDomain(domain); + log.setAction(action); + log.setContent(content); + String staff = resolveMemberName(u); + log.setStaff(staff == null || staff.isBlank() ? u.getUserid() : staff); + log.setProcessedAt(LocalDateTime.now()); + em.persist(log); + } + + private String resolveMemberName(JwtUser u) { + List rows = em.createQuery("select m from Member m where m.groupId=:g and m.userid=:uid", Member.class) + .setParameter("g", u.getGroupId()).setParameter("uid", u.getUserid()).setMaxResults(1).getResultList(); + return rows.isEmpty() ? u.getUserid() : rows.getFirst().getName(); + } + + private static List distinct(List list, + java.util.function.Function getter, + List fallback) { + List values = list.stream().map(getter).filter(s -> s != null && !s.isBlank()) + .distinct().sorted().collect(Collectors.toCollection(ArrayList::new)); + if (values.isEmpty() && fallback != null) return fallback; + return values; + } + + private static String genOrderNumber() { + return "RCP-" + LocalDateTime.now(ZoneId.of("Asia/Seoul")) + .format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + + "-" + (1000 + new Random().nextInt(9000)); + } + + private static List toIds(Object raw) { + if (!(raw instanceof List list)) return List.of(); + List ids = new ArrayList<>(); + for (Object o : list) { + if (o == null) continue; + try { ids.add(Long.valueOf(String.valueOf(o))); } catch (Exception ignored) { } + } + return ids; + } + + private static BigDecimal toDecimal(Object v) { + if (v == null || String.valueOf(v).isBlank()) return null; + try { return new BigDecimal(String.valueOf(v).replace(",", "")); } + catch (Exception e) { return null; } + } + + private static LocalDate parseDate(String s) { + if (s == null || s.isBlank()) return null; + try { return LocalDate.parse(s.substring(0, Math.min(10, s.length()))); } + catch (Exception e) { return null; } + } + + private static LocalDateTime parseDateTime(String s) { + if (s == null || s.isBlank()) return null; + try { + if (s.length() <= 10) return LocalDate.parse(s).atStartOfDay(); + return LocalDateTime.parse(s.replace(' ', 'T').substring(0, Math.min(19, s.length()))); + } catch (Exception e) { + LocalDate d = parseDate(s); + return d == null ? null : d.atStartOfDay(); + } + } + + private static String fmtDt(LocalDateTime dt) { + if (dt == null) return null; + return dt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } + + private static String first(Map f, String... keys) { + for (String k : keys) { + String v = f.get(k); + if (v != null && !v.isBlank()) return v; + } + return null; + } + + private static String blankToNull(String s) { + return s == null || s.isBlank() ? null : s.trim(); + } + + private static boolean blank(String s) { + return s == null || s.isBlank(); + } + + private static String nullToEmpty(String s) { + return s == null ? "" : s; + } + + private static String str(Object v) { + return v == null ? null : String.valueOf(v).trim(); + } + + private static String firstNonBlank(String... values) { + if (values == null) return null; + for (String v : values) { + if (v != null && !v.isBlank()) return v.trim(); + } + return null; + } + + private static String csvCell(Object value) { + if (value == null) return ""; + String s = String.valueOf(value).replace("\"", "\"\""); + if (s.contains(",") || s.contains("\"") || s.contains("\n")) return "\"" + s + "\""; + return s; + } +} diff --git a/backend/src/main/java/com/bizcare/agent/DataInitializer.java b/backend/src/main/java/com/bizcare/agent/DataInitializer.java new file mode 100644 index 0000000..972b036 --- /dev/null +++ b/backend/src/main/java/com/bizcare/agent/DataInitializer.java @@ -0,0 +1,2965 @@ +package com.bizcare.agent; + +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +class DataInitializer implements CommandLineRunner { + private final EntityManager em; + private final PasswordEncoder encoder; + + @Override + @Transactional + public void run(String... args) { + if (em.createQuery("select m from Member m where m.userid='demo'", Member.class).getResultList().isEmpty()) { + seedAll(); + } + seedUsimLedgersIfEmpty(); + seedStockHistoryIfEmpty(); + seedIndocsEnrich(); + seedReturnsEnrich(); + seedBalancesIfEmpty(); + seedOpenAdjustFields(); + seedUnitCostsIfEmpty(); + seedHomeSalesIfEmpty(); + seedCareHomesIfEmpty(); + seedHomeIndocsIfEmpty(); + seedOpenExchangesIfEmpty(); + seedOpenNameChangesIfEmpty(); + seedOpenWithdrawalsIfEmpty(); + seedOpenSettleHistoriesIfEmpty(); + seedOpenDeleteHistoriesIfEmpty(); + seedBbsNoticeBoardIfEmpty(); + seedBbsPolicyBoardIfEmpty(); + seedBbsPdsBoardIfEmpty(); + seedBoardGroupsIfEmpty(); + seedScansEnrich(); + seedSettlementDocsIfEmpty(); + seedOutcompanyAccountsIfEmpty(); + seedAgentShopSettingIfEmpty(); + seedOpenStatsDemo(); + seedFareboxSamplesIfEmpty(); + seedFareboxPaymentsIfEmpty(); + seedSmsHistoryIfEmpty(); + seedSmsAddressIfEmpty(); + seedStaffMembersIfEmpty(); + seedHomesMembersEnrich(); + seedIncompaniesIfEmpty(); + seedOutcompaniesEnrich(); + seedDeliveriesIfEmpty(); + seedPreferenceIfEmpty(); + seedCsNoticesIfEmpty(); + seedCsInquiriesIfEmpty(); + seedInternalBoardIfEmpty(); + seedScheduleSampleIfEmpty(); + seedPostitsIfEmpty(); + seedStockInfoSampleIfEmpty(); + } + + private void seedStockInfoSampleIfEmpty() { + Long exists = em.createQuery("select count(s) from Stock s where s.groupId='demo' and s.serial='0018987'", Long.class).getSingleResult(); + if (exists != null && exists > 0) return; + + Company in = em.createQuery("select c from Company c where c.groupId='demo' and c.name like '%CJ헬로%'", Company.class) + .setMaxResults(1).getResultList().stream().findFirst().orElse(null); + if (in == null) { + in = new Company(); + in.setGroupId("demo"); + in.setType("IN"); + in.setName("CJ헬로(SKT)"); + in.setManagerName("CJ담당"); + in.setPhone("02-0000-0000"); + in.setTelecom("SKT"); + in.setActive(true); + em.persist(in); + em.flush(); + } + Company out = em.createQuery("select c from Company c where c.groupId='demo' and c.name='늘푸른통신'", Company.class) + .setMaxResults(1).getResultList().stream().findFirst().orElse(null); + if (out == null) { + out = new Company(); + out.setGroupId("demo"); + out.setType("OUT"); + out.setName("늘푸른통신"); + out.setManagerName("박총괄"); + out.setPhone("010-1111-1003"); + out.setCategory("직영"); + out.setActive(true); + em.persist(out); + em.flush(); + } else if (out.getCategory() == null || out.getCategory().isBlank()) { + out.setCategory("직영"); + } + + LocalDate day = LocalDate.of(2026, 7, 9); + Stock s = new Stock(); + s.setGroupId("demo"); + s.setGubun("태블릿"); + s.setTelecom("SKT"); + s.setTelcompany("LG헬로비전"); + s.setSerial("0018987"); + s.setBarcode("0018987"); + s.setModel("SM-A175N"); + s.setColor("블랙"); + s.setCond("정상"); + s.setPay("여신"); + s.setState("OUT"); + s.setInprice(new BigDecimal("319000")); + s.setIncompanyId(in.getId()); + s.setOutcompanyId(out.getId()); + s.setOutCategory("직영"); + s.setIndate(day); + s.setOutdate(day); + s.setProcessDate(day); + s.setProcessor("김대표"); + s.setUserid("demo"); + s.setProcid("demo"); + s.setUid("stk-0018987"); + em.persist(s); + em.flush(); + + StockHistory inHist = new StockHistory(); + inHist.setGroupId("demo"); + inHist.setStockId(s.getId()); + inHist.setIncompanyId(in.getId()); + inHist.setProcessDate(day); + inHist.setGubun(s.getGubun()); + inHist.setState("IN"); + inHist.setStatusLabel("입고"); + inHist.setSerial(s.getSerial()); + inHist.setModel(s.getModel()); + inHist.setColor(s.getColor()); + inHist.setCond(s.getCond()); + inHist.setProcessor("김대표"); + inHist.setCompanyLabel(""); + em.persist(inHist); + + StockHistory outHist = new StockHistory(); + outHist.setGroupId("demo"); + outHist.setStockId(s.getId()); + outHist.setIncompanyId(in.getId()); + outHist.setOutcompanyId(out.getId()); + outHist.setProcessDate(day); + outHist.setGubun(s.getGubun()); + outHist.setState("OUT"); + outHist.setStatusLabel("출고"); + outHist.setSerial(s.getSerial()); + outHist.setModel(s.getModel()); + outHist.setColor(s.getColor()); + outHist.setCond(s.getCond()); + outHist.setProcessor("김대표"); + outHist.setCompanyLabel("늘푸른통신 (직영)"); + em.persist(outHist); + } + + private void seedAll() { + Company in = company("IN", "입고처A", "김입고", "02-1234-5678"); + Company shop = company("SHOP", "늘붉은통신", "홍길동", "010-1234-5678"); + shop.setBusinessName("늘붉은통신"); + shop.setBankAccount("국민은행 000-000-0000000 홍길동"); + company("DEALER", "데모 딜러", "박딜러", "010-2222-3333"); + Company out = company("OUT", "출고처 샘플", "이출고", "010-4444-5555"); + Company in2 = company("IN", "비즈케어 물류센터", "최물류", "02-5555-1004"); + company("OUT", "D1", "김대리", "010-1111-1001"); + company("OUT", "D2", "이대리", "010-1111-1002"); + company("OUT", "늘푸른통신", "박총괄", "010-1111-1003"); + company("OUT", "스마트모바일", "최영업", "010-1111-1004"); + company("OUT", "블루폰", "정매니저", "010-1111-1005"); + + Member agent = new Member(); + agent.setGroupId("demo"); + agent.setUserid("demo"); + agent.setPassword(encoder.encode("demo123")); + agent.setName("데모 관리자"); + agent.setPhone("010-9999-0000"); + agent.setRole("AGENT"); + agent.setActive(true); + em.persist(agent); + + Member shopUser = new Member(); + shopUser.setGroupId("demo"); + shopUser.setUserid("demoshop"); + shopUser.setPassword(encoder.encode("demoshop")); + shopUser.setName("늘붉은통신"); + shopUser.setPhone("010-1234-5678"); + shopUser.setRole("SHOP"); + shopUser.setCompanyId(shop.getId()); + shopUser.setActive(true); + shopUser.setLoginCount(0); + shopUser.setPlainPassword("demoshop"); + shopUser.setMenuFlags("{\"policy\":true,\"pds\":true,\"return\":true,\"indoc\":true,\"scan\":true,\"stock\":true,\"open\":true,\"settle\":true,\"farebox\":true}"); + em.persist(shopUser); + + Stock phone = stock("휴대폰", "SKT", "SM-S931N_256G", "아이스블루", "5477154", "OUT", in.getId(), shop.getId(), "1155000"); + Stock tablet = stock("태블릿", "KT", "SM-X910", "그레이", "TAB9001", "IN", in.getId(), null, "900000"); + Stock usim = stock("유심", "SKT", "20044", "공통색상", "10867345", "OUT", in.getId(), shop.getId(), "7700"); + String[] states = {"IN", "OUT", "RETURN", "LOSS", "OPEN", "SELL", "RECOVERY", "RECOVERY", "IN", "OUT", "RECOVERY", "RETURN", "IN", "SELL", "LOSS", "OUT", "RECOVERY"}; + String[] models = {"갤럭시 S25", "아이폰 16 Pro", "갤럭시 A36", "아이폰 16", "갤럭시 Z Flip7", "20044", "갤럭시 S25+", "아이폰 15", "갤럭시 A56"}; + String[] colors = {"티타늄 블루", "블랙", "화이트", "실버", "민트", "공통색상"}; + String[] telecoms = {"SKT", "KT", "LGU+"}; + for (int i = 0; i < states.length; i++) { + String state = states[i]; + Stock sample = stock(i % 6 == 5 ? "유심" : "휴대폰", telecoms[i % telecoms.length], models[i % models.length], + colors[i % colors.length], String.format("SN%010d", 240001 + i), state, i % 2 == 0 ? in.getId() : in2.getId(), + state.equals("IN") ? null : (i % 2 == 0 ? shop.getId() : out.getId()), String.valueOf(7700 + i * 85000)); + sample.setPetname(new String[]{"S25특가", "아이폰프로", "보급형", "유심재고"}[i % 4]); + sample.setOutCategory(i % 2 == 0 ? "판매점" : "도매"); + sample.setSalesperson(new String[]{"김영업", "이영업", "박영업"}[i % 3]); + sample.setSource(i % 2 == 0 ? "본사" : "온라인"); + sample.setProcessDate(LocalDate.now().minusDays(i % 12)); + sample.setProcessor(new String[]{"데모 관리자", "김대리", "이과장"}[i % 3]); + sample.setCond(state.equals("RECOVERY") ? "고품" : "정상"); + sample.setMemo(i % 4 == 0 ? "검수 완료" : (state.equals("RECOVERY") ? "회수대기" : null)); + sample.setIndate(LocalDate.now().minusDays(i + 1)); + if (state.equals("RECOVERY")) { + sample.setOutcompanyId(shop.getId()); + sample.setOutdate(LocalDate.now().minusDays(i + 3)); + sample.setRecalldate(null); + } + } + + OpenRecord firstOpen = null; + String[][] openSamples = { + {"할부", "SKT", "보상", "김*철", "010-****-5678", "319000", "20000", "5000"}, + {"현금", "KT", "신규", "이*수", "010-****-1234", "450000", "35000", "0"}, + {"유심", "LGU+", "번호이동", "박*영", "010-****-9876", "0", "15000", "0"}, + {"할부", "SKT", "기변", "최*민", "010-****-4321", "890000", "40000", "10000"}, + {"현금", "KT", "메이징", "정*아", "010-****-2468", "120000", "8000", "0"}, + {"할부", "SKT", "가개통", "한*진", "010-****-1357", "550000", "25000", "3000"}, + {"유심", "LGU+", "선불개통", "오*희", "010-****-8642", "0", "5000", "0"}, + {"현금", "SKT", "신규", "윤*호", "010-****-7531", "280000", "18000", "0"}, + }; + for (int i = 0; i < openSamples.length; i++) { + String[] s = openSamples[i]; + OpenRecord open = new OpenRecord(); + open.setGroupId("demo"); + open.setOutcompanyId(shop.getId()); + open.setIncompanyId(in.getId()); + open.setGubun(s[0]); + open.setTelecom(s[1]); + open.setOpenhow(s[2]); + open.setName(s[3]); + open.setOpenphone(s[4]); + open.setOpendate(LocalDate.now().minusDays(i)); + open.setOpenHour(10 + (i % 8)); + open.setOpenMinute(i % 2 == 0 ? 0 : 30); + open.setOpenStatus("정상"); + open.setOutCategory("판매점"); + open.setSalesperson(i % 2 == 0 ? "김영업" : "이영업"); + open.setEmployee("demo"); + open.setPlan(i % 2 == 0 ? "5GX 프라임" : "LTE 슬림"); + open.setSalemon("할부".equals(s[0]) ? 24 : 0); + open.setAgreemon(24); + open.setSalehow(s[0]); + open.setJoinhow("일반"); + open.setSupportType(i % 3 == 0 ? "선택약정" : "일반"); + open.setPmodel(i % 2 == 0 ? "SM-S928N" : "A2860"); + open.setPserial("할부".equals(s[0]) || "현금".equals(s[0]) ? "IMEI" + (100000 + i) : null); + open.setPcolor("블랙"); + open.setUserial("유심".equals(s[0]) || i % 2 == 0 ? "USIM" + (200000 + i) : null); + open.setInprice(new BigDecimal(s[5])); + open.setNetprice(new BigDecimal(s[5])); + open.setSellprice(new BigDecimal(s[5])); + open.setDealermargin1(new BigDecimal(s[6])); + open.setDeclprice1(new BigDecimal(s[7])); + open.setCommonSupport(new BigDecimal(i % 2 == 0 ? "300000" : "150000")); + open.setBasicprice1(new BigDecimal("100000")); + open.setBasicprice2(new BigDecimal("50000")); + open.setBasicprice3(new BigDecimal("30000")); + open.setDecltype(i % 2 == 0 ? "단말할인" : "요금할인"); + open.setState("OPEN"); + open.setUserid("demo"); + open.setUid("open-" + UUID.randomUUID()); + if (i == 0) { + open.setPhoneStockId(phone.getId()); + open.setPserial(phone.getSerial()); + open.setPmodel(phone.getModel()); + open.setPcolor(phone.getColor()); + } + em.persist(open); + if (i == 0) { + firstOpen = open; + phone.setOpenId(open.getId()); + phone.setState("OPEN"); + } + } + OpenRecord open = firstOpen; + + // shop/policy/pds boards are seeded in dedicated seedBbs* methods + + ScanDoc scan = new ScanDoc(); + scan.setGroupId("demo"); + scan.setOutcompanyId(shop.getId()); + scan.setIncompanyId(in.getId()); + scan.setOutcompanyName(shop.getName()); + scan.setTelcompany("SKT"); + scan.setGubun("보상기변"); + scan.setName("홍길동/010-1234-5678"); + scan.setFilesCount(1); + scan.setMemo("123456"); + scan.setSendmsg("미비"); + scan.setState("완료"); + scan.setSrc("2"); + scan.setRegisteredAt(java.time.LocalDateTime.now().withHour(13).withMinute(32).withSecond(0).withNano(0)); + em.persist(scan); + + IncompleteDoc indoc = new IncompleteDoc(); + indoc.setGroupId("demo"); + indoc.setOutcompanyId(shop.getId()); + indoc.setIncompanyId(in.getId()); + indoc.setOpenId(open.getId()); + indoc.setOpendate(LocalDate.now()); + indoc.setTelecom("SKT"); + indoc.setCustomerName("김*철"); + indoc.setOpenphone("010-****-5678"); + indoc.setMissingDocs("신분증사본"); + indoc.setDeductAmount(new BigDecimal("30000")); + indoc.setDeductReason("서류미비"); + indoc.setDeadline(LocalDate.now().plusDays(7)); + indoc.setStatus("미비"); + em.persist(indoc); + + IncompleteDoc indoc2 = new IncompleteDoc(); + indoc2.setGroupId("demo"); + indoc2.setOutcompanyId(shop.getId()); + indoc2.setIncompanyId(in.getId()); + indoc2.setOpendate(LocalDate.now().minusDays(3)); + indoc2.setTelecom("KT"); + indoc2.setCustomerName("이*수"); + indoc2.setOpenphone("010-****-1234"); + indoc2.setMissingDocs("인감증명서, 가족관계증명서"); + indoc2.setDeductAmount(new BigDecimal("50000")); + indoc2.setDeductReason("추가서류 미제출"); + indoc2.setDeadline(LocalDate.now().plusDays(3)); + indoc2.setStatus("미비"); + em.persist(indoc2); + + IncompleteDoc indoc3 = new IncompleteDoc(); + indoc3.setGroupId("demo"); + indoc3.setOutcompanyId(out.getId()); + indoc3.setIncompanyId(in2.getId()); + indoc3.setOpendate(LocalDate.now().minusDays(10)); + indoc3.setTelecom("LGU+"); + indoc3.setCustomerName("박*영"); + indoc3.setOpenphone("010-****-9876"); + indoc3.setMissingDocs("위임장"); + indoc3.setDeductAmount(new BigDecimal("20000")); + indoc3.setDeductReason("대리개통 서류"); + indoc3.setDeadline(LocalDate.now().minusDays(1)); + indoc3.setStatus("마감임박"); + em.persist(indoc3); + + UnreturnedPhone ret = new UnreturnedPhone(); + ret.setGroupId("demo"); + ret.setOutcompanyId(shop.getId()); + ret.setIncompanyId(in.getId()); + ret.setOpenId(open.getId()); + ret.setOpendate(LocalDate.now().minusDays(10)); + ret.setTelecom("SKT"); + ret.setCustomerName("이*영"); + ret.setOpenphone("010-****-1111"); + ret.setModel("SM-A175N"); + ret.setSerial("0018987"); + ret.setDeductAmount(new BigDecimal("50000")); + ret.setReturnStatus("미반납"); + em.persist(ret); + + String[][] returnSamples = { + {"KT", "박*지", "010-****-6595", "미반납", "A125", "50000", "0"}, + {"SKT", "홍*동", "010-****-2222", "반납", "sm-g220", "1234", "10000"}, + {"LGU+", "김*수", "010-****-3344", "미반납", "SM-S928N", "IMEI9001", "30000"}, + {"SKT", "최*아", "010-****-7788", "반납", "A2860", "SN7788", ""}, + {"KT", "정*호", "010-****-9900", "미반납", "SM-A546", "A5469900", "15000"}, + }; + for (int i = 0; i < returnSamples.length; i++) { + String[] s = returnSamples[i]; + UnreturnedPhone row = new UnreturnedPhone(); + row.setGroupId("demo"); + row.setOutcompanyId(i % 2 == 0 ? shop.getId() : out.getId()); + row.setIncompanyId(i % 2 == 0 ? in.getId() : in2.getId()); + row.setOpendate(LocalDate.now().minusDays(5 + i * 3)); + row.setTelecom(s[0]); + row.setCustomerName(s[1]); + row.setOpenphone(s[2]); + row.setReturnStatus(s[3]); + row.setModel(s[4]); + row.setSerial(s[5]); + if (!s[6].isBlank()) row.setDeductAmount(new BigDecimal(s[6])); + if ("반납".equals(s[3])) row.setReturndate(LocalDate.now().minusDays(i)); + em.persist(row); + } + + SettlementMonth settle = new SettlementMonth(); + settle.setGroupId("demo"); + settle.setOutcompanyId(shop.getId()); + settle.setOutcompanyName(shop.getName()); + settle.setYearMonth(LocalDate.now().toString().substring(0, 7)); + settle.setOpenCount(0); + settle.setOpenAmount(BigDecimal.ZERO); + settle.setAfterSettleCount(1); + settle.setAfterSettleAmount(new BigDecimal("-100000")); + settle.setHomeProductCount(0); + settle.setHomeProductAmount(BigDecimal.ZERO); + settle.setRealSettleAmount(new BigDecimal("-100000")); + settle.setOutCategory("판매점"); + settle.setStatus("READY"); + settle.setIssued(false); + settle.setMemo(""); + em.persist(settle); + + Farebox fare = new Farebox(); + fare.setGroupId("demo"); + fare.setOutcompanyId(shop.getId()); + fare.setOutcompanyName(shop.getName()); + fare.setOutCategory("판매점"); + fare.setFaredate(LocalDate.now()); + fare.setTelcompany("SKT"); + fare.setName("홍*동"); + fare.setPhone("010-****-5678"); + fare.setHow("수납"); + fare.setPaystate("완결"); + fare.setCashprice(new BigDecimal("50000")); + fare.setCardprice(BigDecimal.ZERO); + fare.setDetail("요금수납"); + fare.setRegistrant("김대표"); + fare.setReceiptNo("R-001"); + em.persist(fare); + + AccountEntry acc = new AccountEntry(); + acc.setGroupId("demo"); + acc.setOutcompanyId(shop.getId()); + acc.setEntryDate(LocalDate.now()); + acc.setType("IN"); + acc.setCategory("정산입금"); + acc.setAmount(new BigDecimal("110000")); + acc.setMemo("월정산"); + acc.setYearMonth(LocalDate.now().toString().substring(0, 7)); + em.persist(acc); + + for (String telecom : new String[]{"SKT", "KT", "LGU+"}) { + PlanMaster p = new PlanMaster(); + p.setGroupId("demo"); + p.setTelecom(telecom); + p.setName(telecom + " 5G 베이직"); + p.setPrice(new BigDecimal("55000")); + p.setActive(true); + em.persist(p); + } + + PhoneModel model = new PhoneModel(); + model.setGroupId("demo"); + model.setTelecom("SKT"); + model.setModelCode("SM-S931N"); + model.setModelName("갤럭시 S25"); + model.setColorOptions("블랙,실버,아이스블루"); + model.setActive(true); + em.persist(model); + + ScheduleItem schedule = new ScheduleItem(); + schedule.setGroupId("demo"); + schedule.setUserid("demo"); + schedule.setSdate(LocalDate.now()); + schedule.setStime("10:00"); + schedule.setContents("월 정산 확인"); + em.persist(schedule); + + Alim alim = new Alim(); + alim.setGroupId("demo"); + alim.setUserid("demo"); + alim.setTitle("개통 서류 확인"); + alim.setContent("미비 서류를 확인해 주세요."); + alim.setConfirmed(false); + em.persist(alim); + + SmsTemplate tpl = new SmsTemplate(); + tpl.setGroupId("demo"); + tpl.setTitle("개통안내"); + tpl.setBody("안녕하세요. 개통이 완료되었습니다."); + em.persist(tpl); + + CsInquiry cs = new CsInquiry(); + cs.setGroupId("demo"); + cs.setUserid("demo"); + cs.setAuthorName("김대표"); + cs.setTitle("정산 문의"); + cs.setContent("이번달 실정산금 확인 부탁드립니다."); + cs.setStatus("답변대기"); + cs.setReplyCount(0); + em.persist(cs); + + Setting setting = new Setting(); + setting.setGroupId("demo"); + setting.setSettingKey("company"); + setting.setValue("{\"name\":\"에이전트 데모\",\"phone\":\"010-0000-0000\",\"managerName\":\"김대표\",\"businessName\":\"에이전트 데모\",\"businessNumber\":\"123-45-67890\",\"ceo\":\"김유신\",\"bizType\":\"도소매\",\"bizItem\":\"휴대폰판매\",\"address\":\"대구시 수성구 수성4가 먼길\"}"); + em.persist(setting); + + tablet.getId(); + usim.getId(); + } + + private void seedUsimLedgersIfEmpty() { + Map outs = new LinkedHashMap<>(); + for (Company c : em.createQuery("select c from Company c where c.groupId='demo' and c.type in ('OUT','SHOP')", Company.class).getResultList()) { + outs.put(c.getName(), c.getId()); + } + for (String name : List.of("D1", "D2", "늘푸른통신", "스마트모바일", "블루폰")) { + if (outs.containsKey(name)) continue; + Company c = company("OUT", name, "담당자", "010-0000-0000"); + em.flush(); + outs.put(c.getName(), c.getId()); + } + if (outs.isEmpty()) return; + Object[][] samples = { + {LocalDate.of(2025, 7, 20), "D1", "SKT", 100, "770000", "YENZI", "김대표"}, + {LocalDate.of(2025, 7, 18), "D2", "KT", 50, "385000", "", "정총괄"}, + {LocalDate.of(2025, 7, 15), "늘푸른통신", "LGT", 20, "154000", "333", "김대표"}, + {LocalDate.of(2025, 7, 10), "스마트모바일", "SKT", 5, "38500", "", "이과장"}, + {LocalDate.of(2025, 6, 28), "블루폰", "KT", 8, "61600", "유심일괄", "정총괄"}, + {LocalDate.of(2025, 6, 20), "늘붉은통신", "SK텔링크", 12, "92400", "", "김대표"}, + {LocalDate.of(2025, 6, 12), "출고처 샘플", "SKT", 15, "115500", "프로모션", "데모 관리자"}, + {LocalDate.of(2025, 5, 30), "늘푸른통신", "KT", 10, "77000", "", "정총괄"}, + {LocalDate.of(2025, 5, 22), "D1", "LGT", 7, "53900", "YENZI", "이과장"}, + {LocalDate.of(2025, 5, 10), "D2", "SKT", 25, "192500", "", "김대표"}, + {LocalDate.of(2025, 4, 18), "스마트모바일", "KT", 3, "23100", "333", "정총괄"}, + {LocalDate.of(2025, 4, 5), "블루폰", "LGT", 32, "246400", "", "김대표"}, + }; + for (Object[] s : samples) { + LocalDate date = (LocalDate) s[0]; + String outName = String.valueOf(s[1]); + Long outId = outs.get(outName); + if (outId == null) continue; + Long exists = em.createQuery( + "select count(e) from UsimLedger e where e.groupId='demo' and e.tradeDate=:d and e.outcompanyId=:o and e.qty=:q", Long.class) + .setParameter("d", date).setParameter("o", outId).setParameter("q", (Integer) s[3]).getSingleResult(); + if (exists != null && exists > 0) continue; + UsimLedger row = new UsimLedger(); + row.setGroupId("demo"); + row.setOutcompanyId(outId); + row.setTradeDate(date); + row.setTelecom(String.valueOf(s[2])); + row.setQty((Integer) s[3]); + row.setAmount(new BigDecimal(String.valueOf(s[4]))); + String detail = String.valueOf(s[5]); + row.setDetail(detail.isBlank() ? null : detail); + row.setRegistrant(String.valueOf(s[6])); + row.setStatus("정상거래"); + em.persist(row); + } + } + + private void seedStockHistoryIfEmpty() { + Long count = em.createQuery("select count(e) from StockHistory e", Long.class).getSingleResult(); + if (count != null && count > 0) return; + List stocks = em.createQuery("select s from Stock s where s.groupId='demo'", Stock.class).getResultList(); + for (Stock s : stocks) { + StockHistory h = new StockHistory(); + h.setGroupId("demo"); + h.setStockId(s.getId()); + h.setIncompanyId(s.getIncompanyId()); + h.setOutcompanyId(s.getOutcompanyId()); + h.setProcessDate(s.getProcessDate() != null ? s.getProcessDate() : (s.getIndate() != null ? s.getIndate() : LocalDate.now())); + h.setGubun(s.getGubun()); + h.setState(s.getState()); + h.setStatusLabel(switch (s.getState() == null ? "" : s.getState()) { + case "IN" -> "입고"; + case "OUT" -> "출고"; + case "RETURN" -> "반품"; + case "LOSS" -> "분실"; + case "OPEN" -> "개통"; + case "SELL" -> "판매"; + case "RECOVERY" -> "회수대기"; + default -> s.getState(); + }); + h.setSerial(s.getSerial()); + h.setModel(s.getModel()); + h.setColor(s.getColor()); + h.setCond(s.getCond()); + h.setMemo(s.getMemo()); + h.setProcessor(s.getProcessor() != null ? s.getProcessor() : "데모 관리자"); + h.setSalesperson(s.getSalesperson()); + em.persist(h); + if (s.getIndate() != null && s.getProcessDate() != null && !s.getIndate().equals(s.getProcessDate())) { + StockHistory in = new StockHistory(); + in.setGroupId("demo"); + in.setStockId(s.getId()); + in.setIncompanyId(s.getIncompanyId()); + in.setProcessDate(s.getIndate()); + in.setGubun(s.getGubun()); + in.setState("IN"); + in.setStatusLabel("입고"); + in.setSerial(s.getSerial()); + in.setModel(s.getModel()); + in.setColor(s.getColor()); + in.setCond(s.getCond()); + in.setProcessor("데모 관리자"); + in.setSalesperson(s.getSalesperson()); + em.persist(in); + } + } + } + + private void seedOpenExchangesIfEmpty() { + Long count = em.createQuery("select count(e) from OpenExchange e where e.groupId='demo'", Long.class).getSingleResult(); + if (count > 0) return; + List shops = em.createQuery("select c from Company c where c.groupId='demo' and c.type in ('SHOP','OUT')", Company.class).getResultList(); + if (shops.isEmpty()) return; + Object[][] samples = { + {"단말기", "홍*동", "010-****-5678", "SM-A235N", "블랙", "77777", "SM-A235N", "블랙", "88888", "김처리", "교품완료"}, + {"단말기", "김*수", "010-****-1234", "SM-S911N", "크림", "11111", "SM-S908N", "그린", "22222", "이처리", null}, + {"유심", "박*지", "010-****-9988", "USIM-KT", "-", "U9001", "USIM-KT", "-", "U8001", "demo", "유심교체"}, + {"단말기", "이*호", "010-****-3344", "LM-G820N", "티탄", "33333", "LM-V500N", "화이트", "44444", "최처리", null}, + {"단말기", "최*아", "010-****-5566", "SM-A546S", "라벤더", "55555", "SM-A536S", "어썸블랙", "66666", "김처리", "색상변경"}, + {"유심", "정*민", "010-****-7788", "USIM-SKT", "-", "U7002", "USIM-SKT", "-", "U6002", "demo", null}, + {"단말기", "윤*희", "010-****-1122", "IPHONE15", "블루", "A1001", "IPHONE14", "미드나잇", "A0901", "박처리", null}, + {"단말기", "강*우", "010-****-4455", "SM-F946N", "그레이", "F1001", "SM-F936N", "그린", "F0901", "이처리", "폴드교품"}, + {"단말기", "조*연", "010-****-6677", "SM-A256N", "라이트블루", "A2561", "SM-A245N", "블랙", "A2451", "demo", null}, + {"유심", "한*석", "010-****-8899", "USIM-LGU", "-", "U5003", "USIM-LGU", "-", "U4003", "최처리", null}, + {"단말기", "오*진", "010-****-2211", "SM-S921N", "그레이", "S9211", "SM-S918N", "크림", "S9181", "김처리", null}, + {"단말기", "서*영", "010-****-3434", "LM-Q920N", "오로라", "Q9201", "LM-Q730N", "모로칸블루", "Q7301", "demo", "메모있음"}, + {"단말기", "문*태", "010-****-5656", "SM-A156N", "라이트그레이", "A1561", "SM-A145N", "블랙", "A1451", "박처리", null}, + {"유심", "배*리", "010-****-7878", "USIM-KT", "-", "U3004", "USIM-KT", "-", "U2004", "이처리", null}, + {"단말기", "신*호", "010-****-9090", "SM-G991N", "팬텀그레이", "G9911", "SM-G990N", "화이트", "G9901", "demo", null}, + {"단말기", "유*나", "010-****-1313", "IPHONE13", "핑크", "I1301", "IPHONE12", "퍼플", "I1201", "최처리", "고객요청"}, + }; + for (int i = 0; i < samples.length; i++) { + Object[] s = samples[i]; + Company shop = shops.get(i % shops.size()); + OpenExchange e = new OpenExchange(); + e.setGroupId("demo"); + e.setOutcompanyId(shop.getId()); + e.setOutcompanyName(shop.getName()); + e.setKind(String.valueOf(s[0])); + e.setCustomerName(String.valueOf(s[1])); + e.setOpenphone(String.valueOf(s[2])); + e.setNewModel(String.valueOf(s[3])); + e.setNewColor(String.valueOf(s[4])); + e.setNewSerial(String.valueOf(s[5])); + e.setOldModel(String.valueOf(s[6])); + e.setOldColor(String.valueOf(s[7])); + e.setOldSerial(String.valueOf(s[8])); + e.setProcessor(String.valueOf(s[9])); + e.setMemo(s[10] == null ? null : String.valueOf(s[10])); + e.setExchangeDate(LocalDate.now().minusDays(i)); + e.setOpenDate(LocalDate.now().minusDays(i + 10)); + e.setProcessedAt(java.time.LocalDateTime.now().minusDays(i).withHour(10 + (i % 8)).withMinute((i * 7) % 60).withSecond(0).withNano(0)); + em.persist(e); + } + } + + private void seedOpenNameChangesIfEmpty() { + Long count = em.createQuery("select count(e) from OpenNameChange e where e.groupId='demo'", Long.class).getSingleResult(); + if (count > 0) return; + List shops = em.createQuery("select c from Company c where c.groupId='demo' and c.type in ('SHOP','OUT')", Company.class).getResultList(); + if (shops.isEmpty()) return; + Object[][] samples = { + {"지*이", "010-****-1234", "김*수", "010-****-9999", "usim (0012345)", "김처리", "명의변경"}, + {"박*지", "010-****-5678", "이*호", "010-****-1111", "-", "이처리", null}, + {"최*아", "010-****-3344", "정*민", "010-****-2222", "usim (0098765)", "demo", null}, + {"윤*희", "010-****-7788", "한*석", "010-****-3333", "-", "최처리", "가족명의"}, + {"강*우", "010-****-5566", "오*진", "010-****-4444", "usim (0054321)", "박처리", null}, + {"조*연", "010-****-9900", "서*영", "010-****-5555", "-", "demo", null}, + {"문*태", "010-****-1122", "배*리", "010-****-6666", "usim (0077001)", "김처리", "법인전환"}, + {"신*호", "010-****-4455", "유*나", "010-****-7777", "-", "이처리", null}, + }; + for (int i = 0; i < samples.length; i++) { + Object[] s = samples[i]; + Company shop = shops.get(i % shops.size()); + OpenNameChange e = new OpenNameChange(); + e.setGroupId("demo"); + e.setOutcompanyId(shop.getId()); + e.setOutcompanyName(shop.getName()); + e.setCustomerName(String.valueOf(s[0])); + e.setOpenphone(String.valueOf(s[1])); + e.setOldOwner(String.valueOf(s[2])); + e.setOldPhone(String.valueOf(s[3])); + e.setOldUsim(String.valueOf(s[4])); + e.setProcessor(String.valueOf(s[5])); + e.setMemo(s[6] == null ? null : String.valueOf(s[6])); + e.setChangeDate(LocalDate.now().minusDays(i * 3)); + e.setOpenDate(LocalDate.now().minusDays(i * 3 + 20)); + e.setProcessedAt(java.time.LocalDateTime.now().minusDays(i * 3).withHour(9 + (i % 8)).withMinute((i * 11) % 60).withSecond(0).withNano(0)); + em.persist(e); + } + } + + private void seedOpenWithdrawalsIfEmpty() { + Long count = em.createQuery("select count(e) from OpenWithdrawal e where e.groupId='demo'", Long.class).getSingleResult(); + if (count > 0) return; + List shops = em.createQuery("select c from Company c where c.groupId='demo' and c.type in ('SHOP','OUT')", Company.class).getResultList(); + if (shops.isEmpty()) return; + String[] names = {"김*수", "이*호", "박*지", "최*아", "정*민", "윤*희", "강*우", "조*연", "한*석", "오*진"}; + String[] reasons = {"고객요청", "개통오류", "서류미비", "요금미납", "중복개통", "본인취소", "정책철회", "-"}; + String[] processors = {"김처리", "이처리", "demo", "최처리", "박처리"}; + for (int i = 0; i < 30; i++) { + Company shop = shops.get(i % shops.size()); + OpenWithdrawal e = new OpenWithdrawal(); + e.setGroupId("demo"); + e.setOutcompanyId(shop.getId()); + e.setOutcompanyName(shop.getName()); + e.setCustomerName(names[i % names.length]); + e.setOpenphone(String.format("010-****-%04d", 1000 + i * 17)); + e.setProcessor(processors[i % processors.length]); + e.setReason(reasons[i % reasons.length]); + e.setWithdrawDate(LocalDate.now().minusDays(i)); + e.setOpenDate(LocalDate.now().minusDays(i + 15)); + e.setProcessedAt(java.time.LocalDateTime.now().minusDays(i).withHour(10 + (i % 8)).withMinute((i * 13) % 60).withSecond(0).withNano(0)); + em.persist(e); + } + } + + private void seedOpenSettleHistoriesIfEmpty() { + Long count = em.createQuery("select count(e) from OpenSettleHistory e where e.groupId='demo'", Long.class).getSingleResult(); + if (count > 0) return; + List shops = em.createQuery("select c from Company c where c.groupId='demo' and c.type in ('SHOP','OUT')", Company.class).getResultList(); + if (shops.isEmpty()) return; + String[] names = {"박*수", "김*한", "이*호", "최*아", "정*민", "윤*희", "강*우", "조*연"}; + String[] processors = {"김대표", "이개통", "demo", "최처리", "박처리"}; + String[] memos = {"정책변경", "단가조정", "추가정산", "차감반영", "재계산", "오류수정"}; + for (int i = 0; i < 40; i++) { + Company shop = shops.get(i % shops.size()); + long oldAmt = (i % 3 == 0 ? -1 : 1) * (50000L + (i * 17311L) % 300000); + long newAmt = oldAmt + ((i % 2 == 0 ? 1 : -1) * (10000L + (i * 7919L) % 80000)); + OpenSettleHistory e = new OpenSettleHistory(); + e.setGroupId("demo"); + e.setOutcompanyId(shop.getId()); + e.setOutcompanyName(shop.getName()); + e.setCustomerName(names[i % names.length]); + e.setOpenphone(String.format("010-****-%04d", 2000 + i * 19)); + e.setOldAmount(BigDecimal.valueOf(oldAmt)); + e.setNewAmount(BigDecimal.valueOf(newAmt)); + e.setProcessor(processors[i % processors.length]); + e.setMemo(memos[i % memos.length]); + e.setChangeDate(LocalDate.now().minusDays(i)); + e.setOpenDate(LocalDate.now().minusDays(i + 12)); + e.setProcessedAt(java.time.LocalDateTime.now().minusDays(i).withHour(9 + (i % 9)).withMinute((i * 17) % 60).withSecond(0).withNano(0)); + em.persist(e); + } + } + + private void seedOpenDeleteHistoriesIfEmpty() { + Long count = em.createQuery("select count(e) from OpenDeleteHistory e where e.groupId='demo'", Long.class).getSingleResult(); + if (count > 0) return; + List shops = em.createQuery("select c from Company c where c.groupId='demo' and c.type in ('SHOP','OUT')", Company.class).getResultList(); + if (shops.isEmpty()) return; + Object[][] samples = { + {"홍***님", "010-****-9870", "정총괄", "개통삭제(변심)"}, + {"a****s", "010-****-1234", "김대표", "개통삭제(출고처수정)"}, + {"홍*동", "010-****-5678", "이개통", "첨부파일삭제(파일명.gif)"}, + {"김*수", "010-****-3344", "demo", "개통삭제(오류)"}, + {"박*지", "010-****-7788", "최처리", "개통삭제(변심)"}, + {"이*호", "010-****-5566", "정총괄", "첨부파일삭제(계약서.pdf)"}, + {"최*아", "010-****-9900", "김대표", "개통삭제(출고처수정)"}, + {"정*민", "010-****-1122", "이개통", "개통삭제(중복)"}, + {"윤*희", "010-****-4455", "demo", "개통삭제(변심)"}, + {"강*우", "010-****-6677", "박처리", "첨부파일삭제(신분증.jpg)"}, + {"조*연", "010-****-8899", "정총괄", "개통삭제(요금미납)"}, + {"한*석", "010-****-2211", "김대표", "개통삭제(변심)"}, + {"오*진", "010-****-3434", "이개통", "개통삭제(출고처수정)"}, + {"서*영", "010-****-5656", "demo", "첨부파일삭제(동의서.png)"}, + {"문*태", "010-****-7878", "최처리", "개통삭제(오류)"}, + {"배*리", "010-****-9090", "정총괄", "개통삭제(변심)"}, + }; + for (int i = 0; i < samples.length; i++) { + Object[] s = samples[i]; + Company shop = shops.get(i % shops.size()); + OpenDeleteHistory e = new OpenDeleteHistory(); + e.setGroupId("demo"); + e.setOutcompanyId(shop.getId()); + e.setOutcompanyName(shop.getName()); + e.setCustomerName(String.valueOf(s[0])); + e.setOpenphone(String.valueOf(s[1])); + e.setProcessor(String.valueOf(s[2])); + e.setMemo(String.valueOf(s[3])); + e.setDeleteDate(LocalDate.now().minusDays(i)); + e.setOpenDate(LocalDate.now().minusDays(i + 5)); + e.setProcessedAt(java.time.LocalDateTime.now().minusDays(i).withHour(10 + (i % 8)).withMinute((i * 19) % 60).withSecond(0).withNano(0)); + em.persist(e); + } + } + + private void seedBbsNoticeBoardIfEmpty() { + List posts = em.createQuery("select b from BbsPost b where b.groupId='demo' and b.code='shop'", BbsPost.class).getResultList(); + Object[][] samples = { + {"124124", "2020-10-05"}, + {"※ 동행보고 ※ 124123n124 5", "2020-02-17"}, + {"테스트", "2019-04-09"}, + }; + for (Object[] s : samples) { + String title = String.valueOf(s[0]); + boolean exists = posts.stream().anyMatch(b -> title.equals(b.getTitle())); + if (exists) continue; + BbsPost b = new BbsPost(); + b.setGroupId("demo"); + b.setCode("shop"); + b.setTitle(title); + b.setContents("

" + title + "

"); + b.setAuthorName("김대표"); + b.setUserid("demo"); + b.setTargetGroup("전체"); + b.setConfirmCount(2); + b.setNotice(false); + b.setViews(0); + b.setState("1"); + em.persist(b); + } + for (BbsPost b : em.createQuery("select b from BbsPost b where b.groupId='demo' and b.code='shop'", BbsPost.class).getResultList()) { + if (b.getTargetGroup() == null || b.getTargetGroup().isBlank()) b.setTargetGroup("전체"); + if (b.getConfirmCount() == null) b.setConfirmCount(2); + if (b.getAuthorName() == null || b.getAuthorName().isBlank()) b.setAuthorName("김대표"); + } + } + + private void seedBbsPolicyBoardIfEmpty() { + List posts = em.createQuery("select b from BbsPost b where b.groupId='demo' and b.code='policy'", BbsPost.class).getResultList(); + Object[][] samples = { + {"31일 1차 단가표", "전체", "김대표", 1}, + {"24124", "전체", "김대표", 3}, + {"124124", "KT", "정총괄", 1}, + }; + for (Object[] s : samples) { + String title = String.valueOf(s[0]); + boolean exists = posts.stream().anyMatch(b -> title.equals(b.getTitle())); + if (exists) continue; + BbsPost b = new BbsPost(); + b.setGroupId("demo"); + b.setCode("policy"); + b.setTitle(title); + b.setContents("

" + title + "

"); + b.setAuthorName(String.valueOf(s[2])); + b.setUserid("demo"); + b.setTargetGroup(String.valueOf(s[1])); + b.setConfirmCount(((Number) s[3]).intValue()); + b.setNotice(false); + b.setViews(0); + b.setState("1"); + em.persist(b); + } + for (BbsPost b : em.createQuery("select b from BbsPost b where b.groupId='demo' and b.code='policy'", BbsPost.class).getResultList()) { + if (b.getTargetGroup() == null || b.getTargetGroup().isBlank()) b.setTargetGroup("전체"); + if (b.getConfirmCount() == null) b.setConfirmCount(1); + if (b.getAuthorName() == null || b.getAuthorName().isBlank()) b.setAuthorName("김대표"); + } + } + + private void seedBbsPdsBoardIfEmpty() { + List posts = em.createQuery("select b from BbsPost b where b.groupId='demo' and b.code='pds'", BbsPost.class).getResultList(); + Object[][] samples = { + {"24124124", "전체", "김대표", 2}, + {"123", "전체", "김대표", 2}, + }; + for (Object[] s : samples) { + String title = String.valueOf(s[0]); + boolean exists = posts.stream().anyMatch(b -> title.equals(b.getTitle())); + if (exists) continue; + BbsPost b = new BbsPost(); + b.setGroupId("demo"); + b.setCode("pds"); + b.setTitle(title); + b.setContents("

" + title + "

"); + b.setAuthorName(String.valueOf(s[2])); + b.setUserid("demo"); + b.setTargetGroup(String.valueOf(s[1])); + b.setConfirmCount(((Number) s[3]).intValue()); + b.setNotice(false); + b.setViews(0); + b.setState("1"); + em.persist(b); + } + for (BbsPost b : em.createQuery("select b from BbsPost b where b.groupId='demo' and b.code='pds'", BbsPost.class).getResultList()) { + if (b.getTargetGroup() == null || b.getTargetGroup().isBlank()) b.setTargetGroup("전체"); + if (b.getConfirmCount() == null) b.setConfirmCount(2); + if (b.getAuthorName() == null || b.getAuthorName().isBlank()) b.setAuthorName("김대표"); + } + } + + private void seedBoardGroupsIfEmpty() { + Long count = em.createQuery("select count(g) from BoardGroup g where g.groupId='demo'", Long.class).getSingleResult(); + if (count > 0) return; + List shops = em.createQuery("select c from Company c where c.groupId='demo' and c.type in ('SHOP','OUT')", Company.class).getResultList(); + String shopIds = shops.stream().map(c -> String.valueOf(c.getId())).collect(java.util.stream.Collectors.joining(",")); + Object[][] samples = { + {"기본통신사", "LG헬로비전", shopIds}, + {"기본통신사", "KT", shopIds}, + {"기본통신사", "SKT", shopIds}, + {"기본통신사", "LGU+", shopIds.isEmpty() ? "" : shops.getFirst().getId().toString()}, + {"기본통신사", "SK브로드밴드", ""}, + {"사용자그룹", "A그룹", shops.size() < 2 ? shopIds : shops.stream().limit(2).map(c -> String.valueOf(c.getId())).collect(java.util.stream.Collectors.joining(","))}, + {"사용자그룹", "테스트", ""}, + {"사용자그룹", "LG유모비", ""}, + {"사용자그룹", "B그룹", shops.size() > 1 ? shops.get(1).getId().toString() : ""}, + {"사용자그룹", "VIP", ""}, + {"사용자그룹", "도매전용", shops.isEmpty() ? "" : shops.getFirst().getId().toString()}, + }; + for (Object[] s : samples) { + BoardGroup g = new BoardGroup(); + g.setGroupId("demo"); + g.setCategory(String.valueOf(s[0])); + g.setName(String.valueOf(s[1])); + g.setCompanyIds(String.valueOf(s[2])); + em.persist(g); + } + } + + private void seedScansEnrich() { + List scans = em.createQuery("select s from ScanDoc s where s.groupId='demo'", ScanDoc.class).getResultList(); + for (ScanDoc s : scans) { + if ("DONE".equals(s.getState()) || "DONE".equalsIgnoreCase(String.valueOf(s.getState()))) s.setState("완료"); + if ("PENDING".equals(s.getState())) s.setState("대기"); + if (s.getRegisteredAt() == null) s.setRegisteredAt(s.getCreatedAt() == null ? java.time.LocalDateTime.now() : java.time.LocalDateTime.ofInstant(s.getCreatedAt(), java.time.ZoneId.of("Asia/Seoul"))); + if (s.getOutcompanyName() == null || s.getOutcompanyName().isBlank()) { + if (s.getOutcompanyId() != null) { + Company c = em.find(Company.class, s.getOutcompanyId()); + if (c != null) s.setOutcompanyName(c.getName()); + } + } + } + if (scans.size() >= 3) return; + List shops = em.createQuery("select c from Company c where c.groupId='demo' and c.type in ('SHOP','OUT')", Company.class).getResultList(); + if (shops.isEmpty()) return; + Company shop = shops.getFirst(); + Object[][] samples = { + {"완료", "SKT", "보상기변", "비즈케어/010-1234-5678", null, null, 1}, + {"접수", "SKT2", "MNP", "비즈케어", null, null, 1}, + }; + for (int i = 0; i < samples.length; i++) { + if (scans.size() + i >= 3) break; + Object[] s = samples[i]; + ScanDoc d = new ScanDoc(); + d.setGroupId("demo"); + d.setOutcompanyId(shop.getId()); + d.setOutcompanyName(shop.getName()); + d.setState(String.valueOf(s[0])); + d.setTelcompany(String.valueOf(s[1])); + d.setGubun(String.valueOf(s[2])); + d.setName(String.valueOf(s[3])); + d.setSendmsg(s[4] == null ? null : String.valueOf(s[4])); + d.setMemo(s[5] == null ? null : String.valueOf(s[5])); + d.setFilesCount(((Number) s[6]).intValue()); + d.setSrc("2"); + d.setRegisteredAt(java.time.LocalDateTime.now().minusDays(i + 1).withHour(12 + i).withMinute(1 + i * 20).withSecond(0).withNano(0)); + em.persist(d); + } + } + + private void seedSettlementDocsIfEmpty() { + String ym = LocalDate.now().minusMonths(1).toString().substring(0, 7); + Company d2 = em.createQuery("select c from Company c where c.groupId='demo' and c.name='D2'", Company.class) + .getResultList().stream().findFirst().orElse(null); + if (d2 == null) { + d2 = company("OUT", "D2", "이대리", "010-1111-1002"); + } + List prev = em.createQuery( + "select s from SettlementMonth s where s.groupId='demo' and s.yearMonth=:y", SettlementMonth.class) + .setParameter("y", ym).getResultList(); + if (prev.isEmpty()) { + SettlementMonth s = new SettlementMonth(); + s.setGroupId("demo"); + s.setOutcompanyId(d2.getId()); + s.setOutcompanyName("D2"); + s.setYearMonth(ym); + s.setOutCategory("판매점"); + s.setOpenCount(0); + s.setOpenAmount(BigDecimal.ZERO); + s.setAfterSettleCount(1); + s.setAfterSettleAmount(new BigDecimal("-100000")); + s.setHomeProductCount(0); + s.setHomeProductAmount(BigDecimal.ZERO); + s.setRealSettleAmount(new BigDecimal("-100000")); + s.setIssued(false); + s.setStatus("READY"); + s.setInquiry("-"); + s.setAgreement("-"); + s.setInvoiceStatus("-"); + s.setRemittanceStatus("-"); + em.persist(s); + } else { + // Align demo sample with original screenshot (출고처 D2 / 후정산 1 / -100,000) + SettlementMonth s = prev.getFirst(); + s.setOutcompanyId(d2.getId()); + s.setOutcompanyName("D2"); + s.setOutCategory(s.getOutCategory() == null ? "판매점" : s.getOutCategory()); + if (s.getAfterSettleCount() == null || s.getAfterSettleCount() == 0) s.setAfterSettleCount(1); + if (s.getAfterSettleAmount() == null || s.getAfterSettleAmount().compareTo(BigDecimal.ZERO) == 0) { + s.setAfterSettleAmount(new BigDecimal("-100000")); + } + if (s.getRealSettleAmount() == null || s.getRealSettleAmount().compareTo(BigDecimal.ZERO) == 0) { + s.setRealSettleAmount(s.getAfterSettleAmount()); + } + } + + for (SettlementMonth row : em.createQuery("select s from SettlementMonth s where s.groupId='demo'", SettlementMonth.class).getResultList()) { + if (row.getOutcompanyName() == null || row.getOutcompanyName().isBlank()) { + Company c = row.getOutcompanyId() == null ? null : em.find(Company.class, row.getOutcompanyId()); + if (c != null) row.setOutcompanyName(c.getName()); + } + if (row.getAfterSettleCount() == null) row.setAfterSettleCount(row.getAfterSettleAmount() == null || row.getAfterSettleAmount().compareTo(BigDecimal.ZERO) == 0 ? 0 : 1); + if (row.getOpenCount() == null) row.setOpenCount(0); + if (row.getOpenAmount() == null) row.setOpenAmount(BigDecimal.ZERO); + if (row.getHomeProductCount() == null) row.setHomeProductCount(0); + if (row.getHomeProductAmount() == null) row.setHomeProductAmount(BigDecimal.ZERO); + if (row.getInquiry() == null) row.setInquiry("-"); + if (row.getAgreement() == null) row.setAgreement("-"); + if (row.getInvoiceStatus() == null) row.setInvoiceStatus("-"); + if (row.getRemittanceStatus() == null) row.setRemittanceStatus("-"); + } + } + + private void seedOutcompanyAccountsIfEmpty() { + String menuAll = "{\"policy\":true,\"pds\":true,\"return\":true,\"indoc\":true,\"scan\":true,\"stock\":true,\"open\":true,\"settle\":true,\"farebox\":true}"; + Object[][] specs = { + {"대구 D1", "도매", "P0000", "늘명훈", "010-1111-1111", "demoshop", "demoshop", true, 0, null}, + {"대구 늘푸른통신", "도매", "P1234", "김담당", "010-2222-2222", "as001", "as001", true, 89, "2026-07-10T22:10:00"}, + {"경산 D2", "도매", "P2001", "이대리", "010-1111-1002", "d2shop", "d2shop", true, 53, "2026-07-09T18:20:00"}, + {"스마트모바일", "소매", "P3001", "최영업", "010-1111-1004", "smart01", "smart01", false, 12, "2026-06-01T10:00:00"}, + {"블루폰", "C채널", "P4001", "정매니저", "010-1111-1005", null, null, true, 0, null}, + {"출고처 샘플", "특판", "P5001", "이출고", "010-4444-5555", "sample1", "sample1", true, 100, "2026-07-08T09:15:00"}, + {"늘붉은통신", "소매", "P0001", "홍길동", "010-1234-5678", null, null, true, 0, null}, + }; + for (Object[] s : specs) { + String name = String.valueOf(s[0]); + Company c = em.createQuery("select c from Company c where c.groupId='demo' and c.name=:n", Company.class) + .setParameter("n", name).getResultList().stream().findFirst().orElse(null); + if (c == null && name.endsWith(" D1")) { + c = em.createQuery("select c from Company c where c.groupId='demo' and c.name='D1'", Company.class) + .getResultList().stream().findFirst().orElse(null); + } + if (c == null && name.endsWith(" D2")) { + c = em.createQuery("select c from Company c where c.groupId='demo' and c.name='D2'", Company.class) + .getResultList().stream().findFirst().orElse(null); + } + if (c == null && name.contains("늘푸른")) { + c = em.createQuery("select c from Company c where c.groupId='demo' and c.name='늘푸른통신'", Company.class) + .getResultList().stream().findFirst().orElse(null); + } + if (c == null) { + String type = "늘붉은통신".equals(name) ? "SHOP" : "OUT"; + c = company(type, name, String.valueOf(s[3]), String.valueOf(s[4])); + } else { + c.setName(name); + } + c.setChannel(String.valueOf(s[1])); + c.setPcode(String.valueOf(s[2])); + c.setManagerName(String.valueOf(s[3])); + c.setPhone(String.valueOf(s[4])); + if (s[5] == null) continue; + String userid = String.valueOf(s[5]); + String password = String.valueOf(s[6]); + boolean active = Boolean.TRUE.equals(s[7]); + int loginCount = ((Number) s[8]).intValue(); + Member m = em.createQuery("select m from Member m where m.groupId='demo' and m.role='SHOP' and m.companyId=:c", Member.class) + .setParameter("c", c.getId()).getResultList().stream().findFirst().orElse(null); + if (m == null) { + List byUser = em.createQuery("select m from Member m where m.userid=:id", Member.class) + .setParameter("id", userid).getResultList(); + if (!byUser.isEmpty()) { + m = byUser.getFirst(); + m.setCompanyId(c.getId()); + m.setRole("SHOP"); + } else { + m = new Member(); + m.setGroupId("demo"); + m.setRole("SHOP"); + m.setCompanyId(c.getId()); + m.setUserid(userid); + m.setPassword(encoder.encode(password)); + em.persist(m); + } + } + m.setUserid(userid); + m.setPassword(encoder.encode(password)); + m.setPlainPassword(password); + m.setName(c.getName()); + m.setPhone(c.getPhone()); + m.setActive(active); + m.setLoginCount(loginCount); + m.setMenuFlags(menuAll); + if (s[9] != null) m.setLastLoginAt(java.time.LocalDateTime.parse(String.valueOf(s[9]))); + } + for (Company c : em.createQuery("select c from Company c where c.groupId='demo' and c.type in ('OUT','SHOP')", Company.class).getResultList()) { + if (c.getChannel() == null || c.getChannel().isBlank()) c.setChannel("기타"); + if (c.getPcode() == null || c.getPcode().isBlank()) c.setPcode("P" + String.format("%04d", c.getId() == null ? 0 : c.getId())); + } + for (Member m : em.createQuery("select m from Member m where m.groupId='demo' and m.role='SHOP'", Member.class).getResultList()) { + if ((m.getPlainPassword() == null || m.getPlainPassword().isBlank()) && "demoshop".equals(m.getUserid())) { + m.setPlainPassword("demoshop"); + } + if (m.getMenuFlags() == null || m.getMenuFlags().isBlank()) m.setMenuFlags(menuAll); + if (m.getLoginCount() == null) m.setLoginCount(0); + } + } + + private void seedAgentShopSettingIfEmpty() { + Long count = em.createQuery("select count(s) from Setting s where s.groupId='demo' and s.settingKey='agentshop'", Long.class).getSingleResult(); + if (count != null && count > 0) return; + Setting s = new Setting(); + s.setGroupId("demo"); + s.setSettingKey("agentshop"); + s.setValue("{\"access\":true,\"policy\":true,\"pds\":true,\"returnPhone\":true,\"indoc\":true,\"scan\":true,\"scanReceiveSms\":false,\"scanReceivePhones\":\"\",\"scanSendSms\":false,\"scanSenderPhone\":\"1600-3903\",\"scanCompleteMsg\":\"스캔신청서가 완료 처리되었습니다.\",\"scanHoldMsg\":\"스캔신청서가 보류 처리되었습니다.\",\"stock\":true,\"stockPeriod\":\"always\",\"open\":true,\"settle\":true,\"farebox\":true,\"shopUrl\":\"https://shop.bizcare.co.kr\"}"); + em.persist(s); + } + + private void seedFareboxSamplesIfEmpty() { + Long count = em.createQuery("select count(e) from Farebox e where e.groupId='demo'", Long.class).getSingleResult(); + if (count != null && count >= 6) return; + List shops = em.createQuery("select c from Company c where c.groupId='demo' and c.type in ('SHOP','OUT') order by c.id", Company.class).getResultList(); + if (shops.isEmpty()) return; + Object[][] samples = { + {LocalDate.of(2025, 11, 16), "SKT", "대박통신", "김*현", "010-****-5555", "수납", "0", "12200", "완결", "", "김대표"}, + {LocalDate.of(2025, 5, 22), "SKT", "늘푸른통신", "1*3", "***-****-****", "수납", "653000", "0", "미결", "", "김대표"}, + {LocalDate.of(2025, 4, 10), "KT엠모바일", "늘푸른통신", "최***신", "010-****-1111", "수납", "20000", "0", "완결", "선불충전", "김대표"}, + {LocalDate.of(2025, 3, 8), "LGT", "테스트", "홍*동", "010-****-2222", "대납", "-20000", "0", "미결", "환불금", "김대표"}, + {LocalDate.of(2025, 2, 1), "SK텔링크", shops.getFirst().getName(), "박*수", "010-****-3333", "수납", "0", "50000", "완결", "요금수납", "김대표"}, + }; + for (int i = 0; i < samples.length; i++) { + Object[] s = samples[i]; + LocalDate date = (LocalDate) s[0]; + String name = String.valueOf(s[3]); + Long exists = em.createQuery("select count(e) from Farebox e where e.groupId='demo' and e.faredate=:d and e.name=:n", Long.class) + .setParameter("d", date).setParameter("n", name).getSingleResult(); + if (exists != null && exists > 0) continue; + Company shop = shops.get(i % shops.size()); + Farebox row = new Farebox(); + row.setGroupId("demo"); + row.setOutcompanyId(shop.getId()); + row.setOutcompanyName(String.valueOf(s[2])); + row.setOutCategory("판매점"); + row.setFaredate(date); + row.setTelcompany(String.valueOf(s[1])); + row.setName(name); + row.setPhone(String.valueOf(s[4])); + row.setHow(String.valueOf(s[5])); + row.setCashprice(new BigDecimal(String.valueOf(s[6]))); + row.setCardprice(new BigDecimal(String.valueOf(s[7]))); + row.setPaystate(String.valueOf(s[8])); + row.setDetail(String.valueOf(s[9])); + row.setRegistrant(String.valueOf(s[10])); + row.setReceiptNo("R-S" + (i + 1)); + em.persist(row); + } + } + + private void seedFareboxPaymentsIfEmpty() { + Long count = em.createQuery("select count(p) from FareboxPayment p where p.groupId='demo'", Long.class).getSingleResult(); + if (count != null && count >= 8) return; + List boxes = em.createQuery("select e from Farebox e where e.groupId='demo' order by e.id", Farebox.class).getResultList(); + Object[][] samples = { + {LocalDate.of(2025, 7, 12), "늘푸른통신", "일반", "653000", "이계통", "정상", "판매점"}, + {LocalDate.of(2025, 7, 10), "D1", "건별", "2500", "재고전용", "정상", "판매점"}, + {LocalDate.of(2025, 6, 20), "대박통신", "일반", "12200", "김대표", "취소됨", "판매점"}, + {LocalDate.of(2025, 5, 15), "늘푸른통신", "건별", "20000", "이계통", "정상", "판매점"}, + {LocalDate.of(2025, 4, 8), "테스트", "일반", "50000", "김대표", "정상", "판매점"}, + {LocalDate.of(2025, 3, 22), "늘붉은통신", "일반", "30000", "재고전용", "정상", "판매점"}, + {LocalDate.of(2025, 2, 14), "대박통신", "건별", "15000", "이계통", "취소됨", "판매점"}, + {LocalDate.of(2025, 1, 5), "D1", "일반", "8800", "김대표", "정상", "판매점"}, + }; + for (int i = 0; i < samples.length; i++) { + Object[] s = samples[i]; + LocalDate paydate = (LocalDate) s[0]; + String outName = String.valueOf(s[1]); + Long exists = em.createQuery( + "select count(p) from FareboxPayment p where p.groupId='demo' and p.paydate=:d and p.outcompanyName=:n and p.amount=:a", Long.class) + .setParameter("d", paydate).setParameter("n", outName).setParameter("a", new BigDecimal(String.valueOf(s[3]))) + .getSingleResult(); + if (exists != null && exists > 0) continue; + FareboxPayment p = new FareboxPayment(); + p.setGroupId("demo"); + p.setPaydate(paydate); + p.setOutcompanyName(outName); + p.setMode(String.valueOf(s[2])); + p.setAmount(new BigDecimal(String.valueOf(s[3]))); + p.setRegistrant(String.valueOf(s[4])); + p.setStatus(String.valueOf(s[5])); + p.setOutCategory(String.valueOf(s[6])); + p.setPayCount(1); + p.setMemo(null); + if (!boxes.isEmpty()) { + Farebox box = boxes.get(i % boxes.size()); + p.setOutcompanyId(box.getOutcompanyId()); + p.setFareboxIds(String.valueOf(box.getId())); + } + em.persist(p); + } + } + + private void seedSmsHistoryIfEmpty() { + Long balCount = em.createQuery("select count(s) from Setting s where s.groupId='demo' and s.settingKey='sms_balance'", Long.class).getSingleResult(); + if (balCount == null || balCount == 0) { + Setting bal = new Setting(); + bal.setGroupId("demo"); + bal.setSettingKey("sms_balance"); + bal.setValue("996"); + em.persist(bal); + } + Long count = em.createQuery("select count(e) from SmsMessage e where e.groupId='demo'", Long.class).getSingleResult(); + if (count != null && count >= 4) return; + Object[][] samples = { + {LocalDateTime.of(2026, 7, 10, 13, 40), "01011111111", "단문", "안녕하세요. 비즈케어 안내 문자입니다.", "김대표"}, + {LocalDateTime.of(2026, 7, 9, 10, 15), "01022222222", "단문", "개통 서류 안내드립니다.", "이계통"}, + {LocalDateTime.of(2026, 7, 8, 16, 5), "01033334444,01055556666", "단문", "유심 수령 안내입니다.", "김대표"}, + {LocalDateTime.of(2026, 7, 7, 9, 30), "01077778888", "단문", "요금수납 확인 부탁드립니다.", "이계통"}, + }; + for (Object[] s : samples) { + LocalDateTime ldt = (LocalDateTime) s[0]; + String phone = String.valueOf(s[1]); + Long exists = em.createQuery( + "select count(e) from SmsMessage e where e.groupId='demo' and e.phone=:p and e.message=:m", Long.class) + .setParameter("p", phone).setParameter("m", String.valueOf(s[3])).getSingleResult(); + if (exists != null && exists > 0) continue; + int sendCount = phone.split(",").length; + SmsMessage row = new SmsMessage(); + row.setGroupId("demo"); + row.setUserid("demo"); + row.setPhone(phone); + row.setMessage(String.valueOf(s[3])); + row.setStatus("발송"); + row.setMsgType(String.valueOf(s[2])); + row.setSenderNumber("1600-3903"); + row.setSenderName(String.valueOf(s[4])); + row.setSendCount(sendCount); + row.setDeductCount(sendCount); + row.setSentAt(ldt.atZone(ZoneId.systemDefault()).toInstant()); + em.persist(row); + } + } + + private void seedSmsAddressIfEmpty() { + Long gCount = em.createQuery("select count(g) from SmsAddressGroup g where g.groupId='demo'", Long.class).getSingleResult(); + if (gCount != null && gCount > 0) return; + SmsAddressGroup g1 = new SmsAddressGroup(); + g1.setGroupId("demo"); + g1.setName("동해물과"); + em.persist(g1); + em.flush(); + SmsAddressGroup g2 = new SmsAddressGroup(); + g2.setGroupId("demo"); + g2.setName("백두산이"); + em.persist(g2); + Object[][] contacts = { + {g1.getId(), "1", "01000000000"}, + {g1.getId(), "홍길동", "01015231244"}, + }; + for (Object[] s : contacts) { + SmsAddressContact c = new SmsAddressContact(); + c.setGroupId("demo"); + c.setAddressGroupId((Long) s[0]); + c.setName(String.valueOf(s[1])); + c.setPhone(String.valueOf(s[2])); + em.persist(c); + } + } + + private void seedStaffMembersIfEmpty() { + // enrich demo owner + List demos = em.createQuery("select m from Member m where m.userid='demo'", Member.class).getResultList(); + if (!demos.isEmpty()) { + Member demo = demos.getFirst(); + if (demo.getStaffPermission() == null || demo.getStaffPermission().isBlank()) { + demo.setStaffPermission("대표"); + demo.setName("김대표"); + demo.setPhone("010-1111-2222"); + if (demo.getLoginCount() == null || demo.getLoginCount() == 0) demo.setLoginCount(5032); + if (demo.getLastLoginAt() == null) demo.setLastLoginAt(LocalDateTime.now().minusMinutes(10)); + if (demo.getAssignedOutNames() == null) demo.setAssignedOutNames("늘푸른통신,대박통신"); + } + } + Long count = em.createQuery("select count(m) from Member m where m.groupId='demo' and m.role='AGENT'", Long.class).getSingleResult(); + if (count != null && count >= 8) return; + Object[][] samples = { + {"demo02", "이총괄", "총괄", "010-2222-3333", "D1,D2", 1204}, + {"demo03", "박재고", "재고", "010-3333-4444", "늘푸른통신", 856}, + {"demo04", "최개통", "개통", "010-4444-5555", "스마트모바일,블루폰", 642}, + {"demo05", "정영업", "영업", "010-5555-6666", "대박통신", 311}, + {"demo06", "한딜러", "딜러", "010-6666-7777", "출고처 샘플", 98}, + {"demo07", "오재고", "재고", "010-7777-8888", "", 45}, + {"demo08", "윤영업", "영업", "010-8888-9999", "늘붉은통신", 22}, + {"demo09", "강총괄", "총괄", "010-9999-1111", "D1", 15}, + {"demo10", "조개통", "개통", "010-1212-3434", "", 8}, + {"demo11", "임영업", "영업", "010-5656-7878", "블루폰", 3}, + {"demo12", "배수납", "영업", "010-9090-1212", "스마트모바일", 1}, + }; + for (int i = 0; i < samples.length; i++) { + Object[] s = samples[i]; + String userid = String.valueOf(s[0]); + Long exists = em.createQuery("select count(m) from Member m where m.userid=:id", Long.class) + .setParameter("id", userid).getSingleResult(); + if (exists != null && exists > 0) continue; + Member m = new Member(); + m.setGroupId("demo"); + m.setUserid(userid); + m.setPassword(encoder.encode("demo123")); + m.setPlainPassword("demo123"); + m.setName(String.valueOf(s[1])); + m.setStaffPermission(String.valueOf(s[2])); + m.setPhone(String.valueOf(s[3])); + String outs = String.valueOf(s[4]); + m.setAssignedOutNames(outs.isBlank() ? null : outs); + m.setRole("AGENT"); + m.setActive(true); + m.setLoginCount((Integer) s[5]); + m.setLastLoginAt(LocalDateTime.now().minusDays(i).minusHours(i % 5)); + em.persist(m); + } + } + + private void seedHomesMembersEnrich() { + // 홈즈 직원관리 원본 샘플 (대표 + demo1~5) + Object[][] samples = { + {"demo", "홍길동", "대표", "010-1234-1234", "대구광역시 동구 동대구로455", "", 205, + LocalDateTime.of(2026, 7, 11, 17, 53), LocalDate.of(2022, 4, 12), "데모 대표계정"}, + {"demo1", "홍총괄", "총괄", "010-1111-1111", "1", "", 6, + LocalDateTime.of(2022, 4, 13, 12, 33), LocalDate.of(2022, 4, 13), null}, + {"demo2", "홍팀장", "팀장", "010-2222-2222", "", "", 3, + LocalDateTime.of(2022, 4, 14, 10, 12), LocalDate.of(2022, 4, 13), null}, + {"demo3", "홍사원", "사원", "010-3333-3333", "", "", 2, + LocalDateTime.of(2022, 4, 13, 16, 36), LocalDate.of(2022, 4, 13), null}, + {"demo4", "홍영업", "영업", "010-4444-4444", "", "", 3, + LocalDateTime.of(2022, 4, 13, 16, 42), LocalDate.of(2022, 4, 13), null}, + {"demo5", "홍딜러", "딜러", "010-5555-5555", "", "", 1, + LocalDateTime.of(2022, 4, 13, 16, 49), LocalDate.of(2022, 4, 13), null}, + }; + java.util.Set keep = new java.util.HashSet<>(); + for (Object[] s : samples) keep.add(String.valueOf(s[0])); + for (Member old : em.createQuery("select m from Member m where m.groupId='demo' and m.role='AGENT'", Member.class).getResultList()) { + if (!keep.contains(old.getUserid())) em.remove(old); + } + em.flush(); + for (Object[] s : samples) { + String userid = String.valueOf(s[0]); + List found = em.createQuery("select m from Member m where m.userid=:id", Member.class) + .setParameter("id", userid).getResultList(); + Member m = found.isEmpty() ? new Member() : found.getFirst(); + m.setGroupId("demo"); + m.setUserid(userid); + m.setName(String.valueOf(s[1])); + m.setStaffPermission(String.valueOf(s[2])); + m.setPhone(String.valueOf(s[3])); + String addr = String.valueOf(s[4]); + m.setAddress(addr.isBlank() ? null : addr); + String limit = String.valueOf(s[5]); + m.setAccessLimit(limit.isBlank() ? null : limit); + m.setLoginCount((Integer) s[6]); + m.setLastLoginAt((LocalDateTime) s[7]); + m.setRole("AGENT"); + m.setActive(true); + if (s[9] != null) m.setMemo(String.valueOf(s[9])); + if (found.isEmpty()) { + m.setPassword(encoder.encode("demo123")); + m.setPlainPassword("demo123"); + em.persist(m); + em.flush(); + } + LocalDate reg = (LocalDate) s[8]; + if (reg != null) { + Instant at = reg.atStartOfDay(ZoneId.of("Asia/Seoul")).toInstant(); + m.setCreatedAt(at); + m.setUpdatedAt(at); + } + } + } + + private void seedIncompaniesIfEmpty() { + Long count = em.createQuery("select count(c) from Company c where c.groupId='demo' and c.type='IN'", Long.class).getSingleResult(); + Object[][] samples = { + {"디엘로(KT)", "LG헬로비전 (KT)", "02-0000-0000", "02-0000-0001", "우담당", "010-0000-0000", "디헬로우", "000-00-00000"}, + {"KT", "KT", "02-1111-1111", "02-1111-1112", "김담당", "010-1111-1111", "케이티", "111-11-11111"}, + {"SKT", "SKT", "02-2222-2222", "02-2222-2223", "이담당", "010-2222-2222", "에스케이티", "222-22-22222"}, + {"LGU+", "LGU+", "02-3333-3333", "", "박담당", "010-3333-3333", "엘지유플러스", "333-33-33333"}, + {"SK텔링크", "SK텔링크", "02-4444-4444", "", "최담당", "010-4444-4444", "텔링크", "444-44-44444"}, + {"KT엠모바일", "KT엠모바일", "02-5555-5555", "", "정담당", "010-5555-5555", "엠모바일", "555-55-55555"}, + {"비즈케어 물류센터", "SKT", "02-5555-1004", "02-5555-1005", "최물류", "010-5555-1004", "비즈케어물류", "666-66-66666"}, + {"입고처A", "KT", "02-1234-5678", "", "김입고", "010-1234-5678", "입고에이", "777-77-77777"}, + }; + if (count != null && count >= 8) { + // enrich existing IN companies missing telecom + List existing = em.createQuery("select c from Company c where c.groupId='demo' and c.type='IN'", Company.class).getResultList(); + for (Company c : existing) { + if (c.getTelecom() == null || c.getTelecom().isBlank()) { + for (Object[] s : samples) { + if (String.valueOf(s[0]).equals(c.getName())) { + c.setTelecom(String.valueOf(s[1])); + c.setFax(blankStr(String.valueOf(s[3]))); + c.setMobile(String.valueOf(s[5])); + c.setBusinessName(String.valueOf(s[6])); + c.setBusinessNumber(String.valueOf(s[7])); + break; + } + } + if (c.getTelecom() == null || c.getTelecom().isBlank()) c.setTelecom("SKT"); + } + } + return; + } + for (Object[] s : samples) { + String name = String.valueOf(s[0]); + Long exists = em.createQuery("select count(c) from Company c where c.groupId='demo' and c.type='IN' and c.name=:n", Long.class) + .setParameter("n", name).getSingleResult(); + if (exists != null && exists > 0) continue; + Company c = new Company(); + c.setGroupId("demo"); + c.setType("IN"); + c.setName(name); + c.setTelecom(String.valueOf(s[1])); + c.setPhone(String.valueOf(s[2])); + c.setFax(blankStr(String.valueOf(s[3]))); + c.setManagerName(String.valueOf(s[4])); + c.setMobile(String.valueOf(s[5])); + c.setBusinessName(String.valueOf(s[6])); + c.setBusinessNumber(String.valueOf(s[7])); + c.setActive(true); + em.persist(c); + } + } + + private String blankStr(String v) { return v == null || v.isBlank() ? null : v; } + + private void seedOutcompaniesEnrich() { + List outs = em.createQuery("select c from Company c where c.groupId='demo' and c.type in ('OUT','SHOP')", Company.class).getResultList(); + Object[][] extras = { + {"대구 D1", "도매", "판매점", "P0001", "김영업", "010-1111-1001", "대구디원", "101-01-10001", "국민 111-111-111111"}, + {"경산 D2", "도매", "판매점", "P0002", "이영업", "010-1111-1002", "경산디투", "102-02-10002", "신한 222-222-222222"}, + {"대구 늘푸른통신", "도매", "판매점", "P0003", "박영업", "010-1111-1003", "늘푸른", "103-03-10003", "우리 333-333-333333"}, + {"스마트모바일", "소매", "판매점", "P0004", "최영업", "010-1111-1004", "스마트", "104-04-10004", "하나 444-444-444444"}, + {"블루폰", "특판", "판매점", "P0005", "정영업", "010-1111-1005", "블루폰", "105-05-10005", "농협 555-555-555555"}, + {"늘붉은통신", "도매", "판매점", "P1000", "홍길동", "010-1234-5678", "늘붉은통신", "106-06-10006", "국민은행 000-000-0000000 홍길동"}, + {"출고처 샘플", "기타", "출고처", "P2000", "이출고", "010-4444-5555", "출고샘플", "107-07-10007", null}, + }; + Map byName = new LinkedHashMap<>(); + for (Object[] s : extras) byName.put(String.valueOf(s[0]), s); + for (Company c : outs) { + Object[] s = byName.get(c.getName()); + if (s != null) { + if (blankStr(c.getChannel()) == null) c.setChannel(String.valueOf(s[1])); + if (blankStr(c.getCategory()) == null) c.setCategory(String.valueOf(s[2])); + if (blankStr(c.getPcode()) == null) c.setPcode(String.valueOf(s[3])); + if (blankStr(c.getSalesperson()) == null) c.setSalesperson(String.valueOf(s[4])); + if (blankStr(c.getMobile()) == null) c.setMobile(String.valueOf(s[5])); + if (blankStr(c.getBusinessName()) == null) c.setBusinessName(String.valueOf(s[6])); + if (blankStr(c.getBusinessNumber()) == null) c.setBusinessNumber(String.valueOf(s[7])); + if (blankStr(c.getBankAccount()) == null && s[8] != null) c.setBankAccount(String.valueOf(s[8])); + } else { + if (blankStr(c.getChannel()) == null) c.setChannel("도매"); + if (blankStr(c.getCategory()) == null) c.setCategory("SHOP".equals(c.getType()) ? "판매점" : "출고처"); + if (blankStr(c.getSalesperson()) == null) c.setSalesperson(c.getManagerName()); + } + } + // ensure sample names exist + for (Object[] s : extras) { + String name = String.valueOf(s[0]); + boolean exists = outs.stream().anyMatch(c -> name.equals(c.getName())); + if (exists) continue; + Company c = new Company(); + c.setGroupId("demo"); + c.setType("OUT"); + c.setName(name); + c.setChannel(String.valueOf(s[1])); + c.setCategory(String.valueOf(s[2])); + c.setPcode(String.valueOf(s[3])); + c.setSalesperson(String.valueOf(s[4])); + c.setManagerName(String.valueOf(s[4])); + c.setPhone(String.valueOf(s[5])); + c.setMobile(String.valueOf(s[5])); + c.setBusinessName(String.valueOf(s[6])); + c.setBusinessNumber(String.valueOf(s[7])); + if (s[8] != null) c.setBankAccount(String.valueOf(s[8])); + c.setActive(true); + em.persist(c); + } + } + + private void seedDeliveriesIfEmpty() { + Long count = em.createQuery("select count(c) from Company c where c.groupId='demo' and c.type='DELIVERY'", Long.class).getSingleResult(); + if (count != null && count >= 3) return; + Object[][] samples = { + {"대한통운", "1588-1255", null}, + {"오성퀵", "000-0000-0000", null}, + {"해운퀵", "000-0000-0000", null}, + }; + for (Object[] s : samples) { + String name = String.valueOf(s[0]); + Long exists = em.createQuery("select count(c) from Company c where c.groupId='demo' and c.type='DELIVERY' and c.name=:n", Long.class) + .setParameter("n", name).getSingleResult(); + if (exists != null && exists > 0) continue; + Company c = new Company(); + c.setGroupId("demo"); + c.setType("DELIVERY"); + c.setName(name); + c.setPhone(String.valueOf(s[1])); + c.setMemo(s[2] == null ? null : String.valueOf(s[2])); + c.setActive(true); + em.persist(c); + } + } + + private void seedPreferenceIfEmpty() { + Long count = em.createQuery("select count(s) from Setting s where s.groupId='demo' and s.settingKey='preference'", Long.class).getSingleResult(); + if (count != null && count > 0) return; + String json = "{" + + "\"company\":{" + + "\"name\":\"에이전트 데모\"," + + "\"managerName\":\"김대표\"," + + "\"mobile\":\"010-0000-0000\"," + + "\"businessName\":\"에이전트 데모\"," + + "\"businessNumber\":\"123-45-67890\"," + + "\"ceo\":\"김유신\"," + + "\"bizType\":\"도소매\"," + + "\"bizItem\":\"휴대폰판매\"," + + "\"address\":\"대구시 수성구 수성4가 먼길\"" + + "}," + + "\"accessIps\":{\"enabled\":false,\"ips\":[]}," + + "\"masking\":{\"enabled\":true,\"name\":true,\"phone\":true,\"rrn\":true,\"address\":false}," + + "\"outCategories\":[\"판매점\",\"도매\"]," + + "\"stock\":{\"duplicateSerialCheck\":true,\"allowNegative\":false,\"autoRecallDays\":0}," + + "\"open\":{\"requireUsim\":true,\"requirePhone\":true,\"defaultOpenHow\":\"\",\"autoSettle\":false}" + + "}"; + Setting s = new Setting(); + s.setGroupId("demo"); + s.setSettingKey("preference"); + s.setValue(json); + em.persist(s); + } + + private void seedCsNoticesIfEmpty() { + Long count = em.createQuery("select count(b) from BbsPost b where b.groupId='demo' and b.code='cs'", Long.class).getSingleResult(); + if (count != null && count >= 20) return; + Object[][] samples = { + {"고객센터 5월 4일(월) 임시휴무안내", 6, LocalDate.of(2026, 4, 29)}, + {"지원금표기 관련 설정업데이트", 7, LocalDate.of(2025, 7, 30)}, + {"지원금표기 관련 설정업데이트 안내", 8, LocalDate.of(2025, 7, 30)}, + {"개통등록 화면 개선 안내", 12, LocalDate.of(2025, 7, 15)}, + {"문자발송 시스템 점검 안내", 18, LocalDate.of(2025, 6, 20)}, + {"요금수납 기능 업데이트 안내", 22, LocalDate.of(2025, 5, 28)}, + {"개인정보 마스킹 기능 추가 안내", 31, LocalDate.of(2025, 4, 10)}, + {"출고처 분류 설정 방법 안내", 15, LocalDate.of(2025, 3, 22)}, + {"재고관리 바코드 스캔 개선", 27, LocalDate.of(2025, 2, 14)}, + {"정산자료 엑셀다운로드 오류 수정", 19, LocalDate.of(2025, 1, 20)}, + {"연말연시 고객센터 운영안내", 45, LocalDate.of(2024, 12, 24)}, + {"에이전트샵 접속 장애 복구 안내", 38, LocalDate.of(2024, 11, 8)}, + {"보안 업데이트 적용 안내", 52, LocalDate.of(2024, 10, 15)}, + {"신규 통신사 요금제 등록 안내", 24, LocalDate.of(2024, 9, 3)}, + {"모바일 앱 버전 업데이트 안내", 41, LocalDate.of(2024, 8, 12)}, + {"스캔신청서 접수 문자 설정 안내", 16, LocalDate.of(2024, 7, 25)}, + {"서버 정기점검 안내 (7/20)", 33, LocalDate.of(2024, 7, 18)}, + {"개통내역 일괄삭제 기능 추가", 29, LocalDate.of(2024, 6, 5)}, + {"유심거래장부 메뉴 오픈 안내", 21, LocalDate.of(2024, 5, 17)}, + {"비밀번호 변경 권고 안내", 67, LocalDate.of(2024, 4, 2)}, + {"고객센터 설 연휴 휴무안내", 55, LocalDate.of(2024, 2, 8)}, + {"프로그램 이용약관 개정 안내", 48, LocalDate.of(2024, 1, 15)}, + {"엑셀 업로드 양식 변경 안내", 36, LocalDate.of(2023, 12, 1)}, + {"출고처 일괄등록 기능 추가", 28, LocalDate.of(2023, 10, 20)}, + {"알림톡 발송 서비스 오픈", 44, LocalDate.of(2023, 9, 8)}, + {"재고실사 기능 사용방법 안내", 39, LocalDate.of(2023, 8, 14)}, + {"로그인 2단계 인증 안내", 61, LocalDate.of(2023, 7, 3)}, + {"고객센터 추석 연휴 휴무안내", 50, LocalDate.of(2023, 9, 25)}, + {"개통수수료 정산 로직 변경", 34, LocalDate.of(2023, 6, 12)}, + {"브라우저 권장사양 안내", 72, LocalDate.of(2023, 5, 1)}, + }; + for (Object[] s : samples) { + String title = String.valueOf(s[0]); + Long exists = em.createQuery("select count(b) from BbsPost b where b.groupId='demo' and b.code='cs' and b.title=:t", Long.class) + .setParameter("t", title).getSingleResult(); + if (exists != null && exists > 0) continue; + BbsPost b = new BbsPost(); + b.setGroupId("demo"); + b.setCode("cs"); + b.setTitle(title); + b.setContents("

" + title + "

자세한 내용은 고객센터(1600-3903)로 문의해 주세요.

"); + b.setAuthorName("비즈케어"); + b.setUserid("system"); + b.setTargetGroup("전체"); + b.setNotice(false); + b.setViews((Integer) s[1]); + b.setConfirmCount(0); + b.setState("1"); + em.persist(b); + em.flush(); + LocalDate d = (LocalDate) s[2]; + b.setCreatedAt(d.atStartOfDay(ZoneId.of("Asia/Seoul")).toInstant()); + b.setUpdatedAt(b.getCreatedAt()); + } + } + + private void seedCsInquiriesIfEmpty() { + Long count = em.createQuery("select count(c) from CsInquiry c where c.groupId='demo'", Long.class).getSingleResult(); + // ensure sample matching original screenshot exists + Long test3 = em.createQuery("select count(c) from CsInquiry c where c.groupId='demo' and c.title='test3'", Long.class).getSingleResult(); + if (test3 == null || test3 == 0) { + CsInquiry c = new CsInquiry(); + c.setGroupId("demo"); + c.setUserid("demo"); + c.setAuthorName("김대표"); + c.setTitle("test3"); + c.setContent("테스트 문의입니다."); + c.setReply("확인되었습니다. 추가 문의사항이 있으시면 남겨주세요."); + c.setStatus("답변완료"); + c.setReplyCount(1); + em.persist(c); + em.flush(); + c.setCreatedAt(LocalDate.of(2019, 11, 5).atStartOfDay(ZoneId.of("Asia/Seoul")).toInstant()); + c.setUpdatedAt(c.getCreatedAt()); + } + if (count != null && count >= 1) { + for (CsInquiry c : em.createQuery("select c from CsInquiry c where c.groupId='demo'", CsInquiry.class).getResultList()) { + if (c.getAuthorName() == null || c.getAuthorName().isBlank()) c.setAuthorName("김대표"); + if (c.getStatus() == null || "OPEN".equalsIgnoreCase(c.getStatus())) c.setStatus("답변대기"); + if ("DONE".equalsIgnoreCase(c.getStatus())) c.setStatus("답변완료"); + if (c.getReplyCount() == null) c.setReplyCount(c.getReply() != null && !c.getReply().isBlank() ? 1 : 0); + } + } + } + + private void seedInternalBoardIfEmpty() { + Long count = em.createQuery("select count(b) from BbsPost b where b.groupId='demo' and b.code='company'", Long.class).getSingleResult(); + if (count != null && count >= 5) return; + Object[][] samples = { + {"7", "김대표", 0, LocalDate.of(2023, 6, 23), 0}, + {"업무공지", "김대표", 20, LocalDate.of(2021, 2, 15), 0}, + {"zcxvzxcv", "김대표", 6, LocalDate.of(2020, 11, 13), 0}, + {"124", "오영업", 14, LocalDate.of(2019, 8, 8), 1}, + {"124", "김대표", 8, LocalDate.of(2019, 8, 2), 0}, + }; + for (Object[] s : samples) { + String title = String.valueOf(s[0]); + String author = String.valueOf(s[1]); + LocalDate date = (LocalDate) s[3]; + Long exists = em.createQuery( + "select count(b) from BbsPost b where b.groupId='demo' and b.code='company' and b.title=:t and b.authorName=:a", Long.class) + .setParameter("t", title).setParameter("a", author).getSingleResult(); + if (exists != null && exists > 0) { + // still update date/views for matching sample + continue; + } + BbsPost b = new BbsPost(); + b.setGroupId("demo"); + b.setCode("company"); + b.setTitle(title); + b.setContents("

" + title + "

"); + b.setAuthorName(author); + b.setUserid("demo"); + b.setTargetGroup("전체"); + b.setNotice(false); + b.setViews((Integer) s[2]); + b.setCommentCount((Integer) s[4]); + b.setConfirmCount(0); + b.setState("1"); + em.persist(b); + em.flush(); + b.setCreatedAt(date.atStartOfDay(ZoneId.of("Asia/Seoul")).toInstant()); + b.setUpdatedAt(b.getCreatedAt()); + } + } + + private void seedScheduleSampleIfEmpty() { + LocalDate d = LocalDate.of(2026, 7, 11); + Long exists = em.createQuery( + "select count(s) from ScheduleItem s where s.groupId='demo' and s.sdate=:d and s.contents=:c", Long.class) + .setParameter("d", d).setParameter("c", "ㅇㄴㅁㄹㅇㄴㅁ").getSingleResult(); + if (exists != null && exists > 0) return; + ScheduleItem s = new ScheduleItem(); + s.setGroupId("demo"); + s.setUserid("demo"); + s.setSdate(d); + s.setStime("01:30"); + s.setContents("ㅇㄴㅁㄹㅇㄴㅁ"); + em.persist(s); + } + + private void seedPostitsIfEmpty() { + String json = "[" + + "{\"slot\":1,\"title\":\"포스트잇 #1\",\"contents\":\"신규 예약 개통 오예\",\"authorName\":\"김대표\",\"noteDate\":\"26-06-08\"}," + + "{\"slot\":2,\"title\":\"포스트잇 #2\",\"contents\":\"demo\",\"authorName\":\"김대표\",\"noteDate\":\"26-06-08\"}," + + "{\"slot\":3,\"title\":\"포스트잇 #3\",\"contents\":\"유심 발주 요청\",\"authorName\":\"이과장\",\"noteDate\":\"26-06-05\"}," + + "{\"slot\":4,\"title\":\"포스트잇 #4\",\"contents\":\"정산서 마감 체크\",\"authorName\":\"김대표\",\"noteDate\":\"26-05-28\"}," + + "{\"slot\":5,\"title\":\"포스트잇 #5\",\"contents\":\"스캔자료 검토\",\"authorName\":\"오영업\",\"noteDate\":\"26-05-20\"}," + + "{\"slot\":6,\"title\":\"포스트잇 #6\",\"contents\":\"정책단가 업데이트\",\"authorName\":\"정총괄\",\"noteDate\":\"26-05-12\"}," + + "{\"slot\":7,\"title\":\"포스트잇 #7\",\"contents\":\"고객센터 휴무 안내\",\"authorName\":\"김대표\",\"noteDate\":\"26-04-29\"}," + + "{\"slot\":8,\"title\":\"포스트잇 #8\",\"contents\":\"월간 리포트 작성\",\"authorName\":\"이과장\",\"noteDate\":\"26-04-15\"}" + + "]"; + List rows = em.createQuery("select s from Setting s where s.groupId='demo' and s.settingKey='postits'", Setting.class).getResultList(); + if (rows.isEmpty()) { + Setting s = new Setting(); + s.setGroupId("demo"); + s.setSettingKey("postits"); + s.setValue(json); + em.persist(s); + return; + } + Setting existing = rows.get(0); + if (existing.getValue() != null && existing.getValue().contains("재고실사")) { + existing.setValue(json); + } + } + + private void seedOpenStatsDemo() { + LocalDate today = LocalDate.now(); + LocalDate day8 = today.withDayOfMonth(Math.min(8, today.lengthOfMonth())); + LocalDate day9 = today.withDayOfMonth(Math.min(9, today.lengthOfMonth())); + List shops = em.createQuery("select c from Company c where c.groupId='demo' and c.type in ('SHOP','OUT') order by c.id", Company.class).getResultList(); + List ins = em.createQuery("select c from Company c where c.groupId='demo' and c.type='IN' order by c.id", Company.class).getResultList(); + if (shops.isEmpty()) return; + Long inId = ins.isEmpty() ? null : ins.getFirst().getId(); + ensureOpenStatSample(day8, "기변", "20000", shops.getFirst().getId(), inId, "SM-S928N"); + ensureOpenStatSample(day9, "보상", "25000", shops.get(Math.min(1, shops.size() - 1)).getId(), inId, "SM-A175N"); + LocalDate apr = LocalDate.of(today.getYear(), 4, 12); + LocalDate jul = LocalDate.of(today.getYear(), 7, Math.min(10, today.lengthOfMonth())); + if (shops.size() > 2) ensureOpenStatSample(apr, "신규", "18000", shops.get(2).getId(), inId, "SM-F766NK"); + if (shops.size() > 3) ensureOpenStatSample(apr, "MNP", "22000", shops.get(3).getId(), + ins.size() > 1 ? ins.get(1).getId() : inId, "SM-S942NK"); + ensureOpenStatSample(jul, "신규", "21000", shops.getFirst().getId(), inId, "SM-S942N_256G"); + if (shops.size() > 1) ensureOpenStatSample(jul, "기변", "19000", shops.get(1).getId(), inId, "SM-A175N"); + } + + private void ensureOpenStatSample(LocalDate date, String openhow, String margin, Long shopId, Long inId) { + ensureOpenStatSample(date, openhow, margin, shopId, inId, "SM-S928N"); + } + + private void ensureOpenStatSample(LocalDate date, String openhow, String margin, Long shopId, Long inId, String model) { + Long count = em.createQuery( + "select count(o) from OpenRecord o where o.groupId='demo' and o.opendate=:d and o.openhow=:h and o.pmodel=:m", Long.class) + .setParameter("d", date).setParameter("h", openhow).setParameter("m", model).getSingleResult(); + if (count != null && count > 0) return; + OpenRecord open = new OpenRecord(); + open.setGroupId("demo"); + open.setOutcompanyId(shopId); + open.setIncompanyId(inId); + open.setGubun("할부"); + open.setTelecom("SKT"); + open.setOpenhow(openhow); + open.setName("통계*샘플"); + open.setOpenphone("010-****-0000"); + open.setOpendate(date); + open.setOpenHour(11); + open.setOpenMinute(0); + open.setOpenStatus("정상"); + open.setOutCategory("판매점"); + open.setSalesperson("김영업"); + open.setEmployee("demo"); + open.setPlan("5GX 프라임"); + open.setPmodel(model); + open.setPserial("STAT" + date.getMonthValue() + date.getDayOfMonth() + model.hashCode()); + open.setSellprice(BigDecimal.ZERO); + open.setDealermargin1(new BigDecimal(margin)); + open.setDealermargin2(BigDecimal.ZERO); + open.setState("OPEN"); + open.setUserid("demo"); + open.setUid("open-stat-" + openhow + "-" + date + "-" + model); + em.persist(open); + } + + private void seedHomeIndocsIfEmpty() { + Long count = em.createQuery("select count(d) from HomeIncompleteDoc d where d.groupId='demo'", Long.class).getSingleResult(); + if (count > 0) return; + List shops = em.createQuery("select c from Company c where c.groupId='demo' and c.type in ('SHOP','OUT')", Company.class).getResultList(); + List ins = em.createQuery("select c from Company c where c.groupId='demo' and c.type='IN'", Company.class).getResultList(); + if (shops.isEmpty()) return; + Long inId = ins.isEmpty() ? null : ins.getFirst().getId(); + Object[][] samples = { + {"SKT", "김*근", "010-****-3566", "인터넷,TV", "신분증사본", "30000", "서류미비"}, + {"KT", "박*지", "010-****-1122", "인터넷", "통장사본", "20000", "추가서류"}, + {"LGU+", "이*수", "010-****-5678", "TV,집전화", "인감증명서", "50000", "대리신청"}, + }; + for (int i = 0; i < samples.length; i++) { + Object[] s = samples[i]; + HomeIncompleteDoc d = new HomeIncompleteDoc(); + d.setGroupId("demo"); + d.setOutcompanyId(shops.get(i % shops.size()).getId()); + d.setIncompanyId(inId); + d.setOutcompanyName(shops.get(i % shops.size()).getName()); + d.setTelecom(String.valueOf(s[0])); + d.setCustomerName(String.valueOf(s[1])); + d.setPhone(String.valueOf(s[2])); + d.setProducts(String.valueOf(s[3])); + d.setMissingDocs(String.valueOf(s[4])); + d.setDeductAmount(new BigDecimal(String.valueOf(s[5]))); + d.setDeductReason(String.valueOf(s[6])); + d.setInstallDate(LocalDate.now().minusDays(i * 2)); + d.setDeadline(LocalDate.now().plusDays(7 - i)); + d.setStatus("미비"); + em.persist(d); + } + } + + private void seedHomeSalesIfEmpty() { + Long count = em.createQuery("select count(h) from HomeSale h where h.groupId='demo'", Long.class).getSingleResult(); + if (count > 0) return; + List shops = em.createQuery("select c from Company c where c.groupId='demo' and c.type in ('SHOP','OUT')", Company.class).getResultList(); + List ins = em.createQuery("select c from Company c where c.groupId='demo' and c.type='IN'", Company.class).getResultList(); + if (shops.isEmpty()) return; + Long inId = ins.isEmpty() ? null : ins.getFirst().getId(); + Object[][] samples = { + {"완료", "SKT", "인터넷,TV", "김*근", "010-****-3566", "-700000", "50000", "D2"}, + {"대기", "KT", "인터넷", "박*지", "010-****-1122", "0", "30000", "늘푸른통신"}, + {"완료", "LGU+", "TV,집전화", "이*수", "010-****-5678", "-1000000", "80000", "스마트모바일"}, + {"완료", "SKT", "홈IOT,기타", "최*아", "010-****-7788", "-200000", "20000", "블루폰"}, + {"대기", "KT", "소호,원스톱", "정*호", "010-****-9900", "0", "15000", "메인텔레콤"}, + {"완료", "SKT", "인터넷전화,신용카드", "홍*동", "010-****-2222", "-350000", "40000", "늘붉은통신"}, + {"완료", "LGU+", "후결합,재약정", "윤*희", "010-****-3344", "-150000", "25000", "D1"}, + {"대기", "KT", "렌탈", "한*진", "010-****-4455", "0", "10000", "늘푸른통신"}, + }; + for (int i = 0; i < samples.length; i++) { + Object[] s = samples[i]; + HomeSale h = new HomeSale(); + h.setGroupId("demo"); + h.setOutcompanyId(shops.get(i % shops.size()).getId()); + h.setIncompanyId(inId); + h.setStatus(String.valueOf(s[0])); + h.setTelecom(String.valueOf(s[1])); + h.setProducts(String.valueOf(s[2])); + h.setCustomerName(String.valueOf(s[3])); + h.setPhone(String.valueOf(s[4])); + h.setSettleAmount(new BigDecimal(String.valueOf(s[5]))); + h.setMargin(new BigDecimal(String.valueOf(s[6]))); + h.setOutcompanyName(String.valueOf(s[7])); + h.setOutCategory("판매점"); + h.setEmployee("demo"); + h.setInstallDate(LocalDate.now().minusDays(i)); + h.setReceiptDate(LocalDate.now().minusDays(i + 1)); + h.setMissingDocs("관계없음"); + h.setExtraPolicy(BigDecimal.ZERO); + h.setAddress("서울시 강남구"); + em.persist(h); + } + } + + private void seedCareHomesIfEmpty() { + if (em.createQuery("select count(p) from HomesProduct p where p.groupId='demo'", Long.class).getSingleResult() == 0) { + Object[][] products = { + {"INTERNET", "인터넷", "KT", "안심 인터넷 베이직 와이드", null, 36, "80000", 3}, + {"INTERNET", "인터넷", "KT", "인터넷 슬림", null, 36, "50000", 1}, + {"INTERNET", "인터넷", "KT", "기가인터넷 500M", null, 36, "80000", 0}, + {"INTERNET", "인터넷", "KT", "기가인터넷 1G", null, 36, "90000", 2}, + {"INTERNET", "인터넷", "KT", "인터넷 베이직 와이파이", null, 36, "60000", 1}, + {"INTERNET", "인터넷", "KT", "인터넷 에센스", null, 36, "70000", 0}, + {"INTERNET", "인터넷", "KT", "인터넷 베이직", null, 36, "55000", 0}, + {"INTERNET", "인터넷", "KT", "인터넷 프리미엄", null, 36, "100000", 3}, + {"INTERNET", "인터넷", "SKB", "B tv 인터넷", null, 36, "75000", 1}, + {"INTERNET", "인터넷", "LGU+", "U+ 인터넷", null, 36, "70000", 0}, + {"INTERNET", "TV", "KT", "올레tv 베이직", null, 36, "30000", 1}, + {"INTERNET", "TV", "KT", "올레tv 슬림", null, 36, "25000", 0}, + {"INTERNET", "TV", "KT", "지니 TV 프리미엄", null, 36, "40000", 2}, + {"INTERNET", "전화", "KT", "집전화 기본", null, 24, "10000", 0}, + {"INTERNET", "전화", "KT", "집전화 스마트", null, 24, "15000", 1}, + {"INTERNET", "와이파이", "KT", "와이파이 공유기", null, 24, "15000", 0}, + {"INTERNET", "와이파이", "SKB", "와이파이 메시", null, 24, "20000", 1}, + {"INTERNET", "스마트홈", "LGU+", "스마트홈 패키지", null, 36, "20000", 0}, + {"INTERNET", "스마트홈", "KT", "GiGA IoT 홈캠", null, 36, "25000", 1}, + {"INTERNET", "기타", "KT", "기타 부가상품", null, 12, "5000", 0}, + {"HOME", "정수기", "청호나이스", "450 (냉온정)", "WP-40C9500M", 60, "120000", 1}, + {"HOME", "정수기", "청호나이스", "550 (얼음냉온정)", "WI-55S9500M", 60, "150000", 0}, + {"HOME", "정수기", "청호나이스", "Compact (정)", "WP-CP1000", 60, "90000", 0}, + {"HOME", "정수기", "청호나이스", "700 (얼음냉온정)", "WI-70S9500M", 60, "180000", 1}, + {"HOME", "정수기", "코웨이", "아이콘 정수기", "CHP-7210N", 60, "130000", 0}, + {"HOME", "정수기", "코웨이", "냉온정수기", "CHP-5610L", 36, "110000", 1}, + {"HOME", "정수기", "SK매직", "스스로살균 정수기", "WPU-A110C", 60, "100000", 0}, + {"HOME", "정수기", "LG전자", "퓨리케어 오브제", "WD503AS", 60, "140000", 0}, + {"HOME", "공기청정기", "LG전자", "퓨리케어 360", "AS180DWFA", 36, "90000", 0}, + {"HOME", "공기청정기", "코웨이", "에어메가", "AP-2021A", 36, "85000", 1}, + {"HOME", "공기청정기", "삼성", "비스포크 큐브", "AX60A", 36, "95000", 0}, + {"HOME", "비데", "SK매직", "비데 기본형", "BIDET-01", 36, "50000", 0}, + {"HOME", "비데", "코웨이", "스스로케어 비데", "BAU-20", 36, "60000", 1}, + {"HOME", "비데", "청호나이스", "방수비데", "BA-17", 0, "45000", 0}, + {"HOME", "매트리스", "코웨이", "매트리스 퀸", "MT-Q", 60, "150000", 0}, + {"HOME", "매트리스", "코웨이", "매트리스 킹", "MT-K", 60, "180000", 1}, + {"HOME", "매트리스", "시몬스", "뷰티레스트", "BR-Q", 0, "200000", 0}, + {"HOME", "가전", "삼성", "김치냉장고", "RQ33A74", 36, "80000", 0}, + {"HOME", "가전", "LG전자", "디오스 냉장고", "M872GBB", 36, "100000", 0}, + {"HOME", "가전", "위닉스", "제습기", "DXAE100-KEK", 24, "40000", 1}, + {"HOME", "기타", "기타", "렌탈 기타상품", "ETC-01", 12, "30000", 0}, + }; + for (Object[] s : products) { + HomesProduct p = new HomesProduct(); + p.setGroupId("demo"); + p.setKind(String.valueOf(s[0])); + p.setCategory(String.valueOf(s[1])); + p.setCompany(String.valueOf(s[2])); + p.setName(String.valueOf(s[3])); + p.setModel(s[4] == null ? null : String.valueOf(s[4])); + p.setMonths((Integer) s[5]); + p.setPolicyAmount(new BigDecimal(String.valueOf(s[6]))); + p.setPolicyCount((Integer) s[7]); + em.persist(p); + } + } + if (em.createQuery("select count(c) from HomesCustomer c where c.groupId='demo'", Long.class).getSingleResult() == 0) { + Object[][] customers = { + {"단골", "개인", "임*무", "010-1111-3203", "010-2222-8715", "서울시 강남구", "김사원", LocalDate.of(2024, 4, 26)}, + {"단골", "개인", "홍*동", "010-3333-2222", "", "서울시 서초구", "홍길동", LocalDate.of(2024, 4, 26)}, + {"단골1", "개인", "박*지", "010-2222-1122", "010-9999-1122", "서울시 마포구", "홍길동", LocalDate.of(2024, 5, 10)}, + {"단골2", "사업자", "최*아", "010-4444-7788", "", "인천시 연수구", "김대표", LocalDate.of(2024, 6, 1)}, + {"단골3", "개인", "정*민", "010-5555-9900", "010-5555-1111", "부산시 해운대", "홍총괄", LocalDate.of(2024, 7, 15)}, + {"분류없음", "개인", "이*수", "010-3333-5678", "", "경기도 성남시", "김대표", LocalDate.of(2024, 8, 20)}, + {"단골4", "개인", "김*근", "010-1111-3566", "", "서울시 송파구", "김사원", LocalDate.of(2024, 9, 5)}, + }; + for (Object[] s : customers) { + HomesCustomer c = new HomesCustomer(); + c.setGroupId("demo"); + c.setGrade(String.valueOf(s[0])); + c.setCustomerType(String.valueOf(s[1])); + c.setName(String.valueOf(s[2])); + c.setPhone(String.valueOf(s[3])); + c.setPhoneAlt(String.valueOf(s[4])); + c.setAddress(String.valueOf(s[5])); + c.setEmployee(String.valueOf(s[6])); + c.setRegisteredDate((LocalDate) s[7]); + em.persist(c); + } + } else { + for (HomesCustomer c : em.createQuery("select c from HomesCustomer c where c.groupId='demo'", HomesCustomer.class).getResultList()) { + if (c.getCustomerType() == null || c.getCustomerType().isBlank()) c.setCustomerType("개인"); + if ("일반".equals(c.getGrade())) c.setGrade("분류없음"); + if (c.getRegisteredDate() == null && c.getCreatedAt() != null) { + c.setRegisteredDate(c.getCreatedAt().atZone(ZoneId.systemDefault()).toLocalDate()); + } + if (c.getRegisteredDate() == null) c.setRegisteredDate(LocalDate.now().minusDays(30)); + } + } + if (em.createQuery("select count(c) from HomesContract c where c.groupId='demo'", Long.class).getSingleResult() == 0) { + Object[][] contracts = { + {"INTERNET", "완료", "홍*동", "010-1111-1019", "광랜인터넷 와이파이, B tv All", "30150000", "28000000", "-940000", "SK브로드밴드", "홍길동"}, + {"INTERNET", "완료", "김*수", "010-2222-3344", "기가인터넷 500M", "80000", "70000", "10000", "늘푸른통신", "홍길동"}, + {"INTERNET", "완료", "이*아", "010-3333-5566", "올레tv 베이직, 집전화 기본", "40000", "35000", "5000", "(주)코웨이", "홍총괄"}, + {"INTERNET", "접수", "박*지", "010-2222-1122", "올레tv 베이직", "30000", "25000", "5000", "대박통신", "김대표"}, + {"INTERNET", "신청", "이*수", "010-3333-5678", "와이파이 공유기", "15000", "12000", "3000", "스마트홈", "홍길동"}, + {"INTERNET", "철회", "최*민", "010-7777-8899", "인터넷 슬림", "50000", "0", "0", "대박통신", "홍총괄"}, + {"INTERNET", "완료", "정*희", "010-8888-9900", "기가인터넷 1G", "90000", "80000", "10000", "늘푸른통신", "김대표"}, + {"HOME", "완료", "최*아", "010-4444-7788", "냉온정수기", "120000", "100000", "20000", "늘푸른통신", "김대표"}, + {"HOME", "접수", "정*민", "010-5555-9900", "공기청정기", "90000", "75000", "15000", "블루홈", "홍길동"}, + {"HOME", "철회", "윤*희", "010-6666-1122", "비데 기본형", "50000", "0", "0", "대박통신", "홍총괄"}, + }; + for (int i = 0; i < contracts.length; i++) { + Object[] s = contracts[i]; + HomesContract c = new HomesContract(); + c.setGroupId("demo"); + c.setKind(String.valueOf(s[0])); + c.setStatus(String.valueOf(s[1])); + c.setCustomerType("개인"); + c.setCustomerName(String.valueOf(s[2])); + c.setPhone(String.valueOf(s[3])); + c.setProducts(String.valueOf(s[4])); + c.setPolicySum(new BigDecimal(String.valueOf(s[5]))); + c.setSettleAmount(new BigDecimal(String.valueOf(s[6]))); + c.setMargin(new BigDecimal(String.valueOf(s[7]))); + c.setAgency(String.valueOf(s[8])); + c.setEmployee(String.valueOf(s[9])); + c.setPayMethod("자동이체"); + c.setContractDate(LocalDate.now().minusDays(i * 2L)); + c.setInstallDate("완료".equals(s[1]) ? LocalDate.now().minusDays(i) : null); + c.setMonths(36); + c.setAddress("서울시"); + c.setCustomerGrade("분류없음"); + c.setNationality("내국인"); + em.persist(c); + } + } + if (em.createQuery("select count(b) from HomesBalance b where b.groupId='demo'", Long.class).getSingleResult() == 0) { + Object[][] balances = { + {"정산차감", "하부점", "늘푸른통신", "서류미비", "-21124", "서류 보완 요청"}, + {"정산추가", "직원", "김대표", "추가정책", "30000", null}, + {"정산차감", "직원", "김대표", "철회차감", "-10000", "철회 건"}, + {"정산추가", "하부점", "대박통신", "프로모션", "15000", null}, + {"정산차감", "하부점", "대박통신", "오등록", "-5000", null}, + {"정산추가", "직원", "홍길동", "인센티브", "20000", "월 인센티브"}, + {"정산차감", "직원", "홍길동", "과지급", "-8000", null}, + }; + for (int i = 0; i < balances.length; i++) { + Object[] s = balances[i]; + HomesBalance b = new HomesBalance(); + b.setGroupId("demo"); + b.setBalanceType(String.valueOf(s[0])); + b.setTargetType(String.valueOf(s[1])); + b.setTargetName(String.valueOf(s[2])); + b.setReason(String.valueOf(s[3])); + b.setAmount(new BigDecimal(String.valueOf(s[4]))); + b.setMemo(s[5] == null ? null : String.valueOf(s[5])); + b.setBaseDate(LocalDate.now().minusDays(i)); + if ("하부점".equals(b.getTargetType())) b.setAgency(b.getTargetName()); + else b.setCustomerName(b.getTargetName()); + em.persist(b); + } + } else { + // 구 데이터(차감/추가) → 원본 라벨로 정규화 + 대상 필드 보강 + for (HomesBalance b : em.createQuery("select b from HomesBalance b where b.groupId='demo'", HomesBalance.class).getResultList()) { + if ("차감".equals(b.getBalanceType())) b.setBalanceType("정산차감"); + if ("추가".equals(b.getBalanceType())) b.setBalanceType("정산추가"); + if (b.getTargetType() == null || b.getTargetType().isBlank()) { + if (b.getAgency() != null && !b.getAgency().isBlank() + && (b.getCustomerName() == null || b.getCustomerName().isBlank() || b.getCustomerName().contains("*"))) { + b.setTargetType("하부점"); + b.setTargetName(b.getAgency()); + } else { + b.setTargetType("직원"); + b.setTargetName(b.getCustomerName() != null && !b.getCustomerName().isBlank() ? b.getCustomerName() : "김대표"); + } + } + if ("정산차감".equals(b.getBalanceType()) && b.getAmount() != null && b.getAmount().signum() > 0) { + b.setAmount(b.getAmount().negate()); + } + } + long balCount = em.createQuery("select count(b) from HomesBalance b where b.groupId='demo'", Long.class).getSingleResult(); + if (balCount < 5) { + Object[][] extra = { + {"정산차감", "하부점", "대박통신", "오등록", "-5000", null}, + {"정산추가", "직원", "홍길동", "인센티브", "20000", "월 인센티브"}, + {"정산차감", "직원", "홍길동", "과지급", "-8000", null}, + {"정산추가", "하부점", "늘푸른통신", "프로모션", "15000", null}, + {"정산차감", "직원", "김대표", "철회차감", "-10000", "철회 건"}, + }; + for (int i = 0; i < extra.length && balCount + i < 7; i++) { + Object[] s = extra[i]; + HomesBalance b = new HomesBalance(); + b.setGroupId("demo"); + b.setBalanceType(String.valueOf(s[0])); + b.setTargetType(String.valueOf(s[1])); + b.setTargetName(String.valueOf(s[2])); + b.setReason(String.valueOf(s[3])); + b.setAmount(new BigDecimal(String.valueOf(s[4]))); + b.setMemo(s[5] == null ? null : String.valueOf(s[5])); + b.setBaseDate(LocalDate.now().minusDays(i + 1)); + if ("하부점".equals(b.getTargetType())) b.setAgency(b.getTargetName()); + else b.setCustomerName(b.getTargetName()); + em.persist(b); + } + } + } + if (em.createQuery("select count(i) from HomesInvoice i where i.groupId='demo'", Long.class).getSingleResult() == 0) { + for (int i = 0; i < 6; i++) { + YearMonth ym = YearMonth.now().minusMonths(i); + HomesInvoice inv = new HomesInvoice(); + inv.setGroupId("demo"); + inv.setScope(i % 2 == 0 ? "PUBLISH" : "MY"); + inv.setYearMonth(ym.toString()); + inv.setEmployee("김대표"); + inv.setAgency("늘푸른통신"); + inv.setInternetCount(2 + i % 3); + inv.setInternetAmount(new BigDecimal(50000 + i * 10000L)); + inv.setHomeCount(1 + i % 2); + inv.setHomeAmount(new BigDecimal(80000 + i * 5000L)); + inv.setBalanceCount(i % 3); + inv.setBalanceAmount(new BigDecimal(i % 2 == 0 ? -5000 : 10000)); + inv.setContractCount(inv.getInternetCount() + inv.getHomeCount()); + inv.setSettleAmount(inv.getInternetAmount().add(inv.getHomeAmount()).add(inv.getBalanceAmount())); + inv.setAmount(inv.getSettleAmount()); + inv.setIssued(i != 0); + inv.setStatus(i == 0 ? "대기" : "발행"); + inv.setConsent(i == 0 ? "-" : "대기"); + inv.setTaxInvoice(i == 0 ? "-" : "미발행"); + inv.setWithdrawStatus(i == 0 ? "-" : "대기"); + em.persist(inv); + } + } else { + for (HomesInvoice inv : em.createQuery("select i from HomesInvoice i where i.groupId='demo'", HomesInvoice.class).getResultList()) { + if (("발행".equals(inv.getStatus()) || "확정".equals(inv.getStatus())) && !Boolean.TRUE.equals(inv.getIssued())) { + inv.setIssued(true); + } + if (inv.getSettleAmount() == null && inv.getAmount() != null) inv.setSettleAmount(inv.getAmount()); + if (inv.getInternetCount() == null && inv.getAmount() != null) { + inv.setInternetCount(2); + inv.setInternetAmount(inv.getAmount().multiply(new BigDecimal("0.4")).setScale(0, java.math.RoundingMode.HALF_UP)); + inv.setHomeCount(1); + inv.setHomeAmount(inv.getAmount().multiply(new BigDecimal("0.5")).setScale(0, java.math.RoundingMode.HALF_UP)); + inv.setBalanceCount(1); + inv.setBalanceAmount(inv.getAmount().subtract(inv.getInternetAmount()).subtract(inv.getHomeAmount())); + inv.setContractCount(inv.getInternetCount() + inv.getHomeCount()); + } + } + } + if (em.createQuery("select count(p) from HomesPending p where p.groupId='demo'", Long.class).getSingleResult() == 0) { + Object[][] pendings = { + {"접수", "일반", "설치 상담", "김*근", "010-1111-3566", "김대표", null, null, "14:00", 0, "방문 전 연락 요망"}, + {"접수", "약속1", "방문 약속", "박*지", "010-2222-1122", "홍길동", null, null, "10:30", 1, null}, + {"해결", "약속2", "계약 설명", "이*수", "010-3333-5678", "홍길동", "김대표", 2, "16:00", 2, "완료 확인"}, + {"접수", "일반", "회선 제한 상담", "제한설", "010-7777-8899", "김대표", null, null, "11:00", 6, null}, + {"해결", "약속1", "설치 일정 조율", "최*아", "010-4444-7788", "홍총괄", "홍총괄", 3, "15:30", 4, null}, + {"접수", "일반", "요금 안내", "정*민", "010-5555-9900", "홍길동", null, null, "09:00", 5, "오전 통화"}, + {"접수", "약속2", "해지 상담", "윤*희", "010-6666-1122", "김대표", null, null, "13:20", 7, null}, + {"해결", "일반", "AS 접수", "한*솔", "010-8888-3344", "홍길동", "홍길동", 1, "17:00", 8, null}, + {"접수", "약속1", "이전설치", "오*진", "010-9999-5566", "김대표", null, null, "12:00", 9, null}, + {"접수", "일반", "결합상품 문의", "배*수", "010-1212-3434", "홍총괄", null, null, "14:40", 10, null}, + {"해결", "약속2", "약정 연장", "임*영", "010-5656-7878", "홍길동", "김대표", 5, "10:00", 11, null}, + {"접수", "일반", "개통 일정", "조*현", "010-9090-1212", "김대표", null, null, "16:30", 12, null}, + }; + for (Object[] s : pendings) { + HomesPending p = new HomesPending(); + p.setGroupId("demo"); + p.setStatus(String.valueOf(s[0])); + p.setPendingType(String.valueOf(s[1])); + p.setContents(String.valueOf(s[2])); + p.setCustomerName(String.valueOf(s[3])); + p.setPhone(String.valueOf(s[4])); + p.setReceiveEmployee(String.valueOf(s[5])); + p.setEmployee(String.valueOf(s[5])); + p.setProcessEmployee(s[6] == null ? null : String.valueOf(s[6])); + if (s[7] != null) p.setProcessDate(LocalDate.now().minusDays(((Number) s[7]).longValue())); + p.setPendingTime(String.valueOf(s[8])); + p.setPendingDate(LocalDate.now().plusDays(((Number) s[9]).longValue())); + p.setMemo(s[10] == null ? null : String.valueOf(s[10])); + em.persist(p); + } + } else { + for (HomesPending p : em.createQuery("select p from HomesPending p where p.groupId='demo'", HomesPending.class).getResultList()) { + if ("예정".equals(p.getStatus())) p.setStatus("접수"); + if ("완료".equals(p.getStatus())) p.setStatus("해결"); + if ((p.getReceiveEmployee() == null || p.getReceiveEmployee().isBlank()) && p.getEmployee() != null) { + p.setReceiveEmployee(p.getEmployee()); + } + } + long cnt = em.createQuery("select count(p) from HomesPending p where p.groupId='demo'", Long.class).getSingleResult(); + if (cnt < 8) { + Object[][] extra = { + {"접수", "일반", "요금 안내", "정*민", "010-5555-9900", "홍길동", 5}, + {"해결", "약속1", "설치 일정 조율", "최*아", "010-4444-7788", "홍총괄", 4}, + {"접수", "약속2", "해지 상담", "윤*희", "010-6666-1122", "김대표", 7}, + {"해결", "일반", "AS 접수", "한*솔", "010-8888-3344", "홍길동", 8}, + {"접수", "약속1", "이전설치", "오*진", "010-9999-5566", "김대표", 9}, + {"접수", "일반", "결합상품 문의", "배*수", "010-1212-3434", "홍총괄", 10}, + {"해결", "약속2", "약정 연장", "임*영", "010-5656-7878", "홍길동", 11}, + {"접수", "일반", "개통 일정", "조*현", "010-9090-1212", "김대표", 12}, + }; + for (int i = 0; i < extra.length && cnt + i < 12; i++) { + Object[] s = extra[i]; + HomesPending p = new HomesPending(); + p.setGroupId("demo"); + p.setStatus(String.valueOf(s[0])); + p.setPendingType(String.valueOf(s[1])); + p.setContents(String.valueOf(s[2])); + p.setCustomerName(String.valueOf(s[3])); + p.setPhone(String.valueOf(s[4])); + p.setReceiveEmployee(String.valueOf(s[5])); + p.setEmployee(String.valueOf(s[5])); + if ("해결".equals(p.getStatus())) { + p.setProcessEmployee(String.valueOf(s[5])); + p.setProcessDate(LocalDate.now().minusDays(1)); + } + p.setPendingTime("10:00"); + p.setPendingDate(LocalDate.now().plusDays(((Number) s[6]).longValue())); + em.persist(p); + } + } + } + if (em.createQuery("select count(b) from HomesBooking b where b.groupId='demo'", Long.class).getSingleResult() == 0) { + Object[][] bookings = { + {"완료", "이*신", "0000-0000-0000", "인터넷, TV, 전화", "매트리스, 가전", LocalDate.of(2022, 7, 14)}, + {"접수", "홍*동", "010-1111-2222", "인터넷, 전화, 스마트홈", "공기청정기, 매트리스, 기타", LocalDate.of(2022, 7, 14)}, + {"보류", "김*근", "010-1000-2000", "인터넷, 와이파이", "정수기", LocalDate.now().plusDays(1)}, + }; + for (Object[] s : bookings) { + HomesBooking b = new HomesBooking(); + b.setGroupId("demo"); + b.setStatus(String.valueOf(s[0])); + b.setCustomerName(String.valueOf(s[1])); + b.setPhone(String.valueOf(s[2])); + b.setInternetProducts(String.valueOf(s[3])); + b.setRentalProducts(String.valueOf(s[4])); + b.setProducts(s[3] + ", " + s[4]); + b.setBookingDate((LocalDate) s[5]); + b.setAddress("서울시"); + b.setEmployee("홍길동"); + em.persist(b); + } + } else { + // 기존 시드에 인터넷/홈렌탈 분리 필드 보강 + List existing = em.createQuery( + "select b from HomesBooking b where b.groupId='demo'", HomesBooking.class).getResultList(); + for (HomesBooking b : existing) { + if (b.getInternetProducts() == null || b.getInternetProducts().isBlank()) { + String p = b.getProducts() == null ? "" : b.getProducts(); + if (p.contains("인터넷") || p.contains("기가") || p.contains("TV") || p.contains("전화") || p.contains("와이파이") || p.contains("스마트")) { + List inet = new ArrayList<>(); + if (p.contains("인터넷") || p.contains("기가")) inet.add("인터넷"); + if (p.contains("TV")) inet.add("TV"); + if (p.contains("전화")) inet.add("전화"); + if (p.contains("와이파이")) inet.add("와이파이"); + if (p.contains("스마트")) inet.add("스마트홈"); + if (inet.isEmpty()) inet.add("인터넷"); + b.setInternetProducts(String.join(", ", inet)); + } + if (p.contains("정수기") || p.contains("공기") || p.contains("비데") || p.contains("매트") || p.contains("가전")) { + List rent = new ArrayList<>(); + if (p.contains("정수기")) rent.add("정수기"); + if (p.contains("공기")) rent.add("공기청정기"); + if (p.contains("비데")) rent.add("비데"); + if (p.contains("매트")) rent.add("매트리스"); + if (p.contains("가전")) rent.add("가전"); + b.setRentalProducts(String.join(", ", rent)); + } + if ((b.getInternetProducts() == null || b.getInternetProducts().isBlank()) + && (b.getRentalProducts() == null || b.getRentalProducts().isBlank())) { + b.setInternetProducts("인터넷"); + b.setRentalProducts("기타"); + } + } + if (b.getEmployee() == null || b.getEmployee().isBlank()) b.setEmployee("홍길동"); + } + } + if (em.createQuery("select count(a) from HomesAbook a where a.groupId='demo'", Long.class).getSingleResult() == 0) { + Object[][] abooks = { + {"입금", "일반", "계약정산", "150000", "홍길동"}, + {"출금", "일반", "사무용품", "35000", "홍길동"}, + {"입금", "123", "후정산추가", "10000", "홍길동"}, + {"카드입금", "일반", "카드매출", "55000", "김대표"}, + {"카드출금", "123", "카드수수료", "2200", "홍길동"}, + {"출금", "일반", "테스트2", "5000", "홍길동"}, + }; + for (int i = 0; i < abooks.length; i++) { + Object[] s = abooks[i]; + HomesAbook a = new HomesAbook(); + a.setGroupId("demo"); + a.setEntryType(String.valueOf(s[0])); + a.setCategory(String.valueOf(s[1])); + a.setItem(String.valueOf(s[2])); + a.setAmount(new BigDecimal(String.valueOf(s[3]))); + a.setEmployee(String.valueOf(s[4])); + a.setEntryDate(LocalDate.now().minusDays(i)); + a.setMemo(i == 0 ? "데모 시드" : null); + em.persist(a); + } + } else { + for (HomesAbook a : em.createQuery("select a from HomesAbook a where a.groupId='demo'", HomesAbook.class).getResultList()) { + if ("수입".equals(a.getEntryType())) a.setEntryType("입금"); + else if ("지출".equals(a.getEntryType())) a.setEntryType("출금"); + String cat = a.getCategory(); + boolean knownCat = cat != null && (cat.equals("일반") || cat.equals("123") || cat.startsWith("단골")); + if (!knownCat && cat != null && !cat.isBlank()) { + if (a.getItem() == null || a.getItem().isBlank()) a.setItem(cat); + a.setCategory("일반"); + } + if ((a.getItem() == null || a.getItem().isBlank()) && a.getAgency() != null && !a.getAgency().isBlank()) { + a.setItem(a.getAgency()); + } + if (a.getCategory() == null || a.getCategory().isBlank()) a.setCategory("일반"); + if (a.getEmployee() == null || a.getEmployee().isBlank()) a.setEmployee("홍길동"); + } + } + { + Object[][] agencies = { + {"(주)코웨이", "", "", "", "", LocalDate.of(2022, 5, 27), true, null}, + {"SK브로드밴드", "", "", "", "", LocalDate.of(2022, 5, 27), true, null}, + {"테스트", "02-1234-5678", "02-9876-5432", "홍길동", "010-1234-5678", LocalDate.of(2022, 4, 13), true, "데모 거래처"}, + }; + java.util.Set keep = new java.util.HashSet<>(); + for (Object[] s : agencies) keep.add(String.valueOf(s[0])); + for (HomesAgency old : em.createQuery("select a from HomesAgency a where a.groupId='demo'", HomesAgency.class).getResultList()) { + if (!keep.contains(old.getName())) em.remove(old); + } + em.flush(); + for (Object[] s : agencies) { + String name = String.valueOf(s[0]); + List found = em.createQuery( + "select a from HomesAgency a where a.groupId='demo' and a.name=:name", HomesAgency.class) + .setParameter("name", name).getResultList(); + HomesAgency a = found.isEmpty() ? new HomesAgency() : found.getFirst(); + a.setGroupId("demo"); + a.setName(name); + a.setPhone(String.valueOf(s[1])); + a.setFax(String.valueOf(s[2])); + a.setManagerName(String.valueOf(s[3])); + a.setMobile(String.valueOf(s[4])); + a.setRegisteredDate((LocalDate) s[5]); + a.setActive((Boolean) s[6]); + a.setAgencyType("대리점"); + a.setAddress(null); + a.setMemo(s[7] == null ? null : String.valueOf(s[7])); + if (found.isEmpty()) em.persist(a); + } + } + // 상부점/하부점관리 원본은 빈 목록 + for (HomesPartner p : em.createQuery("select p from HomesPartner p where p.groupId='demo'", HomesPartner.class).getResultList()) { + em.remove(p); + } + em.flush(); + { + // 공지사항 원본 샘플 1건 + String title = "비즈케어 홈즈를 베타오픈 하였습니다."; + List all = em.createQuery("select n from HomesNotice n where n.groupId='demo'", HomesNotice.class).getResultList(); + HomesNotice target = all.stream().filter(n -> title.equals(n.getTitle())).findFirst().orElse(null); + if (target == null) { + for (HomesNotice old : all) em.remove(old); + em.flush(); + target = new HomesNotice(); + target.setGroupId("demo"); + target.setTitle(title); + target.setAuthorName("비즈케어"); + target.setContents("비즈케어 홈즈를 베타오픈 하였습니다.\n\n이용에 참고 부탁드립니다."); + target.setViews(10); + em.persist(target); + em.flush(); + } else { + target.setViews(target.getViews() == null || target.getViews() < 10 ? 10 : target.getViews()); + if (target.getContents() == null || target.getContents().isBlank()) { + target.setContents("비즈케어 홈즈를 베타오픈 하였습니다.\n\n이용에 참고 부탁드립니다."); + } + } + Instant at = LocalDate.of(2022, 11, 22).atStartOfDay(ZoneId.of("Asia/Seoul")).toInstant(); + target.setCreatedAt(at); + target.setUpdatedAt(at); + // 원본과 다른 데모 공지 제거 + for (HomesNotice n : em.createQuery("select n from HomesNotice n where n.groupId='demo'", HomesNotice.class).getResultList()) { + if (!title.equals(n.getTitle())) em.remove(n); + } + } + // 사용문의 원본은 빈 목록 + for (HomesQuestion q : em.createQuery("select q from HomesQuestion q where q.groupId='demo'", HomesQuestion.class).getResultList()) { + em.remove(q); + } + em.flush(); + // 사내게시판 원본은 빈 목록 + for (HomesBbs b : em.createQuery("select b from HomesBbs b where b.groupId='demo'", HomesBbs.class).getResultList()) { + em.remove(b); + } + em.flush(); + if (em.createQuery("select count(r) from HomesReception r where r.groupId='demo'", Long.class).getSingleResult() == 0) { + Object[][] rows = { + {LocalDateTime.of(2026, 7, 10, 9, 12), "김상담", "강남점", "KT", "상품권", "인터넷", "기가인터넷 1G", "홍*동", "010-1111-2222", "서울", "1.접수완료", "SAMPLE-00001", "서울입고", "박처리", "접수", "현금", "100000", "정산대기", null, null, "미지급", "본사", "36개월", "02-1234-5678"}, + {LocalDateTime.of(2026, 7, 10, 10, 30), "이상담", "서초점", "SKB", "가전", "TV", "B tv 프리미엄", "김*수", "010-2222-3333", "경기", "2.고객통화", "SAMPLE-00002", "", "최처리", "통화완료", "계좌", "80000", "정산대기", null, null, "지급대기", "영업점", "24개월", ""}, + {LocalDateTime.of(2026, 7, 9, 14, 5), "김상담", "강남점", "LGU+", "상품권", "인터넷", "U+ 인터넷", "박*지", "010-3333-4444", "인천", "4.개통완료", "SAMPLE-00003", "서울입고", "박처리", "개통", "현금", "120000", "정산대기", LocalDate.of(2026, 7, 9), null, "미지급", "본사", "36개월", "032-555-1212"}, + {LocalDateTime.of(2026, 7, 8, 11, 20), "정상담", "송파점", "KT", "캐시백", "정수기", "청호 450", "최*아", "010-4444-5555", "서울", "4.개통완료", "SAMPLE-00004", "물류완료", "이처리", "설치완료", "카드", "150000", "정산완료", LocalDate.of(2026, 7, 8), LocalDate.of(2026, 7, 10), "지급완료", "본사", "60개월", ""}, + {LocalDateTime.of(2026, 7, 8, 16, 40), "이상담", "서초점", "SKT", "상품권", "전화", "집전화 스마트", "정*민", "010-5555-6666", "부산", "3.서류검토", "SAMPLE-00005", "", "최처리", "서류요청", "현금", "15000", "정산대기", null, null, "미지급", "본사", "24개월", ""}, + {LocalDateTime.of(2026, 7, 7, 9, 50), "김상담", "강남점", "KT", "가전", "공기청정기", "퓨리케어 360", "이*수", "010-6666-7777", "서울", "5.사은품지급", "SAMPLE-00006", "서울입고", "박처리", "사은품발송", "계좌", "90000", "정산대기", LocalDate.of(2026, 7, 6), LocalDate.of(2026, 7, 7), "지급완료", "본사", "36개월", ""}, + {LocalDateTime.of(2026, 7, 6, 13, 15), "정상담", "송파점", "SKB", "상품권", "인터넷", "B tv 인터넷", "윤*희", "010-7777-8888", "경기", "6.정산완료", "SAMPLE-00007", "완료", "이처리", "정산", "현금", "75000", "정산완료", LocalDate.of(2026, 7, 5), LocalDate.of(2026, 7, 6), "지급완료", "영업점", "36개월", ""}, + {LocalDateTime.of(2026, 7, 5, 15, 0), "이상담", "서초점", "LGU+", "캐시백", "와이파이", "와이파이 메시", "한*진", "010-8888-9999", "대전", "9.취소", "SAMPLE-00008", "", "최처리", "고객취소", "", "0", "회수대기", null, null, "반송", "", "12개월", ""}, + {LocalDateTime.of(2026, 7, 11, 8, 45), "김상담", "강남점", "KT", "상품권", "인터넷", "안심 인터넷 베이직", "오*라", "010-1212-3434", "서울", "1.접수완료", "SAMPLE-00009", "", "박처리", "접수", "현금", "60000", "정산대기", null, null, "미지급", "본사", "36개월", ""}, + {LocalDateTime.of(2026, 7, 11, 11, 10), "정상담", "송파점", "SKB", "가전", "비데", "스스로케어 비데", "서*호", "010-5656-7878", "경남", "2.고객통화", "SAMPLE-00010", "물류대기", "이처리", "통화예약", "계좌", "60000", "정산대기", null, null, "지급대기", "본사", "36개월", ""}, + }; + for (Object[] s : rows) { + HomesReception r = new HomesReception(); + r.setGroupId("demo"); + r.setOrderDate((LocalDateTime) s[0]); + r.setCounselor(String.valueOf(s[1])); + r.setBranch(String.valueOf(s[2])); + r.setCarrier(String.valueOf(s[3])); + r.setGiftCategory(String.valueOf(s[4])); + r.setProductCategory(String.valueOf(s[5])); + r.setProductName(String.valueOf(s[6])); + r.setCustomerName(String.valueOf(s[7])); + r.setPhone(String.valueOf(s[8])); + r.setRegion(String.valueOf(s[9])); + r.setProgressStatus(String.valueOf(s[10])); + r.setOrderNumber(String.valueOf(s[11])); + r.setInboundInfo(String.valueOf(s[12])); + r.setProcessor(String.valueOf(s[13])); + r.setDetailProgress(String.valueOf(s[14])); + r.setPaymentType(String.valueOf(s[15])); + r.setAmount(new BigDecimal(String.valueOf(s[16]))); + r.setSettleStatus(String.valueOf(s[17])); + r.setOpenDate(s[18] == null ? null : (LocalDate) s[18]); + r.setGiftDate(s[19] == null ? null : (LocalDate) s[19]); + r.setGiftStatus(String.valueOf(s[20])); + r.setGiftPlace(String.valueOf(s[21])); + r.setAgreementName(String.valueOf(s[22])); + r.setContractNumber(String.valueOf(s[23])); + r.setBranchGroup("본사그룹"); + r.setPostIt(""); + r.setMemo("데모 접수 데이터"); + em.persist(r); + } + em.flush(); + for (HomesReception r : em.createQuery("select r from HomesReception r where r.groupId='demo'", HomesReception.class).getResultList()) { + HomesReceptionProgressLog log = new HomesReceptionProgressLog(); + log.setGroupId("demo"); + log.setReceptionId(r.getId()); + log.setStatus(r.getProgressStatus()); + log.setMemo("초기 등록"); + log.setAuthorName(r.getCounselor()); + log.setLoggedAt(r.getOrderDate() == null ? LocalDateTime.now() : r.getOrderDate()); + em.persist(log); + } + } + seedReceptionIspsIfEmpty(); + enrichReceptionProgressFields(); + if (em.createQuery("select count(l) from HomesLog l where l.groupId='demo'", Long.class).getSingleResult() == 0) { + Object[][] logs = { + {"PRODUCT", "인터넷상품-등록", "인터넷상품 [센트릭스] 등록하였습니다.", "홍길동", 2}, + {"PRODUCT", "인터넷상품-정책-등록", "인터넷상품 정책 [36개월] 등록하였습니다.", "홍길동", 3}, + {"PRODUCT", "인터넷상품-수정", "인터넷상품 [기가인터넷 500M] 수정하였습니다.", "홍총괄", 5}, + {"PRODUCT", "홈렌탈상품-등록", "홈렌탈상품 [450 (냉온정)] 등록하였습니다.", "홍길동", 8}, + {"PRODUCT", "홈렌탈상품-정책-등록", "홈렌탈상품 정책 [60개월] 등록하였습니다.", "홍총괄", 10}, + {"PRODUCT", "인터넷상품-삭제", "인터넷상품 [인터넷 슬림] 삭제하였습니다.", "김대표", 12}, + {"PRODUCT", "기본상품-동기화", "인터넷상품 기본상품 16건을 동기화하였습니다.", "김대표", 20}, + {"CONTRACT", "인터넷접수-등록", "인터넷접수 [홍*동] 등록하였습니다.", "홍길동", 1}, + {"CONTRACT", "인터넷접수-상태", "인터넷접수 [홍*동] 완료(으)로 변경하였습니다.", "홍길동", 2}, + {"CONTRACT", "인터넷접수-수정", "인터넷접수 [김*수] 수정하였습니다.", "홍총괄", 4}, + {"CONTRACT", "홈렌탈접수-등록", "홈렌탈접수 [최*아] 등록하였습니다.", "김대표", 6}, + {"CONTRACT", "홈렌탈접수-상태", "홈렌탈접수 [윤*희] 철회(으)로 변경하였습니다.", "홍총괄", 9}, + {"CUSTOMER", "고객-등록", "고객 [위*우] 등록하였습니다.", "홍길동", 1}, + {"CUSTOMER", "고객-수정", "고객 [홍*동] 수정하였습니다.", "홍길동", 2}, + {"CUSTOMER", "고객-분류", "고객 [홍*동] 분류수정하였습니다.", "홍길동", 3}, + {"CUSTOMER", "고객-등록", "계약고객 [홍*동] 하부점(인터넷)에서 등록하였습니다.", "홍길동", 4}, + {"CUSTOMER", "고객-등록", "고객 [김*근] 등록하였습니다.", "김대표", 5}, + {"CUSTOMER", "고객-삭제", "고객 [이*수] 삭제하였습니다.", "홍길동", 8}, + {"BOOKING", "예약-상태", "예약 [이*신] 완료(으)로 상태변경하였습니다.", "홍길동", 1}, + {"BOOKING", "예약-수정", "예약 [홍*동] 수정하였습니다.", "홍길동", 2}, + {"BOOKING", "예약-등록", "예약 [이*신] 등록하였습니다.", "홍길동", 3}, + {"BOOKING", "예약-등록", "예약 [홍*동] 등록하였습니다.", "홍길동", 4}, + {"BOOKING", "예약-상태", "예약 [김*근] 보류(으)로 상태변경하였습니다.", "홍길동", 7}, + {"ABOOKS", "간편장부-등록", "간편장부 [입금/일반] 등록하였습니다.", "홍길동", 1}, + {"ABOOKS", "간편장부-수정", "간편장부 [출금/일반] 수정하였습니다.", "홍길동", 2}, + {"ABOOKS", "간편장부-삭제", "간편장부 [카드출금/123] 삭제하였습니다.", "홍길동", 3}, + {"ABOOKS", "간편장부-등록", "간편장부 [카드입금/일반] 등록하였습니다.", "김대표", 5}, + {"ABOOKS", "간편장부-등록", "간편장부 [출금/123] 등록하였습니다.", "홍길동", 8}, + {"INVOICE", "정산서-발행", "정산서 [김사원] 발행하였습니다.", "홍길동", 1}, + {"INVOICE", "정산서-발행", "정산서 [김대표] 발행하였습니다.", "김대표", 3}, + {"INVOICE", "정산서-재발행", "정산서 [홍길동] 재발행하였습니다.", "홍총괄", 5}, + {"INVOICE", "정산서-삭제", "정산서 [이총괄] 삭제하였습니다.", "김대표", 8}, + {"INVOICE", "정산서-발행", "정산서 [박재고] 발행하였습니다.", "홍길동", 12}, + {"INVOICE", "정산서-수정", "정산서 [김대표] 수정하였습니다.", "김대표", 15}, + {"INVOICE", "정산서-삭제", "정산서 [윤영업] 삭제하였습니다.", "홍길동", 20}, + }; + for (Object[] s : logs) { + HomesLog l = new HomesLog(); + l.setGroupId("demo"); + l.setDomain(String.valueOf(s[0])); + l.setAction(String.valueOf(s[1])); + l.setContent(String.valueOf(s[2])); + l.setStaff(String.valueOf(s[3])); + l.setProcessedAt(LocalDateTime.now().minusHours(((Number) s[4]).longValue())); + em.persist(l); + } + } else { + // 정산서이력 데모 보강 + long invoiceLogs = em.createQuery("select count(l) from HomesLog l where l.groupId='demo' and l.domain='INVOICE'", Long.class).getSingleResult(); + if (invoiceLogs < 5) { + Object[][] extra = { + {"정산서-발행", "정산서 [김사원] 발행하였습니다.", "홍길동", 2}, + {"정산서-발행", "정산서 [김대표] 발행하였습니다.", "김대표", 6}, + {"정산서-재발행", "정산서 [홍길동] 재발행하였습니다.", "홍총괄", 10}, + {"정산서-삭제", "정산서 [이총괄] 삭제하였습니다.", "김대표", 14}, + {"정산서-발행", "정산서 [박재고] 발행하였습니다.", "홍길동", 18}, + {"정산서-수정", "정산서 [김대표] 수정하였습니다.", "김대표", 22}, + {"정산서-삭제", "정산서 [윤영업] 삭제하였습니다.", "홍길동", 28}, + }; + for (Object[] s : extra) { + HomesLog l = new HomesLog(); + l.setGroupId("demo"); + l.setDomain("INVOICE"); + l.setAction(String.valueOf(s[0])); + l.setContent(String.valueOf(s[1])); + l.setStaff(String.valueOf(s[2])); + l.setProcessedAt(LocalDateTime.now().minusHours(((Number) s[3]).longValue())); + em.persist(l); + } + } + // 구 라벨 정규화 + for (HomesLog l : em.createQuery("select l from HomesLog l where l.groupId='demo' and l.domain='INVOICE'", HomesLog.class).getResultList()) { + if ("발행".equals(l.getAction())) { + l.setAction("정산서-발행"); + if (l.getContent() != null && !l.getContent().contains("정산서 [")) { + l.setContent("정산서 [" + l.getContent().replace(" 정산서를 발행하였습니다.", "").trim() + "] 발행하였습니다."); + } + } else if ("등록".equals(l.getAction())) { + l.setAction("정산서-등록"); + if (l.getContent() != null && !l.getContent().startsWith("정산서 [")) { + String[] parts = l.getContent().split(" ", 2); + String emp = parts.length > 1 ? parts[1] : parts[0]; + l.setContent("정산서 [" + emp + "] 등록하였습니다."); + } + } else if ("삭제".equals(l.getAction())) { + l.setAction("정산서-삭제"); + } + } + // 예약이력 데모 보강 + long bookingLogs = em.createQuery("select count(l) from HomesLog l where l.groupId='demo' and l.domain='BOOKING'", Long.class).getSingleResult(); + if (bookingLogs < 5) { + Object[][] extra = { + {"예약-상태", "예약 [이*신] 완료(으)로 상태변경하였습니다.", "홍길동", 1}, + {"예약-수정", "예약 [홍*동] 수정하였습니다.", "홍길동", 2}, + {"예약-등록", "예약 [이*신] 등록하였습니다.", "홍길동", 3}, + {"예약-등록", "예약 [홍*동] 등록하였습니다.", "홍길동", 4}, + {"예약-상태", "예약 [김*근] 보류(으)로 상태변경하였습니다.", "홍길동", 7}, + }; + for (Object[] s : extra) { + HomesLog l = new HomesLog(); + l.setGroupId("demo"); + l.setDomain("BOOKING"); + l.setAction(String.valueOf(s[0])); + l.setContent(String.valueOf(s[1])); + l.setStaff(String.valueOf(s[2])); + l.setProcessedAt(LocalDateTime.now().minusHours(((Number) s[3]).longValue())); + em.persist(l); + } + } + for (HomesLog l : em.createQuery("select l from HomesLog l where l.groupId='demo' and l.domain='BOOKING'", HomesLog.class).getResultList()) { + if ("등록".equals(l.getAction())) { + l.setAction("예약-등록"); + if (l.getContent() != null && !l.getContent().startsWith("예약 [")) { + l.setContent("예약 [" + l.getContent().trim() + "] 등록하였습니다."); + } + } else if ("수정".equals(l.getAction())) { + l.setAction("예약-수정"); + } else if ("삭제".equals(l.getAction())) { + l.setAction("예약-삭제"); + } else if ("상태".equals(l.getAction())) { + l.setAction("예약-상태"); + } + } + // 고객이력 데모 보강 + long customerLogs = em.createQuery("select count(l) from HomesLog l where l.groupId='demo' and l.domain='CUSTOMER'", Long.class).getSingleResult(); + if (customerLogs < 5) { + Object[][] extra = { + {"고객-등록", "고객 [위*우] 등록하였습니다.", "홍길동", 1}, + {"고객-수정", "고객 [홍*동] 수정하였습니다.", "홍길동", 2}, + {"고객-분류", "고객 [홍*동] 분류수정하였습니다.", "홍길동", 3}, + {"고객-등록", "계약고객 [홍*동] 하부점(인터넷)에서 등록하였습니다.", "홍길동", 4}, + {"고객-등록", "고객 [김*근] 등록하였습니다.", "김대표", 5}, + {"고객-삭제", "고객 [이*수] 삭제하였습니다.", "홍길동", 8}, + }; + for (Object[] s : extra) { + HomesLog l = new HomesLog(); + l.setGroupId("demo"); + l.setDomain("CUSTOMER"); + l.setAction(String.valueOf(s[0])); + l.setContent(String.valueOf(s[1])); + l.setStaff(String.valueOf(s[2])); + l.setProcessedAt(LocalDateTime.now().minusHours(((Number) s[3]).longValue())); + em.persist(l); + } + } + for (HomesLog l : em.createQuery("select l from HomesLog l where l.groupId='demo' and l.domain='CUSTOMER'", HomesLog.class).getResultList()) { + if ("등록".equals(l.getAction())) { + l.setAction("고객-등록"); + if (l.getContent() != null && !l.getContent().startsWith("고객 [") && !l.getContent().startsWith("계약고객 [")) { + l.setContent("고객 [" + l.getContent().trim() + "] 등록하였습니다."); + } + } else if ("수정".equals(l.getAction())) { + l.setAction("고객-수정"); + if (l.getContent() != null && !l.getContent().startsWith("고객 [")) { + l.setContent("고객 [" + l.getContent().trim() + "] 수정하였습니다."); + } + } else if ("삭제".equals(l.getAction())) { + l.setAction("고객-삭제"); + } else if ("분류".equals(l.getAction()) || "고객-분류변경".equals(l.getAction())) { + l.setAction("고객-분류"); + } + } + // 장부이력 데모 보강 + long abookLogs = em.createQuery("select count(l) from HomesLog l where l.groupId='demo' and l.domain='ABOOKS'", Long.class).getSingleResult(); + if (abookLogs < 5) { + Object[][] extra = { + {"간편장부-등록", "간편장부 [입금/일반] 등록하였습니다.", "홍길동", 1}, + {"간편장부-수정", "간편장부 [출금/일반] 수정하였습니다.", "홍길동", 2}, + {"간편장부-삭제", "간편장부 [카드출금/123] 삭제하였습니다.", "홍길동", 3}, + {"간편장부-등록", "간편장부 [카드입금/일반] 등록하였습니다.", "김대표", 5}, + {"간편장부-등록", "간편장부 [출금/123] 등록하였습니다.", "홍길동", 8}, + {"간편장부-수정", "간편장부 [입금/일반] 수정하였습니다.", "홍길동", 12}, + }; + for (Object[] s : extra) { + HomesLog l = new HomesLog(); + l.setGroupId("demo"); + l.setDomain("ABOOKS"); + l.setAction(String.valueOf(s[0])); + l.setContent(String.valueOf(s[1])); + l.setStaff(String.valueOf(s[2])); + l.setProcessedAt(LocalDateTime.now().minusHours(((Number) s[3]).longValue())); + em.persist(l); + } + } + for (HomesLog l : em.createQuery("select l from HomesLog l where l.groupId='demo' and l.domain='ABOOKS'", HomesLog.class).getResultList()) { + if ("등록".equals(l.getAction()) || "장부-등록".equals(l.getAction())) { + l.setAction("간편장부-등록"); + if (l.getContent() != null && !l.getContent().startsWith("간편장부 [")) { + l.setContent("간편장부 [입금/일반] 등록하였습니다."); + } + } else if ("수정".equals(l.getAction()) || "장부-수정".equals(l.getAction())) { + l.setAction("간편장부-수정"); + if (l.getContent() != null && !l.getContent().startsWith("간편장부 [")) { + l.setContent("간편장부 [입금/일반] 수정하였습니다."); + } + } else if ("삭제".equals(l.getAction()) || "장부-삭제".equals(l.getAction())) { + l.setAction("간편장부-삭제"); + if (l.getContent() != null && !l.getContent().startsWith("간편장부 [")) { + l.setContent("간편장부 [출금/일반] 삭제하였습니다."); + } + } + } + } + ensurePendingDemoLogs(); + } + + /** 일정이력 원본 샘플(목록 48건) */ + private void ensurePendingDemoLogs() { + for (HomesLog l : em.createQuery("select l from HomesLog l where l.groupId='demo' and l.domain='PENDING'", HomesLog.class).getResultList()) { + if ("등록".equals(l.getAction())) { + l.setAction("약속-등록"); + if (l.getContent() == null || !l.getContent().startsWith("약속 [")) { + String name = l.getContent() == null || l.getContent().isBlank() ? "일반" : l.getContent().trim(); + l.setContent("약속 [" + name + "] 등록하였습니다."); + } + } else if ("수정".equals(l.getAction())) { + l.setAction("약속-수정"); + } else if ("삭제".equals(l.getAction())) { + l.setAction("약속-삭제"); + } else if ("처리".equals(l.getAction()) || "약속-처리".equals(l.getAction())) { + l.setAction("약속-해결"); + if (l.getContent() != null && l.getContent().contains("해결 처리")) { + l.setContent(l.getContent().replace("해결 처리하였습니다.", "해결완료하였습니다.")); + } else if (l.getContent() == null || !l.getContent().startsWith("약속 [")) { + l.setContent("약속 [일반] 해결완료하였습니다."); + } + } + } + em.flush(); + long pendingLogs = em.createQuery("select count(l) from HomesLog l where l.groupId='demo' and l.domain='PENDING'", Long.class).getSingleResult(); + if (pendingLogs >= 40) return; + for (HomesLog old : em.createQuery("select l from HomesLog l where l.groupId='demo' and l.domain='PENDING'", HomesLog.class).getResultList()) { + em.remove(old); + } + em.flush(); + String[] types = {"일반", "약속1", "약속2"}; + LocalDateTime base = LocalDateTime.of(2024, 4, 1, 19, 49); + for (int i = 0; i < 48; i++) { + String type = types[i % types.length]; + HomesLog l = new HomesLog(); + l.setGroupId("demo"); + l.setDomain("PENDING"); + if (i % 3 == 1) { + l.setAction("약속-해결"); + l.setContent("약속 [" + type + "] 해결완료하였습니다."); + } else if (i % 9 == 0) { + l.setAction("약속-수정"); + l.setContent("약속 [" + type + "] 수정하였습니다."); + } else if (i % 13 == 0) { + l.setAction("약속-삭제"); + l.setContent("약속 [" + type + "] 삭제하였습니다."); + } else { + l.setAction("약속-등록"); + l.setContent("약속 [" + type + "] 등록하였습니다."); + } + l.setStaff("홍길동"); + l.setProcessedAt(base.minusMinutes(i * 7L)); + em.persist(l); + } + } + + private void seedUnitCostsIfEmpty() { + Long count = em.createQuery("select count(t) from UnitCostTable t where t.groupId='demo'", Long.class).getSingleResult(); + if (count > 0) return; + List ins = em.createQuery("select c from Company c where c.groupId='demo' and c.type='IN'", Company.class).getResultList(); + Object[][] samples = { + {"SKT", "SKT", "정책202103_테스트", 4, 11, 14, 2, null, null, null, 1, "표준"}, + {"KT", "KT", "KT기본정책", 2, 5, 7, 1, null, null, null, 1, "공시/선택구분"}, + {"LGU+", "LGT", "LG물류정책", 1, 3, null, null, null, null, null, 0, "표준"}, + {"SKT", "SKT2", "SKT2테스트", 3, 2, 1, null, null, null, null, 1, "표준"}, + {"LGU+", "LG물류", "LG물류", 2, null, null, null, null, null, null, 0, "표준"}, + {"KT", "KT", "선택약정표", 5, 4, 3, 2, 1, null, null, 1, "공시/선택구분"}, + }; + for (Object[] s : samples) { + UnitCostTable t = new UnitCostTable(); + t.setGroupId("demo"); + t.setTelecom(String.valueOf(s[0])); + t.setIncompanyName(String.valueOf(s[1])); + if (!ins.isEmpty()) t.setIncompanyId(ins.getFirst().getId()); + t.setMemo(String.valueOf(s[2])); + t.setPlanGroupA((Integer) s[3]); + t.setPlanGroupB((Integer) s[4]); + t.setPlanGroupC((Integer) s[5]); + t.setPlanGroupD((Integer) s[6]); + t.setPlanGroupE((Integer) s[7]); + t.setPlanGroupF((Integer) s[8]); + t.setPlanGroupG((Integer) s[9]); + t.setPolicyCount((Integer) s[10]); + t.setCategory(String.valueOf(s[11])); + em.persist(t); + em.flush(); + if ((Integer) s[10] > 0) { + UnitCostPolicy p = new UnitCostPolicy(); + p.setGroupId("demo"); + p.setUnitCostId(t.getId()); + p.setPlanGroup("A"); + p.setModel("SM-"); + p.setPlanName("5GX 프라임"); + p.setAmount(new BigDecimal("100000")); + em.persist(p); + } + } + } + + private void seedOpenAdjustFields() { + List opens = em.createQuery("select o from OpenRecord o where o.groupId='demo'", OpenRecord.class).getResultList(); + int i = 0; + for (OpenRecord o : opens) { + if (o.getDecltype() == null) o.setDecltype(i % 2 == 0 ? "단말할인" : "요금할인"); + if (o.getBasicprice1() == null) o.setBasicprice1(new BigDecimal("100000")); + if (o.getBasicprice2() == null) o.setBasicprice2(new BigDecimal("50000")); + if (o.getBasicprice3() == null) o.setBasicprice3(new BigDecimal("30000")); + i++; + } + } + + private void seedBalancesIfEmpty() { + Long count = em.createQuery("select count(b) from AfterBalance b where b.groupId='demo'", Long.class).getSingleResult(); + if (count > 0) return; + List shops = em.createQuery("select c from Company c where c.groupId='demo' and c.type in ('SHOP','OUT')", Company.class).getResultList(); + List ins = em.createQuery("select c from Company c where c.groupId='demo' and c.type='IN'", Company.class).getResultList(); + if (shops.isEmpty()) return; + Long inId = ins.isEmpty() ? null : ins.getFirst().getId(); + Object[][] samples = { + {"정산차감", "그레이드 누락", "20000", "주*라", "010-****-3566", "D2"}, + {"정산추가", "택배비", "-100000", "남*섭", "010-****-1122", "늘푸른통신"}, + {"홈상품차감", "부가서비스 해지", "30000", "김*철", "010-****-5678", "늘붉은통신"}, + {"유심차감", "유심비 미수", "7700", "이*수", "010-****-1234", "스마트모바일"}, + {"정산추가", "테스트", "-3000", "박*영", "010-****-9876", "블루폰"}, + {"정산차감", "악세사리", "15000", "홍*동", "010-****-2222", "메인텔레콤"}, + {"정산차감", "서류미비 차감", "50000", "최*아", "010-****-7788", "늘푸른통신"}, + {"정산추가", "정책 보정", "-20000", "정*호", "010-****-9900", "D1"}, + }; + for (int i = 0; i < samples.length; i++) { + Object[] s = samples[i]; + AfterBalance b = new AfterBalance(); + b.setGroupId("demo"); + b.setOutcompanyId(shops.get(i % shops.size()).getId()); + b.setIncompanyId(inId); + b.setOutcompanyName(String.valueOf(s[5])); + b.setGubun(String.valueOf(s[0])); + b.setReason(String.valueOf(s[1])); + b.setAmount(new BigDecimal(String.valueOf(s[2]))); + b.setCustomerName(String.valueOf(s[3])); + b.setOpenphone(String.valueOf(s[4])); + b.setTelecom(i % 3 == 0 ? "SKT" : (i % 3 == 1 ? "KT" : "LGU+")); + b.setOutCategory("판매점"); + b.setOpendate(LocalDate.now().minusDays(20 + i * 5)); + b.setBasedate(LocalDate.now().minusDays(i * 2)); + b.setMemo(i % 3 == 0 ? "확인필요" : null); + em.persist(b); + } + } + + private void seedReturnsEnrich() { + List rows = em.createQuery("select r from UnreturnedPhone r where r.groupId='demo'", UnreturnedPhone.class).getResultList(); + List ins = em.createQuery("select c from Company c where c.groupId='demo' and c.type='IN'", Company.class).getResultList(); + List shops = em.createQuery("select c from Company c where c.groupId='demo' and c.type in ('SHOP','OUT')", Company.class).getResultList(); + Long inId = ins.isEmpty() ? null : ins.getFirst().getId(); + Long shopId = shops.isEmpty() ? null : shops.getFirst().getId(); + for (UnreturnedPhone r : rows) { + if (r.getTelecom() == null) r.setTelecom("SKT"); + if (r.getReturnStatus() == null) r.setReturnStatus("미반납"); + if (r.getIncompanyId() == null) r.setIncompanyId(inId); + } + if (rows.size() >= 5 || shopId == null) return; + String[][] samples = { + {"KT", "박*지", "010-****-6595", "미반납", "A125", "50000", "0"}, + {"SKT", "홍*동", "010-****-2222", "반납", "sm-g220", "1234", "10000"}, + {"LGU+", "김*수", "010-****-3344", "미반납", "SM-S928N", "IMEI9001", "30000"}, + {"SKT", "최*아", "010-****-7788", "반납", "A2860", "SN7788", ""}, + {"KT", "정*호", "010-****-9900", "미반납", "SM-A546", "A5469900", "15000"}, + }; + for (int i = 0; i < samples.length; i++) { + String[] s = samples[i]; + UnreturnedPhone row = new UnreturnedPhone(); + row.setGroupId("demo"); + row.setOutcompanyId(shopId); + row.setIncompanyId(inId); + row.setOpendate(LocalDate.now().minusDays(5 + i * 3)); + row.setTelecom(s[0]); + row.setCustomerName(s[1]); + row.setOpenphone(s[2]); + row.setReturnStatus(s[3]); + row.setModel(s[4]); + row.setSerial(s[5]); + if (!s[6].isBlank()) row.setDeductAmount(new BigDecimal(s[6])); + if ("반납".equals(s[3])) row.setReturndate(LocalDate.now().minusDays(i)); + em.persist(row); + } + } + + private void seedIndocsEnrich() { + List docs = em.createQuery("select d from IncompleteDoc d where d.groupId='demo'", IncompleteDoc.class).getResultList(); + List ins = em.createQuery("select c from Company c where c.groupId='demo' and c.type='IN'", Company.class).getResultList(); + List shops = em.createQuery("select c from Company c where c.groupId='demo' and c.type='SHOP'", Company.class).getResultList(); + Long inId = ins.isEmpty() ? null : ins.getFirst().getId(); + Long shopId = shops.isEmpty() ? null : shops.getFirst().getId(); + for (IncompleteDoc d : docs) { + if (d.getTelecom() == null) d.setTelecom("SKT"); + if (d.getStatus() == null) d.setStatus("미비"); + if (d.getIncompanyId() == null) d.setIncompanyId(inId); + } + if (docs.size() >= 3 || shopId == null) return; + String[][] samples = { + {"KT", "이*수", "010-****-1234", "인감증명서, 가족관계증명서", "50000", "추가서류 미제출"}, + {"LGU+", "박*영", "010-****-9876", "위임장", "20000", "대리개통 서류"}, + }; + for (int i = 0; i < samples.length; i++) { + String[] s = samples[i]; + IncompleteDoc d = new IncompleteDoc(); + d.setGroupId("demo"); + d.setOutcompanyId(shopId); + d.setIncompanyId(inId); + d.setOpendate(LocalDate.now().minusDays(3L + i * 7)); + d.setTelecom(s[0]); + d.setCustomerName(s[1]); + d.setOpenphone(s[2]); + d.setMissingDocs(s[3]); + d.setDeductAmount(new BigDecimal(s[4])); + d.setDeductReason(s[5]); + d.setDeadline(LocalDate.now().plusDays(5L - i * 3)); + d.setStatus(i == 1 ? "마감임박" : "미비"); + em.persist(d); + } + } + + private void enrichReceptionProgressFields() { + List rows = em.createQuery( + "select r from HomesReception r where r.groupId='demo'", HomesReception.class).getResultList(); + int i = 0; + for (HomesReception r : rows) { + if (r.getCustomerType() == null || r.getCustomerType().isBlank()) r.setCustomerType("public"); + if (r.getPaymentMethod() == null || r.getPaymentMethod().isBlank()) { + String pt = r.getPaymentType(); + r.setPaymentMethod("카드".equals(pt) ? "card" : "지로".equals(pt) ? "paper" : "전화납부".equals(pt) ? "telephone" : "transfer"); + } + if (r.getProductType() == null || r.getProductType().isBlank()) { + String cat = r.getProductCategory(); + r.setProductType("전화".equals(cat) ? "e02" : "TV".equals(cat) ? "e03" : "렌탈".equals(cat) || (cat != null && (cat.contains("정수") || cat.contains("비데") || cat.contains("공기"))) ? "e04" : "e01"); + } + if (r.getEmail() == null || r.getEmail().isBlank()) { + r.setEmail("sample" + (++i) + "@example.com"); + } + if (r.getAddress() == null || r.getAddress().isBlank()) { + r.setZipcode("06236"); + r.setAddress("서울특별시 강남구 테헤란로 " + (10 + i)); + r.setAddressDetail((100 + i) + "호"); + } + if (r.getIdentifyNumber() == null || r.getIdentifyNumber().isBlank()) { + r.setIdentifyNumber("900101-1******"); + } + if (r.getPostIt() == null || r.getPostIt().isBlank()) r.setPostIt("샘플메모" + (i % 50 + 1)); + if (r.getBranchPhone() == null || r.getBranchPhone().isBlank()) r.setBranchPhone("1599-2889"); + if (r.getTel() == null || r.getTel().isBlank()) r.setTel("02-100" + i); + if (r.getGiftSended() == null && r.getGiftDate() != null) r.setGiftSended(r.getGiftDate()); + if (r.getGiftCashAmount() == null && r.getAmount() != null + && r.getGiftStatus() != null && !"미지급".equals(r.getGiftStatus()) && !"없음".equals(r.getGiftCategory())) { + r.setGiftCashAmount(r.getAmount()); + } + if (r.getIspName() == null || r.getIspName().isBlank()) r.setIspName(r.getCarrier()); + if (r.getProcessor() == null || r.getProcessor().isBlank()) r.setProcessor("본사"); + } + } + + private void seedReceptionIspsIfEmpty() { + if (em.createQuery("select count(i) from HomesReceptionIsp i where i.groupId='demo'", Long.class).getSingleResult() > 0) { + return; + } + Object[][] isps = { + {"LG USIM", "LGU+", 1, false}, + {"LGU+ 온라인", "LGU+", 2, false}, + {"LGU+ (T일반)", "LGU+", 3, false}, + {"KT USIM", "KT", 4, false}, + {"KT일반 (강북)", "KT", 5, false}, + {"KT일반 (강남)", "KT", 6, false}, + {"KT일반 (강서)", "KT", 7, false}, + {"KT일반 (경남)", "KT", 8, false}, + {"KT일반 (경북)", "KT", 9, false}, + {"KT일반 (충청)", "KT", 10, false}, + {"KT일반 (전라)", "KT", 11, false}, + {"SKB USIM", "SKB", 12, false}, + {"SKB다이렉트(서울/경기)", "SKB", 13, false}, + {"SKB다이렉트(부산/경남)", "SKB", 14, false}, + {"SKB다이렉트(대구/경북)", "SKB", 15, false}, + {"SKB다이렉트(충청/강원)", "SKB", 16, false}, + {"SKB다이렉트(전라/제주)", "SKB", 17, false}, + {"SKB POP", "SKB", 18, false}, + {"SKT온/한가족(서울/경기)", "SKT", 19, true}, + {"SKT온/한가족(부산/경남)", "SKT", 20, true}, + {"SKT온/한가족(대구/경북)", "SKT", 21, true}, + {"SKT온/한가족(대전/충청/강원)", "SKT", 22, true}, + {"SKT온/한가족(전라/제주)", "SKT", 23, true}, + {"SK매직", "SK매직", 24, false}, + {"LG렌탈", "LG렌탈", 25, false}, + {"Skylife(전국)", "Skylife", 26, false}, + }; + for (Object[] row : isps) { + HomesReceptionIsp isp = new HomesReceptionIsp(); + isp.setGroupId("demo"); + isp.setName(String.valueOf(row[0])); + isp.setTelecom(String.valueOf(row[1])); + isp.setSeq((Integer) row[2]); + isp.setActive(true); + isp.setIsSktb((Boolean) row[3]); + em.persist(isp); + em.flush(); + + boolean rental = "SK매직".equals(isp.getTelecom()) || "LG렌탈".equals(isp.getTelecom()); + String[][] products = rental + ? new String[][]{{"e04", isp.getName() + " 기본렌탈"}, {"e04", isp.getName() + " 프리미엄"}} + : new String[][]{ + {"e01", isp.getTelecom() + " 인터넷 기본"}, + {"e01", isp.getTelecom() + " 인터넷 프리미엄"}, + {"e03", isp.getTelecom() + " TV"}, + {"e02", isp.getTelecom() + " 집전화"}, + }; + for (String[] p : products) { + HomesReceptionIspProduct prod = new HomesReceptionIspProduct(); + prod.setGroupId("demo"); + prod.setIspId(isp.getId()); + prod.setProductType(p[0]); + prod.setName(p[1]); + prod.setActive(true); + em.persist(prod); + } + for (String agName : List.of("24개월", "36개월", "48개월", "60개월")) { + HomesReceptionIspAgreement ag = new HomesReceptionIspAgreement(); + ag.setGroupId("demo"); + ag.setIspId(isp.getId()); + ag.setName(agName); + ag.setActive(true); + em.persist(ag); + } + } + } + + private Company company(String type, String name, String manager, String phone) { + Company c = new Company(); + c.setGroupId("demo"); + c.setType(type); + c.setName(name); + c.setManagerName(manager); + c.setPhone(phone); + c.setActive(true); + em.persist(c); + em.flush(); + return c; + } + + private Stock stock(String gubun, String telecom, String model, String color, String serial, + String state, Long inId, Long outId, String price) { + Stock s = new Stock(); + s.setGroupId("demo"); + s.setGubun(gubun); + s.setTelecom(telecom); + s.setTelcompany(telecom); + s.setModel(model); + s.setColor(color); + s.setSerial(serial); + s.setBarcode(serial); + s.setCond("정상"); + s.setPay("할부"); + s.setState(state); + s.setOutcode(state.equals("OUT") || state.equals("OPEN") ? "21" : "10"); + s.setIncompanyId(inId); + s.setOutcompanyId(outId); + s.setIndate(LocalDate.now().minusDays(5)); + if (outId != null) s.setOutdate(LocalDate.now().minusDays(1)); + s.setProcessDate(LocalDate.now().minusDays(1)); + s.setProcessor("데모 관리자"); + s.setInprice(new BigDecimal(price)); + s.setUserid("demo"); + s.setProcid("demo"); + s.setUid("stk-" + UUID.randomUUID()); + em.persist(s); + return s; + } +} diff --git a/backend/src/main/java/com/bizcare/agent/DomainEntities.java b/backend/src/main/java/com/bizcare/agent/DomainEntities.java new file mode 100644 index 0000000..271c70f --- /dev/null +++ b/backend/src/main/java/com/bizcare/agent/DomainEntities.java @@ -0,0 +1,587 @@ +package com.bizcare.agent; + +import jakarta.persistence.*; +import lombok.*; +import java.math.BigDecimal; +import java.time.*; + +@MappedSuperclass @Getter @Setter @NoArgsConstructor @AllArgsConstructor +abstract class BaseEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; + String groupId; + Instant createdAt; Instant updatedAt; + @PrePersist void created() { createdAt = updatedAt = Instant.now(); } + @PreUpdate void updated() { updatedAt = Instant.now(); } +} + +@Entity @Table(name="members") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class Member extends BaseEntity { + @Column(unique=true, nullable=false) String userid; String password; String name; String phone; + String role, staffPermission; Long companyId; boolean active = true; + Integer loginCount; + LocalDateTime lastLoginAt; + String plainPassword; + /** 직원 주소 (홈즈 직원관리) */ + String address; + /** 접속제한 메모/IP 등 */ + String accessLimit; + @Column(columnDefinition="TEXT") String menuFlags; + @Column(columnDefinition="TEXT") String assignedOutNames; + @Column(columnDefinition="TEXT") String memo; +} +@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class Company extends BaseEntity { + String type, name, managerName, phone, fax, mobile, telecom, bankAccount, businessName, businessNumber, ceo, address, email; + String channel, pcode, memo, salesperson, category; + @Column(columnDefinition="TEXT") String smsPhones; boolean active = true; +} +@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class Stock extends BaseEntity { + String gubun, telecom, telcompany, serial, serials, barcode, model, color, cond, pay, memo, state, outcode, userid, procid; + String petname, outCategory, salesperson, source, processor; + @Column(unique=true) String uid; + BigDecimal inprice; Long incompanyId, outcompanyId, openId; LocalDate indate, outdate, recalldate, returndate, lossdate, selldate, processDate; +} +@Entity @Table(name="open_records") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class OpenRecord extends BaseEntity { + Long outcompanyId, phoneStockId, usimStockId, incompanyId; + String gubun, telecom, telcompany, pserial, pmodel, pcolor, userial, umodel, name, openphone, openhow, plan, salehow, joinhow, usimhow, decltype, userid, dealermemo, memo1, memo2, memo3, state; + String salesperson, employee, outCategory, openStatus, supportType; + LocalDate opendate; Integer salemon, agreemon, openHour, openMinute; + BigDecimal joinprice, usimprice, inprice, netprice, sellprice, basicprice1, basicprice2, basicprice3, basicprice4, etcprice1, etcprice2, declprice1, declprice2, moveprice, planprice, preprice, memberpoint, dealermargin1, dealermargin2; + BigDecimal commonSupport, extraSupport, switchSupport, pointPay, gradePrice; + @Column(unique=true) String uid; +} +@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class SettlementMonth extends BaseEntity { + Long outcompanyId; + @Column(name = "ym") String yearMonth; + String status, excelUrl, memo, outCategory, inquiry, agreement, invoiceStatus, remittanceStatus, outcompanyName; + Integer openCount, afterSettleCount, homeProductCount; + BigDecimal openAmount, afterSettleAmount, homeProductAmount, realSettleAmount; + boolean issued; +} +@Entity @Table(name="after_balances") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class AfterBalance extends BaseEntity { + Long outcompanyId, incompanyId, openId; + LocalDate opendate, basedate; + String outcompanyName, customerName, openphone, gubun, reason, memo, telecom, outCategory; + BigDecimal amount; +} +@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class Farebox extends BaseEntity { + Long outcompanyId; + LocalDate faredate; + String telcompany, name, phone, how, paystate, receiptNo; + String outcompanyName, outCategory, detail, registrant; + BigDecimal cashprice, cardprice, paidAmount; +} +@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class FareboxPayment extends BaseEntity { + Long outcompanyId; + String outcompanyName, mode, memo, registrant, status, outCategory; + LocalDate paydate; + Integer payCount; + BigDecimal amount; + @Column(columnDefinition="TEXT") String fareboxIds; +} +@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class BbsPost extends BaseEntity { + String code, title, authorName, userid, state, targetGroup, fileName; + @Column(columnDefinition="TEXT") String contents, comments; + boolean notice; + Integer views, confirmCount, commentCount; +} +@Entity @Table(name="board_groups") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class BoardGroup extends BaseEntity { + String category, name; + @Column(columnDefinition="TEXT") String companyIds; +} +@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class ScanDoc extends BaseEntity { + Long outcompanyId, incompanyId; + String telcompany, gubun, name, memo, sendmsg, state, src, outcompanyName; + Integer filesCount; + LocalDateTime registeredAt; +} +@Entity @Table(name="indoc") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class IncompleteDoc extends BaseEntity { + Long outcompanyId, openId, incompanyId; + LocalDate opendate, deadline; + String customerName, openphone, missingDocs, deductReason, telecom, status; + BigDecimal deductAmount; +} +@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class UnreturnedPhone extends BaseEntity { + Long outcompanyId, openId, incompanyId; + LocalDate opendate, returndate; + String customerName, openphone, model, serial, telecom, returnStatus; + BigDecimal deductAmount; +} +@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class ScheduleItem extends BaseEntity { String userid, stime, contents; LocalDate sdate; } +@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class SmsMessage extends BaseEntity { + String userid, phone, status, msgType, senderNumber, senderName; + @Column(columnDefinition="TEXT") String message; + Integer sendCount, deductCount; + Instant sentAt; +} +@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class SmsTemplate extends BaseEntity { String title; @Column(columnDefinition="TEXT") String body; } +@Entity @Table(name="sms_address_groups") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class SmsAddressGroup extends BaseEntity { + String name; +} +@Entity @Table(name="sms_address_contacts") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class SmsAddressContact extends BaseEntity { + Long addressGroupId; + String name, phone; +} +@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class PlanMaster extends BaseEntity { String telecom, name; BigDecimal price; boolean active = true; } +@Entity @Table(name="unit_cost_tables") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class UnitCostTable extends BaseEntity { + Long incompanyId; + String telecom, incompanyName, memo, category; + Integer planGroupA, planGroupB, planGroupC, planGroupD, planGroupE, planGroupF, planGroupG, policyCount; +} +@Entity @Table(name="unit_cost_policies") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class UnitCostPolicy extends BaseEntity { + Long unitCostId; + String planGroup, model, planName; + BigDecimal amount; +} +@Entity @Table(name="home_sales") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomeSale extends BaseEntity { + Long outcompanyId, incompanyId; + LocalDate installDate, receiptDate; + String status, telecom, outCategory, outcompanyName, employee, customerName, phone, address, products, missingDocs, memo; + BigDecimal extraPolicy, settleAmount, margin; +} +@Entity @Table(name="home_indocs") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomeIncompleteDoc extends BaseEntity { + Long outcompanyId, incompanyId, homeSaleId; + LocalDate installDate, deadline; + String customerName, phone, missingDocs, deductReason, telecom, status, products, outcompanyName; + BigDecimal deductAmount; +} +@Entity @Table(name="open_exchanges") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class OpenExchange extends BaseEntity { + Long outcompanyId, openId; + LocalDate exchangeDate, openDate; + LocalDateTime processedAt; + String outcompanyName, customerName, openphone, kind, newModel, newColor, newSerial, oldModel, oldColor, oldSerial, processor, memo; +} +@Entity @Table(name="open_name_changes") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class OpenNameChange extends BaseEntity { + Long outcompanyId, openId; + LocalDate changeDate, openDate; + LocalDateTime processedAt; + String outcompanyName, customerName, openphone, oldOwner, oldPhone, oldUsim, processor, memo; +} +@Entity @Table(name="open_withdrawals") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class OpenWithdrawal extends BaseEntity { + Long outcompanyId, openId; + LocalDate withdrawDate, openDate; + LocalDateTime processedAt; + String outcompanyName, customerName, openphone, processor, reason; +} +@Entity @Table(name="open_settle_histories") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class OpenSettleHistory extends BaseEntity { + Long outcompanyId, openId; + LocalDate changeDate, openDate; + LocalDateTime processedAt; + String outcompanyName, customerName, openphone, processor, memo; + BigDecimal oldAmount, newAmount; +} +@Entity @Table(name="open_delete_histories") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class OpenDeleteHistory extends BaseEntity { + Long outcompanyId, openId; + LocalDate deleteDate, openDate; + LocalDateTime processedAt; + String outcompanyName, customerName, openphone, processor, memo; +} +@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class PhoneModel extends BaseEntity { String telecom, modelCode, modelName; @Column(columnDefinition="TEXT") String colorOptions; boolean active = true; } +@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class CsInquiry extends BaseEntity { + String userid, authorName, title, status; + @Column(columnDefinition="TEXT") String content, reply; + Integer replyCount; +} +@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class Alim extends BaseEntity { String userid, title; @Column(columnDefinition="TEXT") String content; boolean confirmed; } +@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class AccountEntry extends BaseEntity { + Long outcompanyId, dealerId; + LocalDate entryDate; + String type, category, memo; + @Column(name = "ym") String yearMonth; + BigDecimal amount; +} +@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class Setting extends BaseEntity { + @Column(name = "setting_key") String settingKey; + @Lob @Column(columnDefinition = "LONGTEXT") String value; +} +@Entity @Table(name="usim_ledger") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class UsimLedger extends BaseEntity { + Long outcompanyId; + LocalDate tradeDate; + String telecom, detail, registrant, status; + Integer qty; + BigDecimal amount; +} +@Entity @Table(name="stock_history") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class StockHistory extends BaseEntity { + Long stockId, incompanyId, outcompanyId; + LocalDate processDate; + String gubun, state, statusLabel, serial, model, color, cond, memo, processor, salesperson; + String companyLabel; +} +@Entity @Table(name="stock_notes") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class StockNote extends BaseEntity { + Long stockId; + LocalDate noteDate; + String contents, authorName; +} +@Entity @Table(name="care_contracts") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class CareContract extends BaseEntity { + LocalDate contractDate, startDate, endDate; + String status, customerName, phone, productName, telecom, employee, address, memo; + BigDecimal amount, monthlyFee; +} +@Entity @Table(name="care_customers") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class CareCustomer extends BaseEntity { + String name, phone, telecom, address, status, employee, memo; +} +@Entity @Table(name="care_products") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class CareProduct extends BaseEntity { + String category, name, telecom, plan, status, memo; + BigDecimal monthlyFee, price; +} + +@Entity @Table(name="homes_products") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomesProduct extends BaseEntity { + String kind, category, company, name, model, memo; + Integer months, policyCount; + BigDecimal policyAmount; +} + +@Entity @Table(name="homes_product_policies") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomesProductPolicy extends BaseEntity { + Long productId; + String name; + BigDecimal amount; +} + +@Entity @Table(name="homes_contracts") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomesContract extends BaseEntity { + String kind, status, customerType, customerName, phone, address, products, agency, employee, payMethod, memo; + LocalDate contractDate, installDate; + Integer months; + BigDecimal policySum, settleAmount, margin; + String email, gender, nationality, customerGrade, birthDate; + String zipcode, addressDetail; + String installPhone, installZip, installAddress, installAddressDetail; + String feeReduction, invoiceType; + Boolean sameContact; +} + +@Entity @Table(name="homes_balances") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomesBalance extends BaseEntity { + /** 정산차감 / 정산추가 (구: 차감/추가) */ + String balanceType; + /** 직원 / 하부점 */ + String targetType; + /** 대상명 (직원명 또는 하부점명) */ + String targetName; + String customerName, phone, products, reason, agency, memo; + LocalDate baseDate; + BigDecimal amount; + Long contractId; +} + +@Entity @Table(name="homes_invoices") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomesInvoice extends BaseEntity { + String scope, employee, agency, status, memo; + @Column(name = "ym") String yearMonth; + Integer contractCount; + BigDecimal amount; + /** 발행 스냅샷 */ + Integer internetCount, homeCount, balanceCount; + BigDecimal internetAmount, homeAmount, balanceAmount, settleAmount; + Boolean issued; + String consent, taxInvoice, withdrawStatus; +} + +@Entity @Table(name="homes_pendings") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomesPending extends BaseEntity { + /** 과목: 일반/약속1/약속2 ... */ + String pendingType, pendingTime, customerName, phone, contents, employee, status; + LocalDate pendingDate; + /** 접수직원 (없으면 employee 사용) */ + String receiveEmployee; + /** 처리직원 */ + String processEmployee; + LocalDate processDate; + String memo; +} + +@Entity @Table(name="homes_bookings") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomesBooking extends BaseEntity { + String status, customerName, phone, products, address, employee, memo; + LocalDate bookingDate; + /** 인터넷상품 (콤마 구분) */ + String internetProducts; + /** 홈렌탈상품 (콤마 구분) */ + String rentalProducts; + String internetNote; + String rentalNote; + String zipcode; + String addressDetail; + String fileName; +} + +@Entity @Table(name="homes_customers") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomesCustomer extends BaseEntity { + /** 분류 (단골, 단골1 … / 분류없음) */ + String grade; + /** 구분: 개인 / 사업자 */ + String customerType; + String name, phone, phoneAlt, address, agency, employee, memo; + String zipcode, addressDetail, email, gender, nationality; + String birthYear, birthMonth, birthDay; + LocalDate registeredDate; +} + +@Entity @Table(name="homes_abooks") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomesAbook extends BaseEntity { + /** 입금 / 출금 / 카드입금 / 카드출금 (구: 수입/지출) */ + String entryType; + /** 과목 */ + String category; + /** 품목 */ + String item; + String agency, employee, memo; + LocalDate entryDate; + BigDecimal amount; +} + +@Entity @Table(name="homes_agencies") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomesAgency extends BaseEntity { + String name, managerName, phone, fax, mobile, agencyType, address, memo; + boolean active = true; + LocalDate registeredDate; +} + +@Entity @Table(name="homes_partners") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomesPartner extends BaseEntity { + /** 1=상부점, 2=하부점 */ + String partnerLevel, name, parentName, managerName, phone, address, memo; + /** 상부점 연결 아이디 */ + String userid; + /** 대기 / 승인 / 거절 */ + String status; + LocalDate registeredDate; +} + +@Entity @Table(name="homes_logs") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomesLog extends BaseEntity { + String domain, action, content, staff; + LocalDateTime processedAt; +} + +@Entity @Table(name="homes_notices") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomesNotice extends BaseEntity { + String title, authorName; + @Column(columnDefinition="TEXT") String contents; + Integer views; +} + +@Entity @Table(name="homes_questions") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomesQuestion extends BaseEntity { + String title, authorName, status; + @Column(columnDefinition="TEXT") String content, reply; +} + +@Entity @Table(name="homes_bbs") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomesBbs extends BaseEntity { + String title, authorName; + @Column(columnDefinition="TEXT") String contents; + Integer views; +} + +/** 접수관리 — billing_react orders 목록 필드를 Care Homes 멀티테넌트에 맞게 정리 */ +@Entity @Table(name="homes_receptions") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomesReception extends BaseEntity { + /** 접수일시 */ + LocalDateTime orderDate; + /** 상담자 */ + String counselor; + /** 지점/영업점 */ + String branch; + /** 영업점 그룹 */ + String branchGroup; + /** 영업점 연락처 */ + String branchPhone; + /** 통신사 */ + String carrier; + /** 사은품분류 */ + String giftCategory; + /** 상품분류 */ + String productCategory; + /** 상품종류 e01~e04 */ + String productType; + /** 상품명 */ + String productName; + /** 고객유형 public/company/foreigner */ + String customerType; + /** 고객명 */ + String customerName; + /** 연락처(휴대) */ + String phone; + /** 유선전화 */ + String tel; + /** 이메일 */ + String email; + /** 우편번호 */ + String zipcode; + /** 주소 */ + String address; + /** 상세주소 */ + String addressDetail; + /** 지역(시도) */ + String region; + /** 진행상황 */ + String progressStatus; + /** 주문번호 */ + String orderNumber; + /** 연결된 ISP id */ + Long ispId; + /** ISP 표시명 */ + String ispName; + /** 입고/물류 정보 */ + String inboundInfo; + /** 처리자 */ + String processor; + /** 세부진행 */ + String detailProgress; + /** 지급구분(표시용) */ + String paymentType; + /** 납부방식 transfer/card/paper/telephone */ + String paymentMethod; + String paymentName; + String paymentSsid; + String paymentTel; + String paymentBank; + String paymentAccountNumber; + String paymentCardCompany; + String paymentCardYear; + String paymentCardMonth; + String paymentCardNumber; + /** 금액 */ + BigDecimal amount; + /** 정산상태 */ + String settleStatus; + /** 개통일 */ + LocalDate openDate; + /** 해지일 */ + LocalDate cancelDate; + /** 사은품 지급일 */ + LocalDate giftDate; + /** 사은품 진행 */ + String giftStatus; + /** 지급처 */ + String giftPlace; + LocalDate giftRequested; + LocalDate giftSended; + String giftBank; + String giftAccountNumber; + String giftAccountName; + String giftSsid; + String giftGoods; + BigDecimal giftCashAmount; + BigDecimal giftGoodsPrice; + String deliveryCompany; + String trackingNumber; + /** 약정명 */ + String agreementName; + /** 개통/계약번호 */ + String contractNumber; + /** 설치일정 */ + String installSchedule; + /** 고객아이디(통신사) */ + String customerLoginId; + /** 신규전화번호 */ + String newTel; + Boolean multiLine; + Boolean addPayer; + Boolean recordFlag; + /** 주민/식별번호 */ + String identifyNumber; + /** 포스트잇 */ + String postIt; + /** 추가메모 */ + String postAdd; + @Column(columnDefinition="TEXT") String memo; + @Column(columnDefinition="TEXT") String adminMemo; + String file1; + String file2; + String file3; + String file4; + String file5; + String adminFile1; + String adminFile2; + String adminFile3; +} + +/** 접수 진행 이력 */ +@Entity @Table(name="homes_reception_progress_logs") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomesReceptionProgressLog extends BaseEntity { + Long receptionId; + String status; + String authorName; + LocalDateTime loggedAt; + @Column(columnDefinition="TEXT") String memo; + Boolean adminOnly; +} + +/** 사은품 진행 이력 — billing_react order_gift_progress_logs */ +@Entity @Table(name="homes_reception_gift_progress_logs") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomesReceptionGiftProgressLog extends BaseEntity { + Long receptionId; + String status; + String authorName; + LocalDateTime loggedAt; + @Column(columnDefinition="TEXT") String memo; +} + +/** 신규신청 통신사(ISP) 마스터 — billing_react isp 대응 */ +@Entity @Table(name="homes_reception_isps") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomesReceptionIsp extends BaseEntity { + String name; + String telecom; + Integer seq; + Boolean active; + Boolean isSktb; +} + +/** ISP 상품 */ +@Entity @Table(name="homes_reception_isp_products") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomesReceptionIspProduct extends BaseEntity { + Long ispId; + /** e01 인터넷 / e02 전화 / e03 TV / e04 렌탈 */ + String productType; + String name; + Boolean active; +} + +/** ISP 약정 */ +@Entity @Table(name="homes_reception_isp_agreements") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +class HomesReceptionIspAgreement extends BaseEntity { + Long ispId; + String name; + Boolean active; +} diff --git a/backend/src/main/java/com/bizcare/agent/FileStorageService.java b/backend/src/main/java/com/bizcare/agent/FileStorageService.java new file mode 100644 index 0000000..1fde98e --- /dev/null +++ b/backend/src/main/java/com/bizcare/agent/FileStorageService.java @@ -0,0 +1,85 @@ +package com.bizcare.agent; + +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.nio.file.*; +import java.util.UUID; + +/** 공통 파일 저장소 — billing_react FileStorageService 이식. */ +@Service +class FileStorageService { + private final Path baseDir; + + FileStorageService(@Value("${app.upload-dir:./storage/uploads}") String uploadDir) { + this.baseDir = Paths.get(uploadDir).toAbsolutePath().normalize(); + } + + @PostConstruct + void init() { + try { + Files.createDirectories(baseDir); + } catch (IOException e) { + throw new IllegalStateException("업로드 디렉터리를 생성할 수 없습니다: " + baseDir, e); + } + } + + String store(String subDir, MultipartFile file) { + if (file == null || file.isEmpty()) return null; + try { + Path dir = baseDir.resolve(subDir).normalize(); + if (!dir.startsWith(baseDir)) throw new IllegalArgumentException("잘못된 저장 경로입니다."); + Files.createDirectories(dir); + + String original = file.getOriginalFilename() == null ? "file" : file.getOriginalFilename(); + String safe = original.replaceAll("[/\\\\]", "_").replaceAll("[\\r\\n\\t]", ""); + if (safe.length() > 200) { + String ext = safe.contains(".") ? safe.substring(safe.lastIndexOf('.')) : ""; + safe = UUID.randomUUID().toString().replace("-", "") + ext; + } + Path target = dir.resolve(safe); + if (Files.exists(target)) { + safe = UUID.randomUUID().toString().substring(0, 8) + "_" + safe; + target = dir.resolve(safe); + } + file.transferTo(target); + return subDir + "/" + safe; + } catch (IOException e) { + throw new IllegalStateException("파일 저장 중 오류가 발생하였습니다.", e); + } + } + + Resource loadAsResource(String relativePath) { + try { + Path file = baseDir.resolve(relativePath).normalize(); + if (!file.startsWith(baseDir)) throw new IllegalArgumentException("잘못된 파일 경로입니다."); + Resource resource = new UrlResource(file.toUri()); + if (!resource.exists() || !resource.isReadable()) { + throw new IllegalArgumentException("파일을 찾을 수 없습니다."); + } + return resource; + } catch (java.net.MalformedURLException e) { + throw new IllegalArgumentException("파일을 찾을 수 없습니다."); + } + } + + void delete(String relativePath) { + if (relativePath == null || relativePath.isBlank()) return; + try { + Path file = baseDir.resolve(relativePath).normalize(); + if (file.startsWith(baseDir)) Files.deleteIfExists(file); + } catch (IOException ignored) { + } + } + + static String displayName(String relativePath) { + if (relativePath == null) return ""; + int idx = relativePath.lastIndexOf('/'); + return idx >= 0 ? relativePath.substring(idx + 1) : relativePath; + } +} diff --git a/backend/src/main/java/com/bizcare/agent/HealthController.java b/backend/src/main/java/com/bizcare/agent/HealthController.java new file mode 100644 index 0000000..85dbdd5 --- /dev/null +++ b/backend/src/main/java/com/bizcare/agent/HealthController.java @@ -0,0 +1,14 @@ +package com.bizcare.agent; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +class HealthController { + @GetMapping("/api/health") + Map health() { + return Map.of("status", "UP"); + } +} diff --git a/backend/src/main/java/com/bizcare/agent/SecurityConfig.java b/backend/src/main/java/com/bizcare/agent/SecurityConfig.java new file mode 100644 index 0000000..c46e7f8 --- /dev/null +++ b/backend/src/main/java/com/bizcare/agent/SecurityConfig.java @@ -0,0 +1,111 @@ +package com.bizcare.agent; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.*; +import lombok.*; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.*; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.*; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.*; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.*; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.stereotype.Component; +import org.springframework.web.cors.*; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; + +@Configuration @EnableWebSecurity +class SecurityConfig { + @Value("${app.cors.origins}") + private String corsOrigins; + + @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + @Bean SecurityFilterChain filterChain(HttpSecurity http, JwtFilter filter) throws Exception { + return http.csrf(c -> c.disable()) + .cors(c -> c.configurationSource(cors())) + .httpBasic(b -> b.disable()) + .formLogin(f -> f.disable()) + .logout(l -> l.disable()) + .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(a -> a + .requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll() + .requestMatchers("/api/auth/**", "/api/health").permitAll() + .requestMatchers("/api/agent/**").hasRole("AGENT") + .requestMatchers("/api/shop/**").hasAnyRole("SHOP", "AGENT") + .anyRequest().authenticated()) + .exceptionHandling(e -> e + .authenticationEntryPoint((req, res, ex) -> { + res.setStatus(401); + res.setContentType("application/json;charset=UTF-8"); + res.getWriter().write("{\"result\":false,\"msg\":\"로그인이 필요합니다.\",\"data\":null}"); + }) + .accessDeniedHandler((req, res, ex) -> { + res.setStatus(403); + res.setContentType("application/json;charset=UTF-8"); + res.getWriter().write("{\"result\":false,\"msg\":\"권한이 없습니다.\",\"data\":null}"); + })) + .addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class) + .build(); + } + @Bean CorsConfigurationSource cors() { + CorsConfiguration c = new CorsConfiguration(); + List origins = Arrays.stream(corsOrigins.split(",")).map(String::trim).filter(s -> !s.isEmpty()).toList(); + if (origins.isEmpty() || origins.contains("*")) { + c.setAllowedOriginPatterns(List.of("*")); + } else { + // 로컬/사설망 개발 접근을 허용하면서도 credentials 유지 + List patterns = new ArrayList<>(origins); + patterns.add("http://localhost:*"); + patterns.add("http://127.0.0.1:*"); + patterns.add("http://192.168.*.*:*"); + patterns.add("http://10.*.*.*:*"); + c.setAllowedOriginPatterns(patterns); + } + c.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); + c.setAllowedHeaders(List.of("*")); + c.setExposedHeaders(List.of("*")); + c.setAllowCredentials(true); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", c); + return source; + } +} +@Component +class JwtService { + private final byte[] secret; private final long expiration; + JwtService(@Value("${app.jwt.secret}") String secret, @Value("${app.jwt.expiration-ms}") long expiration) { this.secret=secret.getBytes(StandardCharsets.UTF_8); this.expiration=expiration; } + String create(Member m) { return Jwts.builder().subject(m.getUserid()).claim("role",m.getRole()).claim("groupId",m.getGroupId()).claim("companyId",m.getCompanyId()).issuedAt(new Date()).expiration(new Date(System.currentTimeMillis()+expiration)).signWith(Keys.hmacShaKeyFor(secret)).compact(); } + Claims claims(String token) { return Jwts.parser().verifyWith(Keys.hmacShaKeyFor(secret)).build().parseSignedClaims(token).getPayload(); } +} +@Component @RequiredArgsConstructor +class JwtFilter extends OncePerRequestFilter { + private final JwtService jwt; + @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, jakarta.servlet.ServletException { + String header=req.getHeader(HttpHeaders.AUTHORIZATION); + if(header != null && header.startsWith("Bearer ")) try { + Claims c=jwt.claims(header.substring(7)); JwtUser user=new JwtUser(c.getSubject(), c.get("role",String.class), c.get("groupId",String.class), c.get("companyId",Long.class)); + var auth=new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth); + } catch (JwtException | IllegalArgumentException ignored) { SecurityContextHolder.clearContext(); } + chain.doFilter(req,res); + } +} +@Getter @AllArgsConstructor +class JwtUser implements UserDetails { + private String userid, role, groupId; private Long companyId; + @Override public Collection getAuthorities() { return List.of(new SimpleGrantedAuthority("ROLE_"+role)); } + @Override public String getPassword() { return ""; } @Override public String getUsername() { return userid; } + @Override public boolean isAccountNonExpired(){return true;} @Override public boolean isAccountNonLocked(){return true;} @Override public boolean isCredentialsNonExpired(){return true;} @Override public boolean isEnabled(){return true;} +} diff --git a/backend/src/main/java/com/bizcare/agent/ShopController.java b/backend/src/main/java/com/bizcare/agent/ShopController.java new file mode 100644 index 0000000..5df0a9e --- /dev/null +++ b/backend/src/main/java/com/bizcare/agent/ShopController.java @@ -0,0 +1,32 @@ +package com.bizcare.agent; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import java.util.*; + +@RestController @RequestMapping("/api/shop") @RequiredArgsConstructor +class ShopController { + private final CrudService crud; + @GetMapping({"/bbs","/stocks","/opens","/settlements","/fareboxes","/indocs","/returns","/scans"}) + ApiResponse list(HttpServletRequest r,@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="30") int size) { + Map scoped=new HashMap<>(f); Class type=type(r.getRequestURI()); + if(type!=BbsPost.class && u.getCompanyId()!=null) scoped.put("outcompanyId",u.getCompanyId().toString()); + return ApiResponse.ok(crud.list(type,u,scoped,page,size)); + } + @GetMapping("/company") ApiResponse company(@AuthenticationPrincipal JwtUser u) { + if(u.getCompanyId()==null) return ApiResponse.fail("소속 매장이 없습니다."); return ApiResponse.ok(crud.get(Company.class,u.getCompanyId(),u)); + } + @PutMapping("/company") ApiResponse companyPut(@AuthenticationPrincipal JwtUser u,@RequestBody Map body) { + if(u.getCompanyId()==null) return ApiResponse.fail("소속 매장이 없습니다."); return ApiResponse.ok(crud.update(Company.class,u.getCompanyId(),body,u)); + } + @PostMapping("/scans") ApiResponse scan(@AuthenticationPrincipal JwtUser u,@RequestBody Map body) { + body.put("outcompanyId",u.getCompanyId()); return ApiResponse.ok(crud.create(ScanDoc.class,body,u)); + } + @PutMapping("/scans/{id}") ApiResponse scanPut(@AuthenticationPrincipal JwtUser u,@PathVariable Long id,@RequestBody Map body) { return ApiResponse.ok(crud.update(ScanDoc.class,id,body,u)); } + @SuppressWarnings("unchecked") private Class type(String uri) { + if(uri.contains("stocks"))return (Class)Stock.class;if(uri.contains("opens"))return (Class)OpenRecord.class;if(uri.contains("settlements"))return (Class)SettlementMonth.class; + if(uri.contains("fareboxes"))return (Class)Farebox.class;if(uri.contains("indocs"))return (Class)IncompleteDoc.class;if(uri.contains("returns"))return (Class)UnreturnedPhone.class;if(uri.contains("scans"))return (Class)ScanDoc.class;return (Class)BbsPost.class; + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties new file mode 100644 index 0000000..0b1a488 --- /dev/null +++ b/backend/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=agent diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 0000000..ef0ef64 --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -0,0 +1,30 @@ +spring: + datasource: + url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3308/billcare?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8} + username: ${SPRING_DATASOURCE_USERNAME:billcare} + password: ${SPRING_DATASOURCE_PASSWORD:billcare} + jpa: + hibernate: + ddl-auto: update + open-in-view: false + properties: + hibernate: + format_sql: true + jackson: + time-zone: Asia/Seoul + servlet: + multipart: + max-file-size: 20MB + max-request-size: 25MB + autoconfigure: + exclude: + - org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration +server: + port: 8080 +app: + jwt: + secret: ${APP_JWT_SECRET:billcare-bizcare-homes-secret-key-must-be-long-enough-256bits} + expiration-ms: ${APP_JWT_EXPIRATION_MS:86400000} + cors: + origins: ${APP_CORS_ORIGINS:http://localhost:5174,http://localhost:82,http://127.0.0.1:82} + upload-dir: ${APP_UPLOAD_DIR:./storage/uploads} diff --git a/backend/src/test/java/com/bizcare/agent/AgentApplicationTests.java b/backend/src/test/java/com/bizcare/agent/AgentApplicationTests.java new file mode 100644 index 0000000..bafeb68 --- /dev/null +++ b/backend/src/test/java/com/bizcare/agent/AgentApplicationTests.java @@ -0,0 +1,13 @@ +package com.bizcare.agent; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class AgentApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..df60401 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,60 @@ +services: + mysql: + image: mysql:8.0 + container_name: billcare-mysql + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: billcare + MYSQL_USER: billcare + MYSQL_PASSWORD: billcare + ports: + - "3308:3306" + volumes: + - billcare_mysql:/var/lib/mysql + command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"] + interval: 5s + timeout: 5s + retries: 20 + start_period: 20s + + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: billcare-backend + depends_on: + mysql: + condition: service_healthy + environment: + SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/billcare?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8 + SPRING_DATASOURCE_USERNAME: billcare + SPRING_DATASOURCE_PASSWORD: billcare + APP_CORS_ORIGINS: http://localhost:82,http://127.0.0.1:82,http://localhost:5174 + APP_UPLOAD_DIR: /app/storage/uploads + volumes: + - billcare_uploads:/app/storage/uploads + ports: + - "8082:8080" + healthcheck: + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/api/health"] + interval: 5s + timeout: 3s + retries: 20 + start_period: 40s + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: billcare-frontend + depends_on: + backend: + condition: service_healthy + ports: + - "82:80" + +volumes: + billcare_mysql: + billcare_uploads: diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..28d8226 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,6 @@ +node_modules +dist +.git +.DS_Store +*.log +.env* diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/.oxlintrc.json b/frontend/.oxlintrc.json new file mode 100644 index 0000000..6fa991d --- /dev/null +++ b/frontend/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..f757e56 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,14 @@ +# syntax=docker/dockerfile:1 + +FROM node:22-alpine AS build +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM nginx:1.27-alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..d6af7e3 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,32 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the Oxlint configuration + +If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`: + +```json +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "options": { + "typeAware": true + }, + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} +``` + +See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories. diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..acc9162 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + 빌링 케어 / 비즈케어 홈즈 + + +
+ + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..1e7fadb --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,28 @@ +server { + listen 80; + server_name _; + client_max_body_size 20m; + + location /api/ { + proxy_pass http://backend:8080/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Authorization $http_authorization; + proxy_pass_request_headers on; + proxy_connect_timeout 10s; + proxy_send_timeout 120s; + proxy_read_timeout 120s; + proxy_next_upstream error timeout http_502 http_503; + proxy_next_upstream_tries 2; + proxy_next_upstream_timeout 10s; + } + + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html; + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..dd9657a --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3607 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@ant-design/icons": "^6.3.2", + "@ant-design/plots": "^2.6.8", + "@tanstack/react-query": "^5.101.2", + "antd": "^6.5.0", + "axios": "^1.18.1", + "dayjs": "^1.11.21", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-router-dom": "^7.18.1" + }, + "devDependencies": { + "@types/node": "^24.13.2", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "oxlint": "^1.71.0", + "typescript": "~6.0.2", + "vite": "^8.1.1" + } + }, + "node_modules/@ant-design/charts-util": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@ant-design/charts-util/-/charts-util-0.0.3.tgz", + "integrity": "sha512-x1H7UT6t4dXAyGRoHqlOnEsEqBSTANFGTZEAMI0CWYhYUpp13n0o9grl9oPtoL6FEQMjUBTY+zGJKlHkz8smMw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": ">=16.8.4", + "react-dom": ">=16.8.4" + } + }, + "node_modules/@ant-design/colors": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.1.tgz", + "integrity": "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.0" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-2.1.2.tgz", + "integrity": "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-2.1.2.tgz", + "integrity": "sha512-5fTHQ158jJJ5dC/ECeyIdZUzKxE/mpEMRZxthyG1sw/AKRHKgJBg00Yi6ACVXgycdje7KahRNvNET/uBccwCnA==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^2.1.2", + "@babel/runtime": "^7.23.2", + "@rc-component/util": "^1.4.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.1.tgz", + "integrity": "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw==", + "license": "MIT", + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/icons": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.3.2.tgz", + "integrity": "sha512-B6O5a5XJ4wjtNOfZejXYwHW5zvKV5gYkjGf11dHGLEbKn0ABDGndo41+gfIiXyTFhvESj4XTotuud33mUFid0g==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.1", + "@ant-design/icons-svg": "^4.5.0", + "@rc-component/util": "^1.11.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.5.0.tgz", + "integrity": "sha512-1BTUFyKPTBZ53MuTP8s0k5SFEXL7o3VHEOwLgzaoWKwnBeqIcqUtVshc4SKzhI6uACfqhJqBwBUE9FsWR3uULA==", + "license": "MIT" + }, + "node_modules/@ant-design/plots": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/@ant-design/plots/-/plots-2.6.8.tgz", + "integrity": "sha512-QsunUs2d5rbq/1BwVhga/siA5H50OaG23YopMYwPD4sPsza6NQzPQ8FM3elNIsD/BIk298tihqX1cJ/MmvVJbQ==", + "license": "MIT", + "dependencies": { + "@ant-design/charts-util": "0.0.3", + "@antv/event-emitter": "^0.1.3", + "@antv/g": "^6.1.7", + "@antv/g2": "^5.2.7", + "@antv/g2-extension-plot": "^0.2.1", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": ">=16.8.4", + "react-dom": ">=16.8.4" + } + }, + "node_modules/@ant-design/react-slick": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-2.0.0.tgz", + "integrity": "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "clsx": "^2.1.1", + "json2mq": "^0.2.0", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@antv/component": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@antv/component/-/component-2.1.11.tgz", + "integrity": "sha512-dTdz8VAd3rpjOaGEZTluz82mtzrP4XCtNlNQyrxY7VNRNcjtvpTLDn57bUL2lRu1T+iklKvgbE2llMriWkq9vQ==", + "license": "MIT", + "dependencies": { + "@antv/g": "^6.1.11", + "@antv/scale": "^0.4.16", + "@antv/util": "^3.3.10", + "svg-path-parser": "^1.1.0" + } + }, + "node_modules/@antv/component/node_modules/@antv/scale": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@antv/scale/-/scale-0.4.16.tgz", + "integrity": "sha512-5wg/zB5kXHxpTV5OYwJD3ja6R8yTiqIOkjOhmpEJiowkzRlbEC/BOyMvNUq5fqFIHnMCE9woO7+c3zxEQCKPjw==", + "license": "MIT", + "dependencies": { + "@antv/util": "^3.3.7", + "color-string": "^1.5.5", + "fecha": "^4.2.1" + } + }, + "node_modules/@antv/coord": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@antv/coord/-/coord-0.4.7.tgz", + "integrity": "sha512-UTbrMLhwJUkKzqJx5KFnSRpU3BqrdLORJbwUbHK2zHSCT3q3bjcFA//ZYLVfIlwqFDXp/hzfMyRtp0c77A9ZVA==", + "license": "MIT", + "dependencies": { + "@antv/scale": "^0.4.12", + "@antv/util": "^2.0.13", + "gl-matrix": "^3.4.3" + } + }, + "node_modules/@antv/coord/node_modules/@antv/scale": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@antv/scale/-/scale-0.4.16.tgz", + "integrity": "sha512-5wg/zB5kXHxpTV5OYwJD3ja6R8yTiqIOkjOhmpEJiowkzRlbEC/BOyMvNUq5fqFIHnMCE9woO7+c3zxEQCKPjw==", + "license": "MIT", + "dependencies": { + "@antv/util": "^3.3.7", + "color-string": "^1.5.5", + "fecha": "^4.2.1" + } + }, + "node_modules/@antv/coord/node_modules/@antv/scale/node_modules/@antv/util": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@antv/util/-/util-3.3.11.tgz", + "integrity": "sha512-FII08DFM4ABh2q5rPYdr0hMtKXRgeZazvXaFYCs7J7uTcWDHUhczab2qOCJLNDugoj8jFag1djb7wS9ehaRYBg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "gl-matrix": "^3.3.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@antv/coord/node_modules/@antv/util": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@antv/util/-/util-2.0.17.tgz", + "integrity": "sha512-o6I9hi5CIUvLGDhth0RxNSFDRwXeywmt6ExR4+RmVAzIi48ps6HUy+svxOCayvrPBN37uE6TAc2KDofRo0nK9Q==", + "license": "ISC", + "dependencies": { + "csstype": "^3.0.8", + "tslib": "^2.0.3" + } + }, + "node_modules/@antv/event-emitter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@antv/event-emitter/-/event-emitter-0.1.3.tgz", + "integrity": "sha512-4ddpsiHN9Pd4UIlWuKVK1C4IiZIdbwQvy9i7DUSI3xNJ89FPUFt8lxDYj8GzzfdllV0NkJTRxnG+FvLk0llidg==", + "license": "MIT" + }, + "node_modules/@antv/expr": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@antv/expr/-/expr-1.0.2.tgz", + "integrity": "sha512-vrfdmPHkTuiS5voVutKl2l06w1ihBh9A8SFdQPEE+2KMVpkymzGOF1eWpfkbGZ7tiFE15GodVdhhHomD/hdIwg==", + "license": "MIT" + }, + "node_modules/@antv/g": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@antv/g/-/g-6.3.1.tgz", + "integrity": "sha512-WYEKqy86LHB2PzTmrZXrIsIe+3Epeds2f68zceQ+BJtRoGki7Sy4IhlC8LrUMztgfT1t3d/0L745NWZwITroKA==", + "license": "MIT", + "dependencies": { + "@antv/g-lite": "2.7.0", + "@antv/util": "^3.3.5", + "@babel/runtime": "^7.25.6", + "gl-matrix": "^3.4.3", + "html2canvas": "^1.4.1" + } + }, + "node_modules/@antv/g-canvas": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@antv/g-canvas/-/g-canvas-2.2.0.tgz", + "integrity": "sha512-h7zVBBo2aO64DuGKvq9sG+yTU3sCUb9DALCVm7nz8qGPs8hhLuFOkKPEzUDNfNYZGJUGzY8UDtJ3QRGRFcvEQg==", + "license": "MIT", + "dependencies": { + "@antv/g-lite": "2.7.0", + "@antv/g-math": "3.1.0", + "@antv/util": "^3.3.5", + "@babel/runtime": "^7.25.6", + "gl-matrix": "^3.4.3", + "tslib": "^2.5.3" + } + }, + "node_modules/@antv/g-lite": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@antv/g-lite/-/g-lite-2.7.0.tgz", + "integrity": "sha512-uSzgHYa5bwR5L2Au7/5tsOhFmXKZKLPBH90+Q9bP9teVs5VT4kOAi0isPSpDI8uhdDC2/VrfTWu5K9HhWI6FWw==", + "license": "MIT", + "dependencies": { + "@antv/g-math": "3.1.0", + "@antv/util": "^3.3.5", + "@antv/vendor": "^1.0.3", + "@babel/runtime": "^7.25.6", + "eventemitter3": "^5.0.1", + "gl-matrix": "^3.4.3", + "tslib": "^2.5.3" + } + }, + "node_modules/@antv/g-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@antv/g-math/-/g-math-3.1.0.tgz", + "integrity": "sha512-DtN1Gj/yI0UiK18nSBsZX8RK0LszGwqfb+cBYWgE+ddyTm8dZnW4tPUhV7QXePsS6/A5hHC+JFpAAK7OEGo5ZQ==", + "license": "MIT", + "dependencies": { + "@antv/util": "^3.3.5", + "@babel/runtime": "^7.25.6", + "gl-matrix": "^3.4.3", + "tslib": "^2.5.3" + } + }, + "node_modules/@antv/g-plugin-dragndrop": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@antv/g-plugin-dragndrop/-/g-plugin-dragndrop-2.1.1.tgz", + "integrity": "sha512-+aesDUJVQDs6UJ2bOBbDlaGAPCfHmU0MbrMTlQlfpwNplWueqtgVAZ3L57oZ2ZGHRWUHiRwZGPjXMBM3O2LELw==", + "license": "MIT", + "dependencies": { + "@antv/g-lite": "2.7.0", + "@antv/util": "^3.3.5", + "@babel/runtime": "^7.25.6", + "tslib": "^2.5.3" + } + }, + "node_modules/@antv/g2": { + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/@antv/g2/-/g2-5.4.8.tgz", + "integrity": "sha512-IvgIpwmT4M5/QAd3Mn2WiHIDeBqFJ4WA2gcZhRRSZuZ2KmgCqZWZwwIT0hc+kIGxwYeDoCQqf//t6FMVu3ryBg==", + "license": "MIT", + "dependencies": { + "@antv/component": "^2.1.9", + "@antv/coord": "^0.4.7", + "@antv/event-emitter": "^0.1.3", + "@antv/expr": "^1.0.2", + "@antv/g": "^6.1.24", + "@antv/g-canvas": "^2.0.43", + "@antv/g-plugin-dragndrop": "^2.0.35", + "@antv/scale": "^0.5.1", + "@antv/util": "^3.3.10", + "@antv/vendor": "^1.0.11", + "flru": "^1.0.2", + "pdfast": "^0.2.0" + } + }, + "node_modules/@antv/g2-extension-plot": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@antv/g2-extension-plot/-/g2-extension-plot-0.2.2.tgz", + "integrity": "sha512-KJXCXO7as+h0hDqirGXf1omrNuYzQmY3VmBmp7lIvkepbQ7sz3pPwy895r1FWETGF3vTk5UeFcAF5yzzBHWgbw==", + "dependencies": { + "@antv/g2": "^5.1.8", + "@antv/util": "^3.3.5", + "@antv/vendor": "^1.0.10" + } + }, + "node_modules/@antv/scale": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@antv/scale/-/scale-0.5.2.tgz", + "integrity": "sha512-rTHRAwvpHWC5PGZF/mJ2ZuTDqwwvVBDRph0Uu5PV9BXwzV7K8+9lsqGJ+XHVLxe8c6bKog5nlzvV/dcYb0d5Ow==", + "license": "MIT", + "dependencies": { + "@antv/util": "^3.3.7", + "color-string": "^1.5.5", + "fecha": "^4.2.1" + } + }, + "node_modules/@antv/util": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@antv/util/-/util-3.3.11.tgz", + "integrity": "sha512-FII08DFM4ABh2q5rPYdr0hMtKXRgeZazvXaFYCs7J7uTcWDHUhczab2qOCJLNDugoj8jFag1djb7wS9ehaRYBg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "gl-matrix": "^3.3.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@antv/vendor": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@antv/vendor/-/vendor-1.0.11.tgz", + "integrity": "sha512-LmhPEQ+aapk3barntaiIxJ5VHno/Tyab2JnfdcPzp5xONh/8VSfed4bo/9xKo5HcUAEydko38vYLfj6lJliLiw==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.2.1", + "@types/d3-color": "^3.1.3", + "@types/d3-dispatch": "^3.0.6", + "@types/d3-dsv": "^3.0.7", + "@types/d3-ease": "^3.0.2", + "@types/d3-fetch": "^3.0.7", + "@types/d3-force": "^3.0.10", + "@types/d3-format": "^3.0.4", + "@types/d3-geo": "^3.1.0", + "@types/d3-hierarchy": "^3.1.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-path": "^3.1.0", + "@types/d3-quadtree": "^3.0.6", + "@types/d3-random": "^3.0.3", + "@types/d3-scale": "^4.0.9", + "@types/d3-scale-chromatic": "^3.1.0", + "@types/d3-shape": "^3.1.7", + "@types/d3-time": "^3.0.4", + "@types/d3-timer": "^3.0.2", + "d3-array": "^3.2.4", + "d3-color": "^3.1.0", + "d3-dispatch": "^3.0.1", + "d3-dsv": "^3.0.1", + "d3-ease": "^3.0.1", + "d3-fetch": "^3.0.1", + "d3-force": "^3.0.0", + "d3-force-3d": "^3.0.5", + "d3-format": "^3.1.0", + "d3-geo": "^3.1.1", + "d3-geo-projection": "^4.0.0", + "d3-hierarchy": "^3.1.2", + "d3-interpolate": "^3.0.1", + "d3-path": "^3.1.0", + "d3-quadtree": "^3.0.1", + "d3-random": "^3.0.1", + "d3-regression": "^1.3.10", + "d3-scale": "^4.0.2", + "d3-scale-chromatic": "^3.1.0", + "d3-shape": "^3.2.0", + "d3-time": "^3.1.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.73.0.tgz", + "integrity": "sha512-HZQRN/UMBu+Ut+/9MiAChkbP4qZqrNOWBcNI45vOT40GVhbGR0JgHB87L48D4iAqFQIdVmeQYtV9RF89AjTKkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.73.0.tgz", + "integrity": "sha512-Gp+KJRylv2aW7thRpG5p1KTxZq4ZJFbWowrKzufNq9d3ssl3r3JviYV45/+p+7CN1Nv0zDd1e8Ex0b/HUDq4TQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.73.0.tgz", + "integrity": "sha512-3de96NdtXhxERMjIz7wsp2HYMY6pMQycGxFWac2mFecAx6VeARF/IqFb1QIaqiCRIdfzBwzTed+pCTCoiS+CYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.73.0.tgz", + "integrity": "sha512-5zx/uPW32TiaOeVY1dQ/H5iOf0K1HOdFKOJhLqGl4o63+i1fpzoqqu/mKtd7OFgFjNCdhlyTGgjVkQTZm1ELcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.73.0.tgz", + "integrity": "sha512-qNe4gKHaGnLuZJ8toUg90JAa0S2vTVvDw+0bRi3q1avXZXDT4u5mMeECf3nD4HYrbdn1O7dXqWut4onY/yx/Xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.73.0.tgz", + "integrity": "sha512-cCehYh5hTbfShm/fxTD6wwrGUWIpvX+N5OxmAMhFhDeTGXvw+BeNj889tpxsFQ9ZLatQ6wImuY8tsKLZ+FMz7w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.73.0.tgz", + "integrity": "sha512-d5j5GDU/2dMgjVhw7TQT9ITrsIr1Y02KEXKyVGIXUkD+KiaxE9TP65FS2ZdgTBemQvoRL+gSBdbrIm3cQIeacg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.73.0.tgz", + "integrity": "sha512-Eyf1SrP3+yR1DI3OJgOY2Pvrr9dWP9TK37xPaDYycwTtlGlI45erJAVIfH5/m/xosDt6BupJYEFi47bvbTuuyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.73.0.tgz", + "integrity": "sha512-IlT/OJApEDKaMmCooHuncgJZbbCe7T5QIWmTZBEtYscWvzPQuuEinVcid6kwQRVQOUdb7PUCz4jQHnaYXdfJXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.73.0.tgz", + "integrity": "sha512-L+JYcb/vdg5fmcH08V6o0YYLU28cTH1SPNulwJdvK9NK49aXSkYy6oNpKBmddArVOXYqNepriDGiZ04G54kh1Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.73.0.tgz", + "integrity": "sha512-Qtk0g3bKV6OwWjIm7R8kQN1uOZRKQt/MODK2a8QfkwhTpXBD53ozx5XLVWLGDQAVyp2otLW4D2wB98XfAfMPGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.73.0.tgz", + "integrity": "sha512-wX0NQKZVxltkAOVmzFcpOaMpdaUvsq1Eqpx9tkAfl71UdkTlSo1R4AdAnGccR1Fm2+TzFgZ22CyyGuZ41RDr/A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.73.0.tgz", + "integrity": "sha512-vPe7UGBMWyiLTtnqS4xxgMQFSFGmtQwhwCxuiw6lXygaO6bVt0D8dFVg8Xv05eaiN3ybC0HXXHUAohFMFvqoCQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.73.0.tgz", + "integrity": "sha512-2CwIWr9cemFC/CbRBWZvuk5mffz6ObmfFkfcC/9rTQ7f+icNhYr2kOjf9Rt8lLvugvkdGDOmkoVoFFHh6ClCTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.73.0.tgz", + "integrity": "sha512-nDadfJgg7NBBxG0N560wOe7LLX5QiYp6qBaI7viuk5EUORFBktU/NfV0MbTqU3gTqQDCh4VyxKdo5VADxk9w8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.73.0.tgz", + "integrity": "sha512-wGjJC+NLH9xP+IKGn9RDW94ojJR/wPbg5WCnQjj/oReaOtCQthr8ws1zICe77JFmo4ouUdeTHHZL/ESGiF6Pmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.73.0.tgz", + "integrity": "sha512-I7X47GPGljw225YUQ5SbC/rb1Kkdrd0yQf0x+hYxeKS6DpfjMbo9ccQPQ6LNY6BoJQ1sHhgDUGuMn5Vg5gHT6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.73.0.tgz", + "integrity": "sha512-5lWj+3h+74Fm1jYOO9qkJA4xkAlZA099DkXppuXsk7UpnpZLttsefrZU469vChGaG6hcSqrkKXQOvMTZtbjeNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.73.0.tgz", + "integrity": "sha512-WaNRvh4f6zY9CvUQk2YoA1O90ieWrIklI84+HXFr9Isjz9CSESrdqo/RtIYt4Dll/cAchqGDMehfaZd0vqEFZw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rc-component/async-validator": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-6.0.0.tgz", + "integrity": "sha512-D3AGQwdyE58gmvx6waVSXJ80JGO+IY5L2O8HDnSOex7JNlzB3GuN/4hyHNTdhy2qtOhkpbIjmeAN3tL993wKbA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/cascader": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.17.0.tgz", + "integrity": "sha512-3cVNG0zrQF1PoXq262L3wGCU+/YLEC1mGSVHDl577dQmA0ZKkXFbY6nwyXo+beCcM7buo49t24jkr+QZdL7O8w==", + "license": "MIT", + "dependencies": { + "@rc-component/select": "~1.8.0", + "@rc-component/tree": "~1.3.2", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/checkbox": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/checkbox/-/checkbox-2.0.0.tgz", + "integrity": "sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/collapse": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/collapse/-/collapse-1.2.0.tgz", + "integrity": "sha512-ZRYSKSS39qsFx93p26bde7JUZJshsUBEQRlRXPuJYlAiNX0vyYlF5TsAm8JZN3LcF8XvKikdzPbgAtXSbkLUkw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-3.1.1.tgz", + "integrity": "sha512-OHaCHLHszCegdXmIq2ZRIZBN/EtpT6Wm8SG/gpzLATHbVKc/avvuKi+zlOuk05FTWvgaMmpxAko44uRJ3M+2pg==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.1", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/context": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-2.0.2.tgz", + "integrity": "sha512-uiGpAlblCNlziHPwj4S4Iy/oemeuz/hR03mbiEjTCXwsqOIN3BOzsRMyDwpyO5Fm0vIEEJRUf9ZtbRLbhksuTA==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/dialog": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@rc-component/dialog/-/dialog-1.10.0.tgz", + "integrity": "sha512-eDukNlz9vNszAGv7i3zKXdxEd3wgVmNxuJijYt8zvTh17QwTu8KK/bdURRd/lU4qaMzhO1HKKmMrwOnkaw0BvQ==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.3.3", + "@rc-component/portal": "^2.1.0", + "@rc-component/util": "^1.9.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/drawer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@rc-component/drawer/-/drawer-1.4.2.tgz", + "integrity": "sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.1.3", + "@rc-component/util": "^1.9.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/dropdown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/dropdown/-/dropdown-1.0.2.tgz", + "integrity": "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg==", + "license": "MIT", + "dependencies": { + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/@rc-component/form": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.8.5.tgz", + "integrity": "sha512-d24EYtvUOBhxEtSd/EqIu9DaMuqrWF2IRIvAFCTM6NQ/GJIYNr8DvEpUSUlv2uPxEJ0ZPwYQ+wwlGIAaiHvdrw==", + "license": "MIT", + "dependencies": { + "@rc-component/async-validator": "^6.0.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/image": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/image/-/image-1.9.0.tgz", + "integrity": "sha512-khF7w7xkBH5B1bsBcI1FSUZdkyd1aqpl2eYyILCqCzzQH3XdfehGUaZTnptyaJJfs09/R5hv9jXWyazOMFIClQ==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.0.0", + "@rc-component/portal": "^2.1.2", + "@rc-component/util": "^1.10.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/input": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rc-component/input/-/input-1.3.1.tgz", + "integrity": "sha512-iFvTUT9W+JC/MSin2aGAk8NqsVlTzcExNC9DZariON1IWirju9NoNeEk47an4Q8iHazkoVI/y1LnDi88+CPcig==", + "license": "MIT", + "dependencies": { + "@rc-component/resize-observer": "^1.1.1", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/input-number": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@rc-component/input-number/-/input-number-1.6.2.tgz", + "integrity": "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w==", + "license": "MIT", + "dependencies": { + "@rc-component/mini-decimal": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mentions": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@rc-component/mentions/-/mentions-1.10.0.tgz", + "integrity": "sha512-CI1njYUVY0NjHtLhNoVmXlJyy568Sfep9Wsak6vmGjtT6uazx98djGYlCXz2xkHhEm73g91Y3MTvzUyE5avI7w==", + "license": "MIT", + "dependencies": { + "@rc-component/input": "~1.3.0", + "@rc-component/menu": "~1.4.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/menu": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@rc-component/menu/-/menu-1.4.1.tgz", + "integrity": "sha512-3GsVRoQ4cnF/AoIQ4P+Z1haBfgfBPQfLT1RJY3Nu4DzOnheTslfCiGSPj7bv/cLj5sW5pHqN25dDXGP3JELAlQ==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.4.tgz", + "integrity": "sha512-xiuXcaCwyOWpD8a8scdExFl+bntNphAW8XeenL1ig2en0AAZY0Pcp4pC0dI22qJ+NvxKn9RoNIoRdqYU3BLH4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/motion": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@rc-component/motion/-/motion-1.3.3.tgz", + "integrity": "sha512-Xh3IszxvlSv3/PLYFyC2UZi9LNB83yOnkB/LNmRzaypZLvkhqUIPS7MQpGZcCMWrNsXV2p6YTSWbSGvFpEle9A==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-2.0.1.tgz", + "integrity": "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/notification": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@rc-component/notification/-/notification-2.0.7.tgz", + "integrity": "sha512-nqZzpf6BPdaj+3ILx7si79LLmqPKyUmQoXa+/9gg0SkH0v1DbD66oJgRMSBEVnd/zUT3D4gwxWIHUKebYf2ZXQ==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.11.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/overflow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/overflow/-/overflow-1.0.1.tgz", + "integrity": "sha512-syfmgAABaHCnCDzPwHZ/2tuvIcpOO3jefYZMmfkN+pmo8HKTzsfhS57vxo4ksPdN0By+uWVJhJWNFozNBxi2eA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/pagination": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/pagination/-/pagination-1.4.0.tgz", + "integrity": "sha512-CW1g7P9V8u+e8JQdUsl2RWg+GCsoee0mtJjZUCCxn/vb3jzOwDKm6hAdwddHCVBfWJ58eGUBZz3IvnU8rRktjw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/picker": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@rc-component/picker/-/picker-1.11.0.tgz", + "integrity": "sha512-6qXGKtoJvO8sUd17m5cyNEbEJub0zflCHnaZTBBmj63DPRZYc0WEHN8rp6hFSl+yMCJS/dJY5G+1fQ8bLCuD7A==", + "license": "MIT", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/trigger": "^3.6.15", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/@rc-component/portal": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-2.2.1.tgz", + "integrity": "sha512-ck+r1kW/JSv0wxPji3KN2ss9K6Z0qqwusw/mf/0JobXhZ8hC2ejZwCJObW/SvDi0uhA0VzmCnx0CaCci95tcmA==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/progress": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/progress/-/progress-1.0.2.tgz", + "integrity": "sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-2.0.0.tgz", + "integrity": "sha512-aAv3QhPP1xyafuTZOxub6a54pCeBnN3IwQkpETrBtthq4BL5IgxnCbuoBWPDpdLw1y1j6BgBUCAKV92+yX06Dw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/rate": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/rate/-/rate-1.0.1.tgz", + "integrity": "sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/resize-observer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/resize-observer/-/resize-observer-1.1.2.tgz", + "integrity": "sha512-t/Bb0W8uvL4PYKAB3YcChC+DlHh0Wt5kM7q/J+0qpVEUMLe7Hk5zuvc9km0hMnTFPSx5Z7Wu/fzCLN6erVLE8Q==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/segmented": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rc-component/segmented/-/segmented-1.3.0.tgz", + "integrity": "sha512-5J/bJ01mbDnoA6P/FW8SxUvKn+OgUSTZJPzCNnTBntG50tzoP7DydGhqxp7ggZXZls7me3mc2EQDXakU3iTVFg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/select": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@rc-component/select/-/select-1.8.2.tgz", + "integrity": "sha512-HQ9zuYqjfZTlcEMWlU1GAPBajd2OHIMVHyjZSGVTCVARwkfCgvXZMTEn0cduy3L+ejAKkaZluOQvxovZoaJaQw==", + "license": "MIT", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.11.1", + "@rc-component/virtual-list": "^1.2.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/slider": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/slider/-/slider-1.1.1.tgz", + "integrity": "sha512-LSzgWGYDgeCDgR4r1XlU29gbYws6HpLnvJd/uMhLeW/vQgxldeR+Wb4uzHDCHiYEbr1bnEHWdjkPxjJRHxuiig==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/steps": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rc-component/steps/-/steps-1.2.2.tgz", + "integrity": "sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/switch": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rc-component/switch/-/switch-1.0.3.tgz", + "integrity": "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/table": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@rc-component/table/-/table-1.10.4.tgz", + "integrity": "sha512-HwoTnrwc29zeoXkXGhWqzJh8FIibGUxi1jM4LtoSzmR9d5Vv5osUQpZxnXKBP8iOCvyD6BQzZm1nXJRcnrxpAg==", + "license": "MIT", + "dependencies": { + "@rc-component/context": "^2.0.1", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.11.1", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/tabs": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.11.0.tgz", + "integrity": "sha512-hA/drZYOVa/MMIb4M2fWf3yaTyTG4qVuIABmghvEhyfw2nBob5VTH69lMCDjSVKmgODjO6nWlCV+gVn3xBrj5Q==", + "license": "MIT", + "dependencies": { + "@rc-component/dropdown": "~1.0.0", + "@rc-component/menu": "~1.4.0", + "@rc-component/motion": "^1.1.3", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tooltip": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/tooltip/-/tooltip-1.4.0.tgz", + "integrity": "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg==", + "license": "MIT", + "dependencies": { + "@rc-component/trigger": "^3.7.1", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-2.4.0.tgz", + "integrity": "sha512-aui4r4TqmTzwaBgcQxHYep8kM8PTjZFufjokObpy35KfFeZ0k9ArquWFZqegQlH24P14t+F0qO0mGTgzlav1yg==", + "license": "MIT", + "dependencies": { + "@rc-component/portal": "^2.2.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.7.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tree": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@rc-component/tree/-/tree-1.3.2.tgz", + "integrity": "sha512-bJFj46wEkpBPnWyTm18XmgAgNQ/4YvprxMOPPY2a6rmhGJYxLuNKEFiL5Qej4Qctu9wHJm8WW+v2SYskafE0kA==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.0.0", + "@rc-component/util": "^1.11.1", + "@rc-component/virtual-list": "^1.2.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/tree-select": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@rc-component/tree-select/-/tree-select-1.11.0.tgz", + "integrity": "sha512-EhS0X0wtUhBfK4S5TlpSY3MR9ndPMGgujtt1PJW3Ej+ToAlnS/6ohYURtCoXBYGqazUwHmgQGVUDsfpVwhWPkg==", + "license": "MIT", + "dependencies": { + "@rc-component/select": "~1.8.0", + "@rc-component/tree": "~1.3.2", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/trigger": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-3.9.1.tgz", + "integrity": "sha512-LNsYvz60mrLJ/kRvKcHE7boUvcQfVMCfRqZ71x3Fo9AOiZ1KKIEqkzMA8DNvz2V3Bcvir/vwQNn7JF1NPODQ7Q==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.2.0", + "@rc-component/resize-observer": "^1.1.1", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/upload": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/upload/-/upload-1.1.1.tgz", + "integrity": "sha512-GvYWSKeaJTOxxC5p6+nOSadzfvXA1h8C/iHFPFZX+szH3JUXrvs+DLiW8YUTBgvMh8m63mJeHrlYlJzAlg+pDA==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/util": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.11.1.tgz", + "integrity": "sha512-awVlI3ub2vqfqkYxOBc/uQ0efm3jw0wcrhtO/YWLyZfxiKXczKwNbVuhlnyxytDt7H9pbbVQiqr+O6MLATtRYg==", + "license": "MIT", + "dependencies": { + "is-mobile": "^5.0.0", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/virtual-list": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@rc-component/virtual-list/-/virtual-list-1.3.2.tgz", + "integrity": "sha512-/smuvWBFdP/Is9QuNDKynD0+T3XTXWFyNXXNKJ4sno8CE3bTOK8sfgYmQJtYwLUNX+lv0Ytd+PMshgpmdReq5g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^8.0.0", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/virtual-list/node_modules/@babel/runtime": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-8.0.0.tgz", + "integrity": "sha512-sL6cvO2IfkSu/iU+zs2S/w01B7A8V7suXSIKEN4hPFFdZoiPGxrj5pAG0lCaqLWiEIrjKzdznIWuaLcxPR53qw==", + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tanstack/query-core": { + "version": "5.101.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.2.tgz", + "integrity": "sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.2.tgz", + "integrity": "sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", + "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", + "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/antd": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/antd/-/antd-6.5.0.tgz", + "integrity": "sha512-9zbVc9UukfGuqCvIAov01nlpDQWfARNmZQyt21ZhqLX7ilXmi4cdkp12xA48WEmXRXwZvno8A03qQuGE9JG8fg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.1", + "@ant-design/cssinjs": "^2.1.2", + "@ant-design/cssinjs-utils": "^2.1.2", + "@ant-design/fast-color": "^3.0.1", + "@ant-design/icons": "^6.3.1", + "@ant-design/react-slick": "~2.0.0", + "@babel/runtime": "^7.29.2", + "@rc-component/cascader": "~1.17.0", + "@rc-component/checkbox": "~2.0.0", + "@rc-component/collapse": "~1.2.0", + "@rc-component/color-picker": "~3.1.1", + "@rc-component/dialog": "~1.10.0", + "@rc-component/drawer": "~1.4.2", + "@rc-component/dropdown": "~1.0.2", + "@rc-component/form": "~1.8.5", + "@rc-component/image": "~1.9.0", + "@rc-component/input": "~1.3.1", + "@rc-component/input-number": "~1.6.2", + "@rc-component/mentions": "~1.10.0", + "@rc-component/menu": "~1.4.1", + "@rc-component/motion": "^1.3.3", + "@rc-component/mutate-observer": "^2.0.1", + "@rc-component/notification": "~2.0.7", + "@rc-component/pagination": "~1.4.0", + "@rc-component/picker": "~1.11.0", + "@rc-component/progress": "~1.0.2", + "@rc-component/qrcode": "~2.0.0", + "@rc-component/rate": "~1.0.1", + "@rc-component/resize-observer": "^1.1.2", + "@rc-component/segmented": "~1.3.0", + "@rc-component/select": "~1.8.2", + "@rc-component/slider": "~1.1.1", + "@rc-component/steps": "~1.2.2", + "@rc-component/switch": "~1.0.3", + "@rc-component/table": "~1.10.2", + "@rc-component/tabs": "~1.11.0", + "@rc-component/tooltip": "~1.4.0", + "@rc-component/tour": "~2.4.0", + "@rc-component/tree": "~1.3.2", + "@rc-component/tree-select": "~1.11.0", + "@rc-component/trigger": "^3.9.1", + "@rc-component/upload": "~1.1.1", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1", + "dayjs": "^1.11.11", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-binarytree": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz", + "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==", + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force-3d": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz", + "integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==", + "license": "MIT", + "dependencies": { + "d3-binarytree": "1", + "d3-dispatch": "1 - 3", + "d3-octree": "1", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo-projection": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz", + "integrity": "sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg==", + "license": "ISC", + "dependencies": { + "commander": "7", + "d3-array": "1 - 3", + "d3-geo": "1.12.0 - 3" + }, + "bin": { + "geo2svg": "bin/geo2svg.js", + "geograticule": "bin/geograticule.js", + "geoproject": "bin/geoproject.js", + "geoquantize": "bin/geoquantize.js", + "geostitch": "bin/geostitch.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-octree": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz", + "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==", + "license": "MIT" + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-regression": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/d3-regression/-/d3-regression-1.3.10.tgz", + "integrity": "sha512-PF8GWEL70cHHWpx2jUQXc68r1pyPHIA+St16muk/XRokETzlegj5LriNKg7o4LR0TySug4nHYPJNNRz/W+/Niw==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/flru": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flru/-/flru-1.0.2.tgz", + "integrity": "sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gl-matrix": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", + "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, + "node_modules/is-mobile": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz", + "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==", + "license": "MIT" + }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "license": "MIT", + "dependencies": { + "string-convert": "^0.2.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oxlint": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.73.0.tgz", + "integrity": "sha512-u91G9TJzU6yqKWNZUYprQB07W7YvntZXaRxQ6CkoytepYhLWUXWsr1M8zUJ34VatNPuUAr3Z8GH+O2A331CluQ==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.73.0", + "@oxlint/binding-android-arm64": "1.73.0", + "@oxlint/binding-darwin-arm64": "1.73.0", + "@oxlint/binding-darwin-x64": "1.73.0", + "@oxlint/binding-freebsd-x64": "1.73.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.73.0", + "@oxlint/binding-linux-arm-musleabihf": "1.73.0", + "@oxlint/binding-linux-arm64-gnu": "1.73.0", + "@oxlint/binding-linux-arm64-musl": "1.73.0", + "@oxlint/binding-linux-ppc64-gnu": "1.73.0", + "@oxlint/binding-linux-riscv64-gnu": "1.73.0", + "@oxlint/binding-linux-riscv64-musl": "1.73.0", + "@oxlint/binding-linux-s390x-gnu": "1.73.0", + "@oxlint/binding-linux-x64-gnu": "1.73.0", + "@oxlint/binding-linux-x64-musl": "1.73.0", + "@oxlint/binding-openharmony-arm64": "1.73.0", + "@oxlint/binding-win32-arm64-msvc": "1.73.0", + "@oxlint/binding-win32-ia32-msvc": "1.73.0", + "@oxlint/binding-win32-x64-msvc": "1.73.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=0.24.0", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/pdfast": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/pdfast/-/pdfast-0.2.0.tgz", + "integrity": "sha512-cq6TTu6qKSFUHwEahi68k/kqN2mfepjkGrG9Un70cgdRRKLKY6Rf8P8uvP2NvZktaQZNF3YE7agEkLj0vGK9bA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-router": { + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", + "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz", + "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" + }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, + "node_modules/svg-path-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/svg-path-parser/-/svg-path-parser-1.1.0.tgz", + "integrity": "sha512-jGCUqcQyXpfe38R7RFfhrMyfXcBmpMNJI/B+4CE9/Unkh98UporAc461GTthv+TVDuZXsBx7/WiwJb1Oh4tt4A==", + "license": "MIT" + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "license": "MIT", + "engines": { + "node": ">=12.22" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/vite": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz", + "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.16", + "rolldown": "~1.1.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..2480850 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,32 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "@ant-design/icons": "^6.3.2", + "@ant-design/plots": "^2.6.8", + "@tanstack/react-query": "^5.101.2", + "antd": "^6.5.0", + "axios": "^1.18.1", + "dayjs": "^1.11.21", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-router-dom": "^7.18.1" + }, + "devDependencies": { + "@types/node": "^24.13.2", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "oxlint": "^1.71.0", + "typescript": "~6.0.2", + "vite": "^8.1.1" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..1a8623d --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,163 @@ +import { Navigate, Outlet, Route, Routes } from 'react-router-dom' +import { + App as AntApp, + ConfigProvider, + theme as antTheme, +} from 'antd' +import koKR from 'antd/locale/ko_KR' +import AgentShell from './components/AgentShell' +import { useTheme } from './themeContext' +import { THEME_PRIMARY } from './theme' +import { formRequiredMark } from './formRequiredMark' +import './styles/app.css' + +import LoginPage from './pages/LoginPage' +import HomeDashboardPage from './pages/care/HomeDashboardPage' +import ProductInternetPage from './pages/care/ProductInternetPage' +import ProductHomePage from './pages/care/ProductHomePage' +import ProductLogPage from './pages/care/ProductLogPage' +import ContractInternetPage from './pages/care/ContractInternetPage' +import ContractHomePage from './pages/care/ContractHomePage' +import ContractLogPage from './pages/care/ContractLogPage' +import ContractAdjustPage from './pages/care/ContractAdjustPage' +import ContractBalancePage from './pages/care/ContractBalancePage' +import ContractWritePage from './pages/care/ContractWritePage' +import ReceptionListPage from './pages/care/ReceptionListPage' +import InvoicePublishPage from './pages/care/InvoicePublishPage' +import InvoiceMyPage from './pages/care/InvoiceMyPage' +import InvoiceLogPage from './pages/care/InvoiceLogPage' +import PendingCalendarPage from './pages/care/PendingCalendarPage' +import PendingPage from './pages/care/PendingPage' +import PendingLogPage from './pages/care/PendingLogPage' +import BookingPage from './pages/care/BookingPage' +import BookingLogPage from './pages/care/BookingLogPage' +import CustomerPage from './pages/care/CustomerPage' +import CustomerLogPage from './pages/care/CustomerLogPage' +import AbooksPage from './pages/care/AbooksPage' +import AbooksLogPage from './pages/care/AbooksLogPage' +import ReportDailyPage from './pages/care/ReportDailyPage' +import ReportMonthlyPage from './pages/care/ReportMonthlyPage' +import ReportInternetPage from './pages/care/ReportInternetPage' +import ReportHomePage from './pages/care/ReportHomePage' +import ReportMemberPage from './pages/care/ReportMemberPage' +import ReportAgencyPage from './pages/care/ReportAgencyPage' +import ReportIncentivePage from './pages/care/ReportIncentivePage' +import SettingAgencyPage from './pages/care/SettingAgencyPage' +import SettingMemberPage from './pages/care/SettingMemberPage' +import SettingPreferencePage from './pages/care/SettingPreferencePage' +import SettingLevelPage from './pages/care/SettingLevelPage' +import SettingPartner1Page from './pages/care/SettingPartner1Page' +import SettingPartner2Page from './pages/care/SettingPartner2Page' +import CsNoticePage from './pages/care/CsNoticePage' +import CsQuestionPage from './pages/care/CsQuestionPage' +import BbsPage from './pages/care/BbsPage' +import SitemapPage from './pages/care/SitemapPage' + +function ProtectedRoute() { + const token = localStorage.getItem('token') + if (!token) return + return +} + +export default function App() { + const { theme } = useTheme() + return ( + + + + } /> + }> + }> + } /> + } /> + + } /> + } /> + } /> + + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + + } /> + } /> + } /> + + } /> + } /> + } /> + + } /> + } /> + + } /> + } /> + + } /> + } /> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + + } /> + } /> + + + } /> + } /> + + + + ) +} diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts new file mode 100644 index 0000000..2d25773 --- /dev/null +++ b/frontend/src/api/auth.ts @@ -0,0 +1,43 @@ +import client, { apiPrefix, unwrap, type ApiResponse } from './client' +import type { User } from '../types' + +type LoginData = { + token: string + role: string + userid: string + name: string + groupId: string + companyId: number | null + staffPermission?: string + menuFlags?: string | null +} + +export async function login(userid: string, password: string): Promise { + const { data } = await client.post>('/auth/login', { userid, password }) + const body = unwrap(data) + return { + token: body.token, + role: body.role, + userid: body.userid, + name: body.name, + groupId: body.groupId, + companyId: body.companyId, + staffPermission: body.staffPermission, + menuFlags: body.menuFlags, + } +} + +export type MyMenusData = { + staffPermission: string + menus: string[] +} + +export async function fetchMyMenus(): Promise { + const { data } = await client.get>(`${apiPrefix()}/homes/my-menus`) + return unwrap(data) +} + +export async function me(): Promise { + const { data } = await client.get>('/auth/me') + return unwrap(data) +} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..c5f83bf --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,46 @@ +import axios from 'axios' + +export type ApiResponse = { + result: boolean + msg?: string + data?: T +} + +const client = axios.create({ baseURL: '/api', timeout: 20_000 }) + +client.interceptors.request.use((config) => { + const token = localStorage.getItem('token') + if (token) config.headers.Authorization = `Bearer ${token}` + return config +}) + +client.interceptors.response.use( + (res) => res, + (err) => { + if (err.response?.status === 401) { + localStorage.clear() + if (!window.location.pathname.includes('/login')) window.location.href = '/login' + } + const msg = err.response?.data?.msg + if (typeof msg === 'string' && msg && !err.message?.includes(msg)) { + err.message = msg + } + return Promise.reject(err) + }, +) + +export function unwrap(payload: ApiResponse | T): T { + if (payload && typeof payload === 'object' && 'result' in (payload as object)) { + const r = payload as ApiResponse + if (!r.result) throw new Error(r.msg || '요청 실패') + return r.data as T + } + return payload as T +} + +export function apiPrefix() { + const role = localStorage.getItem('role') + return role === 'SHOP' ? '/shop' : '/agent' +} + +export default client diff --git a/frontend/src/api/opens.ts b/frontend/src/api/opens.ts new file mode 100644 index 0000000..822dec1 --- /dev/null +++ b/frontend/src/api/opens.ts @@ -0,0 +1,31 @@ +import client, { apiPrefix, unwrap, type ApiResponse } from './client' + +export type OpenRecord = Record & { id: number } +export type OpenSearch = { total: number; list: OpenRecord[]; counts: Record } + +export const openApi = { + async search(params: Record) { + const { data } = await client.get>(`${apiPrefix()}/opens/search`, { params }) + return unwrap(data) + }, + async get(id: number) { + const { data } = await client.get>(`${apiPrefix()}/opens/${id}`) + return unwrap(data) + }, + async create(payload: Record) { + const { data } = await client.post(`${apiPrefix()}/opens`, payload) + return unwrap(data) + }, + async save(id: number, payload: Record) { + const { data } = await client.put(`${apiPrefix()}/opens/${id}`, payload) + return unwrap(data) + }, + async remove(id: number) { + const { data } = await client.delete(`${apiPrefix()}/opens/${id}`) + return unwrap(data) + }, + async serialLookup(serial: string, gubun = '휴대폰') { + const { data } = await client.get[]>>(`${apiPrefix()}/opens/serial-lookup`, { params: { serial, gubun } }) + return unwrap(data) + }, +} diff --git a/frontend/src/api/resources.ts b/frontend/src/api/resources.ts new file mode 100644 index 0000000..4370136 --- /dev/null +++ b/frontend/src/api/resources.ts @@ -0,0 +1,45 @@ +import client, { apiPrefix, unwrap, type ApiResponse } from './client' +import type { Entity } from '../types' + +export type PageResult = { total: number; list: T[] } + +export const resourceApi = { + async list(path: string, params: Record = {}) { + const { data } = await client.get | ApiResponse>>( + `${apiPrefix()}/${path}`, + { params }, + ) + const body = unwrap(data) as PageResult | Record + if (body && typeof body === 'object' && 'list' in body) { + const page = body as PageResult + return { items: page.list ?? [], total: Number(page.total ?? 0) } + } + // dashboard / summary style + return { items: [body as Entity], total: 1, raw: body } + }, + + async get(path: string, params: Record = {}) { + const { data } = await client.get(`${apiPrefix()}/${path}`, { params }) + return unwrap(data) + }, + + async create(path: string, payload: Record) { + const { data } = await client.post>(`${apiPrefix()}/${path}`, payload) + return unwrap(data) + }, + + async update(path: string, id: number | string, payload: Record) { + const { data } = await client.put>(`${apiPrefix()}/${path}/${id}`, payload) + return unwrap(data) + }, + + async put(path: string, payload: Record, params?: Record) { + const { data } = await client.put(`${apiPrefix()}/${path}`, payload, { params }) + return unwrap(data) + }, + + async remove(path: string, id: number) { + const { data } = await client.delete(`${apiPrefix()}/${path}/${id}`) + return unwrap(data) + }, +} diff --git a/frontend/src/api/stocks.ts b/frontend/src/api/stocks.ts new file mode 100644 index 0000000..07c7067 --- /dev/null +++ b/frontend/src/api/stocks.ts @@ -0,0 +1,77 @@ +import client, { apiPrefix, unwrap, type ApiResponse } from './client' + +export type Stock = Record & { id: number; state: string; statusLabel: string } +export type StockSearch = { total: number; list: Stock[]; counts: Record } + +export type StockHistoryItem = { + id: number + processDate?: string + statusLabel?: string + companyLabel?: string + processor?: string +} + +export type StockNoteItem = { + id: number + stockId?: number + contents?: string + authorName?: string + noteDate?: string +} + +export type StockDetail = { + stock: Stock + histories: StockHistoryItem[] + notes: StockNoteItem[] +} + +export const stockApi = { + async search(params: Record) { + const { data } = await client.get>(`${apiPrefix()}/stocks/search`, { params }) + return unwrap(data) + }, + async detail(id: number) { + const { data } = await client.get>(`${apiPrefix()}/stocks/${id}/detail`) + return unwrap(data) + }, + async save(id: number, payload: Record) { + const { data } = await client.put>(`${apiPrefix()}/stocks/${id}`, payload) + return unwrap(data) + }, + async create(payload: Record) { + const { data } = await client.post>(`${apiPrefix()}/stocks`, payload) + return unwrap(data) + }, + async remove(id: number) { + const { data } = await client.delete(`${apiPrefix()}/stocks/${id}`) + return unwrap(data) + }, + async action(id: number, payload: Record) { + const { data } = await client.post>(`${apiPrefix()}/stocks/${id}/action`, payload) + return unwrap(data) + }, + async addNote(id: number, payload: { contents: string; authorName?: string }) { + const { data } = await client.post>(`${apiPrefix()}/stocks/${id}/notes`, payload) + return unwrap(data) + }, + async deleteNote(id: number, noteId: number) { + const { data } = await client.delete(`${apiPrefix()}/stocks/${id}/notes/${noteId}`) + return unwrap(data) + }, + async bulkIn(payload: Record) { + const { data } = await client.post(`${apiPrefix()}/stocks/bulk-in`, payload) + return unwrap(data) + }, + async bulkAction(ids: number[], state: string) { + const { data } = await client.post(`${apiPrefix()}/stocks/bulk-action`, { ids, state }) + return unwrap(data) + }, + async sheet(params: Record) { + const { data } = await client.get>(`${apiPrefix()}/stocks/sheet`, { params }) + return unwrap(data) + }, + exportUrl(params: Record) { + const query = new URLSearchParams(Object.entries(params).filter(([, value]) => value !== undefined && value !== '').map(([key, value]) => [key, String(value)])) + return `${apiPrefix()}/stocks/export?${query}` + }, +} diff --git a/frontend/src/assets/billcare_mark.png b/frontend/src/assets/billcare_mark.png new file mode 100644 index 0000000000000000000000000000000000000000..391e8d2e3d4dbe9eeca42e24f3e2916261dedb17 GIT binary patch literal 76455 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%0AP5Q^1~3h%*h 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 U>&&@>b^p9pdZ$<=|32jZ0PE=4TL1t6 literal 0 HcmV?d00001 diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..02251f4b956c55af2d76fd0788124d7eee2b45eb GIT binary patch literal 13057 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 P00000NkvXXu0mjft=yBf literal 0 HcmV?d00001 diff --git a/frontend/src/assets/logo_image.png b/frontend/src/assets/logo_image.png new file mode 100644 index 0000000000000000000000000000000000000000..f37a17dd767f1f498e68814ca8aa89b683e89afe GIT binary patch literal 66473 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%0DNAF9kBN28k>YBG`S2YkfF}7|_pWipX|BJu7 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 literal 0 HcmV?d00001 diff --git a/frontend/src/assets/logo_image_theme.png b/frontend/src/assets/logo_image_theme.png new file mode 100644 index 0000000000000000000000000000000000000000..e8195e175b26edc680c9c64ab5a6508ede46a63d GIT binary patch literal 111356 zcmV)7K*zs{P)tmMsLzd6hTs&hAdmxu?_1H~)X%SEpXLgaoWu?aZv^h^BYCU%z+nx#!fW`s%B% zM6Qf0m%pJD__b;!Y}BjOMx_!~MWmGH znP22Z;rU*H?^&K@QIcd?mc@DD#d%i5S&=7s?!|9>%R63uvG3z+ZG2e+_r}+~;=k4^ z!t*>y^DOs0PrSnSgOVTQnRulj z@CuPUFS5MIy<9}{LNdYMBuSE{S(ZrQrG-dQ6ons#p)bDY`Mxh%o);og;A8NeLy7`# zh)>C~T=2PtufNNaRDu$&KmtDqJbVcMF3&*UctK% zFDM0=7yh^J`}i}j$P2;$q`yl+P-^`v!g} zS)TCcSP=Lf7DbX}*pPx#FVA!S4&Q0ivW)u_|6TG!Nt3kju!M?SfvOiDq?OYq%;G07!OQ@&rRR2tnAekOu@RHKS7 zykDAOF&BIfOsCz4{++%CUvZjBUgSks()Y#tl}aTqP1C|J>2;M#W&Rwt67Ly?WxZb! zNs_dHCCA6;^#u}mxW3H8IS>Q^uOlpnUYaC2$M|2#F9Oe(I7u+A1+8R zEYI>%sg&j7C!Tm&mKRZ$=lx-nEaws|4TtGsr$4yx`d|IsUy!H4_?=&S?H^80k6lwK zc!p)anXNWJZxjf-|sFZ7b zHkEQk2Jz56`!Z0q1pp#~@u(;em zeqpI|^jCiM9k06Bb9UJqxbOP57yaqY*KK>oEjR33H_@zyfmehgxu3)_AGTkjX9llI z=+gw)KDycPaA6RVByk(H#P6P`8yB0A4hCI45x6ci3SLQn=F`EFN*GEU$8ck~a4Mw= z|6Q(>_}+!5MztE2C5j@cRBHT=&%>g_bqtfCRI63~9T#AdN-6Z=M)7ZPJ>_bshO{X7 zzSU}3hQpzhOO?@UO65?pB<1ynff*zbRt++e{~_A3hKFXT}@9SHtS*yp+q5#fkWVNTpKY-=>(iiq0E8 zM-8RUCq@!Q&MQlgW*uBl?n_N=h2Lv9cWRSNVC>Ntit!^!RM9mS`W`t zpO3B^7|6k3z-#SxyLfHh=??P2FphI64YMNXcz$_hkQC?I-NEr!z2tL2d=htI(^F|f@%8pmNA@jhWlu<)>;@p*VZeqOnvo@gAy zO_f?r4Q&pu$r%s)cL`w(gKVFlD<$Fm;P2oP=t3XT5UH`#`GkQpLyK$2pyBs)p7Arx zL&*>LxiD~QR8ww1Y$)?GZKICqQs-uRmr&Fgs6zkTJ~W~N#@N?y95Nc(en zG?=KAgRxqrRH-%UUael?GYdpLU;GW?cp9#LpI(|SC%tzwc;!-w`=A^KTx;;4alm2C zYi_$`zdZ!mVgQKtc<+s1$V$Jnshr!)({e~ki`nqRK-+B9uwPeu6lL*SS zG8gvP*qAgL4aqWuYZ^utsd{YK82ERZ#oW}@x;}88OgTI}Y{o=NZo{Sh3cyEFY ztA?%FY)Cv%!wOGLJ;;)D2VJRGs@&jZ=fUCnYPALzCkFmL&Qcdv2GXiEr8gW(l14H% z)}lcibo=~WtyYz^$hq)qHTAa4yTd#ThcVyNJYnpKav0J>z2_OZ`e>tr3{jfRHJSpx}+}Y@*gPCgRO*O0V(n5c%QRh9DD;0#}v|hkyqi85`6nijSSSMIZcuw&8tIaA8 zrYIgrDJT;f@bW_9Ean=+I>Fknx0*EmfKhy3Jv*$<*4P*gV3Z66kU^u_97POx{V3wV z>tQUz5~B<_4y7I!a*o1wT2h^zSrsZJgnvZAd+rBih6^zlNP1(se~t& z+QUP?@S3+=(%$qXk!1MR=k7lK&0qiQnQa^Af+C9%uIkAX_6XZvnaD&vCuuV-vhyyhpuW<8{Hf;=RKXJf?((rCP7jAOpmaJbQmrCX!}E;A57onjhk^kEmnJzrp9TWgLJvL53*I{n zV2KBpmuH3F?)D{0Qt5UE$so;Evb=PvH^`4%Snlq7G+gYd>Ml1i;ihynA0 z=g4(a2n{}-&tTA(a#)g93+n^Wki1Unt5g~uQQkPD@Cyi=Fg0k)pJ=jb99C*pt zJNR!rqkgYPub&V@nsB|-LljTyjaGpJFHRHAcd1g9Xpne|%Zpwdr`~eAn+=9ZHz|Ts z-9fScKE9-Zd*QQg_~3@g>h&-A#^+5}OGPD)hj|$Io>$V)2tgAb zGBAjVi7{>xWcoaYRSArX17kIu0{Vl^3`d=d%55w>aJuiv61q*)?e+?6%x1IZVUfgf z3=ozN9fk*|*Vx2_42J{tq;e!Cu^G$Efo4VKVS&Qa#3r=QVW=#cK=5dYRt+hB2Lpol z#&>-6dVQn_M7)gej~pyFe81=U1-z!9DA$0w!N3iq)oOYf4Ol9Hg@~f3bPm6V_*W{E zljD+PagiY7>#!LM=gr`xQ6h~-oj4v|$IlOd)9L(a2-D|tzJ4H}^!a)8Q1LwQZ~P5@htI&~$F)YoL}3dJ&kF6y$t*a9t68hoD5aJ-S z$O!Xqk&WWqxxzR}QiPQn5=#Y=af4yxU07JiS31M2H%Pjr_&i_LF+X=)ew@w(@3M3#-m)Tj&L?&Q)$7FjHj>L%rJ(Q z*&-GQyevuRjUcqfXEf_o4W&{5?Y`72>Q%vb*f1{;?~V7tg2vBqVy`AMe7*+n1w&h{ z)@XF_)o!;(I3&KqD8w%Lq8??;gbm)m*&L&HfWrYEqTy`_J#o#z5qtf<_<<+QM$27C zG)i#TrVK*;91Ofa<_hzQ*D&;U*NyAN>u?|VZ_Kqz*3fw4UdG4A_&%8T)%#WN78Vt+ zCr($cjSet`@pwOkau&+NsN?hDf#bdDv0xr@Df&^ApPj#uF1H80JP+qHsq8;9-+tuZ z{o3#T(pR#GU-m=ac0AAelhYH;QW_6RfXHsW<$C}6Yj-1Y<;|ZvPe`Lttr3T1LJ4z| zF_}VKjqIE(*R|*{ThN>uV=b;}+!Kr;o)exu?h~)U`rzT~vgh^;@jGB>nm|$mihp|^ zNL2cJTU(erte0xJ#(7hIPfnbg>ppjk0GBAj^T2g5Ocr?Q6&`NzqJ2LovaFEB<#xWj zxLhnOtqd|LEn<#;^k?4i&GI!izN~@!rsqHN&l_QM=QqFT*>h84wIEIUVWkx2{r-Ti zK2NeVA-({xeiEUah}7#?$Pvg`q&XPIfCa7~dZM^mrNZO`LtYrSdR>NtzN8Gt>eB9Y zCG-Ps$dzvA3p@)fE`|n~!;;G7k)do&jPu_sD=Va_XaWGZMjn0QMOdxXd41h> zPo}4*Wf-gX2?M9pmW+Rg&&B(Xx5m^^20|#HKkO^+=sePi$w~emp*u3fn6tI>gZIIF z670q%ScCN$RR{PJ@E*L*6JI?)hQYc~dH)VFpl26`N^8hc8E>|Dzxe#g=_&O<0vL7m z@bRA5fZ~jIn&wh#)ER2wbHYj)J61feBrBa>e(L1>V5K`;N%HV?T2%JE^0mMEgHLoJ z|KyK-_XEkGJ25p@9k12=YSe3&5IEj)(+$D1?s}&5yIomYTA~-P-kD*p5seBwwF=j3 zBqiS)dEjZ5;MzUh7fcTi9+zpR0gobjQ@CGC^jLNrUM`*m!drzvaQOBWdv;I?2$6yB zb3SoEU|mN^Jo4lQ(NHn%YIQ`5vggVOgqijjtW2(BTo0ZD)@vAs-f$4h(n`BHa_n@! zKg^a()v;qso#c^Mz2?m?m#_6ohUeXW%|~v%e%lRqKI6KHTFEOT5=n-UA2STqhX^A> z4+;yRR`sT*CwWSCI&GeqmWflk18|KBL0lv%@L-sp7mphm4;F?1NdpYH;xKT-B<2HF ztg0&|&$9=}MT_6z{b6{RVT={q2+ILC8!gQ@&iT@OqJ1rO}jto-Sf_Y?5ZZ$=@TRit9A6 zo(TjOHcd`WN|I{mo5U&a71ul%_NCt&$oRyV(phl5wVE0}Pc%#DJP~*%wxRGDOdJ3% z@B?0NZ=ePguQ3k|p9AlZ^M*}T()Ysq;&}~+Lzv1@$gCS%lP5SpaP2r)nDp@Vpn%7x zcvB+77>6UU!NMCKo1_tJclz1!lV^MFetJeqwIe_E^KbsvC%T}2^oPFV{ygo@Y@D4K zpKMktOAF^p^=ip`!3&@7t)E*b%S#KA3}FNcsaG}3w!{yU0+^o!5Jw$R01caEa0}i zlg9>$zxStp?)CpdzLrb1Uh=%%`(E_C+qZ7pJPQyE6df`u%=5tFMyQDi&$HOWLvAz? zvV}AlI>|~wKxl`JJm_}$cL-q>n^PVg099t_!jcP-^!Er+nW1&z4-A}9P;683VTTps zLH+(valQ;0$Xu$Gy5!{RDIEguf%n2Sk_J)*->G1hjK3?e0PFlWh`^TBh2i7t9Y8 zql(2J@S5;@ybrDye?!VnrThgUDzLUuRyvBL!OUMGNi%N~%~Nv$jZXlqpZjfL3X!_bc0Isc$|j&UjB+-`d;~3 zG;lAvW9O%C-o5qu+i%&`tdt5bjw7EOHfM0~Pg-nD2N3zGc!XEhhCn;xbSZc z!v^#m@bTQBo)J|wqen^wu{eYintULB7pQw~4;zLN24T6q!fa#7Q$CX85%JpK?EvV; zX2rsYVuTv%tpxBaLAZo5Ch^#^YwE4wx{VO43r)k4u||_2un{|9APsXh-(NGP$n3xg>+9mxvGo-jqdYb(K4*|!sD@cmw>(xUy?-L=J zdT#DH7Z_-Iv?W~+VOb9ez$4srFixzP$@$E2 zKw`ZBt3w!CE|&}A9TLg`aTrifuHIZ=^X=>VNT3xr=H+p3=9$#04M|aA=pph^{$iqxLq3d>sgL18TVt#4(`B%O6S6?PyO9t*6pL^?{O*Onb?|jC!b6eI= zm&MBh?6ARL=#d@(oGQ|cQnOz1^uM)y1wb?m8A?#GK|y=KfPr(FNyjPDOeC;3tw7LF zKmk~Q?}|r&y1*u|&dE|#*{_1c0`z-KZd(*!; zd;IXeTED*>ZknAawHoE%`0)eYtvBB&-}aIh%lWfsWO-ppjaUwv4KXlMNnpqj4cI}Z z$s_gX7y{yuK^EI3Wq_p-8Yn6666ZRoGw_}?^am|ylFt;zpYs&!{o?spGA>PX)M-1u zCva8`eNg~Oa=7DO`YzJQTtyMCi&^*-iKYr+1yEQ3=EkXdfw&+1%rBLTa=BWB6&#)` z#e)w$8nt`zk{31}fBDb8@tfppNloNipL@f-H(j^o>g#uIZ%s}#f;5TpD2k9FSJxaT z2}p`*;)Q`HtyY5%7<3Ih(6MHnSxZZ;;{o%8D_6K76_1M;VlvZKsNn%;jF?8Qm=XYW zhC(=<5c(BLlmuNvOI$QmusTxI{?Q;58j@n*eUa^DwSp8(N?O3k8E;vu=6rgP= zn_WFj7}`b%gq92oTP*{p$A%VvK1nS)$o-c=Wjc$*CFQslbscbv!?j z$Q?GkAIGm_oot{P@s*Vh0eE;<_2yV{^u)<*d8s`pRmT=eweb_*^Fu%Tg1^ri{^eJ{ zZ28pDgXKmwC~e&`>z_Vx*lX3x-Vc1AMrOy39+9bWO;7=|boZyp1wa%Pqvxo{rt8o_ z83@%~rX0`BmAdio^xwLk;K9M`gGa~p;#dYSNWcMczpdsts$Li?8+I9|ii^U)jg2*B z5GT4WoPk3qYaX+a!FgZ=0S^$(uHyQud@lIx-Y}B>AdyDBC4Lwdp!l3Qb2fkM@%_DF z=AVkQ(w?9F#kYL#*Mx!l#^>Jj-m5oH-?Hogoy|- z7nYZpebo)8Sv?`WtvjtsS=6vl&`f-j4Up^TnQX4#Buj1 zpyHqdgrL=0g_&7wsP^(ofzl}a9M40&N%h)*J@I-0xRcB0rU71y!v#@yfMn0&$}*Az zUZswMAe57*&*Z1iFZWwh>z2y3i8J5!Pk;2TziS=;##>*rbY%Zym8ic|+O%=TD+if> z}%kJH~<-p#(QVy6w_26CeFe;JFs1Svy!REmWEE8)Qv?!Ed6`7O}lB3vu zwT4wOU`LT*Ei1xz;~{zq^FUew7$-a{G7emr52GF@YM^lFU{3*t@k34OK;D6CT1(Ct z=~!*H!Xl|27O3HN7$ZV%L_A1Xc|k=wJtc_yrIL&{$0hf?;^94y#s>}_Z-@1JgX4ydxeIe-`CB7y2w$XGE2TfQXUY_sDV>*DLg6SAFh1pGI>L7ecO z{vAR@i~=sFBQY>hzi3%(7&OW8Eq95UgD*DP*=d(Bv09XvUwIzgn3U3yUj@ zk|()`$#) zmVUR*hc!Ol;s%E2Gci7{d@q+p9l7=Nu<$UDo>!wl&&3%tWa_#EWeaIF>N1EFZ?nn- zD>BFDWAP~S9+^!?{*5ADCkl9ay$%-}20ovdTbhhKM!XMxj!;{v8p=5X{)8GGqCv}*uV?rs z46=GTIuPWZkIZV_WrM!|eKK-F$u;APo zM1Xm3R2ww>CJTlGre3cXIMhegGy~<0@_?zpv(>s)rHkmnq!l%aP4hYmgR64J3MYW?ArOzqc+mN!QqgFQiy!#;$rGoFhaP!s;FlW5dvWPAKmWS7 z{J303W0k`HcZ|X-b%sfnA{$e$*E|@4An>W@Mp{9th4KVPp`nn0$l7$EXG(oCJS1Xe z@T&0lsMli#5@9P&egLl_Hxe>X4V>n{&yq-n15^!wyB0}8tu0MvfHdpFMMq_Y6w>Ro zC5g;e{vJ3+rsyiSu~w7!m602UtS`vd;DG7IGVq%R7eZGbUNuAxni3$a1Qo&8 zFBmd4ZYoKIf8z_4BuL;eX*B4Lq=L*agD=L!2LO0TJryZQwwM##55mUDi7_gIE-qe> z+3Bg`8MoZ%O|-)L$s>C<`02{7d++`~$3E~^e{$cKThFh0&9B#Pzw-q}UTPJ`PcIdl zw_Ppg=2zqo|NH-t$;oMH(5sJ%uv|v@8N&(lI7_9lz!xxMWO~(@sE39D?+CO@u)!t6r4YFUXPEka43yhoj46ZI2bp}_!)2w;G9^V6@=&NdNUMt znMw@BlaM_Gr%nM@!#@CQrx`+om^Vpp%>5nzwm|JH@IQp3C^qjfVW1TndJ-bs#fc3( z2Hqp63^+9{EFNiw!2p8RlIrK%&y6l&95eg5DT%l%VH)3xyp7~b{E39R$R*E>~kp5610^XBj!&@m4uo(Lr#Szsr#^-@sZ)NELtvU-Xrfg?5v^G^_8RXJ3x#Ed7_&RQ`5V7IJ46Mg}D+dqi<6d-oqMG6|DyS6$ncps{?%^S=F>FSoA$?2msxer(SJ{?hzOZ+3dj zO9ma0VkkfU(jVrbckc9QnV1;o;fi8+!W?NK4d9HfJ``TOW%ucc0xH0@L<2w|9xz_4 zdE5gc2as_=?#1(_hmNc$Yo$@luQ+NMn6e3KI@v5jC{aRM3F#=ldi86gCG)P0Ylwk; zz)Zfw?kw!JWCXLT5Ewv-qNi&-PO#@7iV;hzF_w?D#=OV&?Ta7XdvNHLTPJ?}U%%;Q zc{&-mAN!&2d}n{@^fR|^n3#L^owt;wh&@yn4~G%ogA+$(S-9$b=0H;)EPk2(DXY(6qArKCbt!(kF>cQK4dB-#w{|6f+^ni`^W}r28{Fsgu5i~ zsvxIsI1Qz&RLKky!Zw2QfXp554`G9ac$Po_21U%v8R)g#IT$9zLVSsbTB3%}vyi6S z1*WF2H7WwGiCH%vAqW*DM&7|FT*v2!t2{Gij#=CeVNlHmV?pch0V3-LVd!K;(M(^5 z_t9r)Mi>9Btb6e_kzgfv6e1XCSk|aD2q73}5Z)OpGw4-lmX^-~S$3G5l4c>X$qVSD zc{+ax+i37JO4P5V+;}_;kd2|(fRoV@-N<|vknSpcNq~(KKdm!MdF-8AAjkO$jZ`^oIQ6+rl+TP zf6L2I7eUCZ;bFVo(X~utjO>1u2NRTbkeAU6Gs?j5tdv}yL^we7nd3d+;bBbz!v$@l zgF0b%$QXUZ$i~PnyHKAP8rMyzSSyCEBUb^DsaI^+A8>GkZq(rj__Ev3!0DotDk!9Ka!_Q)0yvn$zA(5Ow~6%^XA>v zu~y9=4tjYIlt>q0?WwPf{SX365K(mAgeoe47M^KI7R#zU-H>TaqA*?u?iILN^yVD< zp8|tlRFqH5Z_wx8u}mgcZjxmPjkaONCMfBDBXdM!St#Qk2FY+PQ{;ed7jPHodfBz0 zZj+uG3;+ZmGla&dSqxP9*DV+W?_m&WG*}3ad(>nKk}f3h4kFfO;(bgwl=X+Mh#%Lt zT8g73R>;nw6inr!6h}l-#%g6roAF&qn8QX#67Wvk0K>36R+9Juy+d3ZYi%>FePu!j zG#B7K&Lh<%i9Sb5Dy85DLW9o{j&Z=T#LJbm!EiSk;JZQI3VL;Fh!K*hn5lZ(*sy%Q zi4v37O2*RzEHWk^?2rRaXc=|NiTWj})tkJwrIj|b@xV6W%{}tyewkjkDcNz&b;Cg% z3~s#r8`gih_5JH_{`vIy;eFDqdS1QkORv4?{rJE75%M;VA3w(M-7@5MkQzCg9w*gE z6az)6A{45tHISrnusY=!lCf2r&!p!ynS$EbP+H?nmb#Ij8D}(ZMz z4K>@CS1Zid{icUfIaFf#C@LTwNY@lOhQ1zHI2^cNe8s`B7VFJ1t|vrcS*BU@B+iRk zvmuREE8n~CfE+k{VmT0sc#_aDFDfiUl1_5A1D-k6zc`gsGNBj&66z8=g8bW)EhNJ3)F&qcqvo|fv5n4 z7f}WVg0lmwb<^txvjuMVZmJh7_;^kFsn(>? z)QqpGtzbi#_hg11XTNPo8UP1&V{?-i=4e1==i%Ysd{0+U7b-i)Z)*FRF5~@GAw6ZMCv2l8H`wkwGD9-bnZ+%YeSH^}ru6@qbms{h%{qJ9w z9Nzbc2T!gR7GA%-EI;znAM)ns&q=q_rMHE7f+q)Wj2TeYwyKv8okGh{V|_FM4aA5t z)Ju-QKkN62rRkc5(eb2@a27pC3Z?U4KQ(G)CUr`c)yy`?(>#n({)HqD#bNLo8G)0p z>7y)7!&xN)8}Cl<5m`@T%ag<8yihDG>*b(o5vw7ssj1lY8Q*PfU8#kZcz?qWO=uCG zG>YO?Wf&BlnAjF7QA~OXfIqXMzItT%>~-tbakHS(0S}s`Urxe@_u_bbYh}+(85ssk zX+n@s0*0hsA}~?B4+zB=G{ne=Lw`9&l}p7<~Qmp>pEE?kg> z3-g?7gqtS)jtIkeh=c~JAxpks>bbsany> z-W$H*dC#2Mx@ldIr*V-cv1j#)Siqnyge6~TA|TV|u3U#6m^MIxfkSeDye%^F)f=c( zQT7wdg$s+yL#x)cz&uTqf7Q@1M5_Tx#F$JSSe02lqKkdJZs zAWeh?V_>{$t)W+hhDH|l8jnu#5lEAdDJd4&Vkc)!Ovo)5cw;m;wJJRuVpI;~#b#s} zURLZ33*Q>}KvYpZ-G&VMYSiEfa;_a#iI8SQApoX`5Xp%HAj3&PO80D3Z%j+Sj55N_ zSqXVRkdIOx9zv{&qzDTGYaXP_1fH|j4&(YY1O{OkMsEG4jnZhe7((OOc2?RWnRuvk zFbu9K^<^#jRSyjHzTxHLc|sPRlp%2x4P;SaD07ivuRjV4bH5msuxZCcmu%5yJ19kB z6$EHR6O+@@8zi#l(F6JQH$5}ou;r?F-L~6Q_25g4-}|lCCx;&2<2CDn`1#QDyMDU=w?ugU(Q@4TvphgpemFb*s3fR$Inf+ZT`=BjNQgEStZ=P0`b5rnpOep#YrT#gR~=0ApZdCEvj zz{zCYu^*Y}gHl^#V=LmzWX04uHJQ1zGL@I@yx~>)m{7&iZccM|0!-k2=Y7z*2Q517_ zHO&sD)+QJqMk5)ID z;dh#;tk7E{$V~P&LJ?3H*ynSkKB8GpQ#sYmOT!4m1{};o=@5vzz-hb@p9>x%j0k1t zTxbhVi@>W3iAgACVSY+LA5Bp}hpA8vHWOI?FV*gCP zke~>p>_d)6)j!sjW0@!==cs~CV~{Y98J_c&p#LxMB2fb@Ec=3oM30%^KP18jT=3xI zL9`NvO*3YYDAKNGoJa8U5F%No+Ho1VG1lN{m_K}B^Rb__?+bb^z|g{ofd2;Xyx$wj z;lqdJ^Pl~k-2Jh;W&h)QS$8@&JtG_DW~DXJA~$RR?qZ-GVWnJBu3M(Kn3AU`wO}~hav)XWoo)oGK=rACm7V!&}8|q;pi;AdbD2ERnlVeAY z$gqblYa$clEgDqtLPKPCeQpD0fbp3KO%+bk2mpy9V4r$mD)q){tgVJwWwOBkR7io1 z$aCCx2xIQ``yIctvLrWNx2t^9byuy+h6~U5&%g64AC$|@zy(2&cph1@3P2bNlOc;jmSeR(dJ1lf!X3aGXS(P@j zvK1{%$H3p=L^6YFCnAi6c{8@?Oc__hINfZP)r0ZCVN4g_)yd+$%*&%hk@MbE1_vfz zGnf{lIh9<+l%RVz7!s#MR@N{;*CTT@$0jvyr@3*YETaf70@r{=iR-m_f>Fos;r$qf zr=>fp{A-zMg5+I&f9*J?qM~MeDUIcdxUump^M*~q zyXDN8GbE~R+rC{kY*^>&Shc$uz95Wgl@NA+Yh}T?0by9%ZI;S0q;sO9R__v~jq)0;ncGI;Ef`@##S_k39+_fxNYLuJ#pU3t437t^yF zian3-E%xp|Q0(5lOXlY<@IF-(Re^dFWy7^|JzW;M+fBPBig@u^|MNfp>SyJ0GH}Qtpg|D`q6#Lb#NXgDBYdRm47s6F$bw;pI8GG; zn-y$(u;?vJ(MilH(QTUy3qS#Ogq;;Clb|LLm;k<@f(v>C!>LMD3(L_!8cw9kvLc&= z_r*d&SU`y*WOX6w)NHbJ3>iXv1~8;bSan~xcCGJZx*vwEqG6VK1NeK~qxO5~Gn89P zJT_n-a{yeZH9n?wvhYrVvW&GR*dH~@pytqJsAr;*7CO^w0bkG=MGT&#u6n?{UwA?F z25L78){|l*AWO+j%>r?iz{Y1oI7up-P4<_QJ>%?z3?2yVhlfM->rWRiIp z2)97#sDi?Q6CU9;?td83LxtypL&ZFCc#p_U zgtaj0Fj~;3BYg&#R3HT62@wQ}w*o5!PQ|d5y*<#zPt}`EP98=AkcM#wkvyXE25!I< zqKEyW+gX+yuHPlSm4)EZhdv+9pLqDo8o5{e;+w0Jvm0e$sb9=(*jl{*gC8zVojFr% z*u1eoHlJQOGW)}b$psh$Tt5sAjET{Q*qZ^K6PqZ37Z*u$tWIy*g%gZc@GP(oK)|L* zC=@_~keSm)vqfAF&jF-h)H=hXhByJ{|M>A!a_ra%s<4zoUs_`g8rdR`DR4SKnIAm( zz=?N4nygvZOtq{0fme;cQDdxp_{$_mujEQct-zo)d zE-yT~K5>`{gnlj~3zr&H1x&Via(1d4;Z{8&H3sHQtx6230a67GO?KGzD4Saspcz<` zIASxAvdRmw#Nqnfp$q6h~c9l@Nzhfn(%8d#%cd8A~wIEtAFhb<*g-(MF_P z$JMhQ0Q??mGDSl~{>D$6?KDqh>+YuB{aMlpO_el zaw16pPjghsz~qIJN0G6n3Ja(P>^g=+1cp@kY*k2WcY1RFy${HH-}8RC_n!M1)^6Oi zMW*Lwm4}DcdoE$5l`Fcb$W?S*uvjf2>U~49o!N45;L!JhWmKBk#4zfMDI*gzEdw`e z@o*Uqx^mNv*Gj*=9PD}M-txk!$21K165|(s>DMcMxsfj{ck<0!cjSNh?)T*r6H`ti zYCyxLRR!802|q)^#+vwO{i^nSfOp0ossrpD6(Yr^QPgl?HLnqwdl;& zKfgnn8k|wUBB_#@S;rkkKAf5L>*vxtZojFKM@zT;(eJ(S({ec&xVQf1AN*jR7a0X8 zOPUP=9>B^3sMq4Ob#w)j9J14##8CGjm~llD=TJAj0pe>rXMv-J4FKR3_}BR4giKCP z5I-{R5LFR?xrr8M<2`}X7(|TF%rre}+$+4GW&_t)*4j!}rnkCQv}XZ8blf*Grdl?p z>y8^WiRlr-3qdr%R=27V?2!e;GgzPa`fnU}N@bMcJ6v5Qk+{=?o+u^ojK$vRhtW_NJ#Nf@ePS z#zt@X%&y=2jn{ohF7+(xYhL-X|GxK;&pq#kotrn`bluJ{je12Asj{z@1UWoSJP+J9 zs2tEUahbO6iCcRh}&cldc0PI&m(3B^% zHQ_;+jhHfoLQSDI`mf1sofnk_R8yBjeMv+0N^3 zi{`f8{3R(QfA}BY+Bxv(gTdr@#a}pg!oTa8H~Y8Te1rGM!w-7!^4Uzr^`1buNn`J! zWC{dzm79VQQJ4lv9`sajtN`bPV?KG^*0TczR9yd1v>>kF>c%y?2)-cVU^GEevi=+UVr{K5{vNff z?4DLth2c)8Tj09JCZ@7ZyX$@YQ}^|2V;k;&=}-OJ7s;jS!BHQHWkcvZ1cQOpI5J;w zU)7+XjUbGhDMZ*LU|`{!mxd-S#?vx#sLC4YDUFo3{oD%htvGmUdScYH1=x(?Ynr`P z3~d#oZ^K%IPMMl{>$1Q-zx13T0cRm-l3-#|7SrzyNb+@A~e@CC03?_n-G!|-^Z z8NsSjLfFv7Wgv0C2r04rRIt)fGmu?F80Lr4tThB!IJM4+Jb&%%No$ z*}tL;&J(;awE4q*D?iPot?7xy3I7(}C~!bz{cz}DKaH}u;B^%q-_I0mF|cVHl?J^m z5I*5CF>FC!3)c&Flw4vJgOEd)lM1Apgq_F8SyV10JXmZ7u!npvlrX5s<9qhW$3OCM zIkImbg+%8ztY?F7U|hHdlOnPt2d)Qm1F8?Y(8Hm%O=o0h)`aE-y%25ctnDLlup+F& zc|1KnCPk7;dvQT-yZL&#aPDMq02`EKVSY&-dT0+156EbZ zHzDl>qmQyf;Tpl(1HB9CA{AFf1kP812J5)0Eca9#02Mkgh)4uQ%z_g}ia}FJr5Xkv zia2}S6@O}Stn{oqZf)yF{RBU8sUgJoGum9UOSU_1L)vg{{5dwinx1(?=|8|6TL%Z6H($}*ewJKMmP zDFnc6wS_E9_f~thS`xuRR11^velWZ+QZUZs+Br`T$$+(5ZumQFbYN^&Rs~5OEI`E) zHFJv69E7t;G+^IVoR*piR@Dc~a4~b2s`017Mz>226!>76LnGwk{@@Yd+EHga!q{Ag zC;Pp5b=ZWK6~yNnb%r&H4x6!gw}gmQvDl$zl@#W&jby!DXwEtIkfj|{&YK^~SaVEP zmfG_9&)q8@c>f3G!u%qQ>!wXxhy$`Y5E4qEvhwjBrifv*8Y{Q6jHcnI`08|5sEdc% zP$s4Tp3`ch$8OCV4!Yjb;=DZLmK){7(fz@x!~4r;k3P8arS`!q|Lt4H!)hz*_Tp@A z-RA5AANqK@dE2%;&x!)!Cz&nGdrkIy2 zqhb{aNa)%g6IfrJglI%JtBq_h-{uTds|abWnK?W#U!p-Qk3F_ejvP70_nn@YBsm$p zL?jQ8w1Oj36b1UL;25M@kYRH)YFX-~Ndh?~W@T|$Qp~miwu0i}fS=eQPJC}=Wu@4( ze!V>N_8VsgE9dU|Z~y6S?~zN5tP?xH|9lhk3PHf$Bz{rYYpa3eWc;&RGPZUkrxB0?(TmCbGvc#1KLabkAU2po9xp z9oHPfKlC96$B+Ctz$)D?I(h@fdmzVHA`R?6uQ=d$6;Nd+0tMbOSRIJ zvuEe!@WG>;Pl&W45@O%hp=uGr*y-BRCN|KiD?np1W`x_*s8+(G7w9kX&xs!{*=ko8Nfrk6(!xaZTK@$Kg1n=>;HKyvTjRt>a-kNz3 z?78Y%8uGpnjx+m=1+*#zfD#-S5_mK?1S|`SjNEV-;ZQADB1RIm3;%-vY8nL$3$EXu zi8bbi>Iyh_&y}s3ggU~4Ead6kHlzgr~w<>Y~T7Qc8G{J=|pW_I^=w?zG@7&gXchM&Lpp?Im&&o*t@EX(blU*w*j z7oK0K*8O;xdU2BZ(ID}>!1p4Mw=?|KhnjC;1ZNz|Yb#JpL5I!)nirw6$MNY{3dW4K zCiJlPSU8{P>+wEIOUo2}U~}}6tt!E{BZGo{SCoiuQ4S6#tb=a1U0iqFwZ+cu8)x%q`S0wZ@suYS zFuv=>&p3MH?#-L7-nucURf3|(QqN?Hka@uv42K@l;{ZNQs8aDXq|L`w{TBEVGIT{I z+qP{{o?Mg==fRUo_E(hh zVA)bi$MlAy&}Rhfqkuis2Fgf@a}yIsb7WsvjKA7WPK^smr`;a8Y49eoA>lO`S=Bb@ z+QuYyK)^$V=W6e7!j>pK@&m5`mIu5K8SF4DA^nR&io$vpXwn0XN|eT7-i{6ZJ-lLV zKWHI1GQ8V-B4Q~;3tL=8TXpK$KlRR-|K7YoY&xa`;UUluW|Aq?5 z8(;e~N6(%*QY2A3SU)`;e%C+!_ORbw4$?IBzyv6l%RUr$%GGkI9F&4etrDbZ;9iI=r0K|B!&v~G*xNC`Z2I3(zRsMzEHG0+_*Ruxych1tUaA91o7 zM%iw!$XE-W{eT`QJ_7~)&w1XnrBtoT(!vFF>~halv%7=`-1FkU-9d)~QOUkq^+5Uv zj7h54Eg}V_@MyKmAc_n0CYYL>k)tQiNLG~d8*jfW>J0PV?(3g3_r-hSU;T^ke6ZVI z=t~|&wQ?y#Rb3Ltp!Gp?q9jFLuGP!c@m908dGm(a#Q0dP*{s&4CML?$Q;yoL-z zkJt!1ghTmMv!+l!P&uSNPf-#^lOa!NsC8`Gv_TdZ7p2?N&;o$Eoy-V9E!$eHtFR)x zBN7{Pb%7CbkV(%>StbihE3$Y00ZG~5R+Av6okq;Yb?Gx{Z4zGr0;j67K!rAU3X-XA zbP3nik1|W{X9n(q3ZiWiT`N9Ul>vi_5*cy@Jtaz-ZX7TS3??Q#rKp%lr=7lQb=qtg ziP~xgzlPQ^oV1*`oytD!<(L+W&Z$$1)QXI9BKbl zcdhW#@V?ElmS&4{^kmS+Jy3{YCKH%juAVq49)T}2Q`6c|+La-}0D;SxxHg5B*q|(y zn}2LhyMG)O$bjE;(@nBz^JY?c@P7CphM+!ViqtbQZxUe_c#D?7>(+|wd#7SlCOzkez%Uz;V{W6J6w@KM0Euk+_1R~y>&1SvLek3uhAUK z27|ct(NBK1U9Qi5{-rN}^^4_FFmV6;C3l_Ly<=|uRa-WCm9UiOdCE!?&1{uDxU@z! zuqp=&X%KcGb&m;Vc?*b_MMeXM69yjK#1;z1JYfr6$0NshG-!=ePtl&ShWZ;7Z zWvMmBFX9x3iq*PT*ltOghNytX}}dC4#Y;9#mEjF0y%!fH((*;tR;`xD>y^@EDUiuZ7|gVq<@ML>*gALZ&h1mzT)m@# z{znlGmkWypyei3aFOFh55r8LZHRu@jRe={tD=(nu%RPbT8oAyeg{uRH00j1F2tyu_ zx6&tN!nW|Z=0T{3 z%o6eDa)adOew5_nW8=ZeGv|`e-Sf!lpLz9fZkJ2J!2Pptx#QHeSFKxr)3rN2cyN-Z zNR|-8X*B8{GI|Jg$kD3S$rNYb%wbQ)nk^a>s31_&zFd-F)RXmdvyQf)y-YEY3|Sn# z0rhfdP>T%(%`r?W!d4Osoe-lgXt4cjvhUzAIeC6jw(q<_8e`MCi$Rofo)bCx*!-@f z3d1NUYYhiBplWV8!1{;vLflpD%S6w>wE z92q&yZrXiNQQ2h<`Fq#%0U0o45~HYBz`F)!g}|1)Mm$teT(CeE4{}KG^2S@TZf06G zZd}L4LQEJ92VWrLjn9DR1Ve}CsC_xAY@3MRLmgPL8z%w8x?u3=oNy%86H579NQrFl zNO9S9agXy@uD{_1x#hN-dAPyI8L=DJ#n?{;(D69d9ffY%Rib6sJ&;b-~+)2s94U$hq%1{-Y zNp$(bYvH}2oKAeLKdjX6zyo`-lV_HWzWh~hxmGT=f&1>4+<9vE)|vG; z?!Ly$(#ni@9-_qtnezHV}IVyt}j>}fBK z1_k<|q>(D-(Bq91Tf~1;Kd(UdFAc*@`54B!(;JNXqEI&7MHzVhXxgrcAMgkV#g?rb zNz(yif!sJmU?%B>s0h%Dulo^)f~!$3kevtdoLpA)3K_;y4pH@k*J>YtL6j7wN;R*P zE2Z~;@Z+6cQoiSBe&NmEaB&UXx88B;`m5Hj-??L}SMu^a8uWZE90%AKq6Up2Di?}V zbOk61EUjG9VtjfRaAlFIuLQDrqlTzZKtRnYim0K&fdvOo%_M;=#IPON;w(5j8k!@p^(1p}0ggIdH!3Mp8 z+^IMtj}2@S8Szxt0I=Mx+qcLacicfk1Wyj(o6$$CskUK+*oZ1Rpv--0%en9xp_9QA z$OvX>QQ$k)7-YR+An$+Z@#4B$?u;g;xAeB|yz@!*;=bH?!ClwBzEumZo|&rey!rZF zn{K{-*JQI%sm5^>Bym(&vnQpCXs0jj8!SxJn70O;nx3mO`UqX&1)7E-us&r1R6*ob z211_7x^;8K%*+(_A=QIS`W#h5OopMP%!#n#nQGz-{+$|7W>;fNhM?IKo+*-gUeS-D zVtisEm|s|q?*7#M^X1mMdw$|y|I&9pS#!6_A$S4=}49LJ6)+0*fF z03u%*U)lJ*yo zhVX{cJit>!l(2!f<-|0(iC8Z=kSJAZj!8vnzf-LYF=Y@KI7*qLG6vY42U0Ui%Y@>g zhYud%_1$roQmd907FWxnT%yI~yEQrEu&F$C^`Tv@?S;w*Wym$@($6&0KQ%Ea^9vWG z+$zcTtsA`qk3AGT^EuO+y?P49$3M6C_4xj*+pqb>GxHDZ`uu%+uD|W(UE7|0=j{_y z(_@vz3m1YUN!Wzl8vXS9=)K`d8xDXge}n@-_@L2nS##(UqDhmAXTT6bxhIa}0{N(N zsq9G}Kx@uJUGePf%%}=RDMw0MsWw#61Z8(rDK29UQJ~EI3*COna;;(^;TnqBFqDPo zff|G6=bJaKFI|7_j&=JEoqkgLtv+D`hcXTfaHO(0LJ$0I6_7d6cK|Kt8V&Wp`XDuGlM%zJko7_88ay{-O|by2#{fkq$y*vSae=ZQ zWXmA0WLY%)?UnK5jURmJ&whc?`~Ba3Lw@G;37J28N}^##jP>;VN^#=M=Vfl+w6|mX zR=H~XW_neHhb*ASrp@%2Aj0XeXsv?Lj_l;~v5OY4q9HS^@Z1{JRZdpfl|5NP1Ui04 z4jtv1y7ji(nEgelO&X5}Qnu^b<+9!+mZN4eR|zR#vIwcr?aT}?Lon23R#ZZry*i?S ztY0@Rr_as@#}Dl*AAIzaN3Xfzd0!Q9{7a3`-hc4d@cpjacD`z*|M*S&4xG60u4mk^ z_12rO9Sb}^>~-2+8EfA4Sglk;X3WvO4f=zg6Oe_!jJA-30YdY*G;siex>X_NSw>=c zDXe*C&n=MZv(ZIc7?uD;nHMWb*h&E63hIfIenXWXC&p1ytX#cEczvl<>VcQ0S>9={ z$aTAShDVMZpZ$a1e#6Ip{1w0Q!i!SLKp=h%C=V`~#{CKm<0P;O0Ti_33o*K-rA_%3 z55mGuE0RVr`}FkmNB|O?9exh0CR8U3Uf!sBBF#uhrPml~<-poxfrTYk##d*&{1w0A zzu|3v=s)*`-y}EQ_G}4iW0Fcm>dk3s_v7M|pSibq-v>WV#SZAcA?5+O7tf2KBYNIe zb@7|>vdL?i{vGKb@b-|ra@8fK*sMBfjnPKizxRMV_`n0KU9~3OO0RH@uw6e;s}D6? z4}?}qg(D)ULGTkEqVhS@B=QiEV4VTu>UX+w(+#`)Qzs6WyUX*8-lzUPPc9$k3XUw`OxpZG6-^cRQEFLt_DUA;5I{kGd(t#3~C03pM`>G!b`!SNHV z&e|X!>n*Hw9rQF|n*vQ3)*5pD+yz-!SfuA>ED2_z-80piTHS}h+E66|laV8Y6D^Zd zbz-41a03M8JkxA@ukR(pq1 zg}o=c;KxC33FT2}Djlj3{8}D^l~W+WXju&?I>0{Np=5>o$Xw5C-1QH_DSzNcUde;< z&;RG|73a^LE>0XdES;r!8K0VCQD#n1HsP3T|RKYsH6>Ss50y6tRXXouXc^doxq5;N;Q0wXN6O z0yWl)GCqC(fj8m%*FWppw=e$X2cEI-z=54Fdj4}}!b&+Is8rj50~9zF6|*mBD&ueygb*;ARFAE<9E}6zKS+^7R!2#`&Bgz(yT*`Apm+)cj3ugS2Y#zy|C#i%Y#rbLDSzt)@`}& z3D4jE`CtDjKYQl1967L;S=T1IyZ72&JnYJCx85RG@7N-pm1QRq3gWhFUq@b*);P1m zB+}Yh9GdV#ketA}0vK+E`}9g&gK=bgk@ZFMEw3uF zSVRG$vo=1~5Dwnks2wA*rbfXkmP$ny78gON_F5B@-lsqJV6t_`^-Gc0*!#j4f7c7- zB96Omz4}$N6V+!-jg@Zsj&FJ4mK%0n(>!tfXff>fymC1#%H;~(3hWUWdtlWNa#dYJ zHpjsjC)~dP(}7#2X;kdmeKnqqN8&vR^}yDvG;++)qIe%3G@z?e z8EXlJl_O$2HRw{nS{kkOhmoXFR#a+D4|-DXde4XF>l2$k`|_XtmG5{;J-Cq?2RkpR z`wNf~sMic*= zZ+TsQ?AU%O1+`*wX54%5p*^y1-#&THU3W6vTyS_7L)UVR%}ube5|oO?snpP+-BnPq zlRN55T{}IN1tpORMIsA|nqkn1f&F}jrAWSXLEN?|6hFU>%aNCpA=yHA2L4j*}boN#*N!v6@B{QuRC*g z{+93fwr^ZFF*#j6f9?z`^s*%JOnn4Id1j|8T55@q9ncWlPNwR80eizVP^uI+Jk>N2u#^j^B`BFQ)CI!Q5e13S7Er|#hY~?=s?}0~LEH!m zvujn#(%|D2Tt?a&K(5N|XoKO5AS8A4NBpHelMt|5EhI9MjOrt(PfQZ+4uwupnrOfY zw92P?x{tJfz9_Q3yyyS^soZzZCuQfhO%nJiKvM`5@-?hF%$&Cj&nZVZp%nt!!90{r z>t{xV{+Qp>M<0{J`yXNth?&{-vUAt9vToBh=`ZdpqPUPa`Mbl|@BiVS=I?*+x8GbG zKJXX|L#Eelk%u4K>n*ic1lU{!RSv{!t6c2V|AXObXw?HVZ$tEd9UJ`{nRFbIR8wKu zTZzH|tcFMn=`6S9q5B_@TW`Br1_SMzm-!L(*qYt;&i0jiZUD}2bmaMclmDE5%#=|zJgELZ$nZo01dnR_1H z^p4+r;~)L>D}VWipRz0}DlX`jLVXl+)SmX^w7#I$#!X8)6pzCKV#w;`f=qhah`U%& zDClNS1BZX$6gO=#7|g4#x@uH(OgSSe{;G#avLeIeRmF=DQ2I46{34c*y!YMm`On-f zo7c}swGv3fosg3gV{EPr#S*k=L{}+(O|)dZ)sV^Y7R%<6cp%G57i8hWIk_-@RtD`w znVW3M&aE5dx~n!xnD^zud+(ONdguR?5B$|%$oz?e(ySEHyKtbmaC+|-SML6e@e{9j zo%iqG@jv~U4cjHk0vVgxSe!Y(RDAfOcS|X($jscVilmy#jJ2 z5?co-6KJ%KVhFho9Xu$U(oDQ{Kc_bv!7BUvUZ769=iZFx_KYI4IYG&sO9za1DGh zF}5I}YRg6$PCi=M)mk*qw^FU;5s?>zDSGsjETiEvQq|CX^Q2L0&~t!qUoo4!MZnB^5W3YU*c;Mbo$tOSYLD{l#R>sj=6bFP;bRky|Jsr@M3j&{+ zPvmVOxV!le-tq6=`P6uDh3Zt+HuXb1qj#hfP1bO-}+o&A6bSOqP0evXTx+iU+}P@K8+*- zYIawaJ2F2%&(A{9zAFC=SRrD4DewiH6+ocXl#>hTLjk2tFOFPU)U(q2Ba5P#ot+M5 zrkaz(-tv~G#K2`>?Lpt4eNa(EyV_{PnyFaN1nR(pRqaTYIAtM#2hYoJ6o8RnLR%(= zS)CYJQsulcbfOscmgQaV{8RDs zL}n+)Sl^2Q;{<{%`#6~PTPaj;4W&8ZY6x#VN8#1M&_PwCLSf8|&5=oIfj%KlX^6K6O;KZJv{xZ@5NIAKxeMeb@h%1CQS?E9V|7x{JsERzvrl z|Ln*7UwY$zWXKv9fwU&(ig&;F{l&s!TcD_`DzP4W4(yZ(9p-7R3^1Sx$%v2L_8{BQo~ zt8U=(JTH&7{`?Z#d7O#!+xoHi$fIB z^(8H98oGWCR>MmCHz+Yo2L7ii$FcNTo}{{efMzytT8HM*m1Zb`O-+w^>!!!XeHmW; zMDO`4B8r*f0<1b{26^B~=8Hs7W{o4(52BV3y%)h8w{Gk>QSslXNi~CJvfmgg=dB&J zkkJaPac;?%D2b$ohBMfpo-Y>`mz^;LZs*I?)C9A+6k>E`I8WygwX<-fc=VC`BS_tNY zmTRFV7`-qI8v|}>5f{Qv9HKDph@7yOAt;()T|~KDSXg2bLh(4LM;A=)WR5M&$hcGJd<(py=UhaS9FPMM~crDrZNFw*N2T#UiD57A3n?thw?RefuN&UW?!Y^^>74n+_Lx9 z9SuJxUYTVc>Qs$K?i#@qQ&Z#K!u;8Ky;_=)OL%G0Tx5f5vvosEkmit=?+lcl+t@tGhW?D*=2s6_@291R=l$RMcb5zL(^FHNe-v(m$PHCiWW`aBNr^062HV$Z_{)Yh<%-PAPRYi( zSy@>)FZX@^(;|5y&wuV+a`?a=dEZ~ZQ-*!*?eOKsFTC#Gd+RoB^K!rJr4rJ}v8<!e%Sy&zxsyWrrf>0bwSDE}4h` z=HwdWU_hk?nOdmV;5($zF~2a5@G`L3VJxS{C*W0j!|w8!T;k&|KYai9E_Jhg|L@%& zTnhYhIx#s(Tpja-7LQFTX9Mi7vsM)zKHEKFGICW~sk-VC%WxCZEqpnD{`|;sFkkTpW$N+k&gaCucHf9>uTGw;{#-<6! zh8yJOtk%Ik9V;24_VMDr&)zMc|J282{mhh1jI|)wz!q~FX00}|!N2X~wFHc1vnr4u zVkoD5Lh&B(;JV#5d2>_K(|kSv=&{gI_entCd41YVNu|h<@Uc)3&$-#EGZ6vU;K;$f z^4Ok-8f&}o= zNa4qRr+sZzB!iR?)Si;YRhQ+ZHhcc!zk$(%zGE#wG1ev`aVVO-fOA8IVLU_jH^DOm zyn;@niOft-1dUoSUM}T3zG?;z9u;)qhY_%g+=B-P*A|P+9x^kT5F~U>MBs6)bXI^> ztn!N-n;rwksScnLk}b6g`gfC650|&*TwiQ5OaR*puCz5HSV7ufzp&p z9a;pIOP-94qiHfIIStY@u%VeaT0_?mzeEoiUyu$mj+7Zu^g1;K4mK|i%M?%?beHAP zhaQrkNab12yj?#0{=b$z_kH+pHFD2-?h7eDU#*YJ{sV{Q#F?`URe={;atg)TSYP(F z3Rt@)$HcC|?ed&zMn)VM;pc@5i!{)U*4W5s*%uNHC@nJj2^*a|eK>?q=vV~|58fjT zE)QsEYoKm=tR-2}FV`!)6f6@SH-}=(MQcEnA}!X0Cf+; za>%D*nTC=hm0zS<@n*T%)6~e4E@fIeECFH$-^-k%x7bzq;KGwjar}t*_YRb4)IWjU%ju&X1&Jdbn(=w8C{Y+u- zHgDd{*TKLcBr`7v!(avLgNm{C`LIC@N~~PP0NX|eK1+G(m9m^Vc|sm}_(3^&;)J~K zeed~O@AoDD+%Y zrUrcC#0h$67G7b1&S3nloK09Fdch3iU08)oYO94t*xEN19vdzNVHA`$=GM*TMV^H5 zuye^(Hhih^-j6@{qszT~-vbXn-tKgI`S`?mp=(6R_-s8eRPwaSSm{1vH!ip5Fl@gW zRR^V)=g!WLyk`~VfH$qm!sZBRL4ZaU7>1wXfMHTBPS`4{uDrN>%chNCp7dwNnw4$x z)v09gd@K@*?+U<2?LLK*5@HTUN3abB>%X?`ok{l?bD2`(D3w{WQ9v-n-@#pm+l#DC z1>%TEIm2tnGbS^D|A3utATzKSgjsZ>*EDQ?It&z|KXA{d<>3eJk*!-c$XH8T!+|%1 zOd$GR7*?h89?*k=-*J>u=1y|CKC+kx6lN&2SEBDQpHtbs@l97oH`BrwU?<% z9EBq+%MKnDi}Y33%@Jk`XM`6QmgJ)!`-HSR1G)9KJO0jl{+@sFQ{Htq-6lm)kzSO_ zo<|=iCS;x#NXXb^b{&>Qhd0Z9qR!KUJ{)iz<#6RpVy&#S+33jDA4~tB6^DiGL>~gi z5(VcBSY{s`;_wq!D~mqB3~y6Nf3ZTRzy=;~)q`GVvGIYw{*!y;G8pfC|L6Z%H`n`%Oo0N5?M zoq-o;&&4E@?9kAJ0|@AvY(U>+BpYy8vO^jwRHCRD4*LZv4riyv8&R*l{;QK@$TAPb zv$iPU$;FB4z*E^bSGi;5zpZ9RjNE6!8G*1y^NTC)(GsPN?0D!u}o zF~+%Zb)HT%k-^gOVrl-Iy!XBDlJRCmwrreZ*ollZ=D@sLyKk0ft*I2TP)jAGG>p%o zx&md5RIySt=Z260Sm`Wls<%e^d-gn_FoBRn7iB8FzIHr=X9e6Jy)~_#lh^H92x6HX zId(!mbI<)!uGHm4-|$WHjW7A$zn6RcGe7_Dys5bj5{3=gci^zhFDx;WXX?$!%v*H~ z?!i1s<8&E|-nf8l9asva)f|`e=jQoZifaOH#`T@8)B$ZD>n`U-kgQBigB76 zaoIzE*=i*$rf0@wX>q<>35u_3=dCAm%r6fPoty8>-~Zqv@un@Ci+;bK8&0-1Lyd+u zSOb_(7$pqMp&e`(coso`V){}6yw@Tt>|c|oH>QQH*LuC?q3tMMWA(xKZ&a03D;4&+ zK>`h!+}5odE1mX*iMRji-%S~L!Uiq~JdnCkkqSOk0St~B?qt7*RCg%XG)w?!i9p=? zVv-G>=hE$H`kq~n9B;|+Gwp+^RS{;%k%fiU92OMtA0q?X=EO}S1TxQM)Yi?)f=smJ{~nFE7rpkQEcv4of9Da^j?X z=JOAT7gXh$&v~Kzlka=^6M8Vude-yBFW1BmDzbOq0fx@xUqU|&>K2gY$`!K#rHG`` zR1Gsdvzh|e2zd~GB5?xzsN3m~%3wwi_Ya{@pmxaBpdm7Q#Zqh<2da7-467VQhoRn_ zZg!AW$C26hGcSpHmyKrryWan~?^)_3M<3sRbZ}u|u>cMW&pk=g0+=LZ%8)^a7tFO& z3dm$oDi&0|p$0M}bt{hG>6PeJqnSItlLC?_t3?1#VH*3_GO+j@Bol#|LP-XV$I}cL zu)-=lsPa^%rzZSHz0&ZCWcsU-MTPlb^I%m~P56MR0O0T*!_Tnb%(KEUkBZqUbl0J% zo>_3sVBzmgbp+PmLWtZMMhv_Lr#Q<@T%%h9yv?IABX_wE2o&yn^kMnzCqE$@=VsXS z1O#4`)*wZ18_rHXkXU(|x{%RA0mI)EKk>;JN2rS519yWo5(V z@1|RBm%Q+0ab-o0pFGVZ6mTMIS7#oi7O91i*42+0YRq<~Fo68fHWRu<}i@h89c0l7@Zh2_EF zeo`!b_P&SW$*I|bRV0pj1p>4>8rna_aNb%?_bS$!VdR{*3QE5egGY&+MG*)$F`sK~ zfwhaIx`@A;u?KI{GUHlasF)lY2~CZ+D!tCqx zXx3Gw0r*<4H}VQFthm`IA;x<{f&pxFe4*S!wc&Jfki8;XpDcZ)rD(ZF!k(&ZFdS=X z2~=S(cLSI8&KKR~Mfvat-y^=C%2hkI0L^x3c~l<^(Zim+Hykp+)sQ$KC(S8@Ae)We zrhp6J4daBw>#(3;kfx_63ETrjb|h8}P1Bs6mxi&*$otUKFFY#$!pmW47fxLFJV}@p z*S|E>mW@YGo|5|>dPEApEVtb8JlVAMYI*Uu{qwKLt-kh#n`L(0CW+!)4jwv2Zw}Ai znx$|J6{|#D&_q;`2D~{fufp%(xj`)k4ORZ98lVodQI827D%CQ5R_X$N> z^pAktJL*+&|6mK0pt3D4*`wZLvr&_1*r|*)s+TnBckTGVC+_?4<#v4h@Uhb?dmi7H zO-;|B(HJX=29Z`N;Xoq{P=RhA#2M7#uuLhi3iLh&@n7qOa|&r9F=>fLc-ZjD@ZI=u zz~^8mSpJ328w~lk$kVukd_nG{+=Il3}P`_tJ{PPau#m(2&n?KBH!^j?-nsaB6F~8pzr6^K$URmaqSo@BWG($cw)DpLsXldONYWcDpBM&zvWYMv98_;*1k%kp#Q| zOHAR*c!7AG8D_q2rA=r>J58Z&p?;=yWi#?A7{ExJjQWkN!T79J(+>K{$qDqG2=Xkt zY{FEG|Mi!5zc@+5^N;U6ls2HxRwzZoYI6;Lv}nt)P?USIGnsQpU|LYZg=z=Xty}j! zYuIg}sx^W)9GTu<2pfWAB%%V{=ZG#;iI|!1V*UErQYG}qiXxtVat$1|m;-Gws?>jj z+JX{*Y2|T2c<94OI^7;scX8^m@2MmD+UX1L0}B$15%?S=%?;0ht2#C|E-m(H&`_4{ za9Kg*Q3*-pwC22AYIRj1DP87$QRh$XD~=!DFQ2;mBQjR6%7*nbEX88^8!~%U-LSUs zxcH#$2E8&UWuT_g@tMj&DC12nv4MdEnHS&Jty{+|`0{d_VTkeBNbkuDCK}jK#L;fh zaXv-Zn&rd*QFGiIYDu1j$?*F6gGBCq;1L-np6tB#7TLM`X8GoC|ABw-X*08Kv&?PW z282w`&R@_*C$&`w!!{qguR-0nQYWs4g&BAy9;)!Vj7p+7aLzsuz%N-u4=E ztorQJpbiWo4LCG7knt=q$5sMX7b>Qq_AiUJ`2p!xB$K+eG-cfxzSwH5%IFxl~d{c%w0TFZ2BHdUzc6J_xZ? z=d4Pum4&^zQ#iStN7P@Ddp`RKnLl%q#tlXuA(!Hnx~UC##m2U(4%0WooNEoK9tvdL zmjgkjZ)tgf8OK&@oSo01n}!A1ceTKv;9!9Bg@KK2U@(4f7^(H1#peK=Kx4nqD*|V# zAIk#|Ju01EEL*SIB{$ysO!=mN^!-nYEB(#i{(auHH{L9zuqw+dZMm?h3ec80H!RME zMG%JT+I8bTOnC;^edg5Z5f@HFBu!Y^?^&#$s-ni)iV)L?Gk&M_Awj4$F)<0>$8Wcn zEC2a--}Yy68I4XaIhp3=<@+Cebm#{mDIaJ(V3kU!U{FOL_2O{ISk@l%ibz2-swkgR zgEbf+>d@aCUTzWED8uj8D&Q$7nOOGT6ir;sa1kf0lJM5eO;_T0P=C@4Tu|~V;Dtgm ziSf1zgMe4~B`6T2RAMmqAF{~IwxO8^Hw5}8dQ=p_jnUXauGlr^Wwwq#hrz3BQ;|+b zmfCHq6(AIWr@@98P#`TlSz1|kWn!p!um;oGer{BgdYO&+lY0tCAnkeNewi3+$hx^1 zN+4nLfYXKBLUxn2ph(pNmt<|Jia!=rcRWX4 z@=t%{NyQ1)Z`dSr>o$<4g1)C_sDKGkCJBirSH*!fg0*6Y9q}FqTUc1ca0|M3EW?8K z8|DyDCS;d%Gvd8sbcXi|85?U#?ic(*We!9}g21QO#(pFfrObe_Uk-gQOQUk78mjW> zWitNigP-})#g)O)3rpRV{Radz%^`cbieYHj~^oZ)L6C z8>M{!_&Pk(JPt7KVc%ht2udQn~%of9V zSmyCreJ-=H1BFO%(i%prsXR0Ik4sW;D>y!d0 zLSAO$&))sXZ;I1m@xY-Y`Plflhl5Y)S;ZHKY8W-kRD6)xrpMOT?7sHjElahbZ~+by z7&v_Ib#=em-!ielq?W5ocNjYpO_1_Pk!`n!5b7&%}moGdV?9l2LGYl&42_&3x1{fZ+C54M(O3+?gg}qM zedEhQc~v+Dbfj1tLGV;Tb*t6Hp+o6%aK{)D0I~yr5j_qX#Hw)0;sJS(qlcnIrLI*g zv38tSr4CO9aZH5~O!V;+XXNmaOAcq)mt|X#?UiGR8T$bO_C~hk(?|6&ZFrd@uB@ z05{Ca+Ssv-QHn^M4DnUqv$H%2@;v=Yy4PLwk!0S|;^Inr_Uu_a_X6t#0Rr$bcS(iyoIG&WpidSadpFq6;cis`xo7lm;<+ zKZH`LH~582KD@+kw=Z!Nd*D~%%_b)&8iAL7S&`8vY67%N45ChWR+Xw|PXY2ft$f4B z43n^~NBAqlNMNNABAKN&t#^b^R$gQAw~+v`F#(FJM71~KeHk^y5);(sH|oM=mPI{x z;_;%}S(bHGkTK*Jr)^jXH(JXm|3@sx;lK%k&fqQ=|O z(h9?Ul;kvzw-7T_p$F3(O zsqp1StvN31H*FL2NJj`GEIQ&}Yj>)#Yb93!i$NlGNYY@Z`z3L~dgN*aln zvNwihZv!+=hFnv%>dno~)B>-Vd9n-~Uk?lpTqxF|;>0KEawq{QlWRCwu4|JmHYl$G zki-dqg#nLEvv1n07Y0-1%dzoVQboxOK;c+*rr<>&gK7j}U{LIdDFrtlycQ5xK_n!` za+yV_Svr&xM-IxNgL`FedP>GxXrBh7g|eX`RZ-zl0N(*70xvomu7;@qaw%i9M(d0% z6{8-*ENo6Y4DdxQH|O?iBbNZZDYZ8yGG#6VV+SQ*f0`tuvddU=jHF;JiO2UHlI3#{X#rX4gf8slPQMP>iimLRa2a5R8tEh(KBts=3I7DfzF z14+Xy2q<%(8e}jW;s8xZl*Si&XMk60(IWBQ%4%n{@kYH`tprm)`;$NPTk;P#aOh%a zIEP;#K~RPc6hUu26)jFT@-D(q>#qpTRN1LfSiLGHfW%B zYjn#q?vii&&X+zVlRq=JPR7Qkq&JKx%~Nq*)lfb(BAgHPdJ%qFmVY%o)eNX58jK#J z9vkRI`a0uzt;Hd&N(PzhsuLJM%_M0u4laQF1PRE#??;2-Woh6rdV~B-x1aQn96L?# zKS@#w-eMVG|098DJuzUsw9L{q>#+vo%y6T*^lDfOPYqIB$jDNWs0P-A@)UsylP==$ zJV0DrkCbjg@jbqK@C0RUZn`o#)|l{$Wc@$bz$uSPCly`- z`=F}PQ_UwV7Mz}|ssJhBPdJNt6o%Hy`v7Yhr^?0yS^wsI)44h}y{>PXY^J zP5I(-87B`uP@F$~Ob+gQRAwf|2<{uujixEsz)_O1+Yv&tZhy!uQ#Gv8xWSvomw9Il zTOrH6yu`*Z>({U6=C=fbZCuPNwbBxFjH-}!0}lmX2J)IgFfbX17TU#uD|hhdafz}* zuD#(_`R*Um4)#yQfJFB6x{VU0g~Z@jVqNALGnhyktZEIeO87)cA_JF!>bxc~M4 zKKAuV=C=Av#DPZ_^I7=%95eM z3php5uk{y=IZ6G{-@PYeeapAn&eC;l2)~l52fdRtY zfO7~?+`KAaXVyeJK#JbE@5nGCd~LzoOJiAF=+I~pqwC|jgp#DFt@AA7=wc?cfA=zy;tbB8M%~gDOs_A_J0~BVBl{chg7er!mJ*=j-cwY_5evt z%dj~%p@*vL2uJ!tNe!Jv5a=Mq&xzv8mvJs=50Eb}E_XL9uJpp4yLL;vtyCkEf>)Im zL{Q2NBqlfMvpuL*F{$1lFknO;pdS%;&sU=bW?HKn#)dTqdHLE)0|$*_`hsZC8%A+a zsaL%yN~KmGE4FUkB763pDE)&C9NcaaRtO5lUO6lw$>!86loMxZT&0X4W$sIsBFTXI zJ^eV;MKV=St^07q>4F9s44jp_fX`(G&FstI3+WCNV{|wkHme^h?gk9W3j0xPemQw> z^QVu>{>L7Z>4`~MH#^NaGyF*D8{QX${WwjLNeTj-YD$2{Ky5jUq1NyAsXRM5Jtf^f z8gU~`Lq7(UK?0Qod`A2|794ODlyJcl0@n$rZKkZ!9ds+Rx>XeU=Xur5@9^>oCBDR1?j$D~mHN7meC$(G zb5oO;7R5Bvf;G4r+G``AwJkk;&-21kSP<6mm55zJg1|j1ql>xTE1p%&VDLNo(fR?* z0O~{W-nl;KAcJ^mo=H+J*NR@84l}Az{sRph-ruk?a@b;38L$Tr4~{((D(dC~b7Lcm zMDd)YP?~~bW_2l2&B)n=fW1ruO4*s7)F6?AbTC8=K>)1Oq`kCE);$beDNqJA>eI;g zs;MFgu@(sUUnZH;c+i%;k3B3G=FiEsJGatHMGr|BDi=|4Gnu9yLV0u94Hwe>o!0C} z36@JsOENn*ORohj^Wc&75gs6ZkVx9y?uaXgfv_Kck7oqW4j?dC<)8<#%h@nu;s;Z^ zci$ly#3Hvm^BFQZb8%XMHe2J;92=K*Z;?isbHOrP1(4unhzWSeKitR+^QCi46Q-d-JRR%P+p} ztv@Q4;rPoBeCD5g`#0Y4?%trX`P8|kdKmasBo@{2#T;fHDx69lOxh?B#lES94*X0F zopSI!&-3vEkjap^@O1cm2QV^N!B7;)wibErWjW-FP(dhJ=KDch6h)rId6wo`IyN!A z93^t{$p}$N3r8QrQ8NzcYq`!kxG)9z^(GOTRzQ$Mfit>oX3C^zMT6G2(Zj&v0Io)! zR%={!-VwIBEEi7W7PRx7hh|;0YSq>3n9@oR7OMd@!sVqt;5`jGEAsGz_YvHlo11g) zc+R))F8|7Kgf5}CeXe3L( zsk+c!VE8?S20S5FUB+aqqrs|v+cO5?NE+Sj^k z{5?8LS+WOf7WLDtpm2#X#!^tYE>M9DWo19`eDAUjQ{Vled%hjtzwXZKe$NxB`o0(B zMFGEyW(h_r&oj3UI01Q4c(75q=lMD6j_ke2qJ%Y5(+2Z_ZoEJjO8hx~j|qpQW0B`s zR#0F%%km`4Gi;8CoZK8afl_k!|NGp({&Ls-#6nchD?FeaYFQaZnKU+)bzSx@D75&N z6a^+5)OAG)pLMLc@~c26L3DsqZe?X@RO^OKO?8Gm(P{`!)B0^!1BZBid~A|ZD!qQ6 zvMs2sq<-0&P~&nia5$I_?0ZzsojD;pwrr9n%2dk88bxf&MvT+V3uMVPGq>8AeJ!_3 zPt1%ZM2m|TWZRal{5d4Z0m=g-(Z;?Z=P=GPW}@M#m}b6pE~8iO8Yi!P|CLTx4jwux z_0}ZoOuz2M-+9p{83t~Ad{WLF(`uHy;c8@E{UW^iW*r#jU{xC5wVjXYkJZ&`Y8%4b4*^;~4aVJ{fBYxp zG8#`Pi<%?}Eb^$3cJ<2{6Q>|+R01mqH{C28ah4SD*JQoeIK-WTd9m8&&L|3GP-i%G z0T5@(y$f|SGDB7lwR;r42kOyX4#sWq?7<=$_T-@l?<02#t@fa23hxaP%{&kScw4Cq zEc<7HH2co-tarBW(=?aHS}cmlo?Z(fd=QxHCw?v!gJOJzRG8U zqHW+Qa13rD?RoA>jJzT0QmJD8NemtkaWrcS2PIWPpUqE86qJ*GA@~p5TDja8z-*3hHP)lV;K| z>@rH}KaIu{GH@?{#cQv358qp>@?nc(ZS5GtPS+LNs35AdKF|zIpvCo8hh`p$l^dC& zL*~L-yf?xHAe>lC_?h8Q_&4w-t84&nXQ}^7d7v@#xsxa5(82w(Zgz%WKmnklYD{ob zTQSv&wIZP~W~Pimr4aCwvQ)b5j%?Vtj`NJr-+G{G10S6ednWMY6!7(UkQk*$g$Wf% z=kqFu%vK&denw_y*HhmO2IgYVVXf)cG=E2sli9J|W z5L?6QEVY4BeN-}NJt;c zG>q(sY6R=Qi4#5n42ZNxz9#GEfL^Pmq$h#2JvwANj}WRDSWF=L4hB#PsM4Dm_Pg{X zpr!%E3d^$KbGq%GG-^$HIv@#RgW~k27gvb1S615!UWy}+BYE_Z2UzpCapQWH2Qg!b z2Lm>KsIZL{HDlw#5Lt0Oj1eVdVog4ngp`)fjF0$4x1azUUDF_9iE=Hags7*btYh7bhE0rAX02AVucCFM>>Z1sS9Rnp z?6l0R31ez!aqw**X``DQ{4RKI*4F{DKLEz96z5VNrw>0;EH0cc9)0+sVrpWXjhT&q zWnsM4fv!?z)LX;AcZBsuegw8zuT?4jxqic(W)Pih5Sr<7-drn2%q@6bwQ7UUP(2%D z7(2wTtQMH%Tp4H=WjS|tK~A2zAltU@6m+?|REE_=t|j)`f5U(y+rHKl%j|cqQQ zjWlizqpdQ>lz}Z8q1p(NKqw=sYBsjhS8Q05L)v{z4|Qc2fP7F!X6MRy%EuFL{OkaH zD(lwJ7eGVPEKRA2W#Irr#5j}C59v98YlP}a@PmkTxW!{QhbeXfAOU9%Y-cO7CGQQP zXP^rhix0}Z@OUOBG;7ND$zoPL7{>;9;H5fn?Z=NE_Bt!e-p2I^CqvdRS_n!dhFsWP z96T>zkoY&M2jGoB@xnClkgS2HVa47s{HU9x&Q?w?k+yhq^}NcT!#qLas>&`#WbLz~ znR+64Ua0#8_6Av-z55SISZ>J9Yj2Wo{72t+$xN&%#*pj{ekIdwDM`!h?&RxwP{OG8 z>OjU7^I%5K4(GKZ0s(4DQScpVn5iAYGy-8qVANS-4$-f|N&6V&Vmwac&iTGqWS9Dh z`!pR-_*Ah_h(+=Vh>c3rvxjQsE=G7%Tu%3csf`fBNPsVaDx#Q%vCHYjL3U}Vgx`@O zFy31V>2y~Z3PaW>5yd>v>>K04OEFG#hwDuR@Q7U5|b&6!W?CFI=sMY-YDXUW9W->cv5Q#z12)6-tG{HjK@w!SeOESF3DL z{-AoTW}KitlBWJBJT;v>V3;IOCyCVj8V`{qNs3wpxG|pBD#mF&W#J*`1-K5mx{g=I zrR~9)cVr3z_TP9bWRLWm0;a0XN>KZUlM{7isOw{au7#9Q#1j~GXOudo?vwspOS+&M zpc?O}Z#Arl#F+q|TqqY7=jo}Kk+e2~TxhBdaLJAHC-)ZT&z_RQhxW_*xmh-GrpB17 zw+(}kJx<^~;c&oh0GR{z0OfC>D8Q&=0|9dzQ4>6o>Dif)FaiJ8FOdhQo{VOnF>ml7 zF_=g|8$oLJX+{;a8=J$Gc31WvJ|dMyOLks+gFOFRE*>r9OARZMhmkXbO<@Nf5L)el zOe@wHvZDkz#^1vjvd7|Zs4_`wx|yr?oElI|NL*^e_cD3p;rm(1v2J#XB~qqO zW=6|Ov@mC;$3`Z-lbtc6HZ?IpW3O`>kkCCzFa&~@EwqLVbT9cPn2fY1zK3hPI2Hq>%{lVjcFs0MNGX)UCFfn-Fz-b{= zz`hH*?#xp&qh-BAYz}Ou{ofmt)z81$5wg3ht z*ePibRO==cTN-s%cOKtJmn&32xLVWG(?DI*deTIKl2>*of))!t-DVT&}TkimBAvWZ1!KG!zVLg2Th6@UD1o7ioM&765$-*HRcv zzaL?MF~Kc^q+B&k4&YQnsHuE7G-_7a9_wjo%Aad>;70u$*6_Ya6c*94reQRC59@?& zp4+8J_C0g*(c;+Q19IWq8JU}zmPSpvRpvQCR}8~EVKZhhS`cN#hQvLx{D>+ls&;|- z*|KE|vv_FdhUO`#+11Ubn5lXs>fz|YqZvXg#j-x4=(2^-?!v;doH;u$J9h4tv8hYT zd&5AKZLQ?2ZwJ2M^_r5j-P_u=!1`vlHcga~bP~6Y8N$|TqaGo4sN=IJ=T%m$*t|QF z=EC1#7`;Zl;mOT$XAmx%TQv&)j-0aXwe_6}g|Y_*x+LX+)Jhu4gCuL*Fy)YCa`?yzX|yI~ z_q8{&W7Z`%FmF)dHPOmpOjR0$>_z~mv{$tD=iYwg-FOd4Am@ioc8opVG?Ag_kKY1b`H(IZSS-cqO6;{w9QhrTf`HIoq?x0c-bW)hs5LZ3ihk+~)~33hCKpITH|;>I&r=<&5BYOUNLZ&gMs_=KmJeq>Flp*#>#F&$T$&G zQ^TfxP6Oge{Bmi0EF{Zz8t4mmKdh7}eT)I`4VTquv?Opi2|MZlSE-B^mXq&^SwaJq z66>`}kB)2zk9f?bH0Dn|UYt03M3$EpWZld(#RLYZF?IU&Cg)?V>moGE?V>#(40=hL zwfbnIDUWRx-^At|^bwZfnFQp^S<~YY6Ccv>)X-P24Rg+(wc^u5!{1@E;`_1Vr$tIN zxq9by5|l5^ww_RS?R1wF(*=IHMny)GTMCto)MS~pXMMsN)o17xrc8F(Ku`i)l&u&u z4gU>3pg}~(t*nrCN4-bK1Jxu2$mW0*95)lfBZXhm#eQX6+97Iyc0sH~b;$vi^PoUk z!vd7s(E|e|#NiRP=aJ3e*wfxoJifv=SIc zDEM{dnq=k~^OJt(VqBy0LWOob1)CcPuo63ljFMYB2wR~HY@QqvStj~0IXOwnl7+-mQ%y|_6@|)mqY0DaCIWkF z_kfS)MKuVWs^69IRF5ax*}en~bh`dIl$ucEm+&i2(IgiOL2LRnL{+1s>2>WFsT?g` zU~v?NCk>}T^A z>+Xaj-rBIin+5i0bOmG1qu$r_%}AgQQKzaEBN&ZVQ_@5`kJ+5UBeABssFkFqnK<2h zSKefFk=0sEY|fILK7E!_R9El1UMfgfT!Q1!63oVXsq1Jpxo>i7Bh|MIxf)`o&sk3gZTB&7c(O9agjyjQV9cH1_oKne2c- zsYvm%fBm|zyV&#fwK|@df%7Tc$J1OTvDNdo`cn&CZG)NuEexT(&ZLn>h1{mX3Oy~? zhtpnbM!c%#`wr{<3#`%+biqof#$=NffsTu-uZ{ctr z+=H>`Ekw19v3jbpwxEN+Xs<`UmwM7My%HEq$p6rDE$QaAth3Fl;ZdrLvHGm^b*jII zQZSIS&z@V5*7!6$IJxur-*#zDq>Unxm}VW}MEAZB}NfJop?QtcuyjwOzT1A$BCPC=MdQgF3~@~a{HIhf25vABUM@#1e0 z0t6)`QsTdxDCAxyn=8l$2w>A`(_ zCGaw7)+^j}6j4MX0Qch>V{1qWuNKVs61}xyG~m6#IB6(f$S~5nQ{qn!Q&SN~ohM`f zEnEi@x11;e|;@0mgke7Uf=Bugt@xn}3J%%EPX1I>9*4Kh77L0X4- zyu{fgXO`OVF$}}z&=>|r903L^7+iG>iQ+hH3t^=Q1uaR`MG{TB-P^$mtQm7=R4GI& zP_scRMy)(4X$P-3F8osEN_cv?U~pNUg<%l*i2j(F5~X-R4OLYr^mSSfiaq4SD^r94 z%BH8c;xcv!FRZxR)HAH^j~HCGn*CFac27G$0l5`dH<@MJ{4jj@`-!saOR2)gaw&|X z`|mG$-9_2^_@h+pg>ne%AN`QV3v)r~BS&j7ewJ|tS%@E_kuVwz2MqP`Z_FLMHykLS zb);#J<#2efa-j+=m^(9vk5ALl|@1>3PfYdE6h{QMCKDs@@6aVw?JFU4{0#3T8K z-}^4EuPvK4a6Q4rEpnB10u~H!3vt)-i-w9X5$3+RT7zRH1!M^^jr1 z>q0S5hV}x^Cf|J2ceU)2U(maz%vBJ|!r2RG@{>-j-=wVcE927l;QE7}554<5&q(w& zV&y812BAi6mSR;72HsKy1m#(-;fIw}0XMPo7c9Vf`%2C>>t zl^Mj6{-RXQllaTvT{aPH4wXwz6fg+ zTF}8#+{_-as8Xg0=1L`$nDCAbXS3^#CYU(-Qci_FD9E;&;q?jm6mVA$7Xp(kOvK6U!gTW@xwW@j~ zo==znn-_)kEA2kLH+XTE;#fL&C_jGmkeoYxS~hRm#EK5gqxMcnM@qcN5SxDB+6*%0 zjeb`{PFAS&V#6D!4vHJ_+<;s8C11);5AIT-h%B8uRG_`$p?!PlIUpm7 zjQ}I(1)iixz@UED^BC#TP9O;zVDlS)-XBh}Z_IU`Vx){~Oa zyTt**e+R(rknzsQ?1z_w-lu3+Uai+Sw~H%nsn%Pve#2&Y&e#9rOJf4RXynOI?)}^+ zWvmf;8`sTx7E)4!5LGacd&hbKU5Ubz8kQn+T0uSPnhKi8g|HOKjA9a_O(i)=Vk5+j zk}*{71*}q_eI`3h6S~A)Ba09DFKgO^<~&|d8ut3bg^N9BU(4eO#c)tr!*{FWW49+X z6AoNoNJpL=DT5FY*L_06<{F_`+FrP!U5mjruQzoNybt;?u&-yQBL&(5;v{95pbdYk zNXrN6QPy2wR8mA77w%#YU_C2Ki*oqzA$khNxQAEe`#KLKN~80<(|BW95V0h51P3lz zl8UdLumCq@PMi`R9MoiKuvpKlAs9h!WMaz|84P=5)fY(YWRL@@%ODxR_vPy_@OhnX zBnyixvToxhX-rJWr8pK(AIOg$cvKGVdrYp|wT%ge;h@7q6(v*V$(hmzD>c$c%3g^{ z84M)7R9$4iH90=sqUwr3&B#}|eKf>gs~Upgs8RfH_PXfy^nh?$bH*4zMaaU!5)Srk z-Il9*KlSr(V7t95+hf7e7Reqk)`0 zb6i%I7g?HR_f0Mz=F;82(ra*E&=v!RSdvP;Mv&WNxQ&8?FD=c|P|BK~ta9s+04hc* z8mNE87raI!D8K=;#EyD^i7YIwNI%MCGZvA7RBpni8KxBmgyOWKYdVB8fn(EkJT2 zC5Fi)88Bpc$xOiI1q^Mc+mj26ZAnte%9VQOB2U@Z{CHv*oMT;B6B!s5Y#7zf($LZf zl^AC5So^o8R0d&y$s_?&0!diAxND}$FgopZ8qsr^oSx=_!+)!;oX>^5wm+yTn^=wP zrQpHkX(C4tACw|Xlm_GZ#3Z5kflXyy#o)c++L0)*Qa01tvmTz97u-Jvz;$3n*674C zqRPY4vLP+p*Lg6t4GdFT*6gaPhb$~EOQqV7$>}+{^LgKL@h9$!M*Hl+eE;KnP-Wv`^axZf|7{b&yUAvkrVvyY>R-!i4lc8(akpp*feN-*11xOC=5f%|nPAih8F$@6+CsL`Ewbv># zxKP&!JQ<&8@%ISjRF6(Au@Q_f)>xT8Qh)|>HOJ*r3{Bfh=jHB?e@Lb#nzCd2W*PJmn#PiO>aBDAx&8@ipp*xwcx)ggw?^3x z5mgDvQ0mPFF-b%Rw*D{|6tc-ZLLls3<{FOk3$o>4AUTaVw)@S6xG+>)=9gAvxi?4~ z<5QiV`MKA>@M6#1*ZTO11TU@p#S~y+NQ|n1lh70x3>3#ON{MOT+v%$R*2QA@3EBEZ zA)Q{A41MfVgsu2*oT>L za`k+buVGILz36&CR178f;qR8v59wX>=tMpTyx| z6&)xGw1^{$Ves{kv7{ED5gZJdV-kGDH8*lip-n%I!}qu@&zw2yd0r*)!`jltowY0D ziF#PrKHpjS`Q}>xLfF@1NMO&oIZe7d)wb`?0PALI-Vq(PQ;|62)7sd$T zK5`PkAI96v!|mgdYz2^aMw9>_Sj@gy@77^%d+!=x-Yn-Uc{b z)-(mfq=CTcT3TG8n#knzr6+2+bY_2X_`qY{XYT%}w`1F8nV8VpSa@FWu*#vT^is1e zR0UpCA>j9eUY8I7O536}-P`&gKvxyl#d@wfl@_If1T-nZYu=VRSRaUFXG_Qr3Q&*G^tjfF?>+fK~gfJFs8~R?X0e!dQM~+esX=+@1 zC&1ua|5uwA+>a#@tVR;Uus9fSR5O^TN>vMZEoiVK*@#oP+BQ+21+T$92UDs=R?&EZ z=+=b;;=7F=^Dv_Kh66&|CFr7Y!I zmGcF!dl)I_j*$gq4?!vMeDCzx`MlZ~A7);7?qbi~m7(K_)iMEa300M$*(U5JE5%ThsiRy&Xz{EH5u}@v>yB zraZY-&rrE~7dwmE>9*y}nN!kgpo_egtHJv~aU`yHtx7C;a0AsW!$vd@RMl5NWl#bt zvVN9;A*}=-K%d#^xpi_WjoyU=#rxj< zXY$B{pOa_bc?+f4Vc1f?-~rhicE6yTTAKM%E@`$NyhwvPsHlQEUUbi5387Gn51zEG zC(L0w6cW;zc}B0^JkzkB+2*7OAP(vzy3jVzve3X6%GI*Bu-umuXU?Y^x9o2J+^gU6 zoQpkwSH@QqqN;$Pg*Zk_0RIT41at!!E0rlvxbc9SVc22DCybSt1nH<*|QB*O2 zNni3x68fQ(K|%0bpM?~7CCaT5>cRp00ka-=!4jg-X9$Wc;o9;p&6s*R*O}sO@Z#(l zu^W^F%VEf@<2_!7_XUx<-W-=^YwVITZlC|uhvYM#_>kOj`wg;wcA9E323sK1glEMy z#g-KcnLrp7+IToz;mu+F;9e~;hBbxKKP)-b6!fH2sYtE5%KPiPayOIS2M4ZJZ%C3B z(t~1hnv-jXC;@A|*X_$7isjJJ6OtD}(wdyTl3ci4J-9s2QQ5;v0$>$bT!& z+xW@61eV0XcYIgFz&fl4h1;cK~32!WTyvw$yIR<{dX)GUVR=(xKv^ z&woNb@V`aGP0&BDTVASLSCzkLvr9eGLtq4=R1nZ#TaQ8 z7iZ3%F38oR91r?qLVg^*H^Bi!w$7(+7>K#Vfhg^x;T7o#$h4x~7X3^quczS|cwFjD zYv+2SHkeW`ve2NNXfi|L0wYqxNE*o+96B6MD9^h6S>JTg@weW>{$lS#pObgL>wn8t z+t$mq*IY#jB!ttfhH)B$oL?5!=hOsbUs?8+)dT{X@}N(uReq56kt8xRH_PsC;JBfr zie4RXQI{n($_R`Xc!JexwJ4Q*CKWK(=#4@86Z%&$T!os5FGr7`Dw5QTr`K)1aIxp` z%J>HuxF7h@SIj`i-cD20?6JL{%b>1pd~0lS48xOvJ6Te~a3_XRnTlT$_;pGSEDFv|>=N_2w6r44*0@Yf&&tId z%jXXl!{tN8p8G#7fBo)1mu;KpWXG0G^jukD<~oWQ#)^4MonWfbW$-glX*5ZIrr^t} z91_Q}ytF9m*3GgG7d61cVMH1dB&Cd(X?#H~+hW;Of%S^_M`j-fZWF{{*IXvf(xP0c zNV_xi_8&MJPtR>$?)HbrF6u;H8GmDZ1%hen$6+6-$A+Xprs?}!7%H);j05_u5wuc9 zw8It*W09eD5KJ!xe^Kfcgn^7zT6FyZ^y2kcWC%UbGZc%`JVh#vpqo47Y@Ady%4)1Q z8d)`XdlzFI-g|Fx`q+VDY2kuaFyK%KG*l{=aL zC=~6nvgTs>2;Wm4J-BH@qGtn zkQT$u<1?pT_xiVg>qVQ$E8|O!uc#|ak!K!4QS!H(2M5m!8_a-ylR7dT{4dsu3&M(I zY*M4FbW{7qQ2IifuU)rpofd>=T6SgZb!D1YD02f!N$6`QDfawyI&l^PKh-18)8hR3 zv&^DSj6toU#A~D!k5jW?HrR2rMxmwErlO&7g!)pb7;Cb?=2Cl@mMC##TttMdX84Ke zInaG192)6-+51@U04SB9=V@53UNpR|;-~W7cmElU+tu4Q%GQnRNn{2&7v09-bpaa# z#9{YCywUh;uJI7=2S!ZG2N5x3#MTh9Zr;3cgjZtCK;y{P8V*SCp5iPk)GK6(TI{13k11chf}Qb9b0q3GM`Xo(UCuxJ-ptBM~6GClaHJ^tnVdepUq12C56IGmGqU@dtED9gZw<@IKb&#h42jSKfDfLaS0@YxlvI+iNy|#D2f)T z!xek0^3xz@qD%%D3$BN!Jz5r`^;b#5w2Lu@{f=BXf0nvrYUs7@Rzp*5Xp*AF5o#W& zL3ISuWC*$ok|Qwc5U%tHoTFNU|8^TWDBR(J`N&eqfLB&lD@qU!V<1F>H&UxB$r#tc z;{9PP3m2B8-k6X|y&>DLzWu9y5)9igY#n+0!B5Nm_kK>I!HTS#nUHm}lZv+u`jSGW zIVg)CLg=&0th+r!>qWsZzOqU|qCpo=mRCB2DRNIkc4VM&AdnC)9y8bAt>MVP{Fh3| z@}v7z#%w=uHzd2N6>a{UBh%_BaUGdbc*5wjfNGSLb4&Sy4?l|5itRW3`hUCbt37vD z##2~qzqY>9>Hra18i$SEh^JzK=Dk>7Q|r<2#Y(AT6XT;ARns@4daYB^z$Pc*x2YOx zB(?^MF=>W1lvYb+e(-aKs27p+u(Glwo%RwqS6=7`o=Hf-a2XE9ZcHu&vpF)%(E1Bl z2P}wU8^_CJgss=45EaR^|u8x^9{>&y7$bWsYcUsWzm+CN*g=l2!| zAAdmJ^RE9bpa1knrCt%adiy4sog8C0kKUw)MOw)pO4;nbOy=I!pK0RZektydMpCb% z*0e;31ox}GJV2gi?>6f1A)8|vdK?ZgdbVbPM*>?@nuyM!MJ9%4X2_gY>)ykUJeCd; zX|LOS_3^Lf++7(@wPd;PqYxDf2WfOK^nDNfB9jmYX2i;dz>|t&)aN<8IpA;_zJkMs zQW^!((G5J8URSwqV2PJP4I?n%&7d3!T&2lLsNn|m+Mp|E1^4h^DrL>2ft3!NB?SY$ zYT@!zKYCs8wg%FinN$T8S63TULwcp)wVC`5DGF3sQNyRS5aoG6>&^@x>RGA(hC~X% zbC29Oe1#=+g3=zU%nqUxluIcW-*|B85NotTdlDXR}NJVNHRd$2MZgop=5V~vKdcS zx?NRZ9S)drV_1V?`-@=T_V4*@{`rr;SDVGKBa<&^I-@v_RxUtHK)qVcep<6^yatII zoukw>py|=g#>R)18*5IR5Sr6lWS#wlfla2xy#cI->^xdXb;46|mE z*Q%^g6{M8{QY(LwIgjBWPfaN(RO61{4^8!J@}jui(p+8VyYd8OEm>ZC+aex%?9+&lL~v*`G8f=9b>}JAZuZlQMBv#wCQoF=Mw{fdJ10 zn2CxlCJa^0<1n#7h81b1S-er(x>!mFwH4)}KnaZsaaWj4^`C;sIZa`N!Q#pbJSe#!z?bgV~6t+l!;EQzu$;%C4PeZ>n^ zL(HQ`NACr+7RZNF1w~1@2)BmGfOWpy)GX~L{G7T<|0wLi?{TX<2!ZtU@WUKCo6)Q3l>7@Gut2m$jTtdspb3(fE`@89GukvY$8r zdO(H0bywQUGBq{9`&?RSGy4hXGk{D~?vZmzRcVIs87m;PqDBvBumJ`G<-nnlc@W?W z8gHm&^@@Hk_8)rWu{kVlPhuqzNRax>^>suv2m z?~0V_>;G1R0MB4y;R3G9!}YAi+Gy0+#2KYnxR#K7uiV!RFudb90^P+Ico$Inz@;?n zb>ea>9qnadO`cJDh4(^Y$4ZS*YY8H+^2E?JYFv5)EmFrb7$mVQw%al>xrt4DzSPJE zX9{G<(b5oENuRvJj169ZZyh`M2rC6vmX@X8?MSD+%zkUorUSiacB(Z>c6bGPvVwaD z1E%Z4R8+uev~0L}ve5E_CrZitKygJ>uUNM+92QJarCQI6ranu{OENb%$D{&;Phm7s zH;VOPeHHKwkQHTCzKSe;$aF�T0zf1&zrybE=2X{QLzL+o!oN_uhY>oV(B&Y`yBn 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 M07*qoM6N<$f}PoFxc~qF literal 0 HcmV?d00001 diff --git a/frontend/src/assets/logo_login_dark.png b/frontend/src/assets/logo_login_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..7a79f1ebad3c327dfcccf18b58f28d4906a481db GIT binary patch literal 4639 zcmb7{g;P}T+r}4RRYYnLP`ac`LOK=}b_o#)sii?0q(eedN@gff|!{M;k9*zoFgQ+LC@V__%Q~Gb!ok z*F_8=1$&j`NoQXYhty83+j_t3j@R}$SJ8IBwf!` zj4_)yVLB@wAJ=&;8##G;VFEAly;53pt|)5~U9PvcxBpw1)YtM%1csdp=>0^msNGuu z|0B?LUNEG(w!Yq1w*1|0%;)O-pBonD>bmmY>oqTx1B}H}*T`tj024*QQ6tToAyn^3 zn1s7g(PnqIvqNdkz4i7^!?j7cZ3TBzjK*4B0PuNFn#^*g^+VIt1+;v^3r{{8WT0;!|Z zb#n6Y>TfquWr+y3I{nuPn^GjX^YfYG|lp!&GKwW^G&?DI5-W zcXef!3%qi2R;t*S`)t-Sxg%`M@~-^-pMMDEJ$YMJ4Hxh{_#C?Cvg%q9;?3eB}C4E~bBApz%jl z=?-Gj!F+wnDtDo+; zb(1PE-|^X51xTM7FwQC?Q_!$@A#kytW~ysKEnFKXDaFUk9DFGeSJI;CyXp!mqXLUv-4($UAYgv{Zydg_*SZtCW|Qvn3#Y4-dybeE3kOO&~QjH5==H zy!k5K$>h~5=4mgb0`qZJRAl7*{Jdn4tpB;7aid$&j;c!KM_}V7v1biSftiw$k_c1N zT;LCgUobyEzvEuUAkBb))-1|Lgf(CM{BCEISXo&|r=~dGuq!GmUi|B2M3t4n3b#p2 zzT3XS48cFqC5$w#XJu#488C8il*pP6l+AaF3JE3J41Vqe!T`Z}HzS!75PrwmI^0k3 z@k7zFWKl33ZS60Ig*COcJ694C5{>)dx~@AFU%x_l??#X^E?&yf(9*th+Vwr!=vyGf z;Vc4AwkdCMnB4i%(Jo-!S8II~ULGFF&9R9>>m@<8Z~pwP_?9s^h{a;BwkLQ}Rf~YI zgY4|=4^mv5Wewt)koAsF1|-wAcSMb!a&YkIrd5m?)6!;|3`9mntt`&IN{Gg=H2o@ITiJoan=LIZ;t~?+6cETGLlXf$Y}>NA#Ub-$xpD8vB`K#) zL+tmxk1~KDogNV_^BHwbPfnhGbUM!6y?()ZIXXUk^nPo9e`sW6gpPp$?UEym+1McA zJb3Oj+r`Dj<$r!)?&X#xR!T_eevD|qre|chy?rbA!DS9@Ebydt=0+i#;*5;4ℑu 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 literal 0 HcmV?d00001 diff --git a/frontend/src/assets/logo_login_light.png b/frontend/src/assets/logo_login_light.png new file mode 100644 index 0000000000000000000000000000000000000000..e8195e175b26edc680c9c64ab5a6508ede46a63d GIT binary patch literal 111356 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�T0zf1&zrybE=2X{QLzL+o!oN_uhY>oV(B&Y`yBn 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 M07*qoM6N<$f}PoFxc~qF literal 0 HcmV?d00001 diff --git a/frontend/src/assets/logo_mark_gold.png b/frontend/src/assets/logo_mark_gold.png new file mode 100644 index 0000000000000000000000000000000000000000..a6c667175a28d50840f6f63f188b68a4eed08da8 GIT binary patch literal 105168 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%0DJGNvG+~ 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 DYOO&h literal 0 HcmV?d00001 diff --git a/frontend/src/assets/logo_mark_light.png b/frontend/src/assets/logo_mark_light.png new file mode 100644 index 0000000000000000000000000000000000000000..e8195e175b26edc680c9c64ab5a6508ede46a63d GIT binary patch literal 111356 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�T0zf1&zrybE=2X{QLzL+o!oN_uhY>oV(B&Y`yBn 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 M07*qoM6N<$f}PoFxc~qF literal 0 HcmV?d00001 diff --git a/frontend/src/assets/logo_word_theme.png b/frontend/src/assets/logo_word_theme.png new file mode 100644 index 0000000000000000000000000000000000000000..0657ab0faa7bf1102c51a099cdb59124966db365 GIT binary patch literal 18940 zcmd>l^;eb8*Y=^iyHgsZq@_a|Bo82sfCAF+p}SjJ8UzF-4jt0cDcvm~E#1%X{U_cZ z-nGsWT?^r!d-m*o)kJEjDPWdtG(A*3BA=uJ3$mIjleNu57xZ}JdoI7nYCp~%rQ7>G-s~v7>pofUR;)GdT#dRsT>M=i z7d>jzo&DO|-Sg>Pvn#8|_os)ei<8x>B^ULkt77%s$28YcCe=*Jt48~U+I-RD*3W~+ z>QM2g`^ic1)t7D81#j;>cLPyEcTq<Uj$JgV8M0*4ogDK50{HjIngJ(gA!TH zlN+CLH>y;tPJXxDX6$!@W>8{GRRv>XY7zGj@uEkRnhyR5DA+wM$88u8&$AK1z0*D# zYYG9oJOTR!ZivlFtMJ9qa?6r6p_~*HW3TP*NXE&D=iW~O62KE=M#`-k#Hgr2hy>#Z z?(ads5$jQ#6tVdB?OUzu-rJ($V)|FFh%ksbP>)^C{)~LL9?O|)^A&w%zmNQFmX&d<5)|8R%veMmd>O(jAwW z*RwrV$oziVP8M6=+ctfeF0%;JuWbFNbX})s;(L+qqTl3fbzK)nC7MH^e*P<(M5|C9 zYu8geCD-qk_5MjMyE*Ik&1kMfyK{GWI5mE1+}e`)M$aaWX1&9*5|OY5T%KN~u`BOs z877s8YaVPycedgkd)=7ZzMsU7i2h{p*T>x2+FCY=_S4Niy~eTVmu>nH;Ad>?S2E{T zX|4!wV3+mp{F^cSB)62u9M>SX_qV*V_<|WX++*dx@HlJRi^IjMkiMq73cU5JipGt9 z=wsF=uMoaLCz<%2wht|{HO^3U!&+1_&pJon%w2oq|!kuP~ zTt=L(TqacoLrvO!udAFFRkB4==<&j+Un_-fe@!zkbEFv)zvG}9>tix#Sj)2fU8G1P zFlyHAfYzRyl;k{I-+upZh^O*me>Pq|hE!_S#Q$|2*si6!t5tvh(7u+9D0a4GXm1Re z%dmAtt21QzY0cMl8B1nw_bf-?utn=AVJMYT^iK_gd`w=NiC@Oi&nNK@Q$d zP4y{ars0#x?OJfZtZP>Y+En?h+;;s>>z?J>Bab_SG_H3_%Pk%yeh;@3QAF$^5Hv)1 z&`ohs|Nr=+ExhV?$EljhA34}2;Ct(u`Py3`vV{TnUQX=n{PQ{3yj+j$le002uQdKy za?RAV-P|m=RsMHA;sy;z9=Df^pB^W=YMdUB=~{!7y35R&8&{@Q0@q4hsF6Ud6j&gJ6%JxU(mc8OH zmXakbeIhyV#_Z56}C6Qjn7yS&M&Y`t9gxaHn*x?30z8_gC?t5Ioo+jm+> zRk}J_CWF|`!{j33akD@3Id8oYuo@(sDKu0MKt-r%6kBqP6KF3F`tqqQ1Fj>6j4y6c zva;n!q#i8oJJjVv&<=*pn=!GNpdfgNWOXH?crFeS(trKKg8?ts`qmks{ey$D?(Q@H zEx+-5aN~OYbl`G+KB-=lFQXhj;^P0X9TOoFP0Tsh`d>Thy0Cg;xmP%iAAi)c*BNK6 zz4r&%MH7!@x2Y0!8qzRi2pDf!E)dG*if(Sq-R3y3zJvK16fkUv5CSMZwI;Sb-hLFY z8P6j?Uw9tK^0DM@O0ib}Ra|ezOLX*0+u3nYVv~?sa1Y4R;s%WE$oM%>|Ni}3qtf{2 zS$&{N&i$(&#y`nlw%xyNx)a;H%a}U;JwyswdRjv`p9n7`g<#T%$FyCWOMj)|ZCG-~ zuwQ81{EIZBUSh5+^`G8XL~Ycl3eK{whrc6rzZ?1fepq#zea8=)lH(}d z5HG1k6$eI^3oahNO!(MN@HYw;4i4~whz0EC0yqup^E3bW!u;hGpa#rEu13Gf; zM?LZ!@1sxxk*MS5eXd*Z(#sDPTHFs+-2P5=g31k@i&)p$uHs-XILuOGPD)Aw)vY>J zrh`%UaaR6tp)N5iE5@Mi!}Iieqo^q5?9BUN=6hsvGV<YwRTps)TwqHZuq#-mcoPnTTAbUerYjC=_faZBQH`7_K$uxy?4 zU+Tr*X$-@9JG`G-5o#~`>|^9O)S?PHww*h{XY=&=FMFzVIR&$}OjY^@dfGT}*OW$Ao7X>9F}PY=hcphv!1 zavtLBTEHvNBAZU6Ul`tWW*FQI?al|!)v^8W`Lpf_K9V{*{V>e%Zn%<3wD`WzQWnGX zOn%#-1-E@Vc2VX63r;Ak=xyxv&nt1$U}P-mx3P$%sc{U#P!^Wq;J*DX@ODG1EhI#b z5qAJKDY#noJuaH91J!tYdFb0aTBw}H1s2ee&SMdBA}L!*FP%hBkPXVO%F41O(9R?Q z2_CN#{XK*Ls-Z(vY}*Ci8H@u|(e1^)#r9wd9@Tz9G*?;e8?lG;{!PF4m+>x6k8)*9 zlK^L`H7`#GSJ2BPo&Hv?%;K5wy+7@nPoR^r0o7cPhOW%qMRnHiemLuT4Nnf=4A)K^ z;6TpJK~RO7Y8K>z-Al@-7_@j8*0(98txageg|p=F5`UARgN`~(r*klc{q+8f2!~|V z_sU*L!V?v2j_~HrWR(-c!D55D50A8K+01XWNc4zcKC&c6sZ?eE@Rc+xG8jkqo~yKG z^2u>&X=%iRLJ$F;*SSrZR>`PhgR-is+;X$qX%B&V*6EkW_NO+kHhnq~otGk2s4)j2 zbgVX1cqs;4vY1&z35@S?nn`I{>q(tPyNMIl&Y#gcwf8~wXZL>(cUOg4C2H1(t*sN9 zp4Sr!HvhH;y0St~_O#zX*Cj&|xhDC$ySn4ZbMQ3vEv36Tp?{=SI8_^}M0RH6lopZ79e#(-i;Zr)6$kHrY;&orK(Qp0Mt4K3oEV;Lo%7O({%jS)`ib z&{`WjB0@p{u<}ZaNW;tm5<}C|Ng0`$H#eeRh6#$GPnM|n0nmz0rz`G< z89}nZU$q+}-S7HR?^@hnAMM0b4&;BBHs|rZ)!y1l zR)?Ffx2G7%;6nioeRs88!l8MO$vj3%N5>`;Sux3!6uH&-)7LTk<>pn}k<3^6FiSKr zAtb@L0lch#bW~1S8a}#+yn83tUk!_#UlKgvya}5K@o+@_BqkLT)a(h1VNR2MJ||d? zgHebLlr|CpF=`ACfD{|!`IulFe7Ay5+1 zH2+`!7$=zNj0OArssuaqwziAxbr18zlv%p(z78at=m)^*Jz`84TIa0pUuqxW zJ@!H2q)@PnRbCe#z}dhC^9Xz}he8+?$M4|?iHPU~1qHL@;UVmLl?9*&ua%WoNP|QA z58ZkF>MnoKeKDC(c;n!ykWA2l@`v$*=uy*-aq+$gm9R4+7^2;vcO+(HgyJ#CC#)Xh z>ed?m*o;eDGckv_-QpalAM$j6g4Dxuf%6PD%%%#Snaea zKlME$1{4gPxHvV$uDqlawFiK8;2#@Eo3E)Q!sy03U8ns!RL|K{FYzGW&Y=*X!c%ubD~{ z_03*ic>pG@Wv}dqnQ63ib%~L2$Wa>>Vus@i>e(S2ZZf&X&J}n4_}!o`>Ul(MZCRcJ=85#9 z%K1OLG$UuUl;q?PW^)&ASvk3U_hpf}x{o9P9t4A`&;&q?6mow2X{_(g^MKiPETU!J zgh2#P&9nhilqTE~BdIG`+7gn`%{M}3v99(p6OlfwTLIf*^N*^{3hb9+yrzbR>+gF4 z#iWFcj7Xp}psAG*z`oP-@ZgacIl_5e9z=pR&{CzR$P*PEz3$Z1Ng6Gr@J2)_xG#oG zhF$l2M`9Mo)}KFJdIu(lXD$&aJ4Zh_=ED;7^o) z&MwuTp8O6L>M#^vea!RwiLBr1$=a))CE2hn5|ffGdA}I7eZHM$@|H0LPC(P+ zNsQ-A4Y!u!>W$}Wp9zcQP7xfp8v~0UWz(z|jrW`q8rrC;;IgExtt}Y2oCdX$ zcCMJ(I&#KR;?Mgs@d&@74^u|#s16jb-`kgD= zXHkpH_v0mn2%V@Y_HfnCk5rTVW~9gwr~uzfgLXyj+qf}0wUg8~Rjf+kBVqAAbY~m@ z`%55mO^!*7g#k3&y$&XDFMza~Z{G$1;2Fph7F5vXtedrZ9KTEMD{j)%jzcLF08Xvx za#$UBTFso@HQZ5R!;(oVLc`a)-LSsHGB%5=oa|i>sK40d+~&bnkl{%@epVzwdjSv& zX~9wZI9nkHz))Tp8!2D4`7iXXt*!qZvaGIu$IR4)UzO8>@DxB(s0N!loFf{Je9nGx z;d;CftD3i2M=*L?n4nZ32);SR@Ct5~HNFgEg!GvB3xeh|_N@0=LgnAT7jb=uvcro_B@!ZmLoL}IhQ&Z_?shPj2u4D< zbdc7J5}CM<5a60v0BK0|#Zj%himbag+hHv6PUJ};9H=bD0gxQ*M7G%%Z~dw5hH0ka zos1R6#r#*rVvYHrPu7aAMW}p92y3vzayUI@QGRRg0r*5TXet~%08<-FcEj1CBvmT+ zJEu_uQvjJMPj7BAxXZ4%jJ?K$9JTBi8#H+lLGoO7;`#>$I>Fr2_~`}TUz@j&?6ZV2 z$)CUP6|(o{YspFH-KWVZTRz9FemwpUt2YEG#XNEEE)QkAyhJQB7(ka5!kGv@4@OBy$p{3DO3o*qtNs3Z^k259 zM}a~-_3ly=AD~uC4uY1@Fe~UdKj*cef|DYe_@gI>sHWBUjR4{P*8~j!i~@YxcxTQ2A%q++h5Xr{tv;z2qxqLH~DlhDSmY4k$c;2VLATRdqBJ z6tSSUGXRzX=8ViU+u%qKpPI7oAA2bac+zuY2Uvve^=m3w#PIIGm>A3@fTNhj(rEne z6V-HUxwe!_bvdcg(9qtfsK`DuF+eAilL;9a8EIKr(S8`ziTpVOf^=RX&Dygd@4{~_Gpodx7==n z9wkfbA7(3J!Lz3`h04iH-kWJ+%GF1(`A2}mlMjA0o?8GchLk4=aM{prS|xgo%N9?5 z=`%l8BU+A{kqMZ5ZsMz*fmn|k-xqvvVCOgrUImI`(leqQKJQM@-x~1+`)St^07v*C z8k*5}m2rMJS^0L=%t(d*JV4?;VESHMKa50NHN0Y|lv)ofhUZ|ujsoTKDe{GK>hoGB zi$_ptcFF?vw0iS{frKF^_o7s@5ZV1`DRybeNLz#QN7os@8%EkYXI3Bzg*7%dI`-%$ z*(r!-K-5h;{Y=*~taI{#q@I%ErcP;Sz2xuiD*~g;z`Iy|db~;wSucziF90HZKH!0b zQIgj_;*CaJfQwTfgO_IedR?Y~Uh(Z*KGi?=l#!UYDGwr;D>4qXY)1Ix>2}&@HaEX# zJ>GsEhJ+(_0*UTPTqxZ6)p&aQ(;?GO|H2O>}k+lVNT z_apZ?umANk)P9&orxZyJ1if5xlHc`c=_YyX#tfD9jQOGh~>ph_hx*1DSm z{=?ze{slaQw+6dVZfHio-qq^ssP#b7i)$c3sI$?}VpGY&I5tz#j34?`Q_nrc`iF)B zD^PF=4uAdrs+?cOhL2xvaaAF)<|?YubQ6pjg9pKAw$;wq4f#W95EhBwj?;95RHY(0 z`7q9p#g#^(oHhz7H=SqG+x>oc`)W?PvQ_2o>NwO2T1rh4KdC65E%u+Ya3@-%xlLT5 z71XgSa@Xc1dQ?H8b`A1l{i~!g9c`UZQ2H51`RnHMESa{Mc+3w};y$Ec!qS4)60hLj z#g>+E1Kh^X2*_kqC?hdO<-Fpgu@})0%8r7Zh{e{YG6~0ce(N z_J?5{*=1#x8uhOglkgq1GK&K_kSk}S!LbDm@}wwr$b})-DmV5aEB5XH!=1kK=BVWp zgV$|%)+BZ~gVA>W_g7=2!38XijaiN@%XlO3*BFB-k9At_h`Eg3q6^QALgolQzrc|Y$(wA zGShfAiucr?gnPB@L9;(yjzjn>FlLy;ZZRk9VqcJ+)1G8wli)P>N$eM!-p(RifJ_ph z22o_3j812lxcbm&8wx@?QqZA_xaSEipbf@AWSSJ;t(%pv-*U6FJK!-VaDK_%+rfx! z4hct;OX^O&6Mi9*l3`^`q>^jwmt^!zoq&}!;Oy*tvRxGETGm|-nk zV@t-%;D9QNLLuRI&%^X4*Z3z?MFsQyXqIA1_0*tRH!2#o#rN8Ub+Xn`GQRh?7MR`P ztDW>{M1mh@Fv0L_EUneEvOjPc92h_o)hz$(jSU^ncPIQ{lAk+{+qdm3MR+QLk^yK2 zLcns%6KcErqJ2X(VCItna0r8SNdSw$fS~2?T1KEJlNx81(Yc3qNm<4BLImKPc|8z_ES^`(a4T?l zj)|^NE{!cQgo@QdA8(`+_HDAgaB4~$Fpq(c^#>}--AJ)tHg0xI2zAv$?3SwHe!Cy_ zfXBk6pok7B@;q-W6$K~j?x+Q)o%3B-ZT4cd^%%8f5siEU^)lTMCAGSC$|Y8J6~uA3 z816HNKDSvqGNue#nmb;BOEG$l9-IOO_Mpn9gn>_v7aOcVRx>Q!s-K@j=jGL zkay+d$z6VDW0QLlZEtU{4=I|UbxJW>=~_>Mho}Mq0gzx((~P}XRVHQs)^h^z_Oi-) z%-G0#88`FavSoU4q$DkbPv4aNd|5B@t3|c*G;jRnS%_c1h`;O(IH3hkckTW)d=f+V zl#O$nP*KPU_zAxU{_f+}SU0EH+qrM@4KiXF=-kSZ3;(liKItMRef?orv0-F*SQZeX zYR3uud=??ik+nK7eAoc}q@$yQ11~tZY!JY&P9TQF0TQ|nyc5%y{L)fHB&3`E(E*sX z4hp7rxh@tY+zKwNSZ{*gCP?mUVB|Q$4_6zjpfjy6lO7dsbsZlsdW$jC@Xk?hX5 z_|7S}`h&V`EFafH7T&V(elTF_IVpp^--5O!Twlv-3!pG3T{K1vJG#3;6>=-Sbe5P- z4-aHW_|u8s?iPMd``&nD@QfnAro9WRWb$R0Lh6f`ym=t6X#MwrGz?D8NpjZh9^lz@ z!DAA~(L^U94DYKWpm|+rkrya#_Ll0DBNr4FJ`)snuFirN(PpIN+B&g~e>}FNh|7&y zV2~42?J2I+aw*x$(Xa0|dkEMV@Tqhng$6a;@2UQsCSPo8hkiYq;!{^3{Nc5mkHP{_ zC2&g><`;gJbnjQTtL;2;+r9?GG$M{9v4^KFL7X1UF5n+909j~>3rK8%DeQU$0N_1K z^#E5z0=vutOn}YIj}=n!)UO@aLpe2o_2F1=&qC0^#<`H7qjkw_xt?6-HEON!FGDM= z<^WYo$2ukK42`KnDO|h9P6Ee*kkTc*K(E9DgIPUCg)5vuaLExt^kxvtw$!AZ&e-R4 zn~g&E_087jgC%1VU*R7{O_YE{$OEc2|Ec}Ky({dM)nAXTmuPvK!M#B64k#;IqttJ4 z$8tZI3%tJe zM5ak_C*zjte#4GALU8yHF0_enKH^>T_Ctvk;&70*jiiVWR{kID{8HnXdP0~#c)di_ zJHTxHo%BNa1b7v4{NLdN&`2{!9&Th^VWZhl!!E*(X8=QjGz*{emUSrI*OnEy?T4?O$#kG|Y?m~rmW0-G1h zTa{}FK3%8SOus}^*2hOSH0{7lhpE~9&`9fRdm&UfUG{*%S3?DzL_Bo~sjYu&(R?!Gz+M4=HsvNou-31rn` z%1wDW2ZKO$*?me?a>Y}mKNfE9>;$`jFcaO-n@sy4b%M(q#voU4L}lpE)dATn5BC8EAbVT9ftr z^*0q6udPG{aLNv=fXegPVX|W-o9}y8|=eh7@Huw(;=f1C3^ezEt^j0Iwetd zP1+K_|D#Wo_SWUWa;%u71%6%mRiszrk{md{ePOUv@^L?H^*JH)45`{Py@#4qR5J$wsqt_~N&7u-H;Y9!NC zNV(8ESq&S1>lo$YLdS?)+?GzBjRyT=>N}Lc$;l}qDhj<~;{yqGT9K(L1r*&-tc-1O zeuq{+%-HP+p0~UFCvPvr;b{)%r7@F_Lsm7~1T)P2_vB>x`Q6=zjWpL*E1T;K%II89 zAZ*P>;O{#$t|P;=7V%D(suSc047_^zS?%B#kLCQb!C8Ek$>a$CwK;dRvw%Ydh~pmX z>&Iv3`tGRGho^jqs|EAduV1^fl{6`wK@x6%wMEp{neT=6!};jNEc*OZSXo*B=bcn` z!CwRO2<*ms-N3z{_qV+9@xeV;Ak7 zgA=OBh!md#jHQKk|Cd}x%j{iYbiolRTuv&Nw+FIcU91`rxv6hF_^k%DpEpXep~d3* zQ2Xs$=RxHt6;g>f%U@1PxxTs$74r_BEqbWiE?0Ni-mf z=zs01v79#_1el{cEH*Js4>D9X_n&|)PaCD_RC(2&@c0cUU(R+wi!dSVoa`|&FyUk!HlVL`B zZF{+gRi|5NtmxvG89hoR3i>}iz&gd>WOTOC_c2~*$18N~gtzyQ7!i&IjhVzS%!g}& zU&bLc{P%Yg1werbiHT0b-Cfw^xTze*5&yN1{sOfh4W$ZP`%6&e9K#np`{7@7{(XLVxU|GXBT`Dth{fq zfT($WeT|;x%vmFj2P9TG%8R?p#p5~qhGpGvXTDDl9=0p3|6SXh_Z{STA6F*zZGprL z6c}^W07p8D=KwGfv)yQJ(~qZ5OVI1PxWps7yc5)p;^rCs!W8qziW6-6}H@QLQ}{!IN~ zw^^#Gigy&`yj$dK7n@6C;@4{kWd$z+0Lrunffr&h zeSQJDJpp1EHF}p!uU@5O3VwP_`vs|N`a}K zXO_uinNbM*xl$+to^d4!U;7gr==7vw*VpPBAbu&c>T`w#Vn|)9^_ah^%(`s=Lj@vh z4+u=_&%?@Iwi~&zM^Xz_T#Irq{fYyVZO`enS}SLpMizAf&L=&6^6 z0cVGfry3a@{ofkE?^kzVOK5<-99Ddj&w`Fm@G%dlybumB?5QS;)qa8Hj2GK`KRc(* z6%0URo|stqEKjgAau>?gw8iUAS;A%YzIe0vi-Yt}5!Pp`Pz46;MwvG;-G>(tm7wCg zYECzHpsqg$Ni!! zq5^e3$cc7x^8-<^>81Eh|JJGHgi=x1!5Cx zl-vVBeVfw9E4~w8BX}eI-*PKiAu*V~(69SI^c;N?0DUa$!0!6NZ|Y51DDe*ikOJ!9 zR|Pqv6Hl?eSV=C!`jEE`cAX#}%I(*6nieoRG?WhnX_bt7D&UBrd~zie+Idp}#Q<%^o|e@%^>aiXJN%nhGz zXNk^12d2960)~ae6i5LXxlMN{y*oajv7P`Ma(d1;mNVn)b^Ox zcUqxk{t35&KAQ(hN(j$qjYo3=^ zDn|?#N6B*G&69n60@^0hG34@Rfl{AZ9Xp$0`?}`3S%h*yC{ed z+E?SxWl#?2QV#-V_d^wtUEhU&GvvlZnBFKaRLizx?CDz+mRliBPEMv}XU7m+^-@l$ zEBxZn90@!{SNVnQhpSa}qOVoj15!ZnRd@S1+sT|8_b?!)r-Nk zgM7bkWz=e)fPJg_c46(rs*P1hJbetY2t6YG`Vr zVFQbGV?!FG9VtCleP1F3C{X()fZ#LA^g1kix~gVgIqJ}iN}81hbZ0zZ}3Hi zDlHQeGPo@eJ}&_)d!VK%A@~vKSJnzHKxj{(mJG^T_b1TtgXm$q+l~PUv-n=Ghxd<; zR8xGGFx5llk3CtUq=ygHz2n#n)}v%2h11m3Z1lb6#-!#=hU+NB5idfa_Bwgl{t`%d zEI)ySrQ=79BXLjr^XajvDdrSQa7j|s{UD&=%`+xXvlb#7<5dA}2qEDgAm*SV zt^rx5XD64Y#c%FxEVjr>ISt4MB-kZ~h`K?_M;`(q@PUx1XrIQlAPnx^%yOp(EWyw# z5c(RMZ2j*nNZ1&01T|z*5zN4fT={h~W81ERpKHb}E?_Lo;Eha+5{2Qk#I~h#=VWuj z5xjCA;|B9BpZYZy{?*M(^Xn6{glz7 zi+c&M?N+gU38YdPr%E*u4j0@z{BykC`i%WQ&Wd-Qnr;xIBJSXn|4}a}G}&LeVq%h2 zpcR4}B7RWeD^Zj06Cjb^5MfkH5JuoY$a2x|BvvLy*r9}=;7O%g>C-xLb8{icLM&)u zcX_s%fB-bxf{YT&hfGUv!83f@B~N*O)?yg{%W?=ljwQHeI*^ zmGL7`LHvdXEz2d{5)MKbj-ggU;gE(x;!f^q@ZfOK8mX+n(Sh#FWMVr98FZ}PKDZC$ziiE!cV?-~&rjj74 zr}ISs4>2)_Ep!T_@JJ>xf=8qWVJzAq*n^;jStlzyJe{T{BbqD|qhg>H86=I4$FP-S zXeBj*b^tTXRywKaL2774YPWF>6Wqc{=QMzE0MCXvn345lpWJmM!~xe8Bpm(~SU;`f zU34}{sL=bH2@qDuq;NLskOd&g!6dkpF*^+rq9NhkM1*hQNu^Q=4Y`SkjFLKi2+%O1iD>;>9iWr{sVr;1HA+~NY(w9`E zQM(Dvbogc7{4dBwc}Vm_yb&sf-)TdCFaSDU)WPr8VV+<~T#u`gZu(t2pevWI1ICe% z9~@!I1A?Fdiq3NjXjEFdgpOP&5|v%L>PRpAuu_o-iS9ZZ5ti>(ot7T2w@GMzt4pZJ z^a*f|SGZ}FqC*%Ua`XvS3w>+&t_%?H_7w3YDOF%{;XK6kL4?z2y$ALj+;=_^!9(a# zzT`?iaD-_#j*YFmb9cy~s!rn9_s3QXkX)<_l(*7TavnN7_m^F2R9qla zC#MMkn58?PEy0Sc*j8Hig-zyb#H}dI6!!6ffC83Ix_D0Ava$vEo+8H`!N_1`?5*v) z1e2}c4aY*=<*?G#?+T6neou!snWEmr=aM`X15^RH%1G-*7^38I6 zjnzuXYdfT6WDE(LijpbNc^$c#=DX$kPmnMzDMt|+gQ8KEIzx6*AMx&2mxEb*e~st4 z)5l1nF~0Al*p|Qe>l7g8&YX0}G%Or$Mrtah9L{2C>!_a!_inZ`SOyQIawF%v=|KQP zh%iABf(k*{;jq!nFvV}+3}54kKrMC%F~PuE1QvS5zG;k<8{1k!z?vVe6L*M@1f@}` zq-FHi0+uxunjnicVJ|7}Xte)#nkfPMVzg}N{f=o)vP zqxbcIFc`&B-xO($WwyD{LxnsX)wJqxCx^{ z`C$MD#_Pr&xSBXB>ec!Po@uo^XB8>k`vJjW;q7B84OamxlLMviHQj?fC7=wv7_y1Fmc42 zgll|RlFU6Mp-ElpZlZrfo#V`jT_5p-Dl_U|hI9Y|BgAOaOq0Zc!ig}atK?`xHvwm% zL4_{)6_1C&(hv_H0p~V|10z8jNd?|@IZpap8A1SO7U+Jmm@02IwJ9YRDkN|zzJz?3 zCYUI=dxZ%xrV$RR|Fj&jABHV^ScG^T<&w`G)3t67v{V+&jF3y85lNaOCLk%A zgXO___Lo>lCJL(rcgZj;LO(oVMR!eb0qP9Z_4LFem!F3>H)M^0bo0^;eem=~L>ho` za7*huYU^ORn<6VF0$&}_eqf$9(syse7!=jyfR*6qk0z967SzEQ5QO*28jRzeR2Z8otbr z`;i$~OK<2ZsbRTzwdVNkSxBZH4|>E17{cf1CZ@$ZFOSVoa zSbi`~!pELeHZ>KqAwz5^G*!TIJ4o0jTnI-C>Rt~}b^|egWT3bz7#UHz4q-ZcT5 z!{JUVT`oY5n}Z5Ojd-Rvp2I$WtDVL#B!&R zH{|PliL+U;*J#ASp_s}DTNXmpbU+F_Lg)`*obP5ta~H=qHS|oCqD|-K#3(E4T6dxc zaU^irqgamQhqq1-A2y5m7~^Z`!#i2B5VJV3)Xg(A|6{zqm?3lATL zz08Zp9dp75HK%lTEHx)}*UxgoW*1sQhceSeGjGLk`DS;nYe3W-g^ai{F*3q=tKV<4 zeZRIBhVjNrCLWrRre`m9bHJ-0lo6f}kuvr3(n={L>q&)Qj#S$(df6HCFo+$Rb;an| z^?q*}SHb3**W?+3&x%6N;7f1DBR1&%-R<{1;E#f$86Nh*V!Vt$HK@i8(@=lL-XICY zXw2O(8tzxzL*ezD6lQ(JDQ^s)*a4`?5)7ei%U7fPSJ@9U>K!xhpx6!rbcXPJGeo4# zY)*#s4W*GT9V>Qv5AL|?ZZlf@d&yZ<+|PC;tPO<^P~8>tO{4>ax25(AL+p+}x%x1#Q!G8Lp< z5qFS=&1ng^G1-Nt1MOB-xzR0fVh?7iqM}&hNw9UW+Hl@PA*}JDQig%jgl|M-8i1sA zGLjJssla1KcQj#kS236t+b+O*8F#~wmVp9G`(9KR3&RwK`39e(*A$+1)Qkt2X-Fd- zUAe}T%$=AogP5ZmF#}xNMgj1saqa3O$P@|dRvIBhDmscTYcL8^nhTnSA zozaXJ*R#Fz$9(59o24J5gG9k|Vk7fnIlebEuY#xT4!``7X}Vvm(?9n!Dnwnu(U^Pv zM|fl{J@@ZC5lhi#OB+Yg>=Uo$akmlbTNZRl%4P@YNzUt62(Z7f&>%j2rW7*dYR5LE zy&jEaYzjlmog4jnEAtF(rgb%Jg}#o1!vnNDmL1eT!_@DgVGcN|Ao$*szc0er%(ZqM6LovMvVGjVu%xE)F;5aIm{&sDI7dCf9K7)SH>$sv-Oj2*S%Ntxg1fYAut zx~hJN1>(DgUvG7E7I`WgO*8S~!$L+xAA33sbIUaJEGM#%{83a?Q8Jh;MCKU4`$k>lu!uBG6|Uy%P;p|QVC0YY4F*DGRK;IqQk7jY z04n`T;n+k~`uQzN76ZpzSy@oOA8vmX0h5RM;!&`p+_dDCUtg{O9S2qCYInwP5LrjU zfu)GFP$VZm|8F;{VgCGPBgnuwCIS54h8Z8mlNwt7YnB|zq z#!hd|#pJi>xh)fc>c*ahbUuGpzw{Q||6Y(UO*olG!ABoNe35Yr4UYx$nT(7~zvev_ zNLMIpK_LB6RR099u!#3GENMjsBTY(tahkGSWVO7@dP&n&oM;oqgvxD<<)_sh0#IB^ZdXx?OwqlUm30!=@s!+ zYNozhXLX%LfgmDLmgm3=(Mko3xo&tlI5Fy3sBqt>UNHr19oP>DBx)mI@la0N6_XJv zw;z3g?)$R!4&1`sp=k;AyZ(>P;MJ#~RL)GPbzV~&Q*K(GFpv~=oX@FuJTVoix0{b5 z{*hUjmzTG+#7e&?Wd|kxJEPWft14P$KY~M;$%dCm>(`R}`ex0Ie={6uce;k4Ou_rW zmXKD6GN8Q?G1Y2J<0b3mrl}V(IsY4}$V%WWAKbHaS8-~-lc`mJD}g-otC`fe9wE|$ z-On8JWH6P)u9Vv@H#Zdwn0Z<=t%lH+cX9f~h=Qx>n67rTvt%MJS72Gnb%IG&{JF#x zt*OEFX}Msj+C!MlvM)D60LkiO5}f3r$i;3+Q?#RE7zQS=PBsFJC|>C$hMqPlbMF12q(jGs;nks)DL6LQ&$`3}xv z@Gow-T`{62;k0V3H~D%KK7S4_Wx+8wFU;EyTszefrAx%rFAnu``)K~>de&ymUY(Dp z761N$`$t{|&G#t<#LovT8lJ>;j8Z8nRo5YWK{pv6|D3&#DU|TAmkaiKm8XPEij>zC z@|j=>NCX5Ckx3eYtjPE&rjyO9Cl5V(wu^(utZv(O@BuhX!)7Cf9$jN+scVkzc-kKP z+Tj>UMUpzxRjY&B&SukPuhu_e%wAQya_d*U315FHe?j^ZzU^MCt$4zO({yOop=%`= znm}cozNF^06?ItDQ2vUSQY;-?xq&$v-hBW^^$ca77#eXYvhMn(tAyd&8-LIW?dFp- zSs!5Kv;>cmw%^ox?LYQ!Yvd_WS}VB-e7}yIsW1-uzX1ON0R2YwYe9w%(gR?o4v-Gc zOeoBP;V**12ws`nrz#;f+JmLD$I_`@+Xv%+VXN1t zBK=4{WYqY znlP;ZY9z)(Q4}<3+qc{aw(ZaZ%hzl`k>7-(xeGQ~mCm^`?B9xF`#&hSJVdBAPG~8ZswU&$ZY0-;gK79- zXeA&b=m8(dFcAm@;Pd$qpO_GwiEdAKP*oM0rrDXkvJ)0KuDa#l$0*_hPrbp$e6yHs z*t{K@DS^v&*xxV=kY&K(a3C`?6S=v$_AtgWphygQzz>N7maf4vb-0}}ew+KnX_i_!HjIkiW`t*7$!l}b@&4TfHP=@4wC>29J)#H`~3(60)S~i3-}Qi6NQqZ cyzox{AB#gd)_c|8d;kCd07*qoM6N<$f^`f4J^%m! literal 0 HcmV?d00001 diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/frontend/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/frontend/src/components/AccountPopover.tsx b/frontend/src/components/AccountPopover.tsx new file mode 100644 index 0000000..4367600 --- /dev/null +++ b/frontend/src/components/AccountPopover.tsx @@ -0,0 +1,197 @@ +import { useEffect, useState } from 'react' +import { Form, Input, Popover, message } from 'antd' +import { LockOutlined, PoweroffOutlined, UserOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../api/client' +import Modal from './DraggableModal' + +export type AccountProfile = { + name: string + userid: string + staffPermission: string + orgLabel: string + memberCount: number + expireDate: string + remainDays: number +} + +type Props = { + onLogout: () => void +} + +function formatExpireKo(iso: string) { + const [y, m, d] = iso.split('-') + if (!y || !m || !d) return iso + return `${y}년 ${Number(m)}월 ${Number(d)}일` +} + +export default function AccountPopover({ onLogout }: Props) { + const [open, setOpen] = useState(false) + const [profile, setProfile] = useState(null) + const [pwOpen, setPwOpen] = useState(false) + const [pwLoading, setPwLoading] = useState(false) + const [form] = Form.useForm() + + useEffect(() => { + if (!open) return + let cancelled = false + client + .get>(`${apiPrefix()}/homes/account-profile`) + .then(({ data }) => { + if (!cancelled) setProfile(unwrap(data)) + }) + .catch(() => { + if (cancelled) return + setProfile({ + name: localStorage.getItem('userName') || '사용자', + userid: localStorage.getItem('userid') || '', + staffPermission: localStorage.getItem('staffPermission') || '', + orgLabel: '데모', + memberCount: 0, + expireDate: '2030-12-31', + remainDays: 0, + }) + }) + return () => { cancelled = true } + }, [open]) + + const openPassword = () => { + setOpen(false) + form.resetFields() + setPwOpen(true) + } + + const submitPassword = async () => { + try { + const values = await form.validateFields() + setPwLoading(true) + const { data } = await client.put(`${apiPrefix()}/homes/password`, { + currentPassword: values.currentPassword, + newPassword: values.newPassword, + }) + unwrap(data) + message.success('비밀번호를 변경했습니다.') + setPwOpen(false) + form.resetFields() + } catch (e) { + if (e && typeof e === 'object' && 'errorFields' in e) return + message.error(e instanceof Error ? e.message : '비밀번호 변경에 실패했습니다.') + } finally { + setPwLoading(false) + } + } + + const p = profile + const remain = p?.remainDays ?? 0 + const remainText = remain >= 0 ? `${remain}일 남았습니다.` : `${Math.abs(remain)}일 지났습니다.` + + const content = ( +
+
+
{p?.name || '…'}
+
+ {p ? `${p.orgLabel} / ${p.staffPermission}` : '…'} +
+
+
+
+ 계정 + {p?.userid || '…'} +
+
+ 직원 + {p ? `${p.memberCount}명` : '…'} +
+
+
+
이용만료일
+
+ {p ? formatExpireKo(p.expireDate) : '…'} +
+
{p ? remainText : ''}
+
+
+ + +
+
+ ) + + return ( + <> + + + + + setPwOpen(false)} + onOk={() => void submitPassword()} + okText="변경" + cancelText="취소" + confirmLoading={pwLoading} + destroyOnClose + width={400} + > +
+ + + + + + + ({ + validator(_, value) { + if (!value || getFieldValue('newPassword') === value) return Promise.resolve() + return Promise.reject(new Error('새 비밀번호 확인이 일치하지 않습니다.')) + }, + }), + ]} + > + + +
+
+ + ) +} diff --git a/frontend/src/components/AgentShell.tsx b/frontend/src/components/AgentShell.tsx new file mode 100644 index 0000000..a4e52d5 --- /dev/null +++ b/frontend/src/components/AgentShell.tsx @@ -0,0 +1,261 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom' +import { + AppstoreOutlined, CalendarOutlined, ShoppingCartOutlined, +} from '@ant-design/icons' +import SchedulePanel from './SchedulePanel' +import BillCareLogo from './BillCareLogo' +import AccountPopover from './AccountPopover' +import { careMenuSections, filterMenuSections, resolveSelectedKey, sectionContainsPath, shopMenuSections } from '../menu' +import { fetchMyMenus } from '../api/auth' +import { THEMES } from '../theme' +import { useTheme } from '../themeContext' + +const PANEL_MIN = 170 +const PANEL_MAX = 420 + +type Props = { + shop?: boolean +} + +export default function AgentShell({ shop = false }: Props) { + const navigate = useNavigate() + const location = useLocation() + const { theme, changeTheme } = useTheme() + const [scheduleOpen, setScheduleOpen] = useState(false) + const userName = localStorage.getItem('userName') || (shop ? '판매점' : '관리자') + const [allowedMenus, setAllowedMenus] = useState(() => { + if (shop) return null + try { + const cached = localStorage.getItem('allowedMenus') + return cached ? (JSON.parse(cached) as string[]) : [] + } catch { + return [] + } + }) + + const [panelWidth, setPanelWidth] = useState(() => { + const v = Number(localStorage.getItem('lp.width')) + return v >= PANEL_MIN && v <= PANEL_MAX ? v : 220 + }) + const [collapsed, setCollapsed] = useState(() => localStorage.getItem('lp.collapsed') === '1') + const draggingRef = useRef(false) + const [dragging, setDragging] = useState(false) + + useEffect(() => { + if (shop) { + setAllowedMenus(null) + return + } + let cancelled = false + fetchMyMenus() + .then((res) => { + if (cancelled) return + if (res.staffPermission) localStorage.setItem('staffPermission', res.staffPermission) + setAllowedMenus(res.menus || []) + try { + localStorage.setItem('allowedMenus', JSON.stringify(res.menus || [])) + } catch { /* ignore */ } + }) + .catch(() => { + if (cancelled) return + try { + const cached = localStorage.getItem('allowedMenus') + if (cached) setAllowedMenus(JSON.parse(cached) as string[]) + else setAllowedMenus(['home']) + } catch { + setAllowedMenus(['home']) + } + }) + return () => { cancelled = true } + }, [shop]) + + const allSections = useMemo(() => (shop ? shopMenuSections() : careMenuSections()), [shop]) + const sections = useMemo( + () => (shop ? allSections : filterMenuSections(allSections, allowedMenus)), + [shop, allSections, allowedMenus], + ) + const selectedKey = resolveSelectedKey(location.pathname) + + const [openCards, setOpenCards] = useState>({}) + + useEffect(() => { + setOpenCards((prev) => { + const next = { ...prev } + sections.forEach((sec) => { + if (next[sec.id] === undefined) { + next[sec.id] = sectionContainsPath(sec, location.pathname) || sec.items.length <= 1 + } + if (sectionContainsPath(sec, location.pathname)) next[sec.id] = true + }) + return next + }) + }, [location.pathname, sections]) + + useEffect(() => { + localStorage.setItem('lp.width', String(panelWidth)) + }, [panelWidth]) + useEffect(() => { + localStorage.setItem('lp.collapsed', collapsed ? '1' : '0') + }, [collapsed]) + + const startResize = useCallback((e: React.MouseEvent) => { + e.preventDefault() + draggingRef.current = true + setDragging(true) + document.body.style.userSelect = 'none' + document.body.style.cursor = 'col-resize' + }, []) + + useEffect(() => { + const onMove = (e: MouseEvent) => { + if (!draggingRef.current) return + setPanelWidth(Math.min(PANEL_MAX, Math.max(PANEL_MIN, e.clientX))) + } + const onUp = () => { + if (!draggingRef.current) return + draggingRef.current = false + setDragging(false) + document.body.style.userSelect = '' + document.body.style.cursor = '' + } + window.addEventListener('mousemove', onMove) + window.addEventListener('mouseup', onUp) + return () => { + window.removeEventListener('mousemove', onMove) + window.removeEventListener('mouseup', onUp) + } + }, []) + + const logout = () => { + localStorage.clear() + navigate('/login') + } + + const toggleCard = (id: string) => { + setOpenCards((prev) => ({ ...prev, [id]: !prev[id] })) + } + + return ( +
+
+
+ {!collapsed && ( +
+ +
+ )} +
+
+
+ {shop ? ( + 에이전트 데모 / {userName} + ) : ( +
+ [ BillCare ] + + + 문자발송(장바구니) + 잔여SMS 996 +
+ )} +
+
+ {!shop && ( + <> + + + + + )} + +
+
+
+ +
+ {!collapsed && ( + + )} + + {!collapsed && ( +
+ )} + + + +
+
+ +
+
+
+ + {!shop && setScheduleOpen(false)} />} +
+ ) +} diff --git a/frontend/src/components/BillCareLogo.tsx b/frontend/src/components/BillCareLogo.tsx new file mode 100644 index 0000000..12f8ffb --- /dev/null +++ b/frontend/src/components/BillCareLogo.tsx @@ -0,0 +1,64 @@ +import logoImage from '../assets/logo_image.png' +import logoMark from '../assets/logo_mark_light.png' +import { useTheme } from '../themeContext' +import type { Theme } from '../theme' + +interface BillCareLogoProps { + className?: string + title?: string + size?: 'header' | 'login' + forceTheme?: Theme +} + +function isDarkTheme(theme: Theme) { + return theme === 'dark' || theme === 'blue' +} + +/** + * logo_image.png — 라이트 헤더 가로 로고 + * logo_mark_light.png / logo_image_theme.png — 마크 (로그인·다크 헤더) + */ +export default function BillCareLogo({ + className = '', + title = 'BillCare', + size = 'header', + forceTheme, +}: BillCareLogoProps) { + const { theme } = useTheme() + const active = forceTheme ?? theme + const dark = isDarkTheme(active) + const root = ['billcare-logo', `billcare-logo--${size}`, `billcare-logo--${active}`, className] + .filter(Boolean) + .join(' ') + + // 로그인: 항상 마크 + BillCare (색상은 CSS로 고정) + if (size === 'login') { + return ( +
+ + {title} +
+ ) + } + + // 라이트 헤더: 가로 로고 + if (!dark) { + return ( + {title} + ) + } + + // 다크/블루 헤더: 마크 + BillCare + return ( +
+ + {title} +
+ ) +} diff --git a/frontend/src/components/CareTable.tsx b/frontend/src/components/CareTable.tsx new file mode 100644 index 0000000..0952da9 --- /dev/null +++ b/frontend/src/components/CareTable.tsx @@ -0,0 +1,8 @@ +import { Table, type TableProps } from 'antd' +import { normalizeTableColumns } from '../tableAlign' + +/** Ant Design Table 래퍼 — 목록 정렬 규칙(헤더/텍스트 중앙, 숫자·금액 우측) 적용 */ +export default function CareTable(props: TableProps) { + const { columns, ...rest } = props + return {...rest} columns={normalizeTableColumns(columns)} /> +} diff --git a/frontend/src/components/DraggableModal.tsx b/frontend/src/components/DraggableModal.tsx new file mode 100644 index 0000000..746a42b --- /dev/null +++ b/frontend/src/components/DraggableModal.tsx @@ -0,0 +1,310 @@ +import { + useCallback, + useEffect, + useRef, + useState, + type MouseEvent as ReactMouseEvent, + type ReactNode, +} from 'react' +import { Modal as AntModal, type ModalFuncProps, type ModalProps } from 'antd' + +export type DraggableModalProps = ModalProps & { + /** 타이틀바 우측(닫기 앞) 추가 액션 */ + titleActions?: ReactNode + /** 타이틀바 드래그 (기본 true) */ + draggable?: boolean +} + +type DragState = { + startX: number + startY: number + origX: number + origY: number +} + +function useDraggableOffset(open?: boolean) { + const [offset, setOffset] = useState({ x: 0, y: 0 }) + const offsetRef = useRef(offset) + const dragRef = useRef(null) + /** 드래그 후 mask 합성 click 으로 닫히는 것 방지 */ + const suppressCloseUntilRef = useRef(0) + + useEffect(() => { + offsetRef.current = offset + }, [offset]) + + useEffect(() => { + if (!open) { + setOffset({ x: 0, y: 0 }) + offsetRef.current = { x: 0, y: 0 } + dragRef.current = null + suppressCloseUntilRef.current = 0 + } + }, [open]) + + const onTitleMouseDown = useCallback((e: ReactMouseEvent) => { + if (e.button !== 0) return + const target = e.target as HTMLElement | null + if (target?.closest('button, a, input, select, textarea, .app-modal-close, .ant-btn')) return + + e.preventDefault() + e.stopPropagation() + + const current = offsetRef.current + dragRef.current = { + startX: e.clientX, + startY: e.clientY, + origX: current.x, + origY: current.y, + } + let moved = false + + const onMove = (ev: MouseEvent) => { + const d = dragRef.current + if (!d) return + const dx = ev.clientX - d.startX + const dy = ev.clientY - d.startY + if (Math.abs(dx) > 2 || Math.abs(dy) > 2) moved = true + setOffset({ x: d.origX + dx, y: d.origY + dy }) + } + + const blockSyntheticClick = (ev: MouseEvent) => { + if (Date.now() < suppressCloseUntilRef.current) { + ev.preventDefault() + ev.stopPropagation() + } + } + + const onUp = () => { + dragRef.current = null + // mousedown(헤더) → mouseup(마스크) 시 wrap 에 click 이 가서 닫히는 문제 방지 + suppressCloseUntilRef.current = Date.now() + (moved ? 300 : 120) + window.removeEventListener('mousemove', onMove, true) + window.removeEventListener('mouseup', onUp, true) + window.setTimeout(() => { + window.removeEventListener('click', blockSyntheticClick, true) + }, 320) + } + + window.addEventListener('mousemove', onMove, true) + window.addEventListener('mouseup', onUp, true) + window.addEventListener('click', blockSyntheticClick, true) + }, []) + + const shouldSuppressClose = useCallback(() => Date.now() < suppressCloseUntilRef.current, []) + + return { offset, onTitleMouseDown, shouldSuppressClose } +} + +function withModalClass(className?: string) { + return ['app-draggable-modal', className].filter(Boolean).join(' ') +} + +function TitleBar({ + title, + titleActions, + draggable, + showClose, + onClose, + onMouseDown, +}: { + title?: ReactNode + titleActions?: ReactNode + draggable: boolean + showClose: boolean + onClose?: (e: ReactMouseEvent) => void + onMouseDown: (e: ReactMouseEvent) => void +}) { + return ( +
+
{title}
+
+ {titleActions} + {showClose && ( + + )} +
+
+ ) +} + +function DraggableModal({ + className, + rootClassName, + modalRender, + open, + title, + titleActions, + draggable = true, + centered = false, + closable = true, + onCancel, + classNames, + styles, + ...rest +}: DraggableModalProps) { + const { offset, onTitleMouseDown, shouldSuppressClose } = useDraggableOffset(open) + const showClose = closable !== false + + const handleCancel: ModalProps['onCancel'] = (e) => { + if (shouldSuppressClose()) return + onCancel?.(e) + } + + const baseStyles = { + container: { + padding: 0, + overflow: 'hidden' as const, + borderRadius: 8, + border: '1px solid var(--border)', + background: 'var(--surface)', + boxShadow: 'var(--shadow)', + }, + header: { + margin: 0, + padding: 0, + background: 'transparent', + borderBottom: 'none', + }, + body: { + padding: '16px 18px 18px', + background: 'var(--surface)', + color: 'var(--text)', + }, + footer: { + margin: 0, + padding: '12px 16px 14px', + borderTop: '1px solid var(--border)', + background: 'var(--surface)', + }, + } + + return ( + onCancel?.(e)} + onMouseDown={onTitleMouseDown} + /> + )} + modalRender={(node) => { + const wrapped = ( +
e.stopPropagation()} + > + {node} +
+ ) + return modalRender ? modalRender(wrapped) : wrapped + }} + /> + ) +} + +function StaticDragShell({ + title, + children, + onTitleMouseDown, + offset, +}: { + title?: ReactNode + children: ReactNode + onTitleMouseDown: (e: ReactMouseEvent) => void + offset: { x: number; y: number } +}) { + return ( +
e.stopPropagation()} + > +
+ +
{children}
+
+
+ ) +} + +function StaticDragWrap({ title, children }: { title?: ReactNode; children: ReactNode }) { + const [open] = useState(true) + const { offset, onTitleMouseDown, shouldSuppressClose } = useDraggableOffset(open) + + useEffect(() => { + const block = (ev: MouseEvent) => { + if (shouldSuppressClose()) { + ev.preventDefault() + ev.stopPropagation() + } + } + window.addEventListener('click', block, true) + return () => window.removeEventListener('click', block, true) + }, [shouldSuppressClose]) + + return ( + + {children} + + ) +} + +function wrapStatic(config: ModalFuncProps = {}): ModalFuncProps { + const userRender = config.modalRender + const title = config.title + return { + ...config, + title: null, + closable: false, + centered: config.centered ?? false, + className: withModalClass(config.className), + rootClassName: ['app-draggable-modal-root', config.rootClassName].filter(Boolean).join(' '), + modalRender: (node) => { + const wrapped = {node} + return userRender ? userRender(wrapped) : wrapped + }, + } +} + +DraggableModal.confirm = (props?: ModalFuncProps) => AntModal.confirm(wrapStatic(props)) +DraggableModal.info = (props?: ModalFuncProps) => AntModal.info(wrapStatic(props)) +DraggableModal.success = (props?: ModalFuncProps) => AntModal.success(wrapStatic(props)) +DraggableModal.error = (props?: ModalFuncProps) => AntModal.error(wrapStatic(props)) +DraggableModal.warning = (props?: ModalFuncProps) => AntModal.warning(wrapStatic(props)) +DraggableModal.warn = (props?: ModalFuncProps) => AntModal.warn(wrapStatic(props)) +DraggableModal.destroyAll = AntModal.destroyAll +DraggableModal.useModal = AntModal.useModal +DraggableModal.config = AntModal.config + +export default DraggableModal +export type { ModalProps } diff --git a/frontend/src/components/OrderFormLayout.tsx b/frontend/src/components/OrderFormLayout.tsx new file mode 100644 index 0000000..917f78e --- /dev/null +++ b/frontend/src/components/OrderFormLayout.tsx @@ -0,0 +1,92 @@ +import type { ReactNode } from 'react' + +/** 카테고리 섹션 (billing_react OrderFormLayout 이식) */ +export function FormSection({ + title, + desc, + actions, + children, +}: { + title: string + desc?: string + actions?: ReactNode + children: ReactNode +}) { + return ( +
+
+
+

{title}

+ {desc ?

{desc}

: null} +
+ {actions ?
{actions}
: null} +
+
{children}
+
+ ) +} + +export function FormGroup({ label, children }: { label?: string; children: ReactNode }) { + return ( +
+ {label ?
{label}
: null} + {children} +
+ ) +} + +export function FormGrid({ children, cols = 2 }: { children: ReactNode; cols?: 1 | 2 }) { + return
{children}
+} + +export function FormField({ + label, + hint, + full, + children, +}: { + label: string + hint?: string + full?: boolean + children: ReactNode +}) { + return ( +
+
+ {label} + {hint ? {hint} : null} +
+
{children}
+
+ ) +} + +export function FormChoiceRow({ children }: { children: ReactNode }) { + return
{children}
+} + +export function FormChoice({ children }: { children: ReactNode }) { + return +} + +export function FormBlock({ + title, + actions, + children, +}: { + title?: string + actions?: ReactNode + children: ReactNode +}) { + return ( +
+ {(title || actions) && ( +
+ {title ? {title} : null} + {actions ?
{actions}
: null} +
+ )} + {children} +
+ ) +} diff --git a/frontend/src/components/PrgSection.tsx b/frontend/src/components/PrgSection.tsx new file mode 100644 index 0000000..8d5a654 --- /dev/null +++ b/frontend/src/components/PrgSection.tsx @@ -0,0 +1,24 @@ +import type { ReactNode } from 'react' + +/** 업무진행 팝업 섹션 카드 (billing_react PrgSection) */ +export default function PrgSection({ + title, + extra, + children, +}: { + title?: ReactNode + extra?: ReactNode + children: ReactNode +}) { + return ( +
+ {(title || extra) && ( +
+

{title}

+ {extra ?
{extra}
: null} +
+ )} +
{children}
+
+ ) +} diff --git a/frontend/src/components/ReceptionApplyModal.tsx b/frontend/src/components/ReceptionApplyModal.tsx new file mode 100644 index 0000000..3a21700 --- /dev/null +++ b/frontend/src/components/ReceptionApplyModal.tsx @@ -0,0 +1,794 @@ +import { useEffect, useMemo, useState } from 'react' +import { useQuery } from '@tanstack/react-query' +import dayjs from 'dayjs' +import { + Button, + Checkbox, + DatePicker, + Form, + Input, + InputNumber, + Radio, + Select, + message, +} from 'antd' +import Modal from './DraggableModal' +import { + FormBlock, + FormField, + FormGrid, + FormGroup, + FormSection, +} from './OrderFormLayout' +import client, { apiPrefix, unwrap, type ApiResponse } from '../api/client' +import { options } from '../pages/care/homesShared' + +type Isp = { id: number; name: string; telecom: string; isSktb?: boolean } +type Named = { id: number; name: string; type?: string } + +type Props = { + open: boolean + isp: Isp | null + counselorOptions?: string[] + branchOptions?: string[] + onCancel: () => void + onSuccess: () => void +} + +const PRODUCT_TYPES = [ + { value: 'e01', label: '인터넷' }, + { value: 'e02', label: '전화' }, + { value: 'e03', label: 'TV' }, + { value: 'e04', label: '렌탈' }, +] + +const GIFT_TYPES = [ + { value: 'a01', label: '없음' }, + { value: 'a02', label: '현금' }, + { value: 'a03', label: '상품' }, + { value: 'a04', label: '현금+상품' }, +] + +const BANKS = [ + '국민은행', '우리은행', '신한은행', '하나은행', '기업은행', '농협', + 'SC제일', '카카오뱅크', '토스뱅크', '새마을금고', '우체국', '기타', +] + +const CARD_COMPANIES = [ + '삼성카드', '신한카드', '현대카드', 'KB국민카드', '롯데카드', + '우리카드', '하나카드', 'BC카드', 'NH농협카드', '기타', +] + +const CARD_YEARS = Array.from({ length: 31 }, (_, i) => String(i).padStart(2, '0')) +const CARD_MONTHS = Array.from({ length: 12 }, (_, i) => String(i + 1).padStart(2, '0')) + +const DISCOUNTS = [ + { value: 'none', label: '해당없음' }, + { value: '국가유공자', label: '국가유공자(30% 할인)' }, + { value: '장애인', label: '장애인(30% 할인)' }, +] + +export default function ReceptionApplyModal({ + open, + isp, + counselorOptions = [], + branchOptions = [], + onCancel, + onSuccess, +}: Props) { + const [form] = Form.useForm() + const [saving, setSaving] = useState(false) + const [createFiles, setCreateFiles] = useState>({}) + const ispId = isp?.id + const staffPermission = localStorage.getItem('staffPermission') || '' + const isAdmin = ['대표', '총괄', '팀장', '개발자'].includes(staffPermission) + + const productsQuery = useQuery({ + queryKey: ['reception-isp-products', ispId], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/reception-isps/${ispId}/products`) + return unwrap(data) + }, + enabled: open && ispId != null, + }) + + const agreementsQuery = useQuery({ + queryKey: ['reception-isp-agreements', ispId], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/reception-isps/${ispId}/agreements`) + return unwrap(data) + }, + enabled: open && ispId != null, + }) + + const customerType = Form.useWatch('customerType', form) || 'public' + const paymentMethod = Form.useWatch('paymentMethod', form) || 'transfer' + const productType = Form.useWatch('productType', form) + const giftType = Form.useWatch('giftType', form) || 'a01' + const isIssueDate = Form.useWatch('isIssueDate', form) ?? '1' + const sameCustomerToPayment = Form.useWatch('sameCustomerToPayment', form) + const sameCustomerToGift = Form.useWatch('sameCustomerToGift', form) + const samePaymentToGift = Form.useWatch('samePaymentToGift', form) + const watchName = Form.useWatch('name', form) + const watchSsid = Form.useWatch('ssid', form) + const watchPhone = Form.useWatch('phone', form) + const watchTel = Form.useWatch('tel', form) + const watchBank = Form.useWatch('bank', form) + const watchAccountNumber = Form.useWatch('accountNumber', form) + const watchPaymentName = Form.useWatch('paymentName', form) + const watchPaymentSsid = Form.useWatch('paymentSsid', form) + + const filteredProducts = useMemo(() => { + const list = productsQuery.data || [] + if (!productType) return list + return list.filter((p) => !p.type || p.type === productType) + }, [productsQuery.data, productType]) + + const giftCash = giftType === 'a02' || giftType === 'a04' + const giftGoods = giftType === 'a03' || giftType === 'a04' + + const branchEmployeeOptions = useMemo(() => { + const counselors = counselorOptions.length ? counselorOptions : ['홍길동', '김상담', '이상담', '정상담'] + const branches = branchOptions.length ? branchOptions : ['강남점', '서초점', '송파점'] + const out: { value: string; label: string }[] = [] + for (const b of branches) { + for (const c of counselors) { + out.push({ value: `${b}|${c}`, label: `[${b}] ${c}` }) + } + } + return out + }, [counselorOptions, branchOptions]) + + useEffect(() => { + if (!open || !isp) return + setCreateFiles({}) + const rental = isp.telecom === 'SK매직' || isp.telecom === 'LG렌탈' + const firstOpt = branchEmployeeOptions[0]?.value + form.setFieldsValue({ + branchEmployee: firstOpt, + customerType: 'public', + paymentMethod: 'transfer', + productType: rental ? 'e04' : 'e01', + productId: undefined, + agreementId: undefined, + price: rental ? 120000 : 80000, + installDate: dayjs(), + discount: 'none', + giftType: 'a01', + giftPlace: 'b01', + giftDate: dayjs(), + cashAmount: 0, + goods: '', + goodsPrice: 0, + name: '', + postIt: '', + postAdd: '', + ssid: '', + isIssueDate: '1', + issueDate: undefined, + driversLicenseNumber: '', + phone: '', + tel: '', + email: '', + zipcode: '', + address: '', + addressDetail: '', + blName: '', + blNumber: '', + blCorpNumber: '', + blType: '', + blItem: '', + blZipcode: '', + blAddress: '', + blAddressDetail: '', + paymentName: '', + paymentSsid: '', + paymentTel: '', + bank: undefined, + accountNumber: '', + cardCompany: undefined, + cardYear: undefined, + cardMonth: undefined, + cardNumber: '', + giftBank: undefined, + giftAccountNumber: '', + giftAccountName: '', + giftSsid: '', + sameCustomerToPayment: false, + sameCustomerToGift: false, + samePaymentToGift: false, + memo: '', + }) + }, [open, isp, form, branchEmployeeOptions]) + + useEffect(() => { + if (!sameCustomerToPayment) return + form.setFieldsValue({ + paymentName: watchName, + paymentSsid: watchSsid, + paymentTel: watchPhone || watchTel, + }) + }, [sameCustomerToPayment, watchName, watchSsid, watchPhone, watchTel, form]) + + useEffect(() => { + if (!sameCustomerToGift) return + form.setFieldsValue({ + giftAccountName: watchName, + giftSsid: watchSsid, + }) + }, [sameCustomerToGift, watchName, watchSsid, form]) + + useEffect(() => { + if (!samePaymentToGift) return + form.setFieldsValue({ + giftBank: watchBank, + giftAccountNumber: watchAccountNumber, + giftAccountName: watchPaymentName || watchName, + giftSsid: watchPaymentSsid || watchSsid, + }) + }, [samePaymentToGift, watchBank, watchAccountNumber, watchPaymentName, watchPaymentSsid, watchName, watchSsid, form]) + + const searchAddress = (prefix: '' | 'bl') => { + message.info('주소검색(데모) — 직접 입력해 주세요.') + if (prefix === 'bl') { + form.setFieldsValue({ + blZipcode: form.getFieldValue('blZipcode') || '06236', + blAddress: form.getFieldValue('blAddress') || '서울특별시 강남구 테헤란로', + }) + } else { + form.setFieldsValue({ + zipcode: form.getFieldValue('zipcode') || '06236', + address: form.getFieldValue('address') || '서울특별시 강남구 테헤란로', + }) + } + } + + const handleFinish = async (values: Record) => { + if (!isp) return + setSaving(true) + try { + const be = String(values.branchEmployee || '') + const [branch, counselor] = be.includes('|') ? be.split('|') : [values.branch, values.counselor] + const addrParts = [values.zipcode, values.address, values.addressDetail].filter(Boolean) + const fullAddress = addrParts.join(' ') + const memoExtra: string[] = [] + if (values.postAdd) memoExtra.push(`추가메모: ${values.postAdd}`) + if (values.email) memoExtra.push(`이메일: ${values.email}`) + if (values.tel) memoExtra.push(`일반전화: ${values.tel}`) + if (values.paymentName) memoExtra.push(`납입자: ${values.paymentName}`) + if (values.bank) memoExtra.push(`은행: ${values.bank} ${values.accountNumber || ''}`) + if (values.cardCompany) memoExtra.push(`카드: ${values.cardCompany} ${values.cardNumber || ''}`) + if (values.memo) memoExtra.push(String(values.memo)) + + const { data } = await client.post>(`${apiPrefix()}/homes/receptions/apply`, { + ispId: isp.id, + counselor: counselor || counselorOptions[0] || '홍길동', + branch: branch || branchOptions[0] || '', + memo: memoExtra.join('\n'), + customer: { + type: values.customerType, + name: values.name, + phone: values.phone, + tel: values.tel, + ssid: values.ssid, + email: values.email, + postIt: values.postIt, + postAdd: values.postAdd, + zipcode: values.zipcode, + address: fullAddress || values.address, + addressDetail: values.addressDetail, + issueDate: values.issueDate ? dayjs(values.issueDate as dayjs.Dayjs).format('YYYY-MM-DD') : undefined, + driversLicenseNumber: values.driversLicenseNumber, + blName: values.blName, + blNumber: values.blNumber, + }, + payment: { + method: values.paymentMethod, + name: values.paymentName, + ssid: values.paymentSsid, + tel: values.paymentTel, + bank: values.bank, + accountNumber: values.accountNumber, + cardCompany: values.cardCompany, + cardYear: values.cardYear, + cardMonth: values.cardMonth, + cardNumber: values.cardNumber, + }, + product: { + productType: values.productType, + productId: values.productId, + agreementId: values.agreementId, + price: values.price, + installDate: values.installDate ? dayjs(values.installDate as dayjs.Dayjs).format('YYYY-MM-DD') : undefined, + discount: values.discount === 'none' ? '' : values.discount, + }, + gift: { + type: values.giftType, + place: values.giftPlace, + cashAmount: values.cashAmount, + goods: values.goods, + goodsPrice: values.goodsPrice, + giftDate: values.giftDate ? dayjs(values.giftDate as dayjs.Dayjs).format('YYYY-MM-DD') : undefined, + accountBank: values.giftBank, + accountNumber: values.giftAccountNumber, + accountName: values.giftAccountName, + ssid: values.giftSsid, + }, + }) + const created = unwrap(data) as { id: number } + await uploadCreateAttachments(created.id) + message.success(`[${isp.name}] 신규신청을 등록했습니다.`) + onSuccess() + } catch { + message.error('신규신청 등록에 실패했습니다.') + } finally { + setSaving(false) + } + } + + const uploadCreateAttachments = async (receptionId: number) => { + for (const slot of [1, 2, 3, 4, 5] as const) { + const file = createFiles[`file${slot}`] + if (!file) continue + const fd = new FormData() + fd.append('file', file) + await client.post(`${apiPrefix()}/homes/receptions/${receptionId}/file/${slot}`, fd) + } + if (isAdmin) { + for (const slot of [1, 2, 3] as const) { + const file = createFiles[`adminFile${slot}`] + if (!file) continue + const fd = new FormData() + fd.append('file', file) + await client.post(`${apiPrefix()}/homes/receptions/${receptionId}/admin-file/${slot}`, fd) + } + } + } + + const buildMemoAuto = () => { + const v = form.getFieldsValue() + const giftLabel = GIFT_TYPES.find((g) => g.value === v.giftType)?.label || '' + const productLabel = PRODUCT_TYPES.find((p) => p.value === v.productType)?.label || '' + const productName = (productsQuery.data || []).find((p) => p.id === v.productId)?.name || '' + const agreementName = (agreementsQuery.data || []).find((a) => a.id === v.agreementId)?.name || '' + let content = '' + content += `◈ 고객명 : ${v.name || ''}\n` + content += `◈ 연락처 : ${v.phone || ''}\n` + content += `◈ 상품 : ${productLabel} / ${productName}\n` + content += `◈ 약정 : ${agreementName}\n` + content += `◈ 사은품 : ${giftLabel}` + if (v.giftType && v.giftType !== 'a01') { + const parts: string[] = [] + if (v.giftType === 'a02' || v.giftType === 'a04') parts.push(`현금(${v.cashAmount || 0})`) + if (v.giftType === 'a03' || v.giftType === 'a04') parts.push(`${v.goods || ''}(${v.goodsPrice || 0})`) + if (parts.length) content += ` / ${parts.join(', ')}` + } + content += '\n' + content += `◈ 납입자 : ${v.paymentName || ''}\n` + content += `◈ 주소 : ${[v.zipcode, v.address, v.addressDetail].filter(Boolean).join(' ')}\n` + return content + } + + const issueLabel = customerType === 'foreigner' ? '외국인등록증 발급일자' : '주민등록증 발급일자' + const ssidLabel = customerType === 'foreigner' ? '외국인등록번호' : '주민등록번호' + const addrLabel = customerType === 'foreigner' ? '설치주소' : '주소' + + const addrStack = (prefix: '' | 'bl') => ( +
+
+ + + + +
+ + + + + + +
+ ) + + return ( + +
+ + + + + + + +

등록된 인증 링크가 없습니다.

+
+
+
+ + + + + + + 개인 + 사업자 + 외국인 + + + + + + {customerType === 'company' ? ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + {addrStack('bl')} + + + + + + + + + + + + {addrStack('')} + + ) : ( + + + + + + + + + + + + + + + + + + +
+ + + + )} +
+
+ + + + + + + + + + + + {addrStack('')} +
+ )} +
+ + + + + + + 자동이체 + 신용카드 + 지로청구 + 전화번호요금통합 + + + + + + {(paymentMethod === 'transfer' || paymentMethod === 'card') && ( + + +
+ + + + + 고객정보와 동일 + +
+
+ + + + + + + {paymentMethod === 'transfer' ? ( + <> + + + + + + ) : ( + <> + +
+ + ({ value: y, label: y }))} style={{ width: 72 }} /> + + + + + + + )} + + )} + + + + + + + + ({ value: p.id, label: p.name }))} + placeholder="선택" + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 본사요청 + 자체발송 + + + + +
+ + + + +
+ + + + + 고객정보와 동일 + +
+
+ + + + + + + + + +
+ + +
+ + + +
+ {[1, 2, 3, 4, 5].map((slot) => ( + +
+ { + const f = e.target.files?.[0] || null + setCreateFiles((prev) => ({ ...prev, [`file${slot}`]: f })) + }} + /> + {createFiles[`file${slot}`] ? ( + {createFiles[`file${slot}`]?.name} + ) : null} +
+
+ ))} + {isAdmin + ? [1, 2, 3].map((slot) => ( + +
+ { + const f = e.target.files?.[0] || null + setCreateFiles((prev) => ({ ...prev, [`adminFile${slot}`]: f })) + }} + /> + {createFiles[`adminFile${slot}`] ? ( + {createFiles[`adminFile${slot}`]?.name} + ) : null} +
+
+ )) + : null} +
+
+ +
+ + +
+ + + ) +} diff --git a/frontend/src/components/ReceptionBulkModal.tsx b/frontend/src/components/ReceptionBulkModal.tsx new file mode 100644 index 0000000..1d86a85 --- /dev/null +++ b/frontend/src/components/ReceptionBulkModal.tsx @@ -0,0 +1,290 @@ +import { useEffect, useState } from 'react' +import { Button, Checkbox, DatePicker, Input, Select, message } from 'antd' +import dayjs from 'dayjs' +import Modal from './DraggableModal' +import { FormField, FormGrid, FormSection } from './OrderFormLayout' +import client, { apiPrefix, unwrap, type ApiResponse } from '../api/client' +import { options } from '../pages/care/homesShared' + +type Props = { + open: boolean + selectedIds: number[] + progressStatuses: string[] + giftStatuses: string[] + centerOptions?: string[] + onCancel: () => void + onSuccess: () => void +} + +const GIFT_PLACES = [ + { value: 'b01', label: '본사요청' }, + { value: 'b02', label: '자체발송' }, + { value: '본사', label: '본사발송' }, + { value: '영업점', label: '영업점발송' }, +] + +export default function ReceptionBulkModal({ + open, + selectedIds, + progressStatuses, + giftStatuses, + centerOptions = [], + onCancel, + onSuccess, +}: Props) { + const [busy, setBusy] = useState(false) + + const [pgAdminOnly, setPgAdminOnly] = useState(false) + const [pgStatus, setPgStatus] = useState() + const [pgNotNotice, setPgNotNotice] = useState(false) + const [pgCenterId, setPgCenterId] = useState() + const [pgMemo, setPgMemo] = useState('') + const [pgOpended, setPgOpended] = useState('') + const [pgCanceled, setPgCanceled] = useState('') + + const [gPlace, setGPlace] = useState() + const [gStatus, setGStatus] = useState() + const [gProgressLog, setGProgressLog] = useState('') + const [gDeliveryCompany, setGDeliveryCompany] = useState('') + const [gTrackingNumber, setGTrackingNumber] = useState('') + const [gRequested, setGRequested] = useState('') + const [gSended, setGSended] = useState('') + + useEffect(() => { + if (!open) return + setPgAdminOnly(false) + setPgStatus(undefined) + setPgNotNotice(false) + setPgCenterId(undefined) + setPgMemo('') + setPgOpended('') + setPgCanceled('') + setGPlace(undefined) + setGStatus(undefined) + setGProgressLog('') + setGDeliveryCompany('') + setGTrackingNumber('') + setGRequested('') + setGSended('') + }, [open]) + + const need = () => { + if (!selectedIds.length) { + message.warning('대상 접수를 선택하세요.') + return false + } + return true + } + + const run = async (fn: () => Promise) => { + setBusy(true) + try { + await fn() + onSuccess() + } catch (e: unknown) { + message.error(e instanceof Error ? e.message : '일괄 처리에 실패했습니다.') + } finally { + setBusy(false) + } + } + + const runProgress = () => { + if (!need()) return + if (!pgStatus) { + message.warning('진행상황을 선택하세요.') + return + } + if (!window.confirm(`선택 ${selectedIds.length}건을 일괄 처리하시겠습니까?`)) return + void run(async () => { + const { data } = await client.post>(`${apiPrefix()}/homes/receptions/bulk-progress`, { + ids: selectedIds, + status: pgStatus, + memo: pgMemo, + centerId: pgCenterId || null, + opended: pgOpended || null, + canceled: pgCanceled || null, + adminOnly: pgAdminOnly, + notNotice: pgNotNotice, + }) + const res = unwrap(data) as { count: number } + message.success(`${res.count}건 진행상황을 변경했습니다.`) + }) + } + + const runGift = () => { + if (!need()) return + if (!gStatus) { + message.warning('사은품진행상황을 선택하세요.') + return + } + if (!window.confirm(`선택 ${selectedIds.length}건을 일괄 처리하시겠습니까?`)) return + void run(async () => { + const { data } = await client.post>(`${apiPrefix()}/homes/receptions/bulk-gift`, { + ids: selectedIds, + place: gPlace || null, + status: gStatus, + giftProgressLog: gProgressLog, + deliveryCompany: gDeliveryCompany || null, + trackingNumber: gTrackingNumber || null, + requested: gRequested || null, + sended: gSended || null, + }) + const res = unwrap(data) as { count: number } + message.success(`${res.count}건 사은품진행을 변경했습니다.`) + }) + } + + const runCenterWait = () => { + if (!need()) return + if (!window.confirm('입력처 정산대기로 일괄처리 하시겠습니까?')) return + void run(async () => { + const { data } = await client.post>(`${apiPrefix()}/homes/receptions/bulk-center-progress`, { + ids: selectedIds, + }) + const res = unwrap(data) as { count: number } + message.success(`${res.count}건을 입력처 정산대기로 변경했습니다.`) + }) + } + + return ( + +
+ + + +
+ setPgAdminOnly(e.target.checked)}> + 관리자 전용로그 + + ■ 관리자만 보이고 영업점은 보이지 않습니다. +
+
+ +
+ setPgCenterId(v)} + options={options(centerOptions.length ? centerOptions : ['본사'])} + /> + + + setPgMemo(e.target.value)} /> + + + setPgOpended(d ? d.format('YYYY-MM-DD') : '')} + style={{ width: '100%' }} + /> + + + setPgCanceled(d ? d.format('YYYY-MM-DD') : '')} + style={{ width: '100%' }} + /> + + +
+ +
+ + + + + + setGStatus(v)} + options={options(giftStatuses)} + /> + + + setGProgressLog(e.target.value)} /> + + + setGDeliveryCompany(e.target.value)} /> + + + setGTrackingNumber(e.target.value)} /> + + + setGRequested(d ? d.format('YYYY-MM-DD') : '')} + style={{ width: '100%' }} + /> + + + setGSended(d ? d.format('YYYY-MM-DD') : '')} + style={{ width: '100%' }} + /> + + +
+ +
+
+ + + + +

+ ■ 개통완료된 고객중에 입력처 정산이 진행안된 고객은 입력처 정산대기로 일괄처리 진행할수 있습니다. +

+
+
+
+ +
+
+
+ +
+ +
+ + ) +} diff --git a/frontend/src/components/ReceptionProgressModal.tsx b/frontend/src/components/ReceptionProgressModal.tsx new file mode 100644 index 0000000..728e7a0 --- /dev/null +++ b/frontend/src/components/ReceptionProgressModal.tsx @@ -0,0 +1,565 @@ +import { useEffect, useState } from 'react' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import dayjs from 'dayjs' +import { + Button, + Checkbox, + DatePicker, + Input, + Select, + Space, + message, +} from 'antd' +import Modal from './DraggableModal' +import PrgSection from './PrgSection' +import ReceptionProgressReadOnly from './ReceptionProgressReadOnly' +import client, { apiPrefix, unwrap, type ApiResponse } from '../api/client' + +type Props = { + open: boolean + receptionId: number | null + onClose: () => void + onChanged?: () => void + onEdit?: (id: number) => void + onCopyLine?: (ispId: number) => void +} + +type FormState = { + postIt: string + postAdd: string + status: string + adminOnly: boolean + contractNumber: string + installSchedule: string + customerLoginId: string + newTel: string + multiLine: boolean + addPayer: boolean + record: boolean + progressLog: string + adminMemo: string + openDate: string + cancelDate: string + giftStatus: string + giftPlace: string + giftProgressLog: string + giftRequested: string + giftSended: string + changeOrderDate: boolean + orderDate: string +} + +const EMPTY: FormState = { + postIt: '', + postAdd: '', + status: '', + adminOnly: false, + contractNumber: '', + installSchedule: '', + customerLoginId: '', + newTel: '', + multiLine: false, + addPayer: false, + record: false, + progressLog: '', + adminMemo: '', + openDate: '', + cancelDate: '', + giftStatus: '', + giftPlace: '', + giftProgressLog: '', + giftRequested: '', + giftSended: '', + changeOrderDate: false, + orderDate: '', +} + +function d10(v?: string | null) { + return v ? String(v).replace('T', ' ').substring(0, 10) : '' +} +function dt16(v?: string | null) { + return v ? String(v).replace('T', ' ').substring(0, 16) : '' +} + +export default function ReceptionProgressModal({ + open, + receptionId, + onClose, + onChanged, + onEdit, + onCopyLine, +}: Props) { + const qc = useQueryClient() + const [f, setF] = useState(EMPTY) + const [recreateIsp, setRecreateIsp] = useState() + + const detailQuery = useQuery({ + queryKey: ['reception-progress', receptionId], + queryFn: async () => { + const { data } = await client.get>>( + `${apiPrefix()}/homes/receptions/${receptionId}`, + ) + return unwrap(data) as Record + }, + enabled: open && receptionId != null, + }) + + const ispQuery = useQuery({ + queryKey: ['reception-isps'], + queryFn: async () => { + const { data } = await client.get>( + `${apiPrefix()}/homes/reception-isps`, + ) + return unwrap(data) || [] + }, + enabled: open, + }) + + const data = detailQuery.data + const reception = data?.reception || data + const progressStatuses: string[] = data?.progressStatuses || [] + const giftStatuses: string[] = data?.giftStatuses || [] + const progressLogs: any[] = data?.progressLogs || data?.logs || [] + const giftLogs: any[] = data?.giftProgressLogs || [] + + useEffect(() => { + if (!data || !reception) return + const gift = data.gift || {} + const customer = data.customer || {} + setF({ + postIt: customer.postIt ?? reception.postIt ?? '', + postAdd: customer.postAdd ?? reception.postAdd ?? '', + status: '', + adminOnly: false, + contractNumber: reception.contractNumber ?? '', + installSchedule: reception.installSchedule ?? '', + customerLoginId: reception.customerLoginId ?? '', + newTel: reception.newTel ?? '', + multiLine: !!reception.multiLine, + addPayer: !!reception.addPayer, + record: !!reception.record, + progressLog: '', + adminMemo: data.etc?.adminMemo ?? reception.adminMemo ?? '', + openDate: d10(reception.openDate), + cancelDate: d10(reception.cancelDate), + giftStatus: '', + giftPlace: gift.place ?? reception.giftPlace ?? '', + giftProgressLog: '', + giftRequested: d10(gift.requested ?? reception.giftRequested), + giftSended: d10(gift.sended ?? reception.giftSended), + changeOrderDate: false, + orderDate: d10(reception.orderDate), + }) + }, [data]) // eslint-disable-line react-hooks/exhaustive-deps + + const set = (patch: Partial) => setF((p) => ({ ...p, ...patch })) + + const saveMut = useMutation({ + mutationFn: async (closeAfter: boolean) => { + const payload: Record = { + postIt: f.postIt, + postAdd: f.postAdd, + contractNumber: f.contractNumber, + installSchedule: f.installSchedule, + customerLoginId: f.customerLoginId, + newTel: f.newTel, + multiLine: f.multiLine, + addPayer: f.addPayer, + record: f.record, + adminMemo: f.adminMemo, + openDate: f.openDate || null, + cancelDate: f.cancelDate || null, + giftPlace: f.giftPlace, + giftRequested: f.giftRequested || null, + giftSended: f.giftSended || null, + adminOnly: f.adminOnly, + memo: f.progressLog, + giftProgressLog: f.giftProgressLog, + changeOrderDate: f.changeOrderDate, + orderDate: f.changeOrderDate && f.orderDate ? `${f.orderDate} 00:00:00` : undefined, + } + if (f.status) payload.status = f.status + if (f.giftStatus) payload.giftStatus = f.giftStatus + const { data: res } = await client.post( + `${apiPrefix()}/homes/receptions/${receptionId}/progress`, + payload, + ) + return { body: unwrap(res), closeAfter } + }, + onSuccess: ({ closeAfter }) => { + message.success('저장했습니다.') + qc.invalidateQueries({ queryKey: ['reception-progress', receptionId] }) + qc.invalidateQueries({ queryKey: ['receptions'] }) + onChanged?.() + if (closeAfter) onClose() + else { + setF((p) => ({ ...p, status: '', giftStatus: '', progressLog: '', giftProgressLog: '', adminOnly: false })) + detailQuery.refetch() + } + }, + onError: () => message.error('저장에 실패했습니다.'), + }) + + const deleteMut = useMutation({ + mutationFn: async () => { + const { data: res } = await client.delete(`${apiPrefix()}/homes/receptions/${receptionId}`) + return unwrap(res) + }, + onSuccess: () => { + message.success('삭제했습니다.') + qc.invalidateQueries({ queryKey: ['receptions'] }) + onChanged?.() + onClose() + }, + onError: () => message.error('삭제에 실패했습니다.'), + }) + + const delProgressLog = async (logId: number) => { + try { + await client.delete(`${apiPrefix()}/homes/receptions/progress-log/${logId}`) + detailQuery.refetch() + message.success('로그를 삭제했습니다.') + } catch { + message.error('로그 삭제에 실패했습니다.') + } + } + + const delGiftLog = async (logId: number) => { + try { + await client.delete(`${apiPrefix()}/homes/receptions/gift-progress-log/${logId}`) + detailQuery.refetch() + message.success('사은품 로그를 삭제했습니다.') + } catch { + message.error('로그 삭제에 실패했습니다.') + } + } + + const titleWarn = reception?.progressStatus === '9.취소' || reception?.detailProgress?.includes?.('주의') + const title = receptionId + ? `${titleWarn ? '⚠ 주의 요망 ' : ''}업무 진행 내역 — 접수 #${receptionId}` + : '업무 진행 내역' + + return ( + + {detailQuery.isLoading || !data ? ( +
로딩 중...
+ ) : ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
접수자 +
+ + + (관리자만 보이고 영업점은 보이지 않습니다.) +
+
현재진행상황 + +
포스트잇 + set({ postIt: e.target.value })} /> +
추가메모 + set({ postAdd: e.target.value })} /> +
진행상황 +
+
계약번호 + set({ contractNumber: e.target.value })} /> + 설치일정 + set({ installSchedule: e.target.value })} /> +
고객아이디 + set({ customerLoginId: e.target.value })} /> + 신규전화번호 + set({ newTel: e.target.value })} /> +
접수유형 +
+ + + +
+
업무진행메모(LOG) + set({ progressLog: e.target.value })} /> +
관리자메모 + set({ adminMemo: e.target.value })} /> +
접수일자 +
+ set({ orderDate: d ? d.format('YYYY-MM-DD') : '' })} + /> + +
+
개통일자 + set({ openDate: d ? d.format('YYYY-MM-DD') : '' })} + /> + 해지일자 + set({ cancelDate: d ? d.format('YYYY-MM-DD') : '' })} + /> +
+
+ + + + + + + + + + + + + + {progressLogs.map((l) => ( + + + + + + + + ))} + {progressLogs.length === 0 && ( + + )} + +
날짜진행상태담당자내용삭제
{dt16(l.loggedAt || l.createdAt)}{l.status}{l.authorName || l.writer}{l.adminOnly ? '[관리자] ' : ''}{l.memo} + +
진행 내역이 없습니다.
+
+ + + + + + + + + + + + + + + + + + + + + +
사은품현재상황 + + 사은품진행상황 +
사은품 지급처 + 지급요청/완료 + + set({ giftRequested: d ? d.format('YYYY-MM-DD') : '' })} + /> + set({ giftSended: d ? d.format('YYYY-MM-DD') : '' })} + /> + +
사은품진행메모 + set({ giftProgressLog: e.target.value })} /> +
+
+ + + + + + + + + + + + + + {giftLogs.map((l) => ( + + + + + + + + ))} + {giftLogs.length === 0 && ( + + )} + +
날짜사은품진행상태담당자내용삭제
{dt16(l.loggedAt || l.createdAt)}{l.status}{l.authorName || l.writer}{l.memo} + +
사은품 진행 내역이 없습니다.
+
+ +
+
+ + + + +
+
+ 개인 + + +
+ + + + + + + + + {(customer.postAdd || reception.postAdd) && ( + + + + + )} + + + + + + + + + + + + + + + + + + + + + {(customer.sido || reception.region) && ( + + + + + )} + +
고객명{customer.name || reception.customerName || '-'}포스트잇{customer.postIt || reception.postIt || ''}
추가메모{customer.postAdd || reception.postAdd}
식별번호{customer.ssid || reception.identifyNumber || ''}고객유형{ctype === 'company' ? '사업자' : ctype === 'foreigner' ? '외국인' : '개인'}
휴대전화{customer.phone || reception.phone || '-'}유선전화{customer.tel || reception.tel || ''}
이메일{customer.email || reception.email || ''}
주소 + {[customer.zipcode || reception.zipcode ? `[${customer.zipcode || reception.zipcode}]` : '', + customer.address || reception.address, + customer.addressDetail || reception.addressDetail].filter(Boolean).join(' ') || '-'} +
지역{customer.sido || reception.region}
+ + + + + + + + + + {(payment.name || payment.bank || payment.cardCompany) && ( + <> + + + + + + + {pmChecked(paymentMethod, 'card') ? ( + <> + + + + + + + + + + + + ) : pmChecked(paymentMethod, 'transfer') ? ( + + + + + + + ) : null} + + )} + +
결제방법 + + + + +
납입자명{payment.name || ''}주민번호{payment.ssid || ''}
카드사{payment.cardCompany || ''}유효기간{[payment.cardYear, payment.cardMonth].filter(Boolean).join('/')}
카드번호{payment.cardNumber || ''}
은행{payment.bank || ''}계좌번호{payment.accountNumber || ''}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
통신사{data.ispName || reception.ispName || reception.carrier || '-'}
상품종류 + + + +
{productType === 'e02' ? '전화' : productType === 'e03' ? 'TV' : '인터넷'} + + + + + + + + + + + + + + + + + + + +
상품{data.productName || product.productName || product.name || reception.productName || '-'}
약정{data.agreementName || reception.agreementName || '-'}
이벤트{(data.eventNames || []).join(', ') || '-'}
월사용요금{amount.toLocaleString()} 원
+
총 요금{amount.toLocaleString()} 원
할인혜택-
설치희망일자{reception.openDate || '-'}연락처{reception.progressStatus || '-'}
+
+ + {(gift.status || gift.category || reception.giftStatus) && ( + + + + + + + + + + + + + + + + +
사은품분류{gift.category || reception.giftCategory || '-'}진행상황{gift.status || reception.giftStatus || '-'}
지급처{gift.place || reception.giftPlace || '-'}지급일{gift.giftDate || reception.giftDate || '-'}
+
+ )} + + {(data.etc?.memo || reception.memo) && ( + + + + + + + + +
메모{data.etc?.memo || reception.memo}
+
+ )} + + ) +} diff --git a/frontend/src/components/SchedulePanel.tsx b/frontend/src/components/SchedulePanel.tsx new file mode 100644 index 0000000..5143039 --- /dev/null +++ b/frontend/src/components/SchedulePanel.tsx @@ -0,0 +1,267 @@ +import { useEffect, useMemo, useState } from 'react' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { Button, Form, Input, TimePicker, message } from 'antd' +import Modal from './DraggableModal' +import { CloseOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons' +import dayjs, { type Dayjs } from 'dayjs' +import 'dayjs/locale/ko' +import client, { apiPrefix, unwrap, type ApiResponse } from '../api/client' + +dayjs.locale('ko') + +type ScheduleRow = { + id: number + sdate?: string + stime?: string + contents?: string + displayTime?: string +} + +type MonthData = { + days?: Record +} + +const weekDays = ['일', '월', '화', '수', '목', '금', '토'] + +type Props = { + open: boolean + onClose: () => void +} + +export default function SchedulePanel({ open, onClose }: Props) { + const qc = useQueryClient() + const [now, setNow] = useState(() => dayjs()) + const [cursor, setCursor] = useState(() => dayjs()) + const [selected, setSelected] = useState(() => dayjs()) + const [createOpen, setCreateOpen] = useState(false) + const [form] = Form.useForm() + + useEffect(() => { + if (!open) return + const t = window.setInterval(() => setNow(dayjs()), 1000) + return () => window.clearInterval(t) + }, [open]) + + useEffect(() => { + if (open) { + const today = dayjs() + setNow(today) + setCursor(today) + setSelected(today) + } + }, [open]) + + const selectedKey = selected.format('YYYY-MM-DD') + const year = cursor.year() + const month = cursor.month() + 1 + + const monthQuery = useQuery({ + queryKey: ['schedules-month', year, month], + enabled: open, + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/schedules/month`, { + params: { year, month }, + }) + return unwrap(data) + }, + }) + + const dayQuery = useQuery({ + queryKey: ['schedules-day', selectedKey], + enabled: open, + queryFn: async () => { + const { data } = await client.get>( + `${apiPrefix()}/schedules/search`, + { params: { sdate: selectedKey, size: 100 } }, + ) + return unwrap(data) + }, + }) + + const refresh = () => { + qc.invalidateQueries({ queryKey: ['schedules-month'] }) + qc.invalidateQueries({ queryKey: ['schedules-day'] }) + qc.invalidateQueries({ queryKey: ['schedules'] }) + } + + const createMut = useMutation({ + mutationFn: async (values: { contents: string; stime?: Dayjs }) => { + const body = { + sdate: selectedKey, + contents: values.contents, + stime: values.stime ? values.stime.format('HH:mm') : undefined, + } + const { data } = await client.post(`${apiPrefix()}/schedules`, body) + return unwrap(data) + }, + onSuccess: () => { + message.success('일정을 등록했습니다.') + setCreateOpen(false) + form.resetFields() + refresh() + }, + onError: (e: Error) => message.error(e.message), + }) + + const deleteMut = useMutation({ + mutationFn: async (id: number) => { + const { data } = await client.delete(`${apiPrefix()}/schedules/${id}`) + return unwrap(data) + }, + onSuccess: () => { + message.success('일정을 삭제했습니다.') + refresh() + }, + onError: (e: Error) => message.error(e.message), + }) + + const cells = useMemo(() => { + const start = cursor.startOf('month') + const startWeekday = start.day() + const daysInMonth = cursor.daysInMonth() + const prevMonth = cursor.subtract(1, 'month') + const prevDays = prevMonth.daysInMonth() + const list: { date: Dayjs; current: boolean }[] = [] + for (let i = startWeekday - 1; i >= 0; i -= 1) { + list.push({ date: prevMonth.date(prevDays - i), current: false }) + } + for (let d = 1; d <= daysInMonth; d += 1) { + list.push({ date: cursor.date(d), current: true }) + } + while (list.length % 7 !== 0) { + const next = list[list.length - 1].date.add(1, 'day') + list.push({ date: next, current: false }) + } + return list + }, [cursor]) + + const eventDays = monthQuery.data?.days || {} + const events = dayQuery.data?.list || [] + + if (!open) return null + + return ( + <> +
+ + + setCreateOpen(false)} + footer={null} + destroyOnClose + > +
createMut.mutate(v)}> + + + + + + +
+ + +
+
+
+ + ) +} diff --git a/frontend/src/components/SubjectSettingsModal.tsx b/frontend/src/components/SubjectSettingsModal.tsx new file mode 100644 index 0000000..0b2fbc9 --- /dev/null +++ b/frontend/src/components/SubjectSettingsModal.tsx @@ -0,0 +1,120 @@ +import { useEffect, useState } from 'react' +import { Button, Input, Space, message } from 'antd' +import { DeleteOutlined, PlusOutlined } from '@ant-design/icons' +import Modal from './DraggableModal' + +type Props = { + open: boolean + items: string[] + saving?: boolean + submitClassName?: string + onCancel: () => void + onSave: (items: string[]) => void +} + +export default function SubjectSettingsModal({ + open, + items, + saving, + submitClassName, + onCancel, + onSave }: Props) { + const [list, setList] = useState([]) + const [input, setInput] = useState('') + + useEffect(() => { + if (!open) return + setList(items.length ? [...items] : []) + setInput('') + // 팝업이 열릴 때만 초기화 (편집 중 items 참조 변경으로 목록이 리셋되지 않도록) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open]) + + const addItem = () => { + const value = input.trim() + if (!value) { + message.warning('과목을 입력하세요.') + return + } + if (list.some((item) => item === value)) { + message.warning('이미 등록된 과목입니다.') + return + } + setList((prev) => [...prev, value]) + setInput('') + } + + const removeItem = (index: number) => { + setList((prev) => prev.filter((_, i) => i !== index)) + } + + const handleSave = () => { + if (!list.length) { + message.warning('과목을 1개 이상 등록하세요.') + return + } + onSave(list) + } + + return ( + +
+ + setInput(e.target.value)} + onPressEnter={(e) => { + e.preventDefault() + addItem() + }} + /> + + + +
    + {list.map((item, index) => ( +
  • + {item} + +
  • + ))} + {!list.length ? ( +
  • 등록된 과목이 없습니다.
  • + ) : null} +
+ +
+ + +
+
+
+ ) +} diff --git a/frontend/src/formRequiredMark.tsx b/frontend/src/formRequiredMark.tsx new file mode 100644 index 0000000..8287173 --- /dev/null +++ b/frontend/src/formRequiredMark.tsx @@ -0,0 +1,12 @@ +import type { ReactNode } from 'react' + +/** Ant Design Form requiredMark: * 대신 '필수' 뱃지 */ +export function formRequiredMark(label: ReactNode, { required }: { required: boolean }) { + if (!required) return label + return ( + <> + {label} + 필수 + + ) +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..44c8650 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,407 @@ +/* ===== 테마 토큰 (billing_react 방식) ===== + html.theme-* 에만 토큰을 두어 :root 와 충돌하지 않게 한다. */ +html.theme-light { + --primary: #2563eb; + --primary-dark: #1d4ed8; + --on-primary: #ffffff; + + --bg: #f1f5f9; + --surface: #ffffff; + --surface-2: #f8fafc; + --surface-3: #ffffff; + --elevated: #eff6ff; + + --text: #0f172a; + --muted: #64748b; + --border: #e2e8f0; + + --danger: #dc2626; + --success: #16a34a; + + --highlight-bg: #fffbe6; + --highlight-bg-strong: #fff3bf; + --highlight-text: #0f172a; + + --shadow: 0 8px 24px rgba(0,0,0,.14); + --shadow-lg: 0 20px 60px rgba(0,0,0,.3); + --focus-ring: #bfdbfe; + + --navy: #152536; + --blue: #1769aa; + --line: var(--border); +} + +html.theme-dark { + --primary: #7aa2f7; + --primary-dark: #5a7dc7; + --on-primary: #0b1020; + + --bg: #16181e; + --surface: #24283b; + --surface-2: #1f2335; + --surface-3: #1a1b26; + --elevated: #2a2f45; + + --text: #c0caf5; + --muted: #8a93b8; + --border: rgba(122, 162, 247, 0.16); + + --danger: #f7768e; + --success: #9ece6a; + + --highlight-bg: #2f3650; + --highlight-bg-strong: #3a4463; + --highlight-text: #c0caf5; + + --shadow: 0 8px 24px rgba(0,0,0,.5); + --shadow-lg: 0 20px 60px rgba(0,0,0,.45); + --focus-ring: rgba(122, 162, 247, 0.5); +} + +html.theme-blue { + /* 버튼·강조: 흰 글씨가 읽히도록 채도 높은 중청색 */ + --primary: #1e8ad4; + --primary-dark: #1670ad; + --on-primary: #ffffff; + + --bg: #0a1522; + --surface: #15273d; + --surface-2: #0f1e30; + --surface-3: #0c1929; + --elevated: #1e3a5f; + + --text: #e8f4ff; + --muted: #8fbbe0; + --border: rgba(94, 181, 255, 0.16); + + --danger: #ff6b6b; + --success: #5ce0a0; + + --highlight-bg: #1a3352; + --highlight-bg-strong: #21406a; + --highlight-text: #e8f4ff; + + --shadow: 0 8px 24px rgba(0,0,0,.5); + --shadow-lg: 0 20px 60px rgba(0,0,0,.45); + --focus-ring: rgba(30, 138, 212, 0.45); +} + +/* 테마 클래스 적용 전 깜빡임 방지용 기본값 = 라이트 (:root 는 theme-* 보다 특이도가 낮음) */ +html:not(.theme-light):not(.theme-dark):not(.theme-blue), +:root:not(.theme-light):not(.theme-dark):not(.theme-blue) { + --primary: #2563eb; + --primary-dark: #1d4ed8; + --on-primary: #ffffff; + --bg: #f1f5f9; + --surface: #ffffff; + --surface-2: #f8fafc; + --surface-3: #ffffff; + --elevated: #eff6ff; + --text: #0f172a; + --muted: #64748b; + --border: #e2e8f0; + --danger: #dc2626; + --success: #16a34a; + --highlight-bg: #fffbe6; + --highlight-bg-strong: #fff3bf; + --highlight-text: #0f172a; + --shadow: 0 8px 24px rgba(0,0,0,.14); + --shadow-lg: 0 20px 60px rgba(0,0,0,.3); + --focus-ring: #bfdbfe; +} + +* { box-sizing: border-box; } +body { + margin: 0; + font-family: 'Pretendard', 'Noto Sans KR', -apple-system, 'Malgun Gothic', sans-serif; + background: var(--bg); + color: var(--text); + font-size: 14px; +} +#root { min-height: 100vh; } +a { color: inherit; text-decoration: none; } + +/* ===== App shell (billing_react Layout) ===== */ +.app-shell { + display: flex; + flex-direction: column; + height: 100vh; + min-height: 100vh; + background: var(--bg); + color: var(--text); +} +.app-body { + display: flex; + flex: 1; + min-height: 0; + position: relative; +} + +.topbar { + height: 52px; + background: var(--surface-3); + border-bottom: 1px solid var(--border); + display: flex; + align-items: stretch; + flex-shrink: 0; + z-index: 30; +} +.topbar-brand { + display: flex; + align-items: center; + justify-content: center; + padding: 0 14px; + border-right: 1px solid var(--border); + background: var(--surface-3); + overflow: hidden; + flex-shrink: 0; +} +.topbar-brand.is-collapsed { + width: 0 !important; + padding: 0; + border-right: none; +} +.brand-inline { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + max-width: 100%; + overflow: hidden; +} +.topbar-main { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 0 12px; +} +.topbar-left-tools { min-width: 0; overflow: hidden; } +.topbar-right { + display: flex; + align-items: center; + gap: 6px; + flex-shrink: 0; +} +.topbar-user { font-size: 12px; white-space: nowrap; } +.muted { color: var(--muted); } + +.theme-select { + width: auto; + padding: 5px 8px; + font-size: 12px; + border-radius: 6px; + cursor: pointer; + background: var(--surface); + color: var(--text); + border: 1px solid var(--border); +} + +.leftpanel { + flex-shrink: 0; + background: var(--surface-3); + border-right: 1px solid var(--border); + overflow-y: auto; + overflow-x: hidden; + padding: 12px 10px; + scrollbar-width: thin; + scrollbar-color: color-mix(in srgb, var(--primary) 45%, var(--muted)) var(--surface-2); +} +.leftpanel::-webkit-scrollbar { + width: 8px; +} +.leftpanel::-webkit-scrollbar-track { + background: var(--surface-2); + border-radius: 8px; +} +.leftpanel::-webkit-scrollbar-thumb { + background: color-mix(in srgb, var(--primary) 40%, var(--muted)); + border-radius: 8px; + border: 2px solid var(--surface-2); +} +.leftpanel::-webkit-scrollbar-thumb:hover { + background: color-mix(in srgb, var(--primary) 70%, var(--muted)); +} + +.main-area .content, +.content { + scrollbar-width: thin; + scrollbar-color: color-mix(in srgb, var(--primary) 40%, var(--muted)) var(--surface-2); +} +.main-area .content::-webkit-scrollbar, +.content::-webkit-scrollbar { + width: 10px; + height: 10px; +} +.main-area .content::-webkit-scrollbar-track, +.content::-webkit-scrollbar-track { + background: var(--bg); +} +.main-area .content::-webkit-scrollbar-thumb, +.content::-webkit-scrollbar-thumb { + background: color-mix(in srgb, var(--primary) 35%, var(--muted)); + border-radius: 8px; + border: 2px solid var(--bg); +} +.main-area .content::-webkit-scrollbar-thumb:hover, +.content::-webkit-scrollbar-thumb:hover { + background: color-mix(in srgb, var(--primary) 65%, var(--muted)); +} +.lp-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 10px; + margin-bottom: 12px; + overflow: hidden; +} +.lp-card.is-active { + border-color: color-mix(in srgb, var(--primary) 45%, var(--border)); +} +.lp-card-title { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 9px 14px; + font-size: 12px; + font-weight: 700; + color: var(--text); + background: var(--surface-2); + border: none; + border-bottom: 1px solid transparent; + cursor: pointer; + text-align: left; +} +.lp-card:has(.lp-card-body) .lp-card-title, +.lp-card-title[aria-expanded="true"] { + border-bottom-color: var(--border); +} +.lp-card-chevron { + color: var(--muted); + font-size: 11px; + flex-shrink: 0; +} +.lp-card-body { padding: 6px; } +.leftpanel a { + display: block; + width: 100%; + text-align: left; + padding: 8px 12px; + font-size: 13px; + border-radius: 7px; + color: var(--muted); + background: none; + border: none; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.leftpanel a:hover { background: var(--surface-2); color: var(--text); } +.leftpanel a.active { + color: var(--on-primary); + background: var(--primary); + font-weight: 600; +} + +.panel-resizer { + width: 3px; + flex-shrink: 0; + cursor: col-resize; + background: var(--surface-3); + border-right: 1px solid var(--border); +} +.panel-resizer:hover, +.panel-resizer.dragging { + background: var(--primary); + border-right-color: var(--primary); +} +.panel-toggle { + position: absolute; + top: 50%; + transform: translateY(-50%); + z-index: 20; + width: 15px; + height: 42px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + background: var(--surface-3); + color: var(--muted); + border: 1px solid var(--border); + border-left: none; + border-radius: 0 8px 8px 0; + cursor: pointer; + font-size: 12px; + line-height: 1; +} +.panel-toggle:hover { color: var(--primary); background: var(--elevated); } + +.main-area { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + background: var(--bg); +} +.main-area .content { + overflow: auto; + flex: 1; + min-height: 0; + padding: 20px; +} + +/* legacy stubs kept for pages that still reference them */ +.admin-layout { min-height: 100vh; } +.side { background: var(--surface-3) !important; } +.brand { + height: 64px; + display: flex; + align-items: center; + padding: 0 24px; + color: var(--text); + font-size: 19px; + font-weight: 800; +} +.brand span { color: var(--primary); margin-left: 5px; } +.header { + background: var(--surface) !important; + border-bottom: 1px solid var(--border); + display: flex; + justify-content: flex-end; + align-items: center; + gap: 18px; + padding: 0 28px !important; +} +.content { padding: 26px; overflow: auto; } +.page-title { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 18px; + color: var(--text); +} +.page-title .ant-typography { margin: 0; font-weight: 700; color: var(--text) !important; } +.filter-card { margin-bottom: 16px; background: var(--surface-2); } +.dashboard-row { margin-top: 16px; } +.login-wrap { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #1e293b, #2563eb); + padding: 24px; +} +.login-box { width: 400px; max-width: 100%; } +.login-demo { color: #64748b; font-size: 12px; margin-top: 16px; text-align: center; } + +@media (max-width: 640px) { + .main-area .content { padding: 16px; } + .content { padding: 16px; } + .page-title { align-items: flex-start; gap: 10px; flex-direction: column; } +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..ffb9cd9 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,36 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { applyTheme, loadTheme } from './theme' +import { ThemeProvider } from './themeContext' +import './index.css' +import App from './App.tsx' + +applyTheme(loadTheme()) + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: (failureCount, error) => { + const status = (error as { response?: { status?: number } })?.response?.status + if (status === 401 || status === 403) return false + return failureCount < 3 + }, + retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 5000), + refetchOnWindowFocus: false, + }, + }, +}) + +createRoot(document.getElementById('root')!).render( + + + + + + + + + , +) diff --git a/frontend/src/menu.ts b/frontend/src/menu.ts new file mode 100644 index 0000000..7b8c754 --- /dev/null +++ b/frontend/src/menu.ts @@ -0,0 +1,190 @@ +export type MenuItem = { + key: string + label: string +} + +export type MenuSection = { + id: string + title: string + items: MenuItem[] +} + +/** 비즈케어홈즈(h.bizcare.co.kr) 실제 메뉴 — 경로 그대로 */ +export function careMenuSections(): MenuSection[] { + return [ + { + id: 'home', + title: '대시보드', + items: [{ key: '/care/main', label: '대시보드' }], + }, + { + id: 'product', + title: '상품관리', + items: [ + { key: '/care/product/internet', label: '인터넷상품' }, + { key: '/care/product/home', label: '홈렌탈상품' }, + { key: '/care/product/log', label: '상품이력' }, + ], + }, + { + id: 'contract', + title: '계약관리', + items: [ + { key: '/care/contract/internet', label: '인터넷접수' }, + { key: '/care/contract/home', label: '홈렌탈접수' }, + { key: '/care/contract/log', label: '계약이력' }, + { key: '/care/contract/adjust', label: '일괄정책변경' }, + { key: '/care/contract/balance', label: '계약후정산' }, + ], + }, + { + id: 'reception', + title: '접수관리', + items: [ + { key: '/care/reception/list', label: '접수리스트' }, + ], + }, + { + id: 'invoice', + title: '정산관리', + items: [ + { key: '/care/invoice/publish', label: '정산서발행' }, + { key: '/care/invoice/my', label: '나의정산서' }, + { key: '/care/invoice/log', label: '정산서이력' }, + ], + }, + { + id: 'pending', + title: '일정관리', + items: [ + { key: '/care/pending/calendar', label: '캘린더' }, + { key: '/care/pending/pending', label: '일정관리' }, + { key: '/care/pending/log', label: '일정이력' }, + ], + }, + { + id: 'booking', + title: '예약관리', + items: [ + { key: '/care/booking/booking', label: '예약관리' }, + { key: '/care/booking/log', label: '예약이력' }, + ], + }, + { + id: 'customer', + title: '고객관리', + items: [ + { key: '/care/customer/customer', label: '고객관리' }, + { key: '/care/customer/log', label: '고객이력' }, + ], + }, + { + id: 'abooks', + title: '장부관리', + items: [ + { key: '/care/abooks/abooks-mon', label: '간편장부' }, + { key: '/care/abooks/log', label: '장부이력' }, + ], + }, + { + id: 'report', + title: '통계관리', + items: [ + { key: '/care/report/daily', label: '일일리포트' }, + { key: '/care/report/monthly', label: '월간리포트' }, + { key: '/care/report/internet', label: '인터넷통계' }, + { key: '/care/report/home', label: '홈렌탈통계' }, + { key: '/care/report/member', label: '직원별통계' }, + { key: '/care/report/agency', label: '거래처별통계' }, + { key: '/care/report/incentive', label: '인센티브계산' }, + ], + }, + { + id: 'setting', + title: '환경설정', + items: [ + { key: '/care/setting/agency', label: '거래처관리' }, + { key: '/care/setting/member', label: '직원관리' }, + { key: '/care/setting/preference', label: '기본설정' }, + { key: '/care/setting/level', label: '권한설정' }, + { key: '/care/setting/partner1', label: '상부점관리' }, + { key: '/care/setting/partner2', label: '하부점관리' }, + ], + }, + { + id: 'cs', + title: '고객센터', + items: [ + { key: '/care/cs/notice', label: '공지사항' }, + { key: '/care/cs/question', label: '사용문의' }, + ], + }, + { + id: 'bbs', + title: '사내게시판', + items: [{ key: '/care/bbs/bbs', label: '사내게시판' }], + }, + ] +} + +/** @deprecated 호환용 */ +export function agentMenuSections(): MenuSection[] { + return careMenuSections() +} + +export function shopMenuSections(): MenuSection[] { + return [ + { + id: 'shop', + title: '판매점', + items: [ + { key: '/shop/notice', label: '공지사항' }, + { key: '/shop/policy', label: '정책정보' }, + { key: '/shop/customer', label: '고객조회' }, + { key: '/shop/contract', label: '계약조회' }, + ], + }, + ] +} + +/** 권한설정·노출설정에서 허용된 섹션 id만 남김. allowedIds가 null이면 전부 표시(로딩 전). */ +export function filterMenuSections( + sections: MenuSection[], + allowedIds: string[] | null | undefined, +): MenuSection[] { + if (!allowedIds) return sections + const set = new Set(allowedIds) + return sections.filter((sec) => set.has(sec.id)) +} + +export const SITEMAP_TITLE_TO_ID: Record = { + 대시보드: 'home', + 상품관리: 'product', + 계약관리: 'contract', + 접수관리: 'reception', + 정산관리: 'invoice', + 일정관리: 'pending', + 예약관리: 'booking', + 고객관리: 'customer', + 장부관리: 'abooks', + 통계관리: 'report', + 환경설정: 'setting', + 고객센터: 'cs', + 사내게시판: 'bbs', + 게시판: 'bbs', +} + +export function resolveSelectedKey(pathname: string): string { + if (pathname.startsWith('/care/contract/write')) { + const q = typeof window !== 'undefined' ? new URLSearchParams(window.location.search).get('type') : null + if (q === 'home') return '/care/contract/home' + return '/care/contract/internet' + } + if (pathname === '/care/sitemap') return '/care/main/sitemap' + return pathname +} + +export function sectionContainsPath(section: MenuSection, pathname: string): boolean { + const selected = resolveSelectedKey(pathname) + return section.items.some((it) => selected === it.key || selected.startsWith(`${it.key}/`)) +} diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx new file mode 100644 index 0000000..8e37b16 --- /dev/null +++ b/frontend/src/pages/LoginPage.tsx @@ -0,0 +1,68 @@ +import { type FormEvent, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { login } from '../api/auth' +import BillCareLogo from '../components/BillCareLogo' + +export default function LoginPage() { + const navigate = useNavigate() + const [userid, setUserid] = useState('demo') + const [password, setPassword] = useState('demo123') + const [error, setError] = useState('') + const [loading, setLoading] = useState(false) + + const submit = async (e: FormEvent) => { + e.preventDefault() + setError('') + setLoading(true) + try { + const data = await login(userid, password) + localStorage.setItem('token', data.token!) + localStorage.setItem('role', data.role === 'SHOP' ? 'SHOP' : 'AGENT') + localStorage.setItem('userName', data.name) + localStorage.setItem('userid', data.userid) + if (data.staffPermission) localStorage.setItem('staffPermission', data.staffPermission) + else localStorage.removeItem('staffPermission') + navigate('/care/main', { replace: true }) + } catch (err) { + setError(err instanceof Error ? err.message : '로그인에 실패했습니다.') + } finally { + setLoading(false) + } + } + + return ( +
+
+
+ +

통합 영업·정산 관리 시스템

+
+
+ + setUserid(e.target.value)} + autoComplete="username" + autoFocus + /> +
+
+ + setPassword(e.target.value)} + autoComplete="current-password" + /> +
+ {error &&
{error}
} + +
데모: demo / demo123
+
+
+ ) +} diff --git a/frontend/src/pages/care/AbooksLogPage.tsx b/frontend/src/pages/care/AbooksLogPage.tsx new file mode 100644 index 0000000..e506e2a --- /dev/null +++ b/frontend/src/pages/care/AbooksLogPage.tsx @@ -0,0 +1,20 @@ +import { BookOutlined } from '@ant-design/icons' +import { HomesLogListPage } from './HomesLogListPage' + +const ACTIONS = [ + '간편장부-등록', + '간편장부-수정', + '간편장부-삭제', +] + +export default function AbooksLogPage() { + return ( + } + /> + ) +} diff --git a/frontend/src/pages/care/AbooksPage.tsx b/frontend/src/pages/care/AbooksPage.tsx new file mode 100644 index 0000000..d93e771 --- /dev/null +++ b/frontend/src/pages/care/AbooksPage.tsx @@ -0,0 +1,612 @@ +import { useMemo, useState } from 'react' +import { useSearchParams } from 'react-router-dom' +import Modal from '../../components/DraggableModal' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import dayjs, { type Dayjs } from 'dayjs' +import { + Button, + Card, + Checkbox, + DatePicker, + Form, + Input, + InputNumber, + Radio, + Select, + Space, + Tabs, + message } from 'antd' +import CareTable from '../../components/CareTable' +import SubjectSettingsModal from '../../components/SubjectSettingsModal' +import { + BookOutlined, + CloseOutlined, + DownloadOutlined, + EditOutlined, + LeftOutlined, + PlusSquareOutlined, + ReloadOutlined, + RightOutlined, + SearchOutlined, + SettingOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' +import { options } from './homesShared' + +const { RangePicker } = DatePicker + +type AbookRow = { + id: number + entryDate?: string + entryType?: string + category?: string + item?: string + amount?: number + deposit?: number | null + withdraw?: number | null + cardDeposit?: number | null + cardWithdraw?: number | null + memo?: string + employee?: string +} + +type AbookSummary = { + carryOver?: number + deposit?: number + withdraw?: number + total?: number +} + +type AbookResult = { + total: number + list: AbookRow[] + counts?: Record + categoryCounts?: Record + categories?: string[] + employees?: string[] + items?: string[] + summary?: AbookSummary +} + +const ENTRY_TYPES = ['입금', '출금', '카드입금', '카드출금'] as const + +const money = (v?: number | null) => { + if (v == null || Number.isNaN(Number(v))) return '-' + return Number(v).toLocaleString('ko-KR') +} + +const isOut = (type?: string) => type === '출금' || type === '카드출금' +const typeClass = (type?: string) => { + if (type === '입금' || type === '카드입금') return 'abook-in' + if (isOut(type)) return 'abook-out' + return '' +} + +export default function AbooksPage() { + const [form] = Form.useForm() + const [editForm] = Form.useForm() + const qc = useQueryClient() + const [searchParams] = useSearchParams() + const initialView = searchParams.get('view') === 'detail' ? 'detail' : 'month' + const [view, setView] = useState<'month' | 'detail'>(initialView) + const [month, setMonth] = useState(dayjs()) + const [day, setDay] = useState('ALL') + const [categoryTab, setCategoryTab] = useState('ALL') + const [detailFilters, setDetailFilters] = useState>({ + page: 0, + size: 10, + tab: 'ALL' }) + const [selected, setSelected] = useState([]) + const [editing, setEditing] = useState(null) + const [creating, setCreating] = useState(false) + const [catOpen, setCatOpen] = useState(false) + + const monthParams = useMemo(() => ({ + yearMonth: month.format('YYYY-MM'), + day: day === 'ALL' ? undefined : day, + category: categoryTab === 'ALL' ? undefined : categoryTab, + page: 0, + size: 200 }), [month, day, categoryTab]) + + const query = useQuery({ + queryKey: ['homes-abooks', view, view === 'month' ? monthParams : detailFilters], + queryFn: async () => { + const params = view === 'month' + ? monthParams + : { + ...detailFilters, + entryType: detailFilters.tab === 'ALL' ? undefined : detailFilters.tab } + const { data } = await client.get>(`${apiPrefix()}/homes/abooks`, { params }) + return unwrap(data) + } }) + + const members = useQuery({ + queryKey: ['homes/members-options'], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/members`, { + params: { page: 0, size: 200 } }) + return unwrap(data) + } }) + + const refresh = () => { + qc.invalidateQueries({ queryKey: ['homes-abooks'] }) + setSelected([]) + } + + const staffOptions = useMemo(() => { + const fromApi = query.data?.employees || [] + const fromMembers = (members.data?.list || []).map((m) => String(m.name || '')).filter(Boolean) + return Array.from(new Set([...fromApi, ...fromMembers, '홍길동'])).map((v) => ({ value: v, label: v })) + }, [query.data, members.data]) + + const categories = useMemo( + () => query.data?.categories || ['일반', '123'], + [query.data?.categories], + ) + + const categoryTabs = useMemo( + () => [ + { key: 'ALL', label: `전체 ${query.data?.categoryCounts?.ALL ?? query.data?.total ?? 0}` }, + ...categories.map((c) => ({ + key: c, + label: `${c} ${query.data?.categoryCounts?.[c] ?? 0}` })), + ], + [query.data, categories], + ) + + const detailTabs = useMemo( + () => [ + { key: 'ALL', label: `전체 ${query.data?.counts?.ALL ?? query.data?.total ?? 0}` }, + ...ENTRY_TYPES.map((t) => ({ key: t, label: `${t} ${query.data?.counts?.[t] ?? 0}` })), + ], + [query.data], + ) + + const daysInMonth = month.daysInMonth() + const today = dayjs() + + const save = useMutation({ + mutationFn: async (values: Record) => { + const payload: Record = { + employee: values.employee, + entryDate: values.entryDate ? dayjs(values.entryDate as Dayjs).format('YYYY-MM-DD') : undefined, + entryType: values.entryType, + category: values.category, + item: values.item, + amount: values.amount, + memo: values.memo } + if (editing) { + const { data } = await client.put(`${apiPrefix()}/homes/abooks/${editing.id}`, payload) + return unwrap(data as ApiResponse) + } + const { data } = await client.post(`${apiPrefix()}/homes/abooks`, payload) + return unwrap(data as ApiResponse) + }, + onSuccess: () => { + message.success(editing ? '수정했습니다.' : '간편장부를 등록했습니다.') + setCreating(false) + setEditing(null) + editForm.resetFields() + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const deleteMut = useMutation({ + mutationFn: async (ids: number[]) => { + const { data } = await client.post(`${apiPrefix()}/homes/abooks/delete`, { ids }) + return unwrap(data as ApiResponse<{ deleted: number }>) + }, + onSuccess: (data) => { + message.success(`${data.deleted}건을 삭제했습니다.`) + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const saveCategories = useMutation({ + mutationFn: async (cats: string[]) => { + const { data } = await client.put(`${apiPrefix()}/homes/abooks/categories`, { categories: cats }) + return unwrap(data as ApiResponse<{ categories: string[] }>) + }, + onSuccess: () => { + message.success('과목을 저장했습니다.') + setCatOpen(false) + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const openCreate = () => { + setEditing(null) + setCreating(true) + editForm.setFieldsValue({ + employee: staffOptions[0]?.value || '홍길동', + entryDate: dayjs(), + entryType: '입금', + category: categories[0] || '일반', + item: undefined, + amount: undefined, + memo: undefined }) + } + + const openEdit = (row: AbookRow) => { + setCreating(false) + setEditing(row) + editForm.setFieldsValue({ + employee: row.employee, + entryDate: row.entryDate ? dayjs(row.entryDate) : dayjs(), + entryType: row.entryType || '입금', + category: row.category || '일반', + item: row.item, + amount: row.amount, + memo: row.memo }) + } + + const openCatSettings = () => setCatOpen(true) + + const setPreset = (from: Dayjs, to: Dayjs) => form.setFieldsValue({ dates: [from, to] }) + + const onDetailSearch = (values: Record) => { + const dates = values.dates as [Dayjs, Dayjs] | undefined + setDetailFilters({ + page: 0, + size: values.size ?? detailFilters.size ?? 10, + tab: detailFilters.tab, + category: values.category || undefined, + employee: values.employee || undefined, + item: values.item || undefined, + q: values.q || undefined, + dateFrom: dates?.[0]?.format('YYYY-MM-DD'), + dateTo: dates?.[1]?.format('YYYY-MM-DD') }) + } + + const resetDetail = () => { + form.resetFields() + form.setFieldsValue({ size: 10 }) + setDetailFilters({ page: 0, size: 10, tab: 'ALL' }) + } + + const download = async () => { + try { + const params = view === 'month' + ? monthParams + : { ...detailFilters, entryType: detailFilters.tab === 'ALL' ? undefined : detailFilters.tab } + const { data } = await client.get(`${apiPrefix()}/homes/abooks/export`, { + params, + responseType: 'blob' }) + const url = URL.createObjectURL(data) + const a = document.createElement('a') + a.href = url + a.download = '간편장부.csv' + a.click() + URL.revokeObjectURL(url) + } catch { + message.error('엑셀 다운로드에 실패했습니다.') + } + } + + const summary = query.data?.summary + const summaryText = `현금시제 : (전월) 이월금 ${money(summary?.carryOver)} 원 + 입금 ${money(summary?.deposit)} 원 - 출금 ${money(summary?.withdraw)} 원 = 합계 ${money(summary?.total)} 원` + + const monthColumns = [ + { title: '거래일', dataIndex: 'entryDate', width: 110 }, + { title: '과목', dataIndex: 'category', width: 90 }, + { title: '품목', dataIndex: 'item', width: 140, ellipsis: true }, + { + title: '입금(+)', + dataIndex: 'deposit', + width: 100, + align: 'right' as const, + render: (v: number | null) => {v != null ? money(v) : '-'} }, + { + title: '출금(-)', + dataIndex: 'withdraw', + width: 100, + align: 'right' as const, + render: (v: number | null) => {v != null ? money(v) : '-'} }, + { + title: '카드입금(+)', + dataIndex: 'cardDeposit', + width: 110, + align: 'right' as const, + render: (v: number | null) => {v != null ? money(v) : '-'} }, + { + title: '카드출금(-)', + dataIndex: 'cardWithdraw', + width: 110, + align: 'right' as const, + render: (v: number | null) => {v != null ? money(v) : '-'} }, + { title: '비고', dataIndex: 'memo', width: 140, ellipsis: true, render: (v: string) => v || '-' }, + { title: '직원', dataIndex: 'employee', width: 90 }, + { + title: '관리', + width: 64, + fixed: 'right' as const, + align: 'center' as const, + render: (_: unknown, row: AbookRow) => ( + + +
+ + {view === 'detail' && ( + + )} + + +
+ + {view === 'month' ? ( + +
+
+
+ + {Array.from({ length: daysInMonth }, (_, i) => i + 1).map((d) => { + const isToday = month.isSame(today, 'month') && d === today.date() + return ( + + ) + })} +
+
+ + +
+
{summaryText}
+ +
+ ) : ( + <> + +
+
+ + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + ({ value: t, label: t }))} /> + + + + + + + + + + + + +
+ + +
+ + + + setCatOpen(false)} + onSave={(cats) => saveCategories.mutate(cats)} + /> +
+ ) +} diff --git a/frontend/src/pages/care/BbsPage.tsx b/frontend/src/pages/care/BbsPage.tsx new file mode 100644 index 0000000..e7e2f95 --- /dev/null +++ b/frontend/src/pages/care/BbsPage.tsx @@ -0,0 +1,296 @@ +import { useMemo, useState } from 'react' +import Modal from '../../components/DraggableModal' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { + Button, + Card, + Form, + Input, + Select, + Space, + Typography, + message } from 'antd' +import CareTable from '../../components/CareTable' +import { + EditOutlined, + PlusSquareOutlined, + ReloadOutlined, + SearchOutlined, + UnorderedListOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' + +type BbsRow = { + id: number + no?: number + title?: string + authorName?: string + views?: number + createdDate?: string + contents?: string +} + +type BbsResult = { + total: number + list: BbsRow[] +} + +const initial = { page: 0, size: 10, searchType: '제목', sort: 'id,desc' } as Record + +export default function BbsPage() { + const qc = useQueryClient() + const [form] = Form.useForm() + const [editForm] = Form.useForm() + const [filters, setFilters] = useState>(initial) + const [open, setOpen] = useState(false) + const [detail, setDetail] = useState(null) + + const query = useQuery({ + queryKey: ['homes-bbs', filters], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/bbs`, { + params: { + ...filters, + q: filters.keyword || undefined } }) + return unwrap(data) + } }) + + const refresh = () => qc.invalidateQueries({ queryKey: ['homes-bbs'] }) + + const createMut = useMutation({ + mutationFn: async (values: Record) => { + const { data } = await client.post(`${apiPrefix()}/homes/bbs`, { + title: values.title, + contents: values.contents }) + return unwrap(data as ApiResponse) + }, + onSuccess: () => { + message.success('게시물을 등록했습니다.') + setOpen(false) + editForm.resetFields() + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const openDetail = useMutation({ + mutationFn: async (id: number) => { + const { data } = await client.get>(`${apiPrefix()}/homes/bbs/${id}`) + return unwrap(data) + }, + onSuccess: (row) => { + setDetail(row) + qc.invalidateQueries({ queryKey: ['homes-bbs'] }) + }, + onError: (e: Error) => message.error(e.message) }) + + const onSearch = (values: Record) => { + setFilters({ + ...initial, + searchType: values.searchType || '제목', + keyword: values.keyword || undefined, + page: 0, + size: Number(values.size ?? filters.size ?? 10), + sort: filters.sort }) + } + + const onReset = () => { + form.resetFields() + form.setFieldsValue({ searchType: '제목', size: 10 }) + setFilters(initial) + } + + const columns = useMemo( + () => [ + { + title: '번호', + dataIndex: 'no', + width: 90, + align: 'center' as const, + sorter: true }, + { + title: '제목', + dataIndex: 'title', + ellipsis: true, + render: (v: string, row: BbsRow) => ( + + ) }, + { + title: '직원', + dataIndex: 'authorName', + width: 110, + align: 'center' as const, + sorter: true }, + { + title: '조회', + dataIndex: 'views', + width: 90, + align: 'center' as const, + sorter: true, + render: (v?: number) => (v == null ? 0 : Number(v).toLocaleString()) }, + { + title: '등록일', + dataIndex: 'createdDate', + width: 120, + align: 'center' as const, + sorter: true }, + ], + [], + ) + + return ( +
+
+
+

+ + 사내게시판 +

+

등록된 직원끼리 이용하실 수 있는 사내전용 게시판입니다.

+
+ +
+ + +
+
+ + + + + + + + + + + + + + + +
+ + +
+ + + + setDetail(null)} + footer={} + width={640} + destroyOnHidden + centered + > + {detail ? ( +
+ + {detail.title} + +
+ 직원 {detail.authorName || '-'} + 조회 {detail.views ?? 0} + 등록일 {detail.createdDate || '-'} +
+
+ {(detail.contents || '') + .split('\n') + .map((line, idx) => ( +

{line || '\u00A0'}

+ ))} +
+
+ ) : null} +
+
+ ) +} diff --git a/frontend/src/pages/care/BookingLogPage.tsx b/frontend/src/pages/care/BookingLogPage.tsx new file mode 100644 index 0000000..87f4916 --- /dev/null +++ b/frontend/src/pages/care/BookingLogPage.tsx @@ -0,0 +1,31 @@ +import { HomesLogListPage } from './HomesLogListPage' + +const ACTIONS = [ + '예약-등록', + '예약-수정', + '예약-삭제', + '예약-상태', +] + +/** 원본 예약관리 타이틀 잎 아이콘 */ +function LeafOutlined({ className }: { className?: string }) { + return ( + + + + + + ) +} + +export default function BookingLogPage() { + return ( + } + /> + ) +} diff --git a/frontend/src/pages/care/BookingPage.tsx b/frontend/src/pages/care/BookingPage.tsx new file mode 100644 index 0000000..ac44df4 --- /dev/null +++ b/frontend/src/pages/care/BookingPage.tsx @@ -0,0 +1,584 @@ +import { useMemo, useState } from 'react' +import Modal from '../../components/DraggableModal' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import dayjs, { type Dayjs } from 'dayjs' +import { + Button, + Card, + Checkbox, + DatePicker, + Form, + Input, + Select, + Space, + Tabs, + Upload, + message } from 'antd' +import CareTable from '../../components/CareTable' +import { + CloseOutlined, + DownloadOutlined, + EditOutlined, + PaperClipOutlined, + PlusSquareOutlined, + ReloadOutlined, + SearchOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' +import { options } from './homesShared' + +const { RangePicker } = DatePicker + +/** 원본 예약관리 타이틀 잎 아이콘 */ +function LeafOutlined({ className }: { className?: string }) { + return ( + + + + + + ) +} + +const INTERNET_OPTS = ['인터넷', 'TV', '전화', '와이파이', '스마트홈', '기타'] +const RENTAL_OPTS = ['정수기', '공기청정기', '비데', '매트리스', '가전', '기타'] + +type BookingRow = { + id: number + status?: string + bookingDate?: string + customerName?: string + phone?: string + internetProducts?: string + rentalProducts?: string + internetNote?: string + rentalNote?: string + zipcode?: string + address?: string + addressDetail?: string + employee?: string + receiveEmployee?: string + memo?: string + fileName?: string +} + +type BookingResult = { + total: number + list: BookingRow[] + counts?: Record + employees?: string[] + internetOptions?: string[] + rentalOptions?: string[] +} + +const initial = { page: 0, size: 10, tab: 'ALL' } as Record + +const maskName = (name?: string) => { + if (!name) return '-' + if (name.length <= 1) return name + if (name.length === 2) return `${name[0]}*` + return `${name[0]}*${name.slice(2)}` +} + +const maskPhone = (phone?: string) => { + if (!phone) return '-' + const digits = phone.replace(/\D/g, '') + if (digits.length < 7) return phone + if (digits.length === 10) return `${digits.slice(0, 3)}-****-${digits.slice(6)}` + return `${digits.slice(0, 3)}-****-${digits.slice(-4)}` +} + +const splitPhone = (phone?: string) => { + const digits = (phone || '').replace(/\D/g, '') + if (digits.length >= 10) { + return { + phone1: digits.slice(0, 3), + phone2: digits.slice(3, digits.length - 4), + phone3: digits.slice(-4) } + } + return { phone1: '010', phone2: '', phone3: '' } +} + +const splitProducts = (v?: string) => + (v || '') + .split(/[,,]/) + .map((s) => s.trim()) + .filter(Boolean) + +export default function BookingPage() { + const [form] = Form.useForm() + const [editForm] = Form.useForm() + const qc = useQueryClient() + const [filters, setFilters] = useState>(initial) + const [selected, setSelected] = useState([]) + const [editing, setEditing] = useState(null) + const [creating, setCreating] = useState(false) + + const query = useQuery({ + queryKey: ['homes-bookings', filters], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/bookings`, { + params: { + ...filters, + status: filters.tab === 'ALL' ? undefined : filters.tab } }) + return unwrap(data) + } }) + + const members = useQuery({ + queryKey: ['homes/members-options'], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/members`, { + params: { page: 0, size: 200 } }) + return unwrap(data) + } }) + + const refresh = () => { + qc.invalidateQueries({ queryKey: ['homes-bookings'] }) + setSelected([]) + } + + const staffOptions = useMemo(() => { + const fromApi = query.data?.employees || [] + const fromMembers = (members.data?.list || []).map((m) => String(m.name || '')).filter(Boolean) + return Array.from(new Set([...fromApi, ...fromMembers, '홍길동'])).map((v) => ({ value: v, label: v })) + }, [query.data, members.data]) + + const internetOptions = useMemo( + () => options(query.data?.internetOptions?.length ? query.data.internetOptions : INTERNET_OPTS), + [query.data?.internetOptions], + ) + const rentalOptions = useMemo( + () => options(query.data?.rentalOptions?.length ? query.data.rentalOptions : RENTAL_OPTS), + [query.data?.rentalOptions], + ) + + const tabs = useMemo( + () => [ + { key: 'ALL', label: `전체 ${query.data?.counts?.ALL ?? query.data?.total ?? 0}` }, + { key: '접수', label: `접수 ${query.data?.counts?.['접수'] ?? 0}` }, + { key: '완료', label: `완료 ${query.data?.counts?.['완료'] ?? 0}` }, + { key: '보류', label: `보류 ${query.data?.counts?.['보류'] ?? 0}` }, + ], + [query.data], + ) + + const save = useMutation({ + mutationFn: async (values: Record) => { + const payload: Record = { + status: values.status || editing?.status || '접수', + bookingDate: values.bookingDate ? dayjs(values.bookingDate as Dayjs).format('YYYY-MM-DD') : undefined, + receiveEmployee: values.receiveEmployee, + employee: values.receiveEmployee, + customerName: values.customerName, + phone1: values.phone1, + phone2: values.phone2, + phone3: values.phone3, + zipcode: values.zipcode, + address: values.address, + addressDetail: values.addressDetail, + internetProducts: values.internetProducts || [], + rentalProducts: values.rentalProducts || [], + internetNote: values.internetNote, + rentalNote: values.rentalNote, + memo: values.memo, + fileName: values.fileName } + if (editing) { + const { data } = await client.put(`${apiPrefix()}/homes/bookings/${editing.id}`, payload) + return unwrap(data as ApiResponse) + } + const { data } = await client.post(`${apiPrefix()}/homes/bookings`, payload) + return unwrap(data as ApiResponse) + }, + onSuccess: () => { + message.success(editing ? '수정했습니다.' : '예약을 등록했습니다.') + setCreating(false) + setEditing(null) + editForm.resetFields() + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const deleteMut = useMutation({ + mutationFn: async (ids: number[]) => { + const { data } = await client.post(`${apiPrefix()}/homes/bookings/delete`, { ids }) + return unwrap(data as ApiResponse<{ deleted: number }>) + }, + onSuccess: (data) => { + message.success(`${data.deleted}건을 삭제했습니다.`) + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const setPreset = (from: Dayjs, to: Dayjs) => form.setFieldsValue({ dates: [from, to] }) + + const onSearch = (values: Record) => { + const dates = values.dates as [Dayjs, Dayjs] | undefined + setFilters({ + ...initial, + receiveEmployee: values.receiveEmployee || undefined, + internetProduct: values.internetProduct || undefined, + rentalProduct: values.rentalProduct || undefined, + phoneTail: values.phoneTail || undefined, + q: values.q || undefined, + dateFrom: dates?.[0]?.format('YYYY-MM-DD'), + dateTo: dates?.[1]?.format('YYYY-MM-DD'), + page: 0, + size: values.size ?? filters.size ?? 10, + tab: filters.tab }) + } + + const reset = () => { + form.resetFields() + form.setFieldsValue({ size: 10 }) + setFilters(initial) + } + + const openCreate = () => { + setEditing(null) + setCreating(true) + const defaultStaff = staffOptions[0]?.value || '홍길동' + editForm.setFieldsValue({ + bookingDate: dayjs(), + receiveEmployee: defaultStaff, + customerName: undefined, + phone1: '010', + phone2: '', + phone3: '', + zipcode: undefined, + address: undefined, + addressDetail: undefined, + internetProducts: [], + rentalProducts: [], + internetNote: undefined, + rentalNote: undefined, + memo: undefined, + fileName: undefined, + status: '접수' }) + } + + const openEdit = (row: BookingRow) => { + setCreating(false) + setEditing(row) + const phones = splitPhone(row.phone) + editForm.setFieldsValue({ + bookingDate: row.bookingDate ? dayjs(row.bookingDate) : dayjs(), + receiveEmployee: row.receiveEmployee || row.employee, + customerName: row.customerName, + ...phones, + zipcode: row.zipcode, + address: row.address, + addressDetail: row.addressDetail, + internetProducts: splitProducts(row.internetProducts), + rentalProducts: splitProducts(row.rentalProducts), + internetNote: row.internetNote, + rentalNote: row.rentalNote, + memo: row.memo, + fileName: row.fileName, + status: row.status || '접수' }) + } + + const searchAddress = () => { + message.info('주소검색(데모) — 직접 입력해 주세요.') + editForm.setFieldsValue({ + zipcode: editForm.getFieldValue('zipcode') || '06236', + address: editForm.getFieldValue('address') || '서울특별시 강남구' }) + } + + const download = async () => { + try { + const { data } = await client.get(`${apiPrefix()}/homes/bookings/export`, { + params: { ...filters, status: filters.tab === 'ALL' ? undefined : filters.tab }, + responseType: 'blob' }) + const url = URL.createObjectURL(data) + const a = document.createElement('a') + a.href = url + a.download = '예약관리.csv' + a.click() + URL.revokeObjectURL(url) + } catch { + message.error('엑셀 다운로드에 실패했습니다.') + } + } + + const columns = [ + { + title: ( + 0 && selected.length < (query.data?.list?.length || 0)} + onChange={(e) => setSelected(e.target.checked ? (query.data?.list || []).map((r) => r.id) : [])} + /> + ), + width: 42, + render: (_: unknown, row: BookingRow) => ( + setSelected((prev) => (e.target.checked ? [...prev, row.id] : prev.filter((id) => id !== row.id)))} + /> + ) }, + { + title: '접수일', + dataIndex: 'bookingDate', + width: 110, + sorter: (a: BookingRow, b: BookingRow) => String(a.bookingDate || '').localeCompare(String(b.bookingDate || '')) }, + { + title: '상태', + dataIndex: 'status', + width: 70, + render: (v: string) => {v || '-'} }, + { + title: '고객명', + dataIndex: 'customerName', + width: 90, + render: (v: string) => maskName(v) }, + { + title: '연락처', + dataIndex: 'phone', + width: 130, + render: (v: string) => maskPhone(v) }, + { + title: '인터넷상품', + dataIndex: 'internetProducts', + width: 180, + ellipsis: true, + render: (v: string) => v || '-' }, + { + title: '홈렌탈상품', + dataIndex: 'rentalProducts', + width: 180, + ellipsis: true, + render: (v: string) => v || '-' }, + { + title: '접수직원', + dataIndex: 'employee', + width: 90, + render: (_: string, row: BookingRow) => row.receiveEmployee || row.employee || '-' }, + { + title: '관리', + width: 64, + fixed: 'right' as const, + align: 'center' as const, + render: (_: unknown, row: BookingRow) => ( + + + +
+ + +
+
+ + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + { + if (file.size > 20 * 1024 * 1024) { + message.error('최대 20M까지 첨부할 수 있습니다.') + return Upload.LIST_IGNORE + } + editForm.setFieldValue('fileName', file.name) + message.success(`${file.name} 선택됨 (데모)`) + return false + }} + maxCount={1} + showUploadList={false} + > + + +
* 다량의 파일은 압축해서 올려주십시요. (최대 20M)
+ prev.fileName !== cur.fileName} noStyle> + {() => { + const name = editForm.getFieldValue('fileName') + return name ?
{name}
: null + }} +
+
+
+
* 고객명, 연락처는 고객관리에도 동시등록됩니다.
+ + + + +
+ + +
+ ) +} diff --git a/frontend/src/pages/care/CarePlaceholderPage.tsx b/frontend/src/pages/care/CarePlaceholderPage.tsx new file mode 100644 index 0000000..fff08e9 --- /dev/null +++ b/frontend/src/pages/care/CarePlaceholderPage.tsx @@ -0,0 +1,17 @@ +import { Card, Typography } from 'antd' + +type Props = { + title: string + description?: string +} + +export default function CarePlaceholderPage({ + title, + description = '원본 비즈케어홈즈 메뉴와 동일한 경로로 연결해 두었습니다. 상세 화면은 순차 구현 예정입니다.' }: Props) { + return ( + + {title} + {description} + + ) +} diff --git a/frontend/src/pages/care/ContractAdjustPage.tsx b/frontend/src/pages/care/ContractAdjustPage.tsx new file mode 100644 index 0000000..0b20ca1 --- /dev/null +++ b/frontend/src/pages/care/ContractAdjustPage.tsx @@ -0,0 +1,389 @@ +import { useEffect, useMemo, useState } from 'react' +import { useMutation, useQuery } from '@tanstack/react-query' +import dayjs, { type Dayjs } from 'dayjs' +import { Table, Button, Card, DatePicker, Form, InputNumber, Select, Space, Typography, message } from 'antd' +import CareTable from '../../components/CareTable' +import { RadarChartOutlined, SearchOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' +import { money, options } from './homesShared' + +const { RangePicker } = DatePicker + +type AdjustRow = { + id: number + kind?: string + status?: string + contractDate?: string + customerName?: string + phone?: string + products?: string + months?: number + agency?: string + employee?: string + policySum?: number + settleAmount?: number + margin?: number +} + +type AdjustResult = { + total: number + list: AdjustRow[] + employees?: string[] + products?: string[] +} + +const KIND_OPTIONS = [ + { value: 'INTERNET', label: '인터넷' }, + { value: 'HOME', label: '홈렌탈' }, +] +const CATEGORY_OPTIONS = ['인터넷', 'TV', '전화', '와이파이', '스마트홈', '정수기', '공기청정기', '비데', '매트리스', '가전', '기타'] +const MONTH_OPTIONS = [ + { value: '12', label: '12개월' }, + { value: '24', label: '24개월' }, + { value: '36', label: '36개월' }, + { value: '48', label: '48개월' }, + { value: '60', label: '60개월' }, +] +const STATUS_OPTIONS = ['접수', '신청', '완료', '철회'] + +export default function ContractAdjustPage() { + const [form] = Form.useForm() + const [searched, setSearched] = useState(false) + const [filters, setFilters] = useState>({}) + const [rows, setRows] = useState([]) + const [batch, setBatch] = useState({ + policySum: undefined as number | undefined, + settleAmount: undefined as number | undefined, + margin: undefined as number | undefined }) + + const enabled = searched && !!filters.dateFrom && !!filters.dateTo + const optionParams = useMemo(() => ({ + dateFrom: dayjs().startOf('month').format('YYYY-MM-DD'), + dateTo: dayjs().format('YYYY-MM-DD') }), []) + const optionsQuery = useQuery({ + queryKey: ['homes-contract-adjust-options', optionParams], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/contracts/adjust`, { params: optionParams }) + return unwrap(data) + } }) + const query = useQuery({ + queryKey: ['homes-contract-adjust', filters], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/contracts/adjust`, { params: filters }) + return unwrap(data) + }, + enabled }) + + useEffect(() => { + if (!query.data?.list) return + setRows(query.data.list.map((row) => ({ + ...row, + policySum: Number(row.policySum ?? 0), + settleAmount: Number(row.settleAmount ?? 0), + margin: Number(row.margin ?? 0) }))) + }, [query.data]) + + const setPreset = (from: Dayjs, to: Dayjs) => { + if (to.diff(from, 'day') > 30) { + message.warning('최대 31일까지 조회할 수 있습니다.') + return + } + form.setFieldsValue({ dates: [from, to] }) + } + + const onSearch = (values: Record) => { + const dates = values.dates as [Dayjs, Dayjs] | undefined + if (!dates?.[0] || !dates?.[1]) { + message.warning('계약일을 선택하세요.') + return + } + if (dates[1].diff(dates[0], 'day') > 30) { + message.warning('최대 31일까지 조회할 수 있습니다.') + return + } + setSearched(true) + setFilters({ + dateFrom: dates[0].format('YYYY-MM-DD'), + dateTo: dates[1].format('YYYY-MM-DD'), + kind: values.kind || undefined, + category: values.category || undefined, + product: values.product || undefined, + months: values.months || undefined, + status: values.status || undefined, + employee: values.employee || undefined }) + } + + const updateRow = (id: number, field: 'policySum' | 'settleAmount' | 'margin', value: number | null) => { + setRows((prev) => prev.map((row) => (row.id === id ? { ...row, [field]: value ?? 0 } : row))) + } + + const applyBatch = (field: 'policySum' | 'settleAmount' | 'margin') => { + const value = batch[field] + if (value == null) { + message.warning('금액을 입력하세요.') + return + } + setRows((prev) => prev.map((row) => ({ ...row, [field]: value }))) + message.success('일괄 입력했습니다.') + } + + const saveOne = useMutation({ + mutationFn: async (row: AdjustRow) => { + const { data } = await client.put(`${apiPrefix()}/homes/contracts/${row.id}`, { + policySum: row.policySum, + settleAmount: row.settleAmount, + margin: row.margin }) + return unwrap(data as ApiResponse) + }, + onSuccess: () => message.success('변경했습니다.'), + onError: (e: Error) => message.error(e.message) }) + + const saveAll = useMutation({ + mutationFn: async () => { + const { data } = await client.post(`${apiPrefix()}/homes/contracts/adjust/batch`, { + items: rows.map((row) => ({ + id: row.id, + policySum: row.policySum, + settleAmount: row.settleAmount, + margin: row.margin })) }) + return unwrap(data as ApiResponse<{ updated: number }>) + }, + onSuccess: (data) => message.success(`${data.updated}건을 변경했습니다.`), + onError: (e: Error) => message.error(e.message) }) + + const employeeOptions = useMemo(() => { + const fromApi = [...(optionsQuery.data?.employees || []), ...(query.data?.employees || [])] + const fromRows = rows.map((r) => r.employee).filter(Boolean) as string[] + return Array.from(new Set([...fromApi, ...fromRows])) + }, [optionsQuery.data?.employees, query.data?.employees, rows]) + + const productOptions = useMemo(() => { + const fromApi = [...(optionsQuery.data?.products || []), ...(query.data?.products || [])] + return Array.from(new Set(fromApi)).map((v) => ({ value: v, label: v })) + }, [optionsQuery.data?.products, query.data?.products]) + + const columns = [ + { + title: '종류', + dataIndex: 'kind', + width: 80, + render: (v: string) => (v === 'HOME' ? '홈렌탈' : '인터넷') }, + { title: '상태', dataIndex: 'status', width: 70 }, + { title: '계약일', dataIndex: 'contractDate', width: 100 }, + { title: '고객명', dataIndex: 'customerName', width: 90 }, + { title: '가입상품', dataIndex: 'products', width: 180, ellipsis: true }, + { + title: '약정', + dataIndex: 'months', + width: 80, + render: (v: number) => (v ? `${v}개월` : '-') }, + { title: '거래처', dataIndex: 'agency', width: 110, ellipsis: true }, + { title: '직원', dataIndex: 'employee', width: 90 }, + { + title: '정책합산', + dataIndex: 'policySum', + width: 120, + render: (_: unknown, row: AdjustRow) => ( + updateRow(row.id, 'policySum', v)} + /> + ) }, + { + title: '정산금액', + dataIndex: 'settleAmount', + width: 120, + render: (_: unknown, row: AdjustRow) => ( + updateRow(row.id, 'settleAmount', v)} + /> + ) }, + { + title: '판매마진', + dataIndex: 'margin', + width: 120, + render: (_: unknown, row: AdjustRow) => ( + updateRow(row.id, 'margin', v)} + /> + ) }, + { + title: '개별', + width: 70, + fixed: 'right' as const, + render: (_: unknown, row: AdjustRow) => ( + + ) }, + ] + + return ( +
+
+
+

+ + 일괄정책변경 +

+

등록된 계약정보에 일괄적으로 기본정책금액을 변경하실 수 있습니다. (단일상품의 수량이 2개 이상인 경우 제외됩니다.)

+
+
+ + +
+
+ 계약일 + + + + + + + + + + + * 최대 31일 조회 + +
+ +
+ 검색조건 + + + + + + + + + +
+
+
+ + {enabled && ( + +
+ 검색된 계약 {query.data?.total ?? rows.length} +
+ +
+ 원하시는 금액을 입력 후 엔터키를 눌러주세요. + + + setBatch((s) => ({ ...s, policySum: v ?? undefined }))} + onPressEnter={() => applyBatch('policySum')} + /> + + + + setBatch((s) => ({ ...s, settleAmount: v ?? undefined }))} + onPressEnter={() => applyBatch('settleAmount')} + /> + + + + setBatch((s) => ({ ...s, margin: v ?? undefined }))} + onPressEnter={() => applyBatch('margin')} + /> + + + +
+ + ( + + 합계 + + {money(rows.reduce((s, r) => s + Number(r.policySum || 0), 0))} + + + {money(rows.reduce((s, r) => s + Number(r.settleAmount || 0), 0))} + + + {money(rows.reduce((s, r) => s + Number(r.margin || 0), 0))} + + + + )} + /> + +
+ +
+
+ )} +
+ ) +} diff --git a/frontend/src/pages/care/ContractBalancePage.tsx b/frontend/src/pages/care/ContractBalancePage.tsx new file mode 100644 index 0000000..1193f23 --- /dev/null +++ b/frontend/src/pages/care/ContractBalancePage.tsx @@ -0,0 +1,459 @@ +import { useMemo, useState } from 'react' +import Modal from '../../components/DraggableModal' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import dayjs, { type Dayjs } from 'dayjs' +import { + Button, + Card, + Checkbox, + DatePicker, + Form, + Input, + InputNumber, + Radio, + Select, + Space, + Tabs, + Tooltip, + message } from 'antd' +import CareTable from '../../components/CareTable' +import { + CloseOutlined, + DownloadOutlined, + EditOutlined, + MailOutlined, + PlusSquareOutlined, + RadarChartOutlined, + ReloadOutlined, + SearchOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' +import { options, type PageResult } from './homesShared' + +const { RangePicker } = DatePicker + +const TABS = [ + ['ALL', '전체'], + ['정산차감', '정산차감'], + ['정산추가', '정산추가'], +] as const + +type BalanceRow = { + id: number + balanceType?: string + targetType?: string + targetName?: string + baseDate?: string + reason?: string + amount?: number + memo?: string +} + +type BalanceSearch = PageResult & { + list: BalanceRow[] + counts?: Record + employees?: string[] + agencies?: string[] + reasons?: string[] +} + +const isDeduct = (gubun?: string) => Boolean(gubun && gubun.includes('차감')) + +const initial = { + page: 0, + size: 10, + tab: 'ALL' } as Record + +export default function ContractBalancePage() { + const [form] = Form.useForm() + const [editForm] = Form.useForm() + const qc = useQueryClient() + const [filters, setFilters] = useState>(initial) + const [selected, setSelected] = useState([]) + const [editing, setEditing] = useState(null) + const [creating, setCreating] = useState(false) + const [amountSign, setAmountSign] = useState<'+' | '-'>('-') + + const query = useQuery({ + queryKey: ['homes-balances', filters], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/balances`, { params: filters }) + return unwrap(data) + } }) + + const members = useQuery({ + queryKey: ['homes/members-options'], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/members`, { params: { page: 0, size: 200 } }) + return unwrap(data) + } }) + + const agencies = useQuery({ + queryKey: ['homes/agencies-options'], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/agencies`, { params: { page: 0, size: 200 } }) + return unwrap(data) + } }) + + const refresh = () => { + qc.invalidateQueries({ queryKey: ['homes-balances'] }) + setSelected([]) + } + + const employeeOptions = useMemo(() => { + const fromApi = (query.data?.employees || []) as string[] + const fromMembers = (members.data?.list || []).map((m) => String(m.name || '')).filter(Boolean) + return Array.from(new Set([...fromApi, ...fromMembers])).map((v) => ({ value: v, label: v })) + }, [query.data?.employees, members.data]) + + const agencyOptions = useMemo(() => { + const fromApi = (query.data?.agencies || []) as string[] + const fromAgencies = (agencies.data?.list || []).map((a) => String(a.name || '')).filter(Boolean) + return Array.from(new Set([...fromApi, ...fromAgencies])).map((v) => ({ value: v, label: v })) + }, [query.data?.agencies, agencies.data]) + + const reasonOptions = useMemo(() => { + const fromApi = (query.data?.reasons || []) as string[] + return fromApi.map((v) => ({ value: v, label: v })) + }, [query.data?.reasons]) + + const save = useMutation({ + mutationFn: async (values: Record) => { + const raw = Number(values.amountAbs ?? 0) + const signed = amountSign === '-' ? -Math.abs(raw) : Math.abs(raw) + const payload: Record = { + targetType: values.targetType, + targetName: values.targetName, + balanceType: values.balanceType, + reason: values.reason, + memo: values.memo, + amount: signed, + baseDate: values.baseDate ? dayjs(values.baseDate as Dayjs).format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD') } + if (editing) { + const { data } = await client.put(`${apiPrefix()}/homes/balances/${editing.id}`, payload) + return unwrap(data as ApiResponse) + } + const { data } = await client.post(`${apiPrefix()}/homes/balances`, payload) + return unwrap(data as ApiResponse) + }, + onSuccess: () => { + message.success(editing ? '수정했습니다.' : '후정산을 등록했습니다.') + setCreating(false) + setEditing(null) + editForm.resetFields() + refresh() + }, + onError: (e: Error) => message.error(e.message || '저장에 실패했습니다.') }) + + const remove = useMutation({ + mutationFn: async (ids: number[]) => { + const { data } = await client.post(`${apiPrefix()}/homes/balances/delete`, { ids }) + return unwrap(data as ApiResponse<{ deleted: number }>) + }, + onSuccess: (data) => { + message.success(`${data.deleted}건을 삭제했습니다.`) + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const setPreset = (from: Dayjs, to: Dayjs) => form.setFieldsValue({ dates: [from, to] }) + + const onSearch = (values: Record) => { + const dates = values.dates as [Dayjs, Dayjs] | undefined + setFilters({ + ...initial, + employee: values.employee || undefined, + agency: values.agency || undefined, + reason: values.reason || undefined, + q: values.q || undefined, + dateFrom: dates?.[0]?.format('YYYY-MM-DD'), + dateTo: dates?.[1]?.format('YYYY-MM-DD'), + page: 0, + size: values.size ?? filters.size ?? 10, + tab: filters.tab }) + } + + const reset = () => { + form.resetFields() + form.setFieldsValue({ size: 10 }) + setFilters(initial) + } + + const openCreate = () => { + setEditing(null) + setCreating(true) + setAmountSign('-') + editForm.setFieldsValue({ + targetType: '직원', + targetName: undefined, + balanceType: '정산차감', + baseDate: dayjs(), + amountAbs: undefined, + reason: undefined, + memo: undefined }) + } + + const openEdit = (row: BalanceRow) => { + setCreating(false) + setEditing(row) + const amount = Number(row.amount ?? 0) + setAmountSign(amount < 0 || isDeduct(row.balanceType) ? '-' : '+') + editForm.setFieldsValue({ + targetType: row.targetType || '직원', + targetName: row.targetName, + balanceType: row.balanceType || '정산차감', + amountAbs: Math.abs(amount), + reason: row.reason, + memo: row.memo, + baseDate: row.baseDate ? dayjs(row.baseDate) : dayjs() }) + } + + const download = async () => { + try { + const { data } = await client.get(`${apiPrefix()}/homes/balances/export`, { params: filters, responseType: 'blob' }) + const url = URL.createObjectURL(data) + const a = document.createElement('a') + a.href = url + a.download = '계약후정산.csv' + a.click() + URL.revokeObjectURL(url) + } catch { + message.error('엑셀 다운로드에 실패했습니다.') + } + } + + const targetType = Form.useWatch('targetType', editForm) + + const columns = [ + { + title: ( + 0 && selected.length < (query.data?.list?.length || 0)} + onChange={(e) => setSelected(e.target.checked ? (query.data?.list || []).map((r) => r.id) : [])} + /> + ), + width: 42, + render: (_: unknown, row: BalanceRow) => ( + setSelected((prev) => (e.target.checked ? [...prev, row.id] : prev.filter((id) => id !== row.id)))} + /> + ) }, + { title: '후정산일', dataIndex: 'baseDate', width: 110, sorter: (a: BalanceRow, b: BalanceRow) => String(a.baseDate || '').localeCompare(String(b.baseDate || '')) }, + { title: '대상', dataIndex: 'targetType', width: 80 }, + { title: '대상명', dataIndex: 'targetName', width: 120, ellipsis: true }, + { + title: '구분', + dataIndex: 'balanceType', + width: 100, + render: (v: string) => {v} }, + { title: '사유', dataIndex: 'reason', width: 140, ellipsis: true }, + { + title: '정산금액', + dataIndex: 'amount', + width: 110, + align: 'right' as const, + render: (v: unknown, row: BalanceRow) => { + if (v == null || v === '') return '-' + const n = Number(v) + const cls = isDeduct(row.balanceType) || n < 0 ? 'balance-amount-deduct' : 'balance-amount' + return {n.toLocaleString()} + } }, + { + title: '비고', + dataIndex: 'memo', + width: 54, + align: 'center' as const, + render: (v: string) => (v ? : '-') }, + { + title: '관리', + width: 58, + fixed: 'right' as const, + render: (_: unknown, row: BalanceRow) => ( + + + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + setAmountSign(String(e.target.value).includes('차감') ? '-' : '+')} + /> + + + + + + + + + + + + + + + + +
+ + +
+ + +
+ ) +} diff --git a/frontend/src/pages/care/ContractHomePage.tsx b/frontend/src/pages/care/ContractHomePage.tsx new file mode 100644 index 0000000..0a51190 --- /dev/null +++ b/frontend/src/pages/care/ContractHomePage.tsx @@ -0,0 +1,13 @@ +import { ContractListPage } from './ContractListShared' + +export default function ContractHomePage() { + return ( + + ) +} diff --git a/frontend/src/pages/care/ContractInternetPage.tsx b/frontend/src/pages/care/ContractInternetPage.tsx new file mode 100644 index 0000000..31d8d73 --- /dev/null +++ b/frontend/src/pages/care/ContractInternetPage.tsx @@ -0,0 +1,13 @@ +import { ContractListPage } from './ContractListShared' + +export default function ContractInternetPage() { + return ( + + ) +} diff --git a/frontend/src/pages/care/ContractListPage.tsx b/frontend/src/pages/care/ContractListPage.tsx new file mode 100644 index 0000000..8f9bda7 --- /dev/null +++ b/frontend/src/pages/care/ContractListPage.tsx @@ -0,0 +1,139 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { useQuery } from '@tanstack/react-query' +import dayjs, { type Dayjs } from 'dayjs' +import { Button, Card, DatePicker, Form, Input, Select, Space } from 'antd' +import CareTable from '../../components/CareTable' +import { + CloseOutlined, FileTextOutlined, PlusOutlined, ReloadOutlined, SearchOutlined, SolutionOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' + +const { RangePicker } = DatePicker +const options = (values: string[]) => values.map((value) => ({ value, label: value })) +const money = (v: unknown) => (v == null || v === '' ? '-' : Number(v).toLocaleString()) + +type Contract = Record & { id: number } +type SearchResult = { total: number; list: Contract[] } + +const initial = { page: 0, size: 15 } as Record + +export default function ContractListPage() { + const [form] = Form.useForm() + const navigate = useNavigate() + const [filters, setFilters] = useState>(initial) + + const query = useQuery({ + queryKey: ['contracts-search', filters], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/contracts/search`, { params: filters }) + return unwrap(data) + } }) + + const setPreset = (from: Dayjs, to: Dayjs) => form.setFieldsValue({ dates: [from, to] }) + + const onSearch = (values: Record) => { + const dates = values.dates as [Dayjs, Dayjs] | undefined + setFilters({ + ...initial, + ...values, + dateFrom: dates?.[0]?.format('YYYY-MM-DD'), + dateTo: dates?.[1]?.format('YYYY-MM-DD'), + page: 0, + size: values.size ?? filters.size ?? 15 }) + } + + const reset = () => { + form.resetFields() + form.setFieldsValue({ size: 15 }) + setFilters(initial) + } + + const columns = [ + { title: '계약일', dataIndex: 'contractDate', width: 100, sorter: true }, + { title: '상태', dataIndex: 'status', width: 80 }, + { title: '고객명', dataIndex: 'customerName', width: 100, sorter: true }, + { title: '연락처', dataIndex: 'phone', width: 120 }, + { title: '상품', dataIndex: 'productName', width: 140, ellipsis: true }, + { title: '통신사', dataIndex: 'telecom', width: 70 }, + { title: '계약금액', dataIndex: 'amount', width: 110, align: 'right' as const, render: money }, + { title: '월정액', dataIndex: 'monthlyFee', width: 100, align: 'right' as const, render: money }, + { title: '담당자', dataIndex: 'employee', width: 90 }, + { + title: '관리', width: 58, fixed: 'right' as const, + render: (_: unknown, row: Contract) => ( + +
+ + +
+
+ + + + + + + + + +
+
+ + + + + + + + + + ({ value: v, label: v }))} /> + + + + + + + + ({ value: v, label: v }))} /> + +
+ + +
+ + + + setSumOpen(false)} footer={null} destroyOnHidden width={400}> + + {selected.length ? `선택 ${selectedSums.count}건` : `현재 목록 ${selectedSums.count}건`} 기준입니다. + +
+
정산금액 합계{selectedSums.settle.toLocaleString()}원
+
+ 판매마진 합계 + + {selectedSums.margin.toLocaleString()}원 + +
+
+
+ +
+
+
+ ) +} diff --git a/frontend/src/pages/care/ContractLogPage.tsx b/frontend/src/pages/care/ContractLogPage.tsx new file mode 100644 index 0000000..ada92e3 --- /dev/null +++ b/frontend/src/pages/care/ContractLogPage.tsx @@ -0,0 +1,23 @@ +import { HomesLogListPage } from './HomesLogListPage' + +const ACTIONS = [ + '인터넷접수-등록', + '인터넷접수-수정', + '인터넷접수-삭제', + '인터넷접수-상태', + '홈렌탈접수-등록', + '홈렌탈접수-수정', + '홈렌탈접수-삭제', + '홈렌탈접수-상태', +] + +export default function ContractLogPage() { + return ( + + ) +} diff --git a/frontend/src/pages/care/ContractWritePage.tsx b/frontend/src/pages/care/ContractWritePage.tsx new file mode 100644 index 0000000..bf05098 --- /dev/null +++ b/frontend/src/pages/care/ContractWritePage.tsx @@ -0,0 +1,467 @@ +import { useEffect, useMemo, useState } from 'react' +import { useNavigate, useSearchParams } from 'react-router-dom' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import dayjs from 'dayjs' +import { + Button, Card, Checkbox, Col, DatePicker, Form, Input, InputNumber, Radio, Row, Select, Space, message } from 'antd' +import CareTable from '../../components/CareTable' +import { EditOutlined, PlusOutlined, SettingOutlined, UnorderedListOutlined } from '@ant-design/icons' +import Modal from '../../components/DraggableModal' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' +import { money, options, type PageResult, type Row as DataRow } from './homesShared' + +type ProductLine = { + key: string + productId?: number + name: string + months?: number + policyAmount?: number +} + +function splitPhone(phone?: string) { + const d = String(phone ?? '').replace(/\D/g, '') + return { + phone1: d.slice(0, 3) || '010', + phone2: d.slice(3, 7), + phone3: d.slice(7, 11) } +} + +function joinPhone(v: Record, prefix = 'phone') { + const a = String(v[`${prefix}1`] ?? '').trim() + const b = String(v[`${prefix}2`] ?? '').trim() + const c = String(v[`${prefix}3`] ?? '').trim() + if (!a && !b && !c) return '' + return `${a}-${b}-${c}` +} + +export default function ContractWritePage() { + const [params] = useSearchParams() + const type = params.get('type') === 'home' ? 'HOME' : 'INTERNET' + const id = params.get('id') + const navigate = useNavigate() + const qc = useQueryClient() + const [form] = Form.useForm() + const [productForm] = Form.useForm() + const [lines, setLines] = useState([]) + const [productOpen, setProductOpen] = useState(false) + const back = type === 'HOME' ? '/care/contract/home' : '/care/contract/internet' + const title = type === 'HOME' ? '홈렌탈 접수등록' : '인터넷 접수등록' + const guide = type === 'HOME' + ? '홈렌탈상품의 계약정보를 아래의 양식 순서대로 입력해주세요. (필수 표시는 필수항목입니다.)' + : '인터넷상품의 계약정보를 아래의 양식 순서대로 입력해주세요. (필수 표시는 필수항목입니다.)' + const sameContact = Form.useWatch('sameContact', form) + + const detail = useQuery({ + queryKey: ['homes-contract', id], + enabled: !!id, + queryFn: async () => { + const { data } = await client.get>>(`${apiPrefix()}/homes/contracts/${id}`) + return unwrap(data) + } }) + + const agencies = useQuery({ + queryKey: ['homes/agencies-options'], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/agencies`, { params: { page: 0, size: 200 } }) + return unwrap(data) + } }) + + const products = useQuery({ + queryKey: ['homes/products-for-contract', type], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/products`, { + params: { kind: type, category: 'ALL', page: 0, size: 200 } }) + return unwrap(data) + } }) + + useEffect(() => { + if (!detail.data) return + const d = detail.data + const phone = splitPhone(String(d.phone ?? '')) + const installPhone = splitPhone(String(d.installPhone ?? d.phone ?? '')) + const birth = String(d.birthDate ?? '').split('-') + form.setFieldsValue({ + ...d, + ...phone, + installPhone1: installPhone.phone1, + installPhone2: installPhone.phone2, + installPhone3: installPhone.phone3, + birthYear: birth[0] || undefined, + birthMonth: birth[1] || undefined, + birthDay: birth[2] || undefined, + contractDate: d.contractDate ? dayjs(String(d.contractDate)) : dayjs(), + installDate: d.installDate ? dayjs(String(d.installDate)) : undefined, + customerGrade: d.customerGrade || '분류없음', + sameContact: d.sameContact ?? false }) + const names = String(d.products ?? '').split(',').map((v) => v.trim()).filter(Boolean) + setLines(names.map((name, i) => ({ + key: `e-${i}`, + name, + months: Number(d.months || 0) || undefined, + policyAmount: names.length ? Number(d.policySum || 0) / names.length : undefined }))) + }, [detail.data, form]) + + useEffect(() => { + if (!sameContact) return + const values = form.getFieldsValue() + form.setFieldsValue({ + installPhone1: values.phone1, + installPhone2: values.phone2, + installPhone3: values.phone3, + installZip: values.zipcode, + installAddress: values.address, + installAddressDetail: values.addressDetail }) + }, [sameContact, form]) + + const policySum = useMemo( + () => lines.reduce((s, l) => s + Number(l.policyAmount || 0), 0), + [lines], + ) + + const save = useMutation({ + mutationFn: async (values: Record) => { + if (!lines.length) throw new Error('계약 상품을 추가하세요.') + const birthParts = [values.birthYear, values.birthMonth, values.birthDay].filter(Boolean) + const payload: Record = { + ...values, + kind: type, + status: values.status || (id ? detail.data?.status : '접수') || '접수', + phone: joinPhone(values, 'phone'), + installPhone: joinPhone(values, 'installPhone'), + birthDate: birthParts.length === 3 + ? `${values.birthYear}-${String(values.birthMonth).padStart(2, '0')}-${String(values.birthDay).padStart(2, '0')}` + : null, + contractDate: values.contractDate ? dayjs(values.contractDate as string).format('YYYY-MM-DD') : null, + installDate: values.installDate ? dayjs(values.installDate as string).format('YYYY-MM-DD') : null, + products: lines.map((l) => l.name).join(', '), + months: lines[0]?.months ?? null, + policySum, + settleAmount: values.settleAmount ?? policySum, + margin: values.margin ?? 0 } + ;['phone1', 'phone2', 'phone3', 'installPhone1', 'installPhone2', 'installPhone3', 'birthYear', 'birthMonth', 'birthDay'].forEach((k) => { + delete payload[k] + }) + if (id) { + const { data } = await client.put(`${apiPrefix()}/homes/contracts/${id}`, payload) + return unwrap(data) + } + const { data } = await client.post(`${apiPrefix()}/homes/contracts`, payload) + return unwrap(data) + }, + onSuccess: () => { + message.success(id ? '수정했습니다.' : '등록했습니다.') + qc.invalidateQueries({ queryKey: ['homes/contracts'] }) + navigate(back) + }, + onError: (e: Error) => message.error(e.message) }) + + const agencyOptions = useMemo( + () => (agencies.data?.list || []).map((a) => ({ value: String(a.name), label: String(a.name) })), + [agencies.data], + ) + + const productOptions = useMemo( + () => (products.data?.list || []).map((p: DataRow) => ({ + value: p.id, + label: `${p.company ? `[${p.company}] ` : ''}${p.name}`, + raw: p })), + [products.data], + ) + + const addProduct = (values: { productId: number }) => { + const found = productOptions.find((o) => o.value === values.productId) + if (!found) return + const p = found.raw + setLines((prev) => [ + ...prev, + { + key: `p-${p.id}-${Date.now()}`, + productId: Number(p.id), + name: String(p.name), + months: Number(p.months || 0) || undefined, + policyAmount: Number(p.policyAmount || 0) }, + ]) + setProductOpen(false) + productForm.resetFields() + } + + const searchAddress = (target: 'customer' | 'install') => { + message.info('주소검색(데모) — 직접 입력해 주세요.') + if (target === 'customer') { + form.setFieldsValue({ zipcode: form.getFieldValue('zipcode') || '06236', address: form.getFieldValue('address') || '서울특별시 강남구' }) + } else { + form.setFieldsValue({ installZip: form.getFieldValue('installZip') || '06236', installAddress: form.getFieldValue('installAddress') || '서울특별시 강남구' }) + } + } + + return ( +
+
+
+

+ + {title} +

+
+

{guide}

+ +
+
+
+
+ +
+
save.mutate(v)} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 상기 고객연락처와 주소가 동일합니다. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ({ value: o.value, label: o.label }))} + /> + +
+ + +
+ + +
+ ) +} diff --git a/frontend/src/pages/care/CsInquiryPage.tsx b/frontend/src/pages/care/CsInquiryPage.tsx new file mode 100644 index 0000000..ad3095c --- /dev/null +++ b/frontend/src/pages/care/CsInquiryPage.tsx @@ -0,0 +1,255 @@ +import { useMemo, useState } from 'react' +import Modal from '../../components/DraggableModal' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { + Button, + Card, + Form, + Input, + Select, + Space, + Typography, + message } from 'antd' +import CareTable from '../../components/CareTable' +import { EditOutlined, QuestionCircleOutlined, SearchOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' + +type InquiryRow = { + id: number + no?: number + title?: string + answer?: string + status?: string + authorName?: string + createdDate?: string + content?: string + reply?: string +} + +type InquiryData = { + total: number + list: InquiryRow[] +} + +export default function CsInquiryPage() { + const qc = useQueryClient() + const [form] = Form.useForm() + const [editForm] = Form.useForm() + const [open, setOpen] = useState(false) + const [detail, setDetail] = useState(null) + const [filters, setFilters] = useState>({ + searchType: '제목', + page: 0, + size: 15, + sort: 'createdAt,desc' }) + + const query = useQuery({ + queryKey: ['cs-inquiries', filters], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/cs/search`, { params: filters }) + return unwrap(data) + } }) + + const refresh = () => qc.invalidateQueries({ queryKey: ['cs-inquiries'] }) + + const createMut = useMutation({ + mutationFn: async (values: Record) => { + const { data } = await client.post(`${apiPrefix()}/cs`, values) + return unwrap(data) + }, + onSuccess: () => { + message.success('사용문의를 등록했습니다.') + setOpen(false) + editForm.resetFields() + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const openDetail = useMutation({ + mutationFn: async (id: number) => { + const { data } = await client.get>(`${apiPrefix()}/cs/${id}`) + return unwrap(data) + }, + onSuccess: (row) => setDetail(row), + onError: (e: Error) => message.error(e.message) }) + + const onSearch = (values: Record) => { + setFilters((prev) => ({ + ...prev, + searchType: values.searchType || '제목', + keyword: values.keyword, + page: 0, + size: Number(values.size ?? prev.size ?? 15) })) + } + + const onReset = () => { + form.setFieldsValue({ searchType: '제목', keyword: undefined, size: 15 }) + setFilters({ searchType: '제목', page: 0, size: 15, sort: 'createdAt,desc' }) + } + + const columns = useMemo(() => [ + { + title: '번호', + dataIndex: 'no', + width: 90, + align: 'center' as const, + sorter: true }, + { + title: '제목', + dataIndex: 'title', + ellipsis: true, + render: (v: string, row: InquiryRow) => ( + + ) }, + { + title: '답변', + dataIndex: 'answer', + width: 120, + align: 'center' as const, + sorter: true }, + { + title: '작성자', + dataIndex: 'authorName', + width: 100, + align: 'center' as const, + sorter: true }, + { + title: '등록일', + dataIndex: 'createdDate', + width: 120, + align: 'center' as const, + sorter: true }, + ], []) + + return ( +
+
+
+

사용문의

+

궁금하신 사항이나 사용상 문제가 있으시면 언제든지 내용을 남겨주세요. 담당자 확인 후 답변드리겠습니다.

+
+ +
+ + +
+
+ + + + + + + + + + + + + + + +
+ + +
+ + + + setDetail(null)} + footer={} + width={720} + destroyOnClose + > +
+ 작성자 {detail?.authorName || '-'} + 등록일 {detail?.createdDate || '-'} + {detail?.answer || detail?.status || '-'} +
+
+ 문의내용 +
{detail?.content || '-'}
+
+
+ 답변 +
{detail?.reply || '답변 대기 중입니다.'}
+
+
+
+ ) +} diff --git a/frontend/src/pages/care/CsNoticePage.tsx b/frontend/src/pages/care/CsNoticePage.tsx new file mode 100644 index 0000000..a1abb6a --- /dev/null +++ b/frontend/src/pages/care/CsNoticePage.tsx @@ -0,0 +1,225 @@ +import { useMemo, useState } from 'react' +import Modal from '../../components/DraggableModal' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { + Button, + Card, + Form, + Input, + Select, + Space, + Typography, + message } from 'antd' +import CareTable from '../../components/CareTable' +import { + QuestionCircleOutlined, + ReloadOutlined, + SearchOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' + +type NoticeRow = { + id: number + no?: number + title?: string + views?: number + createdDate?: string + authorName?: string + contents?: string +} + +type NoticeResult = { + total: number + list: NoticeRow[] +} + +const initial = { page: 0, size: 10, searchType: '제목', sort: 'id,desc' } as Record + +export default function CsNoticePage() { + const qc = useQueryClient() + const [form] = Form.useForm() + const [filters, setFilters] = useState>(initial) + const [detail, setDetail] = useState(null) + + const query = useQuery({ + queryKey: ['homes-notices', filters], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/notices`, { + params: { + ...filters, + q: filters.keyword || undefined } }) + return unwrap(data) + } }) + + const openDetail = useMutation({ + mutationFn: async (id: number) => { + const { data } = await client.get>(`${apiPrefix()}/homes/notices/${id}`) + return unwrap(data) + }, + onSuccess: (row) => { + setDetail(row) + qc.invalidateQueries({ queryKey: ['homes-notices'] }) + }, + onError: (e: Error) => message.error(e.message) }) + + const onSearch = (values: Record) => { + setFilters({ + ...initial, + searchType: values.searchType || '제목', + keyword: values.keyword || undefined, + page: 0, + size: Number(values.size ?? filters.size ?? 10), + sort: filters.sort }) + } + + const onReset = () => { + form.resetFields() + form.setFieldsValue({ searchType: '제목', size: 10 }) + setFilters(initial) + } + + const columns = useMemo( + () => [ + { + title: '번호', + dataIndex: 'no', + width: 90, + align: 'center' as const, + sorter: true }, + { + title: '제목', + dataIndex: 'title', + ellipsis: true, + render: (v: string, row: NoticeRow) => ( + + ) }, + { + title: '조회', + dataIndex: 'views', + width: 90, + align: 'center' as const, + sorter: true, + render: (v?: number) => (v == null ? 0 : Number(v).toLocaleString()) }, + { + title: '등록일', + dataIndex: 'createdDate', + width: 120, + align: 'center' as const, + sorter: true }, + ], + [], + ) + + return ( +
+
+
+

+ + 공지사항 +

+

프로그램 사용에 대한 공지사항이나 업데이트를 알려드립니다.

+
+
+ + +
+
+ + + + + + + + + + + ({ value: v, label: v }))} + /> + + + + + + + + + + + + + + + + +
+ + +
+ + + + setDetail(null)} + footer={} + width={640} + destroyOnHidden + centered + > + {detail ? ( +
+ + {detail.title} + +
+ 작성자 {detail.authorName || '-'} + 등록일 {detail.createdDate || '-'} + {detail.answer || '미답변'} +
+
+ 문의내용 +
+ {(detail.content || '') + .split('\n') + .map((line, idx) => ( +

{line || '\u00A0'}

+ ))} +
+
+ {detail.reply ? ( +
+ 답변 +
+ {detail.reply + .split('\n') + .map((line, idx) => ( +

{line || '\u00A0'}

+ ))} +
+
+ ) : null} +
+ ) : null} +
+
+ ) +} diff --git a/frontend/src/pages/care/CustomerListPage.tsx b/frontend/src/pages/care/CustomerListPage.tsx new file mode 100644 index 0000000..43660c7 --- /dev/null +++ b/frontend/src/pages/care/CustomerListPage.tsx @@ -0,0 +1,108 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { useQuery } from '@tanstack/react-query' +import { Button, Card, Form, Input, Select } from 'antd' +import CareTable from '../../components/CareTable' +import { FileTextOutlined, PlusOutlined, ReloadOutlined, SearchOutlined, UserOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' + +const options = (values: string[]) => values.map((value) => ({ value, label: value })) + +type Customer = Record & { id: number } +type SearchResult = { total: number; list: Customer[] } + +const initial = { page: 0, size: 15 } as Record + +export default function CustomerListPage() { + const [form] = Form.useForm() + const navigate = useNavigate() + const [filters, setFilters] = useState>(initial) + + const query = useQuery({ + queryKey: ['customers-list', filters], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/customers`, { params: filters }) + return unwrap(data) + } }) + + const onSearch = (values: Record) => { + setFilters({ ...initial, ...values, page: 0, size: values.size ?? filters.size ?? 15 }) + } + + const reset = () => { + form.resetFields() + form.setFieldsValue({ size: 15 }) + setFilters(initial) + } + + const columns = [ + { title: '고객명', dataIndex: 'name', width: 100, sorter: true }, + { title: '연락처', dataIndex: 'phone', width: 120 }, + { title: '통신사', dataIndex: 'telecom', width: 70 }, + { title: '주소', dataIndex: 'address', width: 200, ellipsis: true }, + { title: '상태', dataIndex: 'status', width: 80 }, + { title: '담당자', dataIndex: 'employee', width: 90 }, + { title: '등록일', dataIndex: 'createdDate', width: 100 }, + { + title: '관리', width: 58, fixed: 'right' as const, + render: (_: unknown, row: Customer) => ( + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+ + + +
+ + +
+ + + + setGradeOpen(false)} + footer={null} + destroyOnHidden + width={420} + > +
{ + const grades = String(values.gradesText || '') + .split(/\n|,/) + .map((s) => s.trim()) + .filter(Boolean) + saveGrades.mutate(grades) + }} + > + + + +
+ + +
+
+
+ + setChangeGradeOpen(false)} + footer={null} + destroyOnHidden + width={400} + > +
changeGradeMut.mutate({ ids: selected, grade: String(values.grade) })} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+ ) +} diff --git a/frontend/src/pages/care/DeliveryPage.tsx b/frontend/src/pages/care/DeliveryPage.tsx new file mode 100644 index 0000000..79ffc85 --- /dev/null +++ b/frontend/src/pages/care/DeliveryPage.tsx @@ -0,0 +1,287 @@ +import { useMemo, useState } from 'react' +import Modal from '../../components/DraggableModal' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { + Button, + Card, + Form, + Input, + Select, + Space, + Typography, + message } from 'antd' +import CareTable from '../../components/CareTable' +import { + EditOutlined, FileTextOutlined, PlusOutlined, SearchOutlined, SettingOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' + +type DeliveryRow = { + id: number + status?: string + active?: boolean + name?: string + phone?: string + memo?: string + createdAt?: string +} + +type DeliveryData = { + total: number + list: DeliveryRow[] +} + +export default function DeliveryPage() { + const qc = useQueryClient() + const [form] = Form.useForm() + const [editForm] = Form.useForm() + const [open, setOpen] = useState(false) + const [editing, setEditing] = useState(null) + const [selected, setSelected] = useState([]) + const [filters, setFilters] = useState>({ + searchType: '업체명', + page: 0, + size: 15, + sort: 'name,asc' }) + + const query = useQuery({ + queryKey: ['deliveries', filters], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/deliveries`, { params: filters }) + return unwrap(data) + } }) + + const refresh = () => { + setSelected([]) + qc.invalidateQueries({ queryKey: ['deliveries'] }) + } + + const saveMut = useMutation({ + mutationFn: async (values: Record) => { + if (editing?.id) { + const { data } = await client.put(`${apiPrefix()}/deliveries/${editing.id}`, values) + return unwrap(data) + } + const { data } = await client.post(`${apiPrefix()}/deliveries`, values) + return unwrap(data) + }, + onSuccess: () => { + message.success(editing ? '배송처를 수정했습니다.' : '배송처를 등록했습니다.') + setOpen(false) + setEditing(null) + editForm.resetFields() + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const statusMut = useMutation({ + mutationFn: async (status: '사용' | '미사용') => { + const { data } = await client.post(`${apiPrefix()}/deliveries/status`, { ids: selected, status }) + return unwrap(data) + }, + onSuccess: (_d, status) => { + message.success(`${status} 처리했습니다.`) + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const onSearch = (values: Record) => { + setFilters((prev) => ({ + ...prev, + searchType: values.searchType || '업체명', + keyword: values.keyword, + page: 0, + size: Number(values.size ?? prev.size ?? 15) })) + } + + const onReset = () => { + form.setFieldsValue({ searchType: '업체명', keyword: undefined, size: 15 }) + setFilters({ searchType: '업체명', page: 0, size: 15, sort: 'name,asc' }) + } + + const openCreate = () => { + setEditing(null) + editForm.setFieldsValue({ name: undefined, phone: undefined, memo: undefined, status: '사용' }) + setOpen(true) + } + + const openEdit = (row: DeliveryRow) => { + setEditing(row) + editForm.setFieldsValue({ + name: row.name, + phone: row.phone, + memo: row.memo, + status: row.status || '사용' }) + setOpen(true) + } + + const download = async () => { + const { data } = await client.get(`${apiPrefix()}/deliveries/export`, { + params: filters, + responseType: 'blob' }) + const url = URL.createObjectURL(data) + const a = document.createElement('a') + a.href = url + a.download = '배송처관리.csv' + a.click() + URL.revokeObjectURL(url) + } + + const columns = useMemo(() => [ + { + title: '상태', + dataIndex: 'status', + width: 80, + align: 'center' as const, + render: (v: string) => {v} }, + { title: '업체명', dataIndex: 'name', sorter: true }, + { title: '연락처', dataIndex: 'phone', width: 160, align: 'center' as const }, + { title: '비고', dataIndex: 'memo', width: 200, ellipsis: true }, + { title: '등록일', dataIndex: 'createdAt', width: 120, align: 'center' as const, sorter: true }, + { + title: '관리', + key: 'manage', + width: 80, + align: 'center' as const, + render: (_: unknown, row: DeliveryRow) => ( + + + + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + ({ value: n, label: `목록 ${n}개씩 보기` }))} + onChange={(size) => { + form.setFieldValue('size', size) + setFilters((prev) => ({ ...prev, size, page: 0 })) + }} + /> + +
+
+
+ + +
+ 목록 {query.data?.total ?? 0} + +
+ { + setSelected([]) + setFilters((prev) => ({ ...prev, page: page - 1 })) + } }} + locale={{ emptyText: '등록되어 있는 자료가 없습니다.' }} + /> +
+
+ ) +} diff --git a/frontend/src/pages/care/InCompanyPage.tsx b/frontend/src/pages/care/InCompanyPage.tsx new file mode 100644 index 0000000..8e1fe54 --- /dev/null +++ b/frontend/src/pages/care/InCompanyPage.tsx @@ -0,0 +1,342 @@ +import { useMemo, useState } from 'react' +import Modal from '../../components/DraggableModal' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { + Button, + Card, + Form, + Input, + Select, + Space, + Typography, + message } from 'antd' +import CareTable from '../../components/CareTable' +import { + EditOutlined, FileTextOutlined, PlusOutlined, SearchOutlined, SettingOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' + +type InCompanyRow = { + id: number + status?: string + active?: boolean + name?: string + telecom?: string + phone?: string + fax?: string + managerName?: string + mobile?: string + businessName?: string + businessNumber?: string + createdAt?: string + memo?: string +} + +type InCompanyData = { + total: number + list: InCompanyRow[] +} + +const telecomOptions = ['SKT', 'KT', 'LGU+', 'LGT', 'SK텔링크', 'KT엠모바일', 'LG헬로비전 (KT)', 'mKT'] + +export default function InCompanyPage() { + const qc = useQueryClient() + const [form] = Form.useForm() + const [editForm] = Form.useForm() + const [open, setOpen] = useState(false) + const [editing, setEditing] = useState(null) + const [selected, setSelected] = useState([]) + const [filters, setFilters] = useState>({ + searchType: '업체명', + page: 0, + size: 15, + sort: 'name,asc' }) + + const query = useQuery({ + queryKey: ['incompanies', filters], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/incompanies`, { params: filters }) + return unwrap(data) + } }) + + const refresh = () => { + setSelected([]) + qc.invalidateQueries({ queryKey: ['incompanies'] }) + } + + const saveMut = useMutation({ + mutationFn: async (values: Record) => { + if (editing?.id) { + const { data } = await client.put(`${apiPrefix()}/incompanies/${editing.id}`, values) + return unwrap(data) + } + const { data } = await client.post(`${apiPrefix()}/incompanies`, values) + return unwrap(data) + }, + onSuccess: () => { + message.success(editing ? '입고처를 수정했습니다.' : '입고처를 등록했습니다.') + setOpen(false) + setEditing(null) + editForm.resetFields() + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const statusMut = useMutation({ + mutationFn: async (status: '사용' | '미사용') => { + const { data } = await client.post(`${apiPrefix()}/incompanies/status`, { ids: selected, status }) + return unwrap(data) + }, + onSuccess: (_d, status) => { + message.success(`${status} 처리했습니다.`) + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const onSearch = (values: Record) => { + setFilters((prev) => ({ + ...prev, + searchType: values.searchType || '업체명', + keyword: values.keyword, + page: 0, + size: Number(values.size ?? prev.size ?? 15) })) + } + + const onReset = () => { + form.setFieldsValue({ searchType: '업체명', keyword: undefined, size: 15 }) + setFilters({ searchType: '업체명', page: 0, size: 15, sort: 'name,asc' }) + } + + const openCreate = () => { + setEditing(null) + editForm.setFieldsValue({ + name: undefined, + telecom: undefined, + phone: undefined, + fax: undefined, + managerName: undefined, + mobile: undefined, + businessName: undefined, + businessNumber: undefined, + memo: undefined, + status: '사용' }) + setOpen(true) + } + + const openEdit = (row: InCompanyRow) => { + setEditing(row) + editForm.setFieldsValue({ + name: row.name, + telecom: row.telecom, + phone: row.phone, + fax: row.fax, + managerName: row.managerName, + mobile: row.mobile, + businessName: row.businessName, + businessNumber: row.businessNumber, + memo: row.memo, + status: row.status || '사용' }) + setOpen(true) + } + + const download = async () => { + const { data } = await client.get(`${apiPrefix()}/incompanies/export`, { + params: filters, + responseType: 'blob' }) + const url = URL.createObjectURL(data) + const a = document.createElement('a') + a.href = url + a.download = '입고처관리.csv' + a.click() + URL.revokeObjectURL(url) + } + + const requireSelected = (status: '사용' | '미사용') => { + if (!selected.length) { + message.warning('항목을 선택하세요.') + return + } + statusMut.mutate(status) + } + + const columns = useMemo(() => [ + { + title: '상태', + dataIndex: 'status', + width: 70, + align: 'center' as const, + render: (v: string) => {v} }, + { title: '업체명', dataIndex: 'name', width: 140, ellipsis: true, sorter: true }, + { title: '통신사', dataIndex: 'telecom', width: 130, ellipsis: true, sorter: true }, + { title: '전화', dataIndex: 'phone', width: 120, align: 'center' as const }, + { title: '팩스', dataIndex: 'fax', width: 120, align: 'center' as const }, + { title: '담당자', dataIndex: 'managerName', width: 90, align: 'center' as const }, + { title: '휴대폰', dataIndex: 'mobile', width: 120, align: 'center' as const }, + { title: '상호', dataIndex: 'businessName', width: 110, ellipsis: true }, + { title: '사업자번호', dataIndex: 'businessNumber', width: 120, align: 'center' as const }, + { title: '등록일', dataIndex: 'createdAt', width: 110, align: 'center' as const, sorter: true }, + { title: '비고', dataIndex: 'memo', width: 120, ellipsis: true }, + { + title: '관리', + key: 'manage', + width: 70, + align: 'center' as const, + fixed: 'right' as const, + render: (_: unknown, row: InCompanyRow) => ( + + + + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ({ value: v, label: v }))} /> + + + + + + + + + + + + + + + + +
+ + +
+ + + + setDetail(null)} + footer={ + + + + + } + width={720} + destroyOnClose + > +
+ 직원 {detail?.authorName || '-'} + 등록일 {detail?.createdDate || '-'} + 조회 {detail?.views ?? 0} +
+
') }} + /> + +
+ ) +} + +export function CompanyBoardPage() { + return +} + +export function DataBoardPage() { + return +} diff --git a/frontend/src/pages/care/InvoiceLogPage.tsx b/frontend/src/pages/care/InvoiceLogPage.tsx new file mode 100644 index 0000000..565e925 --- /dev/null +++ b/frontend/src/pages/care/InvoiceLogPage.tsx @@ -0,0 +1,22 @@ +import { HomesLogListPage } from './HomesLogListPage' +import { FileTextOutlined } from '@ant-design/icons' + +const ACTIONS = [ + '정산서-발행', + '정산서-재발행', + '정산서-삭제', + '정산서-등록', + '정산서-수정', +] + +export default function InvoiceLogPage() { + return ( + } + /> + ) +} diff --git a/frontend/src/pages/care/InvoiceMyPage.tsx b/frontend/src/pages/care/InvoiceMyPage.tsx new file mode 100644 index 0000000..605f2bb --- /dev/null +++ b/frontend/src/pages/care/InvoiceMyPage.tsx @@ -0,0 +1,163 @@ +import { useMemo } from 'react' +import { useQuery } from '@tanstack/react-query' +import dayjs from 'dayjs' +import { Button, Card, message } from 'antd' +import CareTable from '../../components/CareTable' +import { CalculatorOutlined, FileExcelOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' +import { money } from './homesShared' + +type MyInvoiceRow = { + id: number + yearMonth?: string + employee?: string + internetCount?: number + internetAmount?: number + homeCount?: number + homeAmount?: number + balanceCount?: number + balanceAmount?: number + settleAmount?: number + amount?: number + status?: string +} + +type MyInvoiceResult = { + total: number + employee?: string + list: MyInvoiceRow[] +} + +const num = (v: unknown) => { + const n = Number(v ?? 0) + return Number.isFinite(n) ? n : 0 +} +const cell = (v: unknown) => (num(v) === 0 ? '' : num(v).toLocaleString()) +const moneyCell = (v: unknown) => (num(v) === 0 ? '' : money(v)) + +const formatYm = (ym?: string) => { + if (!ym) return '-' + const d = dayjs(`${ym}-01`) + return d.isValid() ? d.format('YYYY년 M월') : ym +} + +export default function InvoiceMyPage() { + const query = useQuery({ + queryKey: ['homes-invoice-my'], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/invoices/my`) + return unwrap(data) + } }) + + const download = async (row: MyInvoiceRow) => { + try { + const { data } = await client.get(`${apiPrefix()}/homes/invoices/${row.id}/export`, { responseType: 'blob' }) + const url = URL.createObjectURL(data) + const a = document.createElement('a') + a.href = url + a.download = `나의정산서_${row.yearMonth || row.id}.csv` + a.click() + URL.revokeObjectURL(url) + } catch { + message.error('엑셀 다운로드에 실패했습니다.') + } + } + + const columns = useMemo(() => [ + { + title: '기준월', + dataIndex: 'yearMonth', + width: 110, + align: 'center' as const, + render: (v: string) => formatYm(v) }, + { title: '직원', dataIndex: 'employee', width: 100, align: 'center' as const }, + { + title: '인터넷', + dataIndex: 'internetCount', + width: 80, + align: 'center' as const, + render: cell }, + { + title: '인터넷금액', + dataIndex: 'internetAmount', + width: 110, + align: 'center' as const, + render: moneyCell }, + { + title: '홈렌탈', + dataIndex: 'homeCount', + width: 80, + align: 'center' as const, + render: cell }, + { + title: '홈렌탈금액', + dataIndex: 'homeAmount', + width: 110, + align: 'center' as const, + render: moneyCell }, + { + title: '후정산', + dataIndex: 'balanceCount', + width: 80, + align: 'center' as const, + render: cell }, + { + title: '후정산금액', + dataIndex: 'balanceAmount', + width: 110, + align: 'center' as const, + render: moneyCell }, + { + title: '정산금액', + dataIndex: 'settleAmount', + width: 110, + align: 'center' as const, + render: (_: unknown, row: MyInvoiceRow) => moneyCell(row.settleAmount ?? row.amount) }, + { + title: '엑셀', + width: 70, + align: 'center' as const, + render: (_: unknown, row: MyInvoiceRow) => ( + + ) : ( + + ) + ) }, + { + title: '동의', + dataIndex: 'consent', + width: 80, + align: 'center' as const, + render: (v: string) => v || '-' }, + { + title: '계산서', + dataIndex: 'taxInvoice', + width: 80, + align: 'center' as const, + render: (v: string) => v || '-' }, + { + title: '출금상태', + dataIndex: 'withdrawStatus', + width: 90, + align: 'center' as const, + render: (v: string) => v || '-' }, + ], [issue]) + + return ( +
+
+
+

+ + 정산서발행 +

+

+ 직원 및 하부점에 정산서를 발행 및 관리하실 수 있습니다. (계약 진행상태가 완료인 자료만 정산서를 발행하실 수 있습니다.) +

+
+
+ + +
+
+ + {cursor.format('YYYY년 M월')} + +
+ +
+
+ + + `${row.employee}-${row.yearMonth}`} + size="small" + loading={query.isLoading} + dataSource={query.data?.list} + columns={columns} + pagination={false} + scroll={{ x: 1200 }} + locale={{ emptyText: '등록된 자료가 없습니다.' }} + rowClassName={(row) => (row.mismatched ? 'invoice-row-mismatch' : '')} + /> +
+ + * 정산서발행 시 자료의 양에 따라 처리시간이 오래 걸릴 수 있습니다. + + + * 붉은색 표기는 추가/수정으로 인해 발행정산서와 일치하지 않는 내용입니다. (필요한 경우 재발행하시면 해결됩니다.) + +
+
+
+ ) +} diff --git a/frontend/src/pages/care/KindStatReportPage.tsx b/frontend/src/pages/care/KindStatReportPage.tsx new file mode 100644 index 0000000..dfeb61f --- /dev/null +++ b/frontend/src/pages/care/KindStatReportPage.tsx @@ -0,0 +1,154 @@ +import { useMemo, useState } from 'react' +import { useQuery } from '@tanstack/react-query' +import dayjs, { type Dayjs } from 'dayjs' +import { Table, Button, Card, Empty } from 'antd' +import CareTable from '../../components/CareTable' +import { BarChartOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' +import { money } from './homesShared' + +type StatRow = { + key?: number + label?: string + day?: number + month?: number + count?: number + policySum?: number + settleAmount?: number + margin?: number +} + +type KindStatData = { + mode?: 'daily' | 'monthly' + year?: number + month?: number + title?: string + rows?: StatRow[] + totals?: StatRow +} + +const num = (v?: number) => Number(v ?? 0) +const cell = (v?: number) => (num(v) === 0 ? '' : money(v)) +const countCell = (v?: number) => (num(v) === 0 ? '' : String(v)) + +type Props = { + kind: 'internet' | 'home' + title: string + description: string +} + +export function KindStatReportPage({ kind, title, description }: Props) { + const [mode, setMode] = useState<'daily' | 'monthly'>('daily') + const [cursor, setCursor] = useState(dayjs()) + + const params = useMemo(() => ({ + mode, + year: cursor.year(), + month: mode === 'daily' ? cursor.month() + 1 : undefined }), [mode, cursor]) + + const query = useQuery({ + queryKey: [`homes-report-${kind}`, params], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/reports/${kind}`, { + params }) + return unwrap(data) + } }) + + const rows = query.data?.rows || [] + const totals = query.data?.totals + + const columns = useMemo(() => [ + { + title: mode === 'daily' ? '일자' : '월', + dataIndex: 'label', + width: 90, + align: 'center' as const }, + { + title: '건수', + dataIndex: 'count', + width: 90, + align: 'right' as const, + render: countCell }, + { + title: '정책합산', + dataIndex: 'policySum', + width: 120, + align: 'right' as const, + render: cell }, + { + title: '정산합계', + dataIndex: 'settleAmount', + width: 120, + align: 'right' as const, + render: cell }, + { + title: '마진합계', + dataIndex: 'margin', + width: 120, + align: 'right' as const, + render: cell }, + ], [mode]) + + const shift = (dir: -1 | 1) => { + setCursor((d) => (mode === 'daily' ? d.add(dir, 'month') : d.add(dir, 'year'))) + } + + const periodLabel = mode === 'daily' + ? cursor.format('YYYY년 M월') + : cursor.format('YYYY년') + + return ( +
+
+
+

+ + {title} +

+

{description}

+
+
+ + +
+
* 철회된 계약은 제외됩니다.
+
+ + +
+
+
+ + + {rows.length ? ( + String(r.key ?? r.label)} + size="small" + dataSource={rows} + columns={columns} + pagination={false} + scroll={{ x: 600 }} + summary={() => ( + + 합계 + {countCell(totals?.count)} + {cell(totals?.policySum)} + {cell(totals?.settleAmount)} + {cell(totals?.margin)} + + )} + /> + ) : ( + + )} + +
+ ) +} diff --git a/frontend/src/pages/care/OpenAdjustPage.tsx b/frontend/src/pages/care/OpenAdjustPage.tsx new file mode 100644 index 0000000..9e4ca09 --- /dev/null +++ b/frontend/src/pages/care/OpenAdjustPage.tsx @@ -0,0 +1,356 @@ +import { useEffect, useMemo, useState } from 'react' +import { useMutation, useQuery } from '@tanstack/react-query' +import dayjs, { type Dayjs } from 'dayjs' +import { Button, Card, DatePicker, Form, Input, InputNumber, Select, Space, Typography, message } from 'antd' +import CareTable from '../../components/CareTable' +import { DownloadOutlined, RightOutlined, SearchOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' +import { resourceApi } from '../../api/resources' + +const { RangePicker } = DatePicker +const sortFields = ['종류', '모델명', '요금제', '개통일', '출고처', '지원방법', '고객명', '통신사'] +const defaultSorts = ['종류', '모델명', '요금제', '개통일', '출고처', '지원방법', '고객명'] +const options = (values: string[]) => values.map((value) => ({ value, label: value })) +const money = (v: unknown) => (v == null || v === '' ? '0' : Number(v).toLocaleString()) + +type AdjustRow = { + id: number + opendate?: string + incompanyName?: string + outcompanyName?: string + name?: string + openhow?: string + pmodel?: string + pserial?: string + decltype?: string + plan?: string + sellprice?: number + basicprice1?: number + basicprice2?: number + basicprice3?: number +} + +export default function OpenAdjustPage() { + const [form] = Form.useForm() + const [searched, setSearched] = useState(false) + const [filters, setFilters] = useState>({}) + const [rows, setRows] = useState([]) + const [batch, setBatch] = useState({ basicprice1: undefined as number | undefined, basicprice2: undefined as number | undefined, basicprice3: undefined as number | undefined }) + + const companies = useQuery({ queryKey: ['adjust-companies'], queryFn: () => resourceApi.list('companies', { size: 200 }) }) + const inOptions = useMemo( + () => (companies.data?.items ?? []).filter((c) => String(c.type) === 'IN').map((c) => ({ value: c.id, label: String(c.name) })), + [companies.data], + ) + const outOptions = useMemo( + () => (companies.data?.items ?? []).filter((c) => ['SHOP', 'DEALER', 'OUT'].includes(String(c.type))).map((c) => ({ value: c.id, label: String(c.name) })), + [companies.data], + ) + + const enabled = searched && !!filters.dateFrom && !!filters.dateTo + const query = useQuery({ + queryKey: ['open-adjust', filters], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/opens/adjust`, { params: filters }) + return unwrap(data) + }, + enabled }) + + useEffect(() => { + if (query.data?.list) { + setRows(query.data.list.map((row) => ({ + ...row, + basicprice1: Number(row.basicprice1 ?? 0), + basicprice2: Number(row.basicprice2 ?? 0), + basicprice3: Number(row.basicprice3 ?? 0) }))) + } + }, [query.data]) + + const setPreset = (from: Dayjs, to: Dayjs) => { + if (to.diff(from, 'day') > 30) { + message.warning('최대 31일까지 조회할 수 있습니다.') + return + } + form.setFieldsValue({ dates: [from, to] }) + } + + const onSearch = (values: Record) => { + const dates = values.dates as [Dayjs, Dayjs] | undefined + if (!dates?.[0] || !dates?.[1]) { + message.warning('개통일을 선택하세요.') + return + } + if (dates[1].diff(dates[0], 'day') > 30) { + message.warning('최대 31일까지 조회할 수 있습니다.') + return + } + const next: Record = { + dateFrom: dates[0].format('YYYY-MM-DD'), + dateTo: dates[1].format('YYYY-MM-DD'), + openhow: values.openhow, + telecom: values.telecom, + incompanyId: values.incompanyId, + outcompanyId: values.outcompanyId, + salesperson: values.salesperson, + model: values.model, + plan: values.plan } + for (let i = 1; i <= 7; i++) next[`sort${i}`] = values[`sort${i}`] + setSearched(true) + setFilters(next) + } + + const updateRow = (id: number, field: 'basicprice1' | 'basicprice2' | 'basicprice3', value: number | null) => { + setRows((prev) => prev.map((row) => (row.id === id ? { ...row, [field]: value ?? 0 } : row))) + } + + const applyBatch = (field: 'basicprice1' | 'basicprice2' | 'basicprice3') => { + const value = batch[field] + if (value == null) { + message.warning('금액을 입력하세요.') + return + } + setRows((prev) => prev.map((row) => ({ ...row, [field]: value }))) + message.success('일괄 입력했습니다.') + } + + const saveOne = useMutation({ + mutationFn: async (row: AdjustRow) => { + const { data } = await client.put(`${apiPrefix()}/opens/${row.id}`, { + basicprice1: row.basicprice1, + basicprice2: row.basicprice2, + basicprice3: row.basicprice3 }) + return unwrap(data as ApiResponse) + }, + onSuccess: () => message.success('변경했습니다.'), + onError: (e: Error) => message.error(e.message) }) + + const saveAll = useMutation({ + mutationFn: async () => { + const { data } = await client.post(`${apiPrefix()}/opens/adjust/batch`, { + items: rows.map((row) => ({ + id: row.id, + basicprice1: row.basicprice1, + basicprice2: row.basicprice2, + basicprice3: row.basicprice3 })) }) + return unwrap(data as ApiResponse<{ updated: number }>) + }, + onSuccess: (data) => message.success(`${data.updated}건을 변경했습니다.`), + onError: (e: Error) => message.error(e.message) }) + + const download = async () => { + if (!filters.dateFrom || !filters.dateTo) { + message.warning('개통일을 선택하세요.') + return + } + const { data } = await client.get(`${apiPrefix()}/opens/adjust/export`, { params: filters, responseType: 'blob' }) + const url = URL.createObjectURL(data) + const a = document.createElement('a') + a.href = url + a.download = '일괄개통정산.csv' + a.click() + URL.revokeObjectURL(url) + } + + const columns = [ + { title: '개통일', dataIndex: 'opendate', width: 100 }, + { title: '입고처', dataIndex: 'incompanyName', width: 100, ellipsis: true }, + { title: '출고처', dataIndex: 'outcompanyName', width: 100, ellipsis: true }, + { title: '고객명', dataIndex: 'name', width: 80 }, + { title: '종류', dataIndex: 'openhow', width: 70 }, + { title: '모델명', dataIndex: 'pmodel', width: 140, ellipsis: true }, + { title: '일련번호', dataIndex: 'pserial', width: 110, ellipsis: true }, + { title: '지원방법', dataIndex: 'decltype', width: 90, render: (v: string) => v || '-' }, + { title: '요금제', dataIndex: 'plan', width: 110, ellipsis: true }, + { title: '정산금액', dataIndex: 'sellprice', width: 90, align: 'right' as const, render: money }, + { + title: '기본정책', + dataIndex: 'basicprice1', + width: 110, + render: (_: unknown, row: AdjustRow) => ( + updateRow(row.id, 'basicprice1', v)} + /> + ) }, + { + title: '구두정책', + dataIndex: 'basicprice2', + width: 110, + render: (_: unknown, row: AdjustRow) => ( + updateRow(row.id, 'basicprice2', v)} + /> + ) }, + { + title: '추가정책', + dataIndex: 'basicprice3', + width: 110, + render: (_: unknown, row: AdjustRow) => ( + updateRow(row.id, 'basicprice3', v)} + /> + ) }, + { + title: '개별', + width: 70, + fixed: 'right' as const, + render: (_: unknown, row: AdjustRow) => ( + + ) }, + ] + + return ( +
+
+
+

일괄개통정산

+

등록된 개통정보에 일괄적으로 정책금액(정산)을 변경하실 수 있습니다. (유심개통 제외)

+
+
+ + +
+
+ 개통일 + + + + + + + + + + + * 최대 31일 조회 +
+ +
+ 검색조건 + + + + + + + + + + + + +
+ +
+ 정렬순서 + {defaultSorts.map((_, index) => ( + + {index > 0 && } + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + ({ value: v, label: v }))} /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ({ value: v, label: v }))} /> + +

판매하신 상품을 선택해주세요.

+
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ setFilters((v) => ({ ...v, tab, page: 0 }))} + items={tabs.map(([key, label]) => ({ + key, + label: `${label} ${query.data?.counts?.[key] ?? 0}` }))} + /> + + + + + + + + + + + + ({ value: v, label: v }))} disabled={Boolean(editing)} /> + + + + * 지정입고처의 개통만 적용할 경우 선택해주세요.}> + ({ value: g, label: `${g}군` }))} /> + + + + + + + + + Number(v ?? 0).toLocaleString() }, + ]} + /> + +
+ ) +} diff --git a/frontend/src/pages/care/OpenWithdrawPage.tsx b/frontend/src/pages/care/OpenWithdrawPage.tsx new file mode 100644 index 0000000..d4917ea --- /dev/null +++ b/frontend/src/pages/care/OpenWithdrawPage.tsx @@ -0,0 +1,199 @@ +import { useMemo, useState } from 'react' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import dayjs, { type Dayjs } from 'dayjs' +import { Button, Card, DatePicker, Form, Input, Select, Space, Typography, message } from 'antd' +import CareTable from '../../components/CareTable' +import Modal from '../../components/DraggableModal' +import { + CloseOutlined, DownloadOutlined, ReloadOutlined, RollbackOutlined, SearchOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' + +const { RangePicker } = DatePicker +const searchTypes = ['개통번호(뒷4자리)', '고객명', '사유 및 비고'] + +type WithdrawalRow = Record & { id: number; no?: number } +type WithdrawalData = { total: number; list: WithdrawalRow[] } + +const initial = { page: 0, size: 15, searchType: '개통번호(뒷4자리)' } as Record + +const withdrawalApi = { + async search(params: Record) { + const { data } = await client.get>(`${apiPrefix()}/withdrawals/search`, { params }) + return unwrap(data) + }, + async remove(ids: number[]) { + const { data } = await client.post(`${apiPrefix()}/withdrawals/delete`, { ids }) + return unwrap(data) + } } + +export default function OpenWithdrawPage() { + const [form] = Form.useForm() + const qc = useQueryClient() + const [selected, setSelected] = useState([]) + const [filters, setFilters] = useState>(initial) + + const query = useQuery({ + queryKey: ['open-withdraw', filters], + queryFn: () => withdrawalApi.search(filters) }) + + const remove = useMutation({ + mutationFn: () => withdrawalApi.remove(selected), + onSuccess: () => { + message.success('삭제했습니다.') + setSelected([]) + qc.invalidateQueries({ queryKey: ['open-withdraw'] }) + }, + onError: (e: Error) => message.error(e.message) }) + + const setPreset = (from: Dayjs, to: Dayjs) => form.setFieldsValue({ dates: [from, to] }) + + const onSearch = (values: Record) => { + const dates = values.dates as [Dayjs, Dayjs] | undefined + setSelected([]) + setFilters({ + ...initial, + outcompanyName: values.outcompanyName ? String(values.outcompanyName) : undefined, + searchType: values.searchType || '개통번호(뒷4자리)', + keyword: values.keyword ? String(values.keyword) : undefined, + dateFrom: dates?.[0]?.format('YYYY-MM-DD'), + dateTo: dates?.[1]?.format('YYYY-MM-DD'), + page: 0, + size: values.size ?? filters.size ?? 15 }) + } + + const reset = () => { + form.resetFields() + form.setFieldsValue({ searchType: '개통번호(뒷4자리)', size: 15 }) + setSelected([]) + setFilters(initial) + } + + const clearDates = () => form.setFieldValue('dates', undefined) + + const download = async () => { + try { + const { data } = await client.get(`${apiPrefix()}/withdrawals/export`, { params: filters, responseType: 'blob' }) + const url = URL.createObjectURL(data) + const a = document.createElement('a') + a.href = url + a.download = '개통철회이력.csv' + a.click() + URL.revokeObjectURL(url) + } catch { + message.error('엑셀 다운로드에 실패했습니다.') + } + } + + const columns = useMemo(() => [ + { title: '번호', dataIndex: 'no', width: 64, align: 'center' as const }, + { title: '철회일', dataIndex: 'withdrawDate', width: 110 }, + { title: '출고처', dataIndex: 'outcompanyName', width: 130, ellipsis: true }, + { title: '고객명', dataIndex: 'customerName', width: 90 }, + { title: '개통일', dataIndex: 'openDate', width: 110 }, + { title: '개통번호', dataIndex: 'openphone', width: 130 }, + { title: '처리직원', dataIndex: 'processor', width: 90 }, + { title: '처리일', dataIndex: 'processedAt', width: 140 }, + { title: '사유 및 비고', dataIndex: 'reason', width: 160, ellipsis: true, render: (v: string) => v || '-' }, + ], []) + + return ( +
+
+
+

개통 철회이력

+

모바일 개통과 관련된 이력을 확인하실 수 있습니다.

+
+ + * 대표권한은 이력을 삭제하실 수 있습니다. + + +
+ + +
+
+ + + + + + + + + + + + + + changeType(type)} /> + {type}개통 + + ))} +
+
+ + save.mutate(values)} + > + + + + + +
+ 단말기 + {openType !== '유심' && } +
+ {openType === '유심' ? ( +
관계없음
+ ) : ( + <> + + lookupSerial(v, 'phone')} + onSelect={(_, opt) => applyPhone((opt as { stock: Record }).stock)} + placeholder="단말기 일련번호를 입력해주세요" + > + { const v = e.target.value; if (v.length >= 3) lookupSerial(v, 'phone').then(() => { const hit = phoneOptions.find((o) => o.value === v); if (hit) applyPhone(hit.stock) }) }} /> + + +

(3자리 이상 입력 시 검색됩니다)

+
+
단말기정보{phoneInfo ? `${phoneInfo.model ?? '-'} / ${phoneInfo.color ?? '-'}` : '-'}
+
입고정보{phoneInfo ? `${phoneInfo.incompanyName ?? '-'} / ${phoneInfo.inprice != null ? Number(phoneInfo.inprice).toLocaleString() + '원' : '-'}` : '-'}
+
출고정보{phoneInfo ? `${phoneInfo.outcompanyName ?? '-'} / ${phoneInfo.statusLabel ?? phoneInfo.state ?? '-'}` : '-'}
+
+ + )} + + +
+ 유심 + +
+ + lookupSerial(v, 'usim')} + onSelect={(_, opt) => applyUsim((opt as { stock: Record }).stock)} + placeholder="유심 일련번호를 입력해주세요" + > + + + +

(3자리 이상 입력 시 검색됩니다)

+
+
유심정보{usimInfo ? String(usimInfo.model ?? usimInfo.serial ?? '-') : '-'}
+
입고정보{usimInfo ? `${usimInfo.incompanyName ?? '-'} / ${usimInfo.inprice != null ? Number(usimInfo.inprice).toLocaleString() + '원' : '-'}` : '-'}
+
출고정보{usimInfo ? `${usimInfo.outcompanyName ?? '-'} / ${usimInfo.statusLabel ?? usimInfo.state ?? '-'}` : '-'}
+
+ +
+
+ + + {openType === '유심' && ( + + + + )} + + + {openType !== '유심' && ( + <> + + + + + 요금할인 + + + {openType === '할부' && } + + + + + + + + + {openType === '할부' && } + {openType === '현금' && } + {openType === '현금' && } + + + )} + {openType === '유심' && ( + <> + + + + + + )} + + + + {openType !== '유심' && ( + <> + + + + + + + + )} + {openType === '유심' && ( + <> + + + + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + v && setKeepBase(v)} style={{ width: '100%', marginBottom: 8 }} /> +
+ + + setKeepDays(v)} style={{ width: 70 }} /> + 일 = + +
+
+ +
+ +
+ ) +} diff --git a/frontend/src/pages/care/OutCompanyPage.tsx b/frontend/src/pages/care/OutCompanyPage.tsx new file mode 100644 index 0000000..5c53f24 --- /dev/null +++ b/frontend/src/pages/care/OutCompanyPage.tsx @@ -0,0 +1,449 @@ +import { useMemo, useState } from 'react' +import Modal from '../../components/DraggableModal' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { + Button, + Card, + Form, + Input, + Popover, + Select, + Space, + Typography, + message } from 'antd' +import CareTable from '../../components/CareTable' +import { + EditOutlined, FileTextOutlined, InfoCircleOutlined, MailOutlined, PlusOutlined, SearchOutlined, SettingOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' + +type OutCompanyRow = { + id: number + status?: string + active?: boolean + channel?: string + category?: string + name?: string + pcode?: string + salesperson?: string + phone?: string + managerName?: string + mobile?: string + businessName?: string + businessNumber?: string + bankAccount?: string + createdAt?: string + memo?: string +} + +type OutCompanyData = { + total: number + list: OutCompanyRow[] +} + +const channelOptions = ['도매', '소매', 'C채널', '특판', '기타'] +const categoryOptions = ['판매점', '출고처', '도매', '특판', '본사'] + +export default function OutCompanyPage() { + const qc = useQueryClient() + const [form] = Form.useForm() + const [editForm] = Form.useForm() + const [memoForm] = Form.useForm() + const [open, setOpen] = useState(false) + const [editing, setEditing] = useState(null) + const [memoOpen, setMemoOpen] = useState(false) + const [memoTarget, setMemoTarget] = useState(null) + const [selected, setSelected] = useState([]) + const [filters, setFilters] = useState>({ + searchType: '출고처명', + page: 0, + size: 15, + sort: 'name,asc' }) + + const query = useQuery({ + queryKey: ['outcompanies', filters], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/outcompanies`, { params: filters }) + return unwrap(data) + } }) + + const refresh = () => { + setSelected([]) + qc.invalidateQueries({ queryKey: ['outcompanies'] }) + } + + const saveMut = useMutation({ + mutationFn: async (values: Record) => { + if (editing?.id) { + const { data } = await client.put(`${apiPrefix()}/outcompanies/${editing.id}`, values) + return unwrap(data) + } + const { data } = await client.post(`${apiPrefix()}/outcompanies`, values) + return unwrap(data) + }, + onSuccess: () => { + message.success(editing ? '출고처를 수정했습니다.' : '출고처를 등록했습니다.') + setOpen(false) + setEditing(null) + editForm.resetFields() + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const statusMut = useMutation({ + mutationFn: async (status: '사용' | '미사용') => { + const { data } = await client.post(`${apiPrefix()}/outcompanies/status`, { ids: selected, status }) + return unwrap(data) + }, + onSuccess: (_d, status) => { + message.success(`${status} 처리했습니다.`) + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const memoMut = useMutation({ + mutationFn: async (memo: string) => { + const { data } = await client.put(`${apiPrefix()}/outcompanies/${memoTarget!.id}`, { memo }) + return unwrap(data) + }, + onSuccess: () => { + message.success('비고를 저장했습니다.') + setMemoOpen(false) + setMemoTarget(null) + memoForm.resetFields() + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const onSearch = (values: Record) => { + setFilters((prev) => ({ + ...prev, + status: values.status, + channel: values.channel, + category: values.category, + searchType: values.searchType || '출고처명', + keyword: values.keyword, + page: 0, + size: Number(values.size ?? prev.size ?? 15) })) + } + + const onReset = () => { + form.setFieldsValue({ + status: undefined, + channel: undefined, + category: undefined, + searchType: '출고처명', + keyword: undefined, + size: 15 }) + setFilters({ searchType: '출고처명', page: 0, size: 15, sort: 'name,asc' }) + } + + const openCreate = () => { + setEditing(null) + editForm.setFieldsValue({ + name: undefined, + channel: '도매', + category: '판매점', + pcode: undefined, + salesperson: undefined, + phone: undefined, + managerName: undefined, + mobile: undefined, + businessName: undefined, + businessNumber: undefined, + bankAccount: undefined, + memo: undefined, + status: '사용', + type: 'OUT' }) + setOpen(true) + } + + const openEdit = (row: OutCompanyRow) => { + setEditing(row) + editForm.setFieldsValue({ + name: row.name, + channel: row.channel, + category: row.category, + pcode: row.pcode, + salesperson: row.salesperson, + phone: row.phone, + managerName: row.managerName, + mobile: row.mobile, + businessName: row.businessName, + businessNumber: row.businessNumber, + bankAccount: row.bankAccount, + memo: row.memo, + status: row.status || '사용' }) + setOpen(true) + } + + const openMemo = (row: OutCompanyRow) => { + setMemoTarget(row) + memoForm.setFieldsValue({ memo: row.memo }) + setMemoOpen(true) + } + + const download = async () => { + const { data } = await client.get(`${apiPrefix()}/outcompanies/export`, { + params: filters, + responseType: 'blob' }) + const url = URL.createObjectURL(data) + const a = document.createElement('a') + a.href = url + a.download = '출고처관리.csv' + a.click() + URL.revokeObjectURL(url) + } + + const columns = useMemo(() => [ + { + title: '상태', + dataIndex: 'status', + width: 70, + align: 'center' as const, + render: (v: string) => {v} }, + { title: '채널', dataIndex: 'channel', width: 80, align: 'center' as const, sorter: true }, + { title: '업체명', dataIndex: 'name', width: 140, ellipsis: true, sorter: true }, + { title: 'P코드', dataIndex: 'pcode', width: 90, align: 'center' as const, sorter: true }, + { title: '영업담당', dataIndex: 'salesperson', width: 90, align: 'center' as const }, + { title: '전화', dataIndex: 'phone', width: 120, align: 'center' as const }, + { title: '담당자', dataIndex: 'managerName', width: 90, align: 'center' as const }, + { title: '휴대폰', dataIndex: 'mobile', width: 120, align: 'center' as const }, + { title: '상호', dataIndex: 'businessName', width: 110, ellipsis: true }, + { title: '사업자번호', dataIndex: 'businessNumber', width: 120, align: 'center' as const }, + { + title: '계좌', + dataIndex: 'bankAccount', + width: 70, + align: 'center' as const, + render: (v?: string) => ( + v + ? ( + {v}
} title="계좌정보"> + + + + +
+ + +
+
+ + ({ value: v, label: v }))} /> + + + ({ value: v, label: v }))} + /> + + + + + + + + + + + + + + + ({ value: v, label: v }))} /> + + + + + + + + + + + + + + + + + + + + + + + + + + + setPendingType(v)} + /> + setView(v)} + /> +
+
+ +
+
+ {WEEK_LABELS.map((w) => ( +
{w.label}
+ ))} +
+
+ {cells.map((date) => { + const key = date.format('YYYY-MM-DD') + const inMonth = date.month() === cursor.month() + const isToday = key === todayKey + const isMonthStart = date.date() === 1 + const items = byDate[key] || [] + const dateLabel = isMonthStart ? `${date.month() + 1}월 ${date.date()}일` : String(date.date()) + return ( +
openCreate(date)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + openCreate(date) + } + }} + > +
+ {dateLabel} +
+
+ {items.slice(0, view === 'simple' ? 3 : 6).map((row) => { + const text = eventLabel(row, view) + return ( + +
{ + e.stopPropagation() + openEdit(row) + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + e.stopPropagation() + openEdit(row) + } + }} + > + {text} +
+
+ ) + })} + {items.length > (view === 'simple' ? 3 : 6) && ( +
+{items.length - (view === 'simple' ? 3 : 6)}
+ )} +
+
+ ) + })} +
+
+ + + + save.mutate(values)} + className="balance-edit-form" + > + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+ + o.value)} + saving={saveTypes.isPending} + submitClassName="pending-submit-btn" + onCancel={() => setTypeOpen(false)} + onSave={(types) => saveTypes.mutate(types)} + /> +
+ ) +} diff --git a/frontend/src/pages/care/PendingLogPage.tsx b/frontend/src/pages/care/PendingLogPage.tsx new file mode 100644 index 0000000..5922ee8 --- /dev/null +++ b/frontend/src/pages/care/PendingLogPage.tsx @@ -0,0 +1,21 @@ +import { ClockCircleOutlined } from '@ant-design/icons' +import { HomesLogListPage } from './HomesLogListPage' + +const ACTIONS = [ + '약속-등록', + '약속-수정', + '약속-삭제', + '약속-해결', +] + +export default function PendingLogPage() { + return ( + } + /> + ) +} diff --git a/frontend/src/pages/care/PendingPage.tsx b/frontend/src/pages/care/PendingPage.tsx new file mode 100644 index 0000000..b1d070b --- /dev/null +++ b/frontend/src/pages/care/PendingPage.tsx @@ -0,0 +1,515 @@ +import { useMemo, useState } from 'react' +import Modal from '../../components/DraggableModal' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import dayjs, { type Dayjs } from 'dayjs' +import { + Button, + Card, + Checkbox, + DatePicker, + Form, + Input, + Select, + Space, + Tabs, + Tooltip, + message } from 'antd' +import CareTable from '../../components/CareTable' +import SubjectSettingsModal from '../../components/SubjectSettingsModal' +import { + ClockCircleOutlined, + CloseOutlined, + DownloadOutlined, + EditOutlined, + MailOutlined, + PlusSquareOutlined, + ReloadOutlined, + SearchOutlined, + SettingOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' +import { options } from './homesShared' + +const { RangePicker } = DatePicker + +type PendingRow = { + id: number + status?: string + pendingDate?: string + pendingType?: string + contents?: string + customerName?: string + phone?: string + receiveEmployee?: string + processDate?: string + processEmployee?: string + memo?: string + pendingTime?: string +} + +type PendingResult = { + total: number + list: PendingRow[] + counts?: Record + types?: string[] + statuses?: string[] + receiveEmployees?: string[] + processEmployees?: string[] +} + +const initial = { page: 0, size: 10, tab: 'ALL' } as Record + +const maskName = (name?: string) => { + if (!name) return '-' + if (name.length <= 1) return name + if (name.length === 2) return `${name[0]}*` + return `${name[0]}*${name.slice(2)}` +} + +const maskPhone = (phone?: string) => { + if (!phone) return '-' + const digits = phone.replace(/\D/g, '') + if (digits.length < 7) return phone + if (digits.length === 10) return `${digits.slice(0, 3)}-****-${digits.slice(6)}` + return `${digits.slice(0, 3)}-****-${digits.slice(-4)}` +} + +const splitPhone = (phone?: string) => { + const digits = (phone || '').replace(/\D/g, '') + if (digits.length >= 10) { + return { + phone1: digits.slice(0, 3), + phone2: digits.slice(3, digits.length - 4), + phone3: digits.slice(-4) } + } + return { phone1: '010', phone2: '', phone3: '' } +} + +export default function PendingPage() { + const [form] = Form.useForm() + const [editForm] = Form.useForm() + const qc = useQueryClient() + const [filters, setFilters] = useState>(initial) + const [selected, setSelected] = useState([]) + const [editing, setEditing] = useState(null) + const [creating, setCreating] = useState(false) + const [typeOpen, setTypeOpen] = useState(false) + + const query = useQuery({ + queryKey: ['homes-pendings', filters], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/pendings`, { + params: { + ...filters, + pendingType: filters.tab === 'ALL' ? undefined : filters.tab } }) + return unwrap(data) + } }) + + const members = useQuery({ + queryKey: ['homes/members-options'], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/members`, { + params: { page: 0, size: 200 } }) + return unwrap(data) + } }) + + const refresh = () => { + qc.invalidateQueries({ queryKey: ['homes-pendings'] }) + setSelected([]) + } + + const staffOptions = useMemo(() => { + const fromApi = [ + ...(query.data?.receiveEmployees || []), + ...(query.data?.processEmployees || []), + ] + const fromMembers = (members.data?.list || []).map((m) => String(m.name || '')).filter(Boolean) + return Array.from(new Set([...fromApi, ...fromMembers])).map((v) => ({ value: v, label: v })) + }, [query.data, members.data]) + + const typeOptions = useMemo(() => { + const fromApi = query.data?.types || ['일반', '약속1', '약속2'] + return fromApi.map((v) => ({ value: v, label: v })) + }, [query.data?.types]) + + const tabs = useMemo(() => { + const types = query.data?.types || ['일반', '약속1', '약속2'] + return [ + { key: 'ALL', label: `전체 ${query.data?.counts?.ALL ?? query.data?.total ?? 0}` }, + ...types.map((t) => ({ key: t, label: `${t} ${query.data?.counts?.[t] ?? 0}` })), + ] + }, [query.data]) + + const save = useMutation({ + mutationFn: async (values: Record) => { + const phone = [values.phone1, values.phone2, values.phone3] + .map((v) => String(v || '').replace(/\D/g, '')) + .filter(Boolean) + .join('-') + const payload: Record = { + receiveEmployee: values.receiveEmployee, + employee: values.receiveEmployee, + pendingDate: values.pendingDate ? dayjs(values.pendingDate as Dayjs).format('YYYY-MM-DD') : undefined, + pendingType: values.pendingType, + contents: values.contents, + customerName: values.customerName, + phone: phone || undefined, + memo: values.memo, + status: values.status || (editing?.status ?? '접수') } + if (editing) { + const { data } = await client.put(`${apiPrefix()}/homes/pendings/${editing.id}`, payload) + return unwrap(data as ApiResponse) + } + const { data } = await client.post(`${apiPrefix()}/homes/pendings`, payload) + return unwrap(data as ApiResponse) + }, + onSuccess: () => { + message.success(editing ? '수정했습니다.' : '일정을 등록했습니다.') + setCreating(false) + setEditing(null) + editForm.resetFields() + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const processMut = useMutation({ + mutationFn: async (ids: number[]) => { + const { data } = await client.post(`${apiPrefix()}/homes/pendings/process`, { ids }) + return unwrap(data as ApiResponse<{ updated: number }>) + }, + onSuccess: (data) => { + message.success(`${data.updated}건을 처리했습니다.`) + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const saveTypes = useMutation({ + mutationFn: async (types: string[]) => { + const { data } = await client.put(`${apiPrefix()}/homes/pendings/types`, { types }) + return unwrap(data as ApiResponse<{ types: string[] }>) + }, + onSuccess: () => { + message.success('과목을 저장했습니다.') + setTypeOpen(false) + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const setPreset = (from: Dayjs, to: Dayjs) => form.setFieldsValue({ dates: [from, to] }) + + const onSearch = (values: Record) => { + const dates = values.dates as [Dayjs, Dayjs] | undefined + setFilters({ + ...initial, + status: values.status || undefined, + receiveEmployee: values.receiveEmployee || undefined, + processEmployee: values.processEmployee || undefined, + phoneTail: values.phoneTail || undefined, + dateFrom: dates?.[0]?.format('YYYY-MM-DD'), + dateTo: dates?.[1]?.format('YYYY-MM-DD'), + page: 0, + size: values.size ?? filters.size ?? 10, + tab: filters.tab }) + } + + const reset = () => { + form.resetFields() + form.setFieldsValue({ size: 10 }) + setFilters(initial) + } + + const openCreate = () => { + setEditing(null) + setCreating(true) + const defaultStaff = staffOptions[0]?.value || '김대표' + editForm.setFieldsValue({ + receiveEmployee: defaultStaff, + pendingDate: undefined, + pendingType: typeOptions[0]?.value || '일반', + contents: undefined, + customerName: undefined, + phone1: '010', + phone2: '', + phone3: '', + memo: undefined, + status: '접수' }) + } + + const openEdit = (row: PendingRow) => { + setCreating(false) + setEditing(row) + const phones = splitPhone(row.phone) + editForm.setFieldsValue({ + receiveEmployee: row.receiveEmployee, + pendingDate: row.pendingDate ? dayjs(row.pendingDate) : undefined, + pendingType: row.pendingType, + contents: row.contents, + customerName: row.customerName, + ...phones, + memo: row.memo, + status: row.status || '접수' }) + } + + const download = async () => { + try { + const { data } = await client.get(`${apiPrefix()}/homes/pendings/export`, { + params: { ...filters, pendingType: filters.tab === 'ALL' ? undefined : filters.tab }, + responseType: 'blob' }) + const url = URL.createObjectURL(data) + const a = document.createElement('a') + a.href = url + a.download = '일정관리.csv' + a.click() + URL.revokeObjectURL(url) + } catch { + message.error('엑셀 다운로드에 실패했습니다.') + } + } + + const openTypeSettings = () => setTypeOpen(true) + + const typeItems = query.data?.types || ['일반', '약속1', '약속2'] + + const columns = [ + { + title: ( + 0 && selected.length < (query.data?.list?.length || 0)} + onChange={(e) => setSelected(e.target.checked ? (query.data?.list || []).map((r) => r.id) : [])} + /> + ), + width: 42, + render: (_: unknown, row: PendingRow) => ( + setSelected((prev) => (e.target.checked ? [...prev, row.id] : prev.filter((id) => id !== row.id)))} + /> + ) }, + { + title: '상태', + dataIndex: 'status', + width: 70, + render: (v: string) => {v || '-'} }, + { + title: '일자', + dataIndex: 'pendingDate', + width: 100, + sorter: (a: PendingRow, b: PendingRow) => String(a.pendingDate || '').localeCompare(String(b.pendingDate || '')) }, + { title: '과목', dataIndex: 'pendingType', width: 80 }, + { title: '내용', dataIndex: 'contents', width: 160, ellipsis: true }, + { + title: '고객명', + dataIndex: 'customerName', + width: 90, + render: (v: string) => maskName(v) }, + { + title: '연락처', + dataIndex: 'phone', + width: 120, + render: (v: string) => maskPhone(v) }, + { title: '접수직원', dataIndex: 'receiveEmployee', width: 90 }, + { + title: '처리일', + dataIndex: 'processDate', + width: 100, + sorter: (a: PendingRow, b: PendingRow) => String(a.processDate || '').localeCompare(String(b.processDate || '')), + render: (v: string) => v || '-' }, + { + title: '처리직원', + dataIndex: 'processEmployee', + width: 90, + render: (v: string) => v || '-' }, + { + title: '비고', + dataIndex: 'memo', + width: 54, + align: 'center' as const, + render: (v: string) => (v ? : '-') }, + { + title: '관리', + width: 72, + fixed: 'right' as const, + align: 'center' as const, + render: (_: unknown, row: PendingRow) => ( + + + + + + +
+
+ + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + setTypeOpen(false)} + onSave={(types) => saveTypes.mutate(types)} + /> +
+ ) +} diff --git a/frontend/src/pages/care/PreferencePage.tsx b/frontend/src/pages/care/PreferencePage.tsx new file mode 100644 index 0000000..80083e5 --- /dev/null +++ b/frontend/src/pages/care/PreferencePage.tsx @@ -0,0 +1,624 @@ +import { useEffect, useRef, useState } from 'react' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { + Button, Card, Checkbox, Form, Input, InputNumber, Radio, Select, Space, Tabs, Typography, message } from 'antd' +import CareTable from '../../components/CareTable' +import { PlusOutlined, SettingOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' + +type CompanyInfo = { + name?: string + managerName?: string + mobile?: string + businessName?: string + businessNumber?: string + ceo?: string + bizType?: string + bizItem?: string + address?: string +} + +type AccessIps = { + enabled?: boolean + ips?: string[] +} + +type Masking = { + enabled?: boolean + name?: boolean + phone?: boolean + rrn?: boolean + address?: boolean +} + +type StockSetting = { + duplicateSerialCheck?: boolean + allowNegative?: boolean + autoRecallDays?: number +} + +type OpenSetting = { + requireUsim?: boolean + requirePhone?: boolean + defaultOpenHow?: string + autoSettle?: boolean +} + +type Preference = { + company?: CompanyInfo + accessIps?: AccessIps + masking?: Masking + outCategories?: string[] + stock?: StockSetting + open?: OpenSetting +} + +const TAB_KEYS = [ + { key: 'company', label: '사업자정보' }, + { key: 'accessIp', label: '접속아이피' }, + { key: 'masking', label: '개인정보마스킹' }, + { key: 'outCategory', label: '출고처분류' }, + { key: 'stock', label: '재고설정' }, + { key: 'open', label: '개통설정' }, +] as const + +type TabKey = (typeof TAB_KEYS)[number]['key'] + +const splitPhone = (mobile?: string) => { + const digits = String(mobile || '').replace(/\D/g, '') + if (digits.length >= 10) { + return { + p1: digits.slice(0, 3), + p2: digits.slice(3, 7), + p3: digits.slice(7, 11) } + } + const parts = String(mobile || '').split('-') + return { p1: parts[0] || '010', p2: parts[1] || '', p3: parts[2] || '' } +} + +const splitBizNo = (no?: string) => { + const parts = String(no || '').split('-') + if (parts.length >= 3) return { b1: parts[0], b2: parts[1], b3: parts[2] } + const digits = String(no || '').replace(/\D/g, '') + return { + b1: digits.slice(0, 3), + b2: digits.slice(3, 5), + b3: digits.slice(5, 10) } +} + +function SectionHead({ title, desc }: { title: string; desc: string }) { + return ( +
+

{title}

+

{desc}

+
+ ) +} + +export default function PreferencePage() { + const qc = useQueryClient() + const [tab, setTab] = useState('company') + const [companyForm] = Form.useForm() + const [maskForm] = Form.useForm() + const [stockForm] = Form.useForm() + const [openForm] = Form.useForm() + const [ipInput, setIpInput] = useState('') + const [categoryInput, setCategoryInput] = useState('') + const [localIps, setLocalIps] = useState([]) + const [ipEnabled, setIpEnabled] = useState(false) + const [categories, setCategories] = useState([]) + const sectionRefs = useRef>({ + company: null, + accessIp: null, + masking: null, + outCategory: null, + stock: null, + open: null }) + + const query = useQuery({ + queryKey: ['preference'], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/settings/preference`) + return unwrap(data) + } }) + + useEffect(() => { + if (!query.data) return + const c = query.data.company || {} + const phone = splitPhone(c.mobile) + const biz = splitBizNo(c.businessNumber) + companyForm.setFieldsValue({ + name: c.name, + managerName: c.managerName, + phone1: phone.p1 || '010', + phone2: phone.p2, + phone3: phone.p3, + businessName: c.businessName, + biz1: biz.b1, + biz2: biz.b2, + biz3: biz.b3, + ceo: c.ceo, + bizType: c.bizType, + bizItem: c.bizItem, + address: c.address }) + setLocalIps(query.data.accessIps?.ips || []) + setIpEnabled(!!query.data.accessIps?.enabled) + maskForm.setFieldsValue(query.data.masking || {}) + setCategories(query.data.outCategories || []) + stockForm.setFieldsValue(query.data.stock || {}) + openForm.setFieldsValue(query.data.open || {}) + }, [query.data, companyForm, maskForm, stockForm, openForm]) + + const save = useMutation({ + mutationFn: async (patch: Preference) => { + const { data } = await client.put>(`${apiPrefix()}/settings/preference`, patch) + return unwrap(data) + }, + onSuccess: (data) => { + qc.setQueryData(['preference'], data) + message.success('적용했습니다.') + }, + onError: (e: Error) => message.error(e.message) }) + + const onTabChange = (key: string) => { + const k = key as TabKey + setTab(k) + sectionRefs.current[k]?.scrollIntoView({ behavior: 'smooth', block: 'start' }) + } + + const saveCompany = async () => { + const v = await companyForm.validateFields() + const mobile = [v.phone1, v.phone2, v.phone3].filter(Boolean).join('-') + const businessNumber = [v.biz1, v.biz2, v.biz3].filter(Boolean).join('-') + save.mutate({ + company: { + name: v.name, + managerName: v.managerName, + mobile, + businessName: v.businessName, + businessNumber, + ceo: v.ceo, + bizType: v.bizType, + bizItem: v.bizItem, + address: v.address } }) + } + + const saveAccessIps = () => { + save.mutate({ accessIps: { enabled: ipEnabled, ips: localIps } }) + } + + const saveMasking = async () => { + const v = await maskForm.validateFields() + save.mutate({ masking: v }) + } + + const saveCategories = () => { + save.mutate({ outCategories: categories }) + } + + const saveStock = async () => { + const v = await stockForm.validateFields() + save.mutate({ stock: v }) + } + + const saveOpen = async () => { + const v = await openForm.validateFields() + save.mutate({ open: v }) + } + + const addIp = () => { + const ip = ipInput.trim() + if (!ip) { + message.warning('아이피를 입력하세요.') + return + } + if (localIps.includes(ip)) { + message.warning('이미 등록된 아이피입니다.') + return + } + setLocalIps((prev) => [...prev, ip]) + setIpInput('') + } + + const addCategory = () => { + const name = categoryInput.trim() + if (!name) { + message.warning('분류명을 입력하세요.') + return + } + if (categories.includes(name)) { + message.warning('이미 등록된 분류입니다.') + return + } + setCategories((prev) => [...prev, name]) + setCategoryInput('') + } + + return ( +
+
+
+

기본설정

+

에이전트 이용에 필요한 기본적인 설정을 하실 수 있습니다.

+
+
+ + + ({ key: t.key, label: t.label }))} + /> + +
{ sectionRefs.current.company = el }} + className="pref-section" + id="pref-company" + > + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
업체명 + + + +
빌링에이전트 담당자 + + + + 휴대폰 + + + + + + + + +
상호 + + + +
사업자등록번호 + + + + + - + + + + - + + + + + 대표 + + + +
업태 + + + + 종목 + + + +
주소 + + + +
+
+ +
+
+
+ +
{ sectionRefs.current.accessIp = el }} + className="pref-section" + id="pref-accessIp" + > + +
+
+ 접속제한 + setIpEnabled(e.target.value)} + options={[ + { value: true, label: '사용' }, + { value: false, label: '미사용' }, + ]} + /> +
+
+ 아이피 등록 + + setIpInput(e.target.value)} + onPressEnter={addIp} + /> + + +
+ ({ ip }))} + columns={[ + { title: 'No', width: 60, align: 'center' as const, render: (_: unknown, __: { ip: string }, i: number) => i + 1 }, + { title: '아이피', dataIndex: 'ip' }, + { + title: '관리', + width: 90, + align: 'center' as const, + render: (_: unknown, row: { ip: string }) => ( + + ) }, + ]} + locale={{ emptyText: '등록된 아이피가 없습니다.' }} + /> +
+ +
+
+
+ +
{ sectionRefs.current.masking = el }} + className="pref-section" + id="pref-masking" + > + +
+
+ 마스킹 사용 + + + +
+
+ 마스킹 항목 + + + 고객명 + + + 연락처 + + + 주민번호 + + + 주소 + + +
+
+ +
+
+
+ +
{ sectionRefs.current.outCategory = el }} + className="pref-section" + id="pref-outCategory" + > + +
+
+ 분류 등록 + + setCategoryInput(e.target.value)} + onPressEnter={addCategory} + /> + + +
+ ({ name }))} + columns={[ + { title: 'No', width: 60, align: 'center' as const, render: (_: unknown, __: { name: string }, i: number) => i + 1 }, + { title: '분류명', dataIndex: 'name' }, + { + title: '관리', + width: 90, + align: 'center' as const, + render: (_: unknown, row: { name: string }) => ( + + ) }, + ]} + locale={{ emptyText: '등록된 분류가 없습니다.' }} + /> +
+ +
+
+
+ +
{ sectionRefs.current.stock = el }} + className="pref-section" + id="pref-stock" + > + +
+
+ 일련번호 중복검사 + + + +
+
+ 마이너스 재고 허용 + + + +
+
+ 자동회수 기준일 + + + + + 일 (0이면 미사용) + +
+
+ +
+
+
+ +
{ sectionRefs.current.open = el }} + className="pref-section" + id="pref-open" + > + +
+
+ 유심 필수 + + + +
+
+ 단말기 필수 + + + +
+
+ 기본 개통유형 + + + + + + + + + + + + +
+ {editing && ( + { remove.mutate(editing.id); setOpen(false) }}> + + + )} +
+ + +
+
+ + + + setPolicyTarget(null)} + footer={null} + width={640} + destroyOnHidden + > +
addPolicy.mutate(v)} + initialValues={{ amount: 100000 }} + > + + + + + + + +
+ ( + removePolicy.mutate(row.id)}> + + + ) }, + ]} + /> +
+
+ ) +} diff --git a/frontend/src/pages/care/ProductInternetPage.tsx b/frontend/src/pages/care/ProductInternetPage.tsx new file mode 100644 index 0000000..5e0082e --- /dev/null +++ b/frontend/src/pages/care/ProductInternetPage.tsx @@ -0,0 +1,388 @@ +import { useMemo, useState } from 'react' +import { useSearchParams } from 'react-router-dom' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { + Button, Card, Checkbox, Form, Input, InputNumber, Popconfirm, Select, Space, Tabs, message } from 'antd' +import CareTable from '../../components/CareTable' +import { + AppstoreOutlined, FormOutlined, PlusSquareOutlined, SettingOutlined, SyncOutlined } from '@ant-design/icons' +import Modal from '../../components/DraggableModal' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' +import { money, options, type PageResult, type Row } from './homesShared' + +const CATEGORIES = ['인터넷', 'TV', '전화', '와이파이', '스마트홈', '기타'] as const +const COMPANIES = ['KT', 'SKB', 'LGU+', '기타'] +const MONTH_OPTIONS = [ + { value: 12, label: '1년(12개월)' }, + { value: 24, label: '2년(24개월)' }, + { value: 36, label: '3년(36개월)' }, + { value: 48, label: '4년(48개월)' }, + { value: 60, label: '5년(60개월)' }, +] + +function formatMonths(v: unknown) { + const months = Number(v) + if (!months) return '-' + if (months % 12 === 0) return `${months / 12}년(${months}개월)` + return `${months}개월` +} + +export default function ProductInternetPage() { + const qc = useQueryClient() + const [searchParams] = useSearchParams() + const [form] = Form.useForm() + const [policyForm] = Form.useForm() + const initialCategory = CATEGORIES.includes(searchParams.get('category') as (typeof CATEGORIES)[number]) + ? (searchParams.get('category') as string) + : '인터넷' + const [category, setCategory] = useState(initialCategory) + const [page, setPage] = useState(0) + const [size, setSize] = useState(15) + const [selected, setSelected] = useState([]) + const [open, setOpen] = useState(false) + const [editing, setEditing] = useState(null) + const [policyTarget, setPolicyTarget] = useState(null) + + const filters = useMemo( + () => ({ kind: 'INTERNET', category, page, size }), + [category, page, size], + ) + + const query = useQuery({ + queryKey: ['homes/products', filters], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/products`, { params: filters }) + return unwrap(data) + } }) + + const policies = useQuery({ + queryKey: ['homes/product-policies', policyTarget?.id], + queryFn: async () => { + const { data } = await client.get>( + `${apiPrefix()}/homes/products/${policyTarget!.id}/policies`, + ) + return unwrap(data) + }, + enabled: Boolean(policyTarget) }) + + const save = useMutation({ + mutationFn: async (values: Record) => { + const payload = { kind: 'INTERNET', ...values } + if (editing) { + const { data } = await client.put(`${apiPrefix()}/homes/products/${editing.id}`, payload) + return unwrap(data) + } + const { data } = await client.post(`${apiPrefix()}/homes/products`, payload) + return unwrap(data) + }, + onSuccess: () => { + message.success(editing ? '수정했습니다.' : '등록했습니다.') + setOpen(false) + setEditing(null) + form.resetFields() + qc.invalidateQueries({ queryKey: ['homes/products'] }) + }, + onError: (e: Error) => message.error(e.message) }) + + const remove = useMutation({ + mutationFn: async (id: number) => { + const { data } = await client.delete(`${apiPrefix()}/homes/products/${id}`) + return unwrap(data) + }, + onSuccess: () => { + message.success('삭제했습니다.') + qc.invalidateQueries({ queryKey: ['homes/products'] }) + }, + onError: (e: Error) => message.error(e.message) }) + + const sync = useMutation({ + mutationFn: async () => { + const { data } = await client.post>(`${apiPrefix()}/homes/products/sync`, { kind: 'INTERNET' }) + return unwrap(data) + }, + onSuccess: (res) => { + message.success(`기본상품을 동기화했습니다. (+${res?.added ?? 0})`) + qc.invalidateQueries({ queryKey: ['homes/products'] }) + }, + onError: (e: Error) => message.error(e.message) }) + + const addPolicy = useMutation({ + mutationFn: async (values: Record) => { + const { data } = await client.post(`${apiPrefix()}/homes/products/${policyTarget!.id}/policies`, values) + return unwrap(data) + }, + onSuccess: () => { + message.success('정책을 등록했습니다.') + policyForm.resetFields() + policyForm.setFieldsValue({ amount: 50000 }) + qc.invalidateQueries({ queryKey: ['homes/product-policies', policyTarget?.id] }) + qc.invalidateQueries({ queryKey: ['homes/products'] }) + }, + onError: (e: Error) => message.error(e.message) }) + + const removePolicy = useMutation({ + mutationFn: async (id: number) => { + const { data } = await client.delete(`${apiPrefix()}/homes/products/policies/${id}`) + return unwrap(data) + }, + onSuccess: () => { + message.success('정책을 삭제했습니다.') + qc.invalidateQueries({ queryKey: ['homes/product-policies', policyTarget?.id] }) + qc.invalidateQueries({ queryKey: ['homes/products'] }) + }, + onError: (e: Error) => message.error(e.message) }) + + const openCreate = () => { + setEditing(null) + form.resetFields() + form.setFieldsValue({ category: category === 'ALL' ? '인터넷' : category, months: 36 }) + setOpen(true) + } + + const openEdit = (row: Row) => { + setEditing(row) + form.setFieldsValue({ + category: row.category, + company: row.company, + name: row.name, + months: row.months, + memo: row.memo }) + setOpen(true) + } + + const counts = query.data?.counts + const list = query.data?.list || [] + + const columns = [ + { + title: ( + 0 && selected.length === list.length} + indeterminate={selected.length > 0 && selected.length < list.length} + onChange={(e) => setSelected(e.target.checked ? list.map((r) => r.id) : [])} + /> + ), + width: 48, + align: 'center' as const, + render: (_: unknown, row: Row) => ( + { + setSelected((prev) => (e.target.checked ? [...prev, row.id] : prev.filter((id) => id !== row.id))) + }} + /> + ) }, + { title: '회사', dataIndex: 'company', width: 90 }, + { title: '상품명', dataIndex: 'name', ellipsis: true }, + { + title: '기본약정', + dataIndex: 'months', + width: 130, + render: formatMonths }, + { title: '비고', dataIndex: 'memo', width: 160, ellipsis: true, render: (v: unknown) => v || '' }, + { + title: '정책설정', + width: 130, + align: 'center' as const, + render: (_: unknown, row: Row) => { + const count = Number(row.policyCount || 0) + return ( + + ) + } }, + { + title: '관리', + width: 64, + align: 'center' as const, + render: (_: unknown, row: Row) => ( + + + +
+ + +
+ { + setCategory(key) + setPage(0) + setSelected([]) + }} + items={CATEGORIES.map((c) => ({ + key: c, + label: counts?.[c] != null ? `${c} ${counts[c]}` : c }))} + /> +
+ + +
+
+ + { + setPage(p - 1) + setSize(s) + setSelected([]) + }, + showTotal: (t) => `총 ${t}건` }} + /> +
+ + setOpen(false)} + footer={null} + width={520} + destroyOnHidden + className="product-internet-modal" + > +
save.mutate(values)} + className="product-internet-form" + > + + + + + + + + + +
+ {editing && ( + { remove.mutate(editing.id); setOpen(false) }}> + + + )} +
+ + +
+
+
+
+ + setPolicyTarget(null)} + footer={null} + width={640} + destroyOnHidden + > +
addPolicy.mutate(v)} + initialValues={{ amount: 50000 }} + > + + + + + + + +
+ ( + removePolicy.mutate(row.id)}> + + + ) }, + ]} + /> +
+
+ ) +} diff --git a/frontend/src/pages/care/ProductListPage.tsx b/frontend/src/pages/care/ProductListPage.tsx new file mode 100644 index 0000000..4e664f9 --- /dev/null +++ b/frontend/src/pages/care/ProductListPage.tsx @@ -0,0 +1,144 @@ +import { useEffect, useState } from 'react' +import { useNavigate, useLocation } from 'react-router-dom' +import { useQuery } from '@tanstack/react-query' +import { Button, Card, Form, Input, Select, Tabs } from 'antd' +import CareTable from '../../components/CareTable' +import { FileTextOutlined, PlusOutlined, ReloadOutlined, SearchOutlined, ShoppingOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' + +const CATEGORIES = ['인터넷', 'TV', '집전화', '렌탈', '정수기', '공기청정기', '비데', '홈IOT'] as const +const categoryFromPath: Record = { + '/care/product/internet': '인터넷', + '/care/product/tv': 'TV', + '/care/product/phone': '집전화', + '/care/product/rental': '렌탈', + '/care/product/water': '정수기', + '/care/product/air': '공기청정기', + '/care/product/bidet': '비데', + '/care/product/iot': '홈IOT' } + +const options = (values: string[]) => values.map((value) => ({ value, label: value })) +const money = (v: unknown) => (v == null || v === '' ? '-' : Number(v).toLocaleString()) + +type Product = Record & { id: number } +type SearchResult = { total: number; list: Product[]; counts?: Record } + +const initial = { page: 0, size: 15, category: 'ALL' } as Record + +export default function ProductListPage() { + const [form] = Form.useForm() + const navigate = useNavigate() + const location = useLocation() + const pathCategory = categoryFromPath[location.pathname] + const [filters, setFilters] = useState>({ + ...initial, + category: pathCategory ?? 'ALL' }) + + useEffect(() => { + setFilters((v) => ({ ...v, category: pathCategory ?? 'ALL', page: 0 })) + }, [pathCategory]) + + const query = useQuery({ + queryKey: ['products-list', filters], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/products`, { params: filters }) + return unwrap(data) + } }) + + const onSearch = (values: Record) => { + setFilters({ + ...initial, + ...values, + category: filters.category, + page: 0, + size: values.size ?? filters.size ?? 15 }) + } + + const reset = () => { + form.resetFields() + form.setFieldsValue({ size: 15 }) + setFilters({ ...initial, category: pathCategory ?? 'ALL' }) + } + + const columns = [ + { title: '분류', dataIndex: 'category', width: 100 }, + { title: '상품명', dataIndex: 'name', width: 180, ellipsis: true, sorter: true }, + { title: '통신사', dataIndex: 'telecom', width: 70 }, + { title: '요금제/모델', dataIndex: 'plan', width: 140, ellipsis: true }, + { title: '월정액', dataIndex: 'monthlyFee', width: 100, align: 'right' as const, render: money }, + { title: '판매가', dataIndex: 'price', width: 100, align: 'right' as const, render: money }, + { title: '상태', dataIndex: 'status', width: 80 }, + { + title: '관리', width: 58, fixed: 'right' as const, + render: (_: unknown, row: Product) => ( + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+ ) +} diff --git a/frontend/src/pages/care/ReceptionListPage.tsx b/frontend/src/pages/care/ReceptionListPage.tsx new file mode 100644 index 0000000..7ab99f8 --- /dev/null +++ b/frontend/src/pages/care/ReceptionListPage.tsx @@ -0,0 +1,830 @@ +import { useMemo, useState } from 'react' +import Modal from '../../components/DraggableModal' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import dayjs from 'dayjs' +import { + Button, + Checkbox, + DatePicker, + Dropdown, + Form, + Input, + Select, + message, +} from 'antd' +import ReceptionApplyModal from '../../components/ReceptionApplyModal' +import ReceptionBulkModal from '../../components/ReceptionBulkModal' +import ReceptionProgressModal from '../../components/ReceptionProgressModal' +import { DownOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' +import { options } from './homesShared' + +type ReceptionRow = { + id: number + orderDate?: string + today?: boolean + counselor?: string + employeeName?: string + branch?: string + branchOfficeName?: string + branchGroup?: string + branchGroupName?: string + carrier?: string + ispName?: string + giftCategory?: string + giftType?: string + productCategory?: string + productType?: string + productName?: string + customerName?: string + phone?: string + region?: string + sido?: string + progressStatus?: string + status?: string + settleStatus?: string + centerStatus?: string + centerName?: string + openDate?: string + opened?: string + cancelDate?: string + canceled?: string + giftDate?: string + giftSended?: string + giftStatus?: string + giftPlace?: string + giftAmount?: number + agreementName?: string + contractNumber?: string + identifyNumber?: string + postIt?: string + postAdd?: string + amount?: number +} + +type ReceptionResult = { + total: number + list: ReceptionRow[] + progressStatuses?: string[] + settleStatuses?: string[] + giftStatuses?: string[] + carriers?: string[] + branches?: string[] + counselors?: string[] + processors?: string[] + regions?: string[] + giftCategories?: string[] + productCategories?: string[] +} + +const SIDO_LIST = [ + '서울', '경기', '인천', '강원', '충북', '충남', '대전', '전북', '전남', '광주', + '경북', '대구', '경남', '울산', '부산', '제주', +] + +const GIFT_PLACE_LABEL: Record = { + b01: '본사요청', + b02: '자체발송', + 본사: '본사발송', + 영업점: '영업점발송', +} + +const initialFilters = () => ({ + page: 0, + size: 50, + orderDateStart: dayjs().startOf('month').format('YYYY-MM-DD'), + orderDateEnd: dayjs().format('YYYY-MM-DD'), + sorting: 'order_date', + postIt: '', + agreementName: '', + customerSsid: '', + customerEmail: '', + customerName: '', + customerPhone: '', + contractNumber: '', + productName: '', + ispName: '', + branchEmployee: '', + branchName: '', + giftType: undefined as string | undefined, + productType: undefined as string | undefined, + progressStatus: undefined as string | undefined, + giftStatus: undefined as string | undefined, + giftPlace: undefined as string | undefined, + customerSido: undefined as string | undefined, + carrier: undefined as string | undefined, + branch: undefined as string | undefined, + settleStatus: undefined as string | undefined, + opended: undefined as string | undefined, + giftSended: undefined as string | undefined, +}) + +const num = (n?: number | null) => (n == null ? '' : Number(n).toLocaleString()) + +const giftPlaceLabel = (v?: string) => { + if (!v) return '' + return GIFT_PLACE_LABEL[v] || v +} + +const splitDt = (v?: string) => { + if (!v) return ['', ''] + const s = String(v).replace('T', ' ') + const [d, t] = s.split(' ') + return [d || '', (t || '').substring(0, 8)] +} + +export default function ReceptionListPage() { + const [editForm] = Form.useForm() + const qc = useQueryClient() + const [draft, setDraft] = useState(initialFilters) + const [filters, setFilters] = useState(initialFilters) + const [selected, setSelected] = useState([]) + const [detail, setDetail] = useState(null) + const [creating, setCreating] = useState(false) + const [bulkOpen, setBulkOpen] = useState(false) + const [progressId, setProgressId] = useState(null) + const [applyIsp, setApplyIsp] = useState<{ id: number; name: string; telecom: string; isSktb?: boolean } | null>(null) + + const ispQuery = useQuery({ + queryKey: ['homes-reception-isps'], + queryFn: async () => { + const { data } = await client.get>( + `${apiPrefix()}/homes/reception-isps`, + ) + return unwrap(data) + }, + }) + + const query = useQuery({ + queryKey: ['homes-receptions', filters], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/receptions`, { + params: { + page: filters.page, + size: filters.size, + orderDateStart: filters.orderDateStart || undefined, + orderDateEnd: filters.orderDateEnd || undefined, + sorting: filters.sorting, + postIt: filters.postIt || undefined, + agreementName: filters.agreementName || undefined, + customerSsid: filters.customerSsid || undefined, + customerEmail: filters.customerEmail || undefined, + customerName: filters.customerName || undefined, + customerPhone: filters.customerPhone || undefined, + contractNumber: filters.contractNumber || undefined, + productName: filters.productName || undefined, + ispName: filters.ispName || undefined, + branchEmployee: filters.branchEmployee || undefined, + branchName: filters.branchName || undefined, + giftType: filters.giftType || undefined, + productType: filters.productType || undefined, + progressStatus: filters.progressStatus || undefined, + giftStatus: filters.giftStatus || undefined, + giftPlace: filters.giftPlace || undefined, + customerSido: filters.customerSido || undefined, + carrier: filters.carrier || undefined, + branch: filters.branch || undefined, + settleStatus: filters.settleStatus || undefined, + opended: filters.opended || undefined, + giftSended: filters.giftSended || undefined, + }, + }) + return unwrap(data) + }, + }) + + const refresh = () => { + qc.invalidateQueries({ queryKey: ['homes-receptions'] }) + setSelected([]) + } + + const progressStatuses = query.data?.progressStatuses || [] + const settleStatuses = query.data?.settleStatuses || [] + const giftStatuses = query.data?.giftStatuses || [] + const list = query.data?.list || [] + + const setD = (patch: Partial>) => setDraft((p) => ({ ...p, ...patch })) + const applyF = (patch: Partial>) => { + const next = { ...draft, ...patch, page: 0 } + setDraft(next) + setFilters(next) + } + + const search = () => setFilters({ ...draft, page: 0 }) + const reset = () => { + const next = initialFilters() + setDraft(next) + setFilters(next) + setSelected([]) + } + + const quick = (key: string) => { + const today = dayjs() + let start = today + let end = today + switch (key) { + case 'today': + break + case 'yesterday': + start = today.subtract(1, 'day') + end = start + break + case 'thisWeek': + start = today.startOf('week') + break + case 'lastWeek': + start = today.subtract(1, 'week').startOf('week') + end = today.subtract(1, 'week').endOf('week') + break + case 'thisMonth': + start = today.startOf('month') + break + case 'lastMonth': + start = today.subtract(1, 'month').startOf('month') + end = today.subtract(1, 'month').endOf('month') + break + case 'thisYear': + start = today.startOf('year') + break + case 'lastYear': + start = today.subtract(1, 'year').startOf('year') + end = today.subtract(1, 'year').endOf('year') + break + default: + break + } + applyF({ + orderDateStart: start.format('YYYY-MM-DD'), + orderDateEnd: end.format('YYYY-MM-DD'), + }) + } + + const saveMut = useMutation({ + mutationFn: async (payload: Record) => { + if (detail?.id && !creating) { + const { data } = await client.put(`${apiPrefix()}/homes/receptions/${detail.id}`, payload) + return unwrap(data) + } + const { data } = await client.post(`${apiPrefix()}/homes/receptions`, payload) + return unwrap(data) + }, + onSuccess: () => { + message.success(creating ? '등록했습니다.' : '수정했습니다.') + setCreating(false) + setDetail(null) + refresh() + }, + onError: () => message.error('저장에 실패했습니다.'), + }) + + const deleteMut = useMutation({ + mutationFn: async (ids: number[]) => { + const { data } = await client.post(`${apiPrefix()}/homes/receptions/delete`, { ids }) + return unwrap(data) + }, + onSuccess: () => { + message.success('삭제했습니다.') + refresh() + }, + onError: () => message.error('삭제에 실패했습니다.'), + }) + + const openEditFromProgress = async (id: number) => { + try { + const { data } = await client.get>(`${apiPrefix()}/homes/receptions/${id}`) + const full = unwrap(data) as ReceptionRow + setProgressId(null) + setCreating(false) + setDetail(full) + editForm.setFieldsValue({ + ...full, + orderDate: full.orderDate ? dayjs(full.orderDate) : undefined, + openDate: full.openDate || full.opened ? dayjs(full.openDate || full.opened) : undefined, + giftDate: full.giftDate ? dayjs(full.giftDate) : undefined, + }) + } catch { + message.error('상세 조회에 실패했습니다.') + } + } + + const downloadExcel = async () => { + try { + const { data } = await client.get(`${apiPrefix()}/homes/receptions/excel`, { + params: { ...filters, page: undefined, size: undefined }, + responseType: 'blob', + }) + const url = URL.createObjectURL(data) + const a = document.createElement('a') + a.href = url + a.download = '접수리스트.csv' + a.click() + URL.revokeObjectURL(url) + } catch { + message.error('엑셀 다운로드에 실패했습니다.') + } + } + + const allChecked = list.length > 0 && selected.length === list.length + const toggleAll = (checked: boolean) => setSelected(checked ? list.map((r) => r.id) : []) + + const productTypeLabel = (row: ReceptionRow) => { + const t = row.productType + if (t === 'e01') return '인터넷' + if (t === 'e02') return '전화' + if (t === 'e03') return 'TV' + if (t === 'e04') return '렌탈' + return row.productCategory || '-' + } + + const rows = useMemo(() => list, [list]) + + return ( +
+
+
+

접수관리 - 접수리스트

+

접수 내역을 조회하고 업무 진행을 관리합니다.

+
+
+ ( +
+
신규신청
+
    + {(ispQuery.data || []).map((isp) => ( +
  • + +
  • + ))} + {!ispQuery.data?.length && !ispQuery.isLoading ? ( +
  • 등록된 통신사가 없습니다.
  • + ) : null} +
+
+ )} + > + +
+ + +
+
+ +
+ + + + +
+ +
+
+ setD({ orderDateStart: d ? d.format('YYYY-MM-DD') : '' })} + /> + ~ + setD({ orderDateEnd: d ? d.format('YYYY-MM-DD') : '' })} + /> +
+ {(['today', 'yesterday', 'thisWeek', 'lastWeek', 'thisMonth', 'lastMonth', 'thisYear', 'lastYear'] as const).map((k) => ( + + ))} +
+ + 검색 결과: 총 {(query.data?.total ?? 0).toLocaleString()} 건 +
+ +
+ setD({ postIt: e.target.value })} /> + setD({ agreementName: e.target.value })} /> + setD({ customerSsid: e.target.value })} /> + setD({ customerEmail: e.target.value })} /> + + setD({ branchEmployee: e.target.value })} /> + + setD({ branchName: e.target.value })} /> + setD({ ispName: e.target.value })} /> + setD({ productName: e.target.value })} /> +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {query.isLoading && ( + + )} + {!query.isLoading && rows.length === 0 && ( + + )} + {rows.map((o, idx) => { + const rowNum = Number(filters.page) * Number(filters.size) + idx + 1 + const [od, ot] = splitDt(o.orderDate) + const giftAmt = o.giftAmount ?? null + return ( + setProgressId(o.id)}> + + + + + + + + + + + + + + + + + + ) + })} + +
+ toggleAll(e.target.checked)} /> + 접수번호접수일입력처그룹
영업점
통신사사은품종류
상품종류
상품명고객명
(포스트잇)
약정
지역
주민번호
연락처
담당자명
진행현황
개통번호
개통일자
지급처
지급날짜
사은품현황
금액
입력처
진행현황
   + + + + + +   + setD({ customerName: e.target.value })} + onKeyDown={(e) => e.key === 'Enter' && search()} + /> + + + setD({ customerPhone: e.target.value })} + onKeyDown={(e) => e.key === 'Enter' && search()} + /> + + + setD({ contractNumber: e.target.value })} + onKeyDown={(e) => e.key === 'Enter' && search()} + /> + + +  
불러오는 중...
검색 결과가 없습니다.
e.stopPropagation()}> + setSelected((prev) => (e.target.checked ? [...prev, o.id] : prev.filter((id) => id !== o.id)))} + /> + +

{o.id.toLocaleString()}

+

({rowNum})

+
+

{od}

+

{ot}

+
+

{o.centerName || '본사'}

+
+

{o.branchGroupName || o.branchGroup || ''}

+

{o.branchOfficeName || o.branch || ''}

+
{o.ispName || o.carrier || '-'} +

{o.giftType || o.giftCategory || '-'}

+

{productTypeLabel(o)}

+
{o.productName || '-'} +

{o.customerName || '-'}

+ {o.postIt ?

({o.postIt})

: null} + {o.postAdd ?

({o.postAdd})

: null} +
+

{o.agreementName || '-'}

+

{o.sido || o.region || ''}

+
+

{o.identifyNumber || ''}

+

{o.phone || ''}

+
+

{o.employeeName || o.counselor || ''}

+

{o.status || o.progressStatus || ''}

+
+

개통일: {o.opened || o.openDate || '-'}

+

해지일: {o.canceled || '-'}

+

정산상태: {o.settleStatus || o.centerStatus || '-'}

+
+
+

{o.contractNumber || ''}

+

{o.opened || o.openDate || ''}

+
+

{giftPlaceLabel(o.giftPlace)}

+

{o.giftSended || o.giftDate || ''}

+
+

{o.giftStatus || ''}

+

{num(giftAmt)}

+
+ {o.centerStatus || o.settleStatus || ''} +
+

정산상태: {o.settleStatus || '-'}

+

개통일: {o.opened || o.openDate || '-'}

+

해지일: {o.canceled || o.cancelDate || '-'}

+
+
+
+ +
+ + + {Number(filters.page) + 1} / {Math.max(1, Math.ceil((query.data?.total || 0) / Number(filters.size || 50)))} + + +
+ + { setCreating(false); setDetail(null) }} + footer={null} + destroyOnHidden + width={720} + > +
{ + saveMut.mutate({ + ...values, + orderDate: values.orderDate ? dayjs(values.orderDate).format('YYYY-MM-DD HH:mm:ss') : undefined, + openDate: values.openDate ? dayjs(values.openDate).format('YYYY-MM-DD') : undefined, + giftDate: values.giftDate ? dayjs(values.giftDate).format('YYYY-MM-DD') : undefined, + }) + }} + > +
+ + + + + + + + + + + + + + + + + + + + )} + + + + + ({ value: v, label: v }))} /> + + + + + + + + + +
+ + + + { + if (row._summary) return 'open-stat-summary' + if (mode === 'daily' && query.data?.today != null && row.key === query.data.today) return 'open-stat-today' + if (mode === 'monthly' && query.data?.currentMonth != null && row.key === query.data.currentMonth) return 'open-stat-today' + return '' + }} + locale={{ emptyText: '등록되어 있는 자료가 없습니다.' }} + /> + +
+ ) +} diff --git a/frontend/src/pages/care/ReportOutPage.tsx b/frontend/src/pages/care/ReportOutPage.tsx new file mode 100644 index 0000000..5998a21 --- /dev/null +++ b/frontend/src/pages/care/ReportOutPage.tsx @@ -0,0 +1,215 @@ +import { useMemo, useState } from 'react' +import { useQuery } from '@tanstack/react-query' +import dayjs from 'dayjs' +import { Button, Card, Form, Select, Space, message } from 'antd' +import CareTable from '../../components/CareTable' +import { BarChartOutlined, FileTextOutlined, SearchOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' +import { resourceApi } from '../../api/resources' + +type OutStatRow = { + id: number + region: string + name: string + openCount: number + openAmount: number + afterCount: number + afterAmount: number + homeCount: number + homeAmount: number + realSettleAmount: number + margin: number + _summary?: boolean +} + +type OutStatData = { + year: number + month: number + title: string + total: number + rows: OutStatRow[] + totals: OutStatRow +} + +const num = (v: unknown) => { + const n = Number(v ?? 0) + return Number.isFinite(n) ? n : 0 +} +const cell = (v: unknown) => (num(v) === 0 ? '' : num(v).toLocaleString()) +const money = (v: unknown) => (num(v) === 0 ? '' : num(v).toLocaleString()) + +export default function ReportOutPage() { + const now = useMemo(() => dayjs(), []) + const [form] = Form.useForm() + const [filters, setFilters] = useState>({ + year: now.year(), + month: now.month() + 1 }) + + const companies = useQuery({ + queryKey: ['out-stats-companies'], + queryFn: () => resourceApi.list('companies', { size: 200 }) }) + + const query = useQuery({ + queryKey: ['outcompany-stats', filters], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/reports/outcompany-stats`, { + params: filters }) + return unwrap(data) + } }) + + const inOptions = useMemo( + () => (companies.data?.items ?? []) + .filter((c) => String(c.type) === 'IN') + .map((c) => ({ value: Number(c.id), label: String(c.name) })), + [companies.data], + ) + + const yearOptions = useMemo(() => { + const years: number[] = [] + for (let y = now.year(); y >= now.year() - 5; y--) years.push(y) + return years.map((y) => ({ value: y, label: `${y}년` })) + }, [now]) + + const monthOptions = useMemo( + () => Array.from({ length: 12 }, (_, i) => ({ value: i + 1, label: `${i + 1}월` })), + [], + ) + + const shiftMonth = (delta: number) => { + const year = Number(form.getFieldValue('year') ?? now.year()) + const month = Number(form.getFieldValue('month') ?? now.month() + 1) + const next = dayjs(`${year}-${String(month).padStart(2, '0')}-01`).add(delta, 'month') + form.setFieldsValue({ year: next.year(), month: next.month() + 1 }) + } + + const onSearch = (values: Record) => { + setFilters({ + year: values.year, + month: values.month, + telecom: values.telecom, + incompanyId: values.incompanyId, + outCategory: values.outCategory }) + } + + const onReset = () => { + const init = { year: now.year(), month: now.month() + 1, telecom: undefined, incompanyId: undefined, outCategory: undefined } + form.setFieldsValue(init) + setFilters({ year: init.year, month: init.month }) + } + + const download = async () => { + try { + const { data } = await client.get(`${apiPrefix()}/reports/outcompany-stats/export`, { + params: filters, + responseType: 'blob' }) + const url = URL.createObjectURL(data) + const a = document.createElement('a') + a.href = url + a.download = `출고처통계_${filters.year}-${filters.month}.csv` + a.click() + URL.revokeObjectURL(url) + } catch { + message.error('엑셀 다운로드에 실패했습니다.') + } + } + + const dataSource = useMemo(() => { + const list = query.data?.rows ?? [] + if (!list.length || !query.data?.totals) return list + return [ + ...list, + { + ...query.data.totals, + id: -1, + region: '합계', + name: '', + _summary: true }, + ] + }, [query.data]) + + const dateTitle = `${filters.year}년 ${filters.month}월` + + const columns = [ + { + title: dateTitle, + dataIndex: 'region', + width: 90, + align: 'center' as const, + render: (v: string, row: OutStatRow) => (row._summary ? {v} : v), + onCell: (row: OutStatRow) => (row._summary ? { colSpan: 2 } : {}) }, + { + title: '', + dataIndex: 'name', + width: 140, + align: 'center' as const, + onCell: (row: OutStatRow) => (row._summary ? { colSpan: 0 } : {}), + render: (v: string) => v }, + { title: '개통', dataIndex: 'openCount', width: 80, align: 'center' as const, render: cell }, + { title: '개통금액', dataIndex: 'openAmount', width: 110, align: 'center' as const, render: money }, + { title: '후정산', dataIndex: 'afterCount', width: 80, align: 'center' as const, render: cell }, + { title: '후정산금액', dataIndex: 'afterAmount', width: 110, align: 'center' as const, render: money }, + { title: '홈상품', dataIndex: 'homeCount', width: 80, align: 'center' as const, render: cell }, + { title: '홈상품금액', dataIndex: 'homeAmount', width: 110, align: 'center' as const, render: money }, + { title: '실정산금액', dataIndex: 'realSettleAmount', width: 110, align: 'center' as const, render: money }, + { title: '마진', dataIndex: 'margin', width: 110, align: 'center' as const, render: money }, + ] + + return ( +
+
+
+

출고처통계

+

출고처별로 정산에 대한 통계를 확인하실 수 있습니다.

+
+
+ + +
+
+ + + + + + + + + ({ value: v, label: v }))} /> + + + ({ value: v, label: v }))} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + ({ value: v, label: v }))} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ({ value: v, label: v }))} disabled={!!editing?.isOwner} /> + + + + +
+ + +
+
+ + + { + setMemoOpen(false) + setMemoTarget(null) + }} + footer={null} + width={420} + destroyOnHidden + centered + > +
memoMut.mutate(String(v.memo || ''))}> + + + +
+ + +
+
+
+ + { + setExpoOpen(false) + setExpoTarget(null) + }} + footer={null} + width={480} + destroyOnHidden + centered + > +

메뉴 노출 여부를 설정합니다. 체크된 메뉴만 해당 직원에게 표시됩니다.

+
expoMut.mutate(values as Record)} + > +
+ {MENU_EXPOSURE.map((m) => ( + + {m.label} + + ))} +
+
+ + +
+ +
+
+ ) +} diff --git a/frontend/src/pages/care/SettingPartner1Page.tsx b/frontend/src/pages/care/SettingPartner1Page.tsx new file mode 100644 index 0000000..f9bf73e --- /dev/null +++ b/frontend/src/pages/care/SettingPartner1Page.tsx @@ -0,0 +1,301 @@ +import { useState } from 'react' +import Modal from '../../components/DraggableModal' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { + Button, + Card, + Form, + Input, + Space, + Typography, + message } from 'antd' +import CareTable from '../../components/CareTable' +import { + EditOutlined, + PlusSquareOutlined, + SettingOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' + +type PartnerRow = { + id: number + status?: string + name?: string + userid?: string + managerName?: string + phone?: string + registeredDate?: string + memo?: string + partnerLevel?: string +} + +type PartnerResult = { + total: number + list: PartnerRow[] +} + +/** 원본 안내 문구용 잎 아이콘 */ +function LeafOutlined({ className }: { className?: string }) { + return ( + + + + + + ) +} + +export default function SettingPartner1Page() { + const qc = useQueryClient() + const [editForm] = Form.useForm() + const [filters, setFilters] = useState({ page: 0, size: 15, partnerLevel: '1' }) + const [open, setOpen] = useState(false) + const [editing, setEditing] = useState(null) + + const query = useQuery({ + queryKey: ['homes-partners-1', filters], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/partners`, { + params: filters }) + return unwrap(data) + } }) + + const refresh = () => { + qc.invalidateQueries({ queryKey: ['homes-partners-1'] }) + } + + const saveMut = useMutation({ + mutationFn: async (values: Record) => { + const payload = { + partnerLevel: '1', + name: values.name, + userid: values.userid, + managerName: values.managerName, + phone: values.phone, + memo: values.memo, + status: values.status || editing?.status || '대기' } + if (editing?.id) { + const { data } = await client.put(`${apiPrefix()}/homes/partners/${editing.id}`, payload) + return unwrap(data as ApiResponse) + } + const { data } = await client.post(`${apiPrefix()}/homes/partners`, payload) + return unwrap(data as ApiResponse) + }, + onSuccess: () => { + message.success(editing ? '수정했습니다.' : '상부점 등록을 신청했습니다.') + setOpen(false) + setEditing(null) + editForm.resetFields() + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const deleteMut = useMutation({ + mutationFn: async (id: number) => { + const { data } = await client.delete(`${apiPrefix()}/homes/partners/${id}`) + return unwrap(data as ApiResponse) + }, + onSuccess: () => { + message.success('삭제했습니다.') + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const openCreate = () => { + setEditing(null) + editForm.setFieldsValue({ + name: undefined, + userid: undefined, + managerName: undefined, + phone: undefined, + memo: undefined, + status: '대기' }) + setOpen(true) + } + + const openEdit = (row: PartnerRow) => { + setEditing(row) + editForm.setFieldsValue({ + name: row.name, + userid: row.userid, + managerName: row.managerName, + phone: row.phone, + memo: row.memo, + status: row.status || '대기' }) + setOpen(true) + } + + const columns = [ + { + title: '상태', + dataIndex: 'status', + width: 90, + align: 'center' as const, + render: (v?: string) => { + const status = v || '대기' + const cls = + status === '승인' ? 'partner-status-ok' : status === '거절' ? 'partner-status-deny' : 'partner-status-wait' + return {status} + } }, + { + title: '업체명', + dataIndex: 'name', + width: 180, + ellipsis: true, + render: (v?: string) => v || '' }, + { + title: '아이디', + dataIndex: 'userid', + width: 120, + align: 'center' as const, + render: (v?: string) => v || '' }, + { + title: '담당자', + dataIndex: 'managerName', + width: 100, + align: 'center' as const, + render: (v?: string) => v || '' }, + { + title: '연락처', + dataIndex: 'phone', + width: 130, + align: 'center' as const, + render: (v?: string) => v || '' }, + { + title: '등록일', + dataIndex: 'registeredDate', + width: 120, + align: 'center' as const }, + { + title: '관리', + key: 'manage', + width: 64, + align: 'center' as const, + render: (_: unknown, row: PartnerRow) => ( + +
+ + +
+ + 목록 {query.data?.total ?? 0} + +
+ setFilters((prev) => ({ ...prev, page: page - 1 })) }} + locale={{ emptyText: '등록되어 있는 상부점이 없습니다.' }} + /> +
+ + + 상부점에서 승인처리 후 계약자료 전송이 가능합니다. (또한 상부점에서 등록신청을 삭제할 경우 목록에서 제외됩니다.) + +
+
+ + { + setOpen(false) + setEditing(null) + }} + footer={null} + width={480} + destroyOnHidden + centered + > +
saveMut.mutate(values)} + > + + + + + + + + + + + + + + + +
+ + + {editing ? ( + + ) : null} + + +
+
+
+ + ) +} diff --git a/frontend/src/pages/care/SettingPartner2Page.tsx b/frontend/src/pages/care/SettingPartner2Page.tsx new file mode 100644 index 0000000..5b7cead --- /dev/null +++ b/frontend/src/pages/care/SettingPartner2Page.tsx @@ -0,0 +1,280 @@ +import { useState } from 'react' +import Modal from '../../components/DraggableModal' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { + Button, + Card, + Form, + Input, + Radio, + Space, + Typography, + message } from 'antd' +import CareTable from '../../components/CareTable' +import { + EditOutlined, + SettingOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' + +type PartnerRow = { + id: number + status?: string + name?: string + userid?: string + managerName?: string + phone?: string + registeredDate?: string + memo?: string + partnerLevel?: string +} + +type PartnerResult = { + total: number + list: PartnerRow[] +} + +export default function SettingPartner2Page() { + const qc = useQueryClient() + const [editForm] = Form.useForm() + const [filters, setFilters] = useState({ page: 0, size: 15, partnerLevel: '2' }) + const [open, setOpen] = useState(false) + const [editing, setEditing] = useState(null) + + const query = useQuery({ + queryKey: ['homes-partners-2', filters], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/partners`, { + params: filters }) + return unwrap(data) + } }) + + const refresh = () => { + qc.invalidateQueries({ queryKey: ['homes-partners-2'] }) + } + + const saveMut = useMutation({ + mutationFn: async (values: Record) => { + if (!editing?.id) throw new Error('하부점을 선택하세요.') + const payload = { + partnerLevel: '2', + name: values.name ?? editing.name, + userid: values.userid ?? editing.userid, + managerName: values.managerName ?? editing.managerName, + phone: values.phone ?? editing.phone, + memo: values.memo, + status: values.status || editing.status || '대기' } + const { data } = await client.put(`${apiPrefix()}/homes/partners/${editing.id}`, payload) + return unwrap(data as ApiResponse) + }, + onSuccess: () => { + message.success('적용했습니다.') + setOpen(false) + setEditing(null) + editForm.resetFields() + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const deleteMut = useMutation({ + mutationFn: async (id: number) => { + const { data } = await client.delete(`${apiPrefix()}/homes/partners/${id}`) + return unwrap(data as ApiResponse) + }, + onSuccess: () => { + message.success('삭제했습니다.') + setOpen(false) + setEditing(null) + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const openEdit = (row: PartnerRow) => { + setEditing(row) + editForm.setFieldsValue({ + name: row.name, + userid: row.userid, + managerName: row.managerName, + phone: row.phone, + memo: row.memo, + status: row.status || '대기' }) + setOpen(true) + } + + const columns = [ + { + title: '상태', + dataIndex: 'status', + width: 90, + align: 'center' as const, + render: (v?: string) => { + const status = v || '대기' + const cls = + status === '승인' + ? 'partner-status-ok' + : status === '보류' + ? 'partner-status-hold' + : status === '거절' + ? 'partner-status-deny' + : 'partner-status-wait' + return {status} + } }, + { + title: '업체명', + dataIndex: 'name', + width: 180, + ellipsis: true, + render: (v?: string) => v || '' }, + { + title: '아이디', + dataIndex: 'userid', + width: 120, + align: 'center' as const, + render: (v?: string) => v || '' }, + { + title: '담당자', + dataIndex: 'managerName', + width: 100, + align: 'center' as const, + render: (v?: string) => v || '' }, + { + title: '연락처', + dataIndex: 'phone', + width: 130, + align: 'center' as const, + render: (v?: string) => v || '' }, + { + title: '등록일', + dataIndex: 'registeredDate', + width: 120, + align: 'center' as const }, + { + title: '관리', + key: 'manage', + width: 64, + align: 'center' as const, + render: (_: unknown, row: PartnerRow) => ( + + + {editing ? ( + + ) : null} + + + + + + + ) +} diff --git a/frontend/src/pages/care/SettingPreferencePage.tsx b/frontend/src/pages/care/SettingPreferencePage.tsx new file mode 100644 index 0000000..e8dd30a --- /dev/null +++ b/frontend/src/pages/care/SettingPreferencePage.tsx @@ -0,0 +1,501 @@ +import { useEffect, useRef, useState } from 'react' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { + Button, + Card, + Form, + Input, + InputNumber, + Radio, + Space, + Tabs, + Typography, + message } from 'antd' +import CareTable from '../../components/CareTable' +import { PlusOutlined, SettingOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' + +type ManagerInfo = { name?: string; phone?: string } +type LoginBlock = { mode?: 'none' | 'block_staff' } +type AccessIps = { enabled?: boolean; ips?: string[] } +type ContractSetting = { + autoRegisterCustomer?: boolean + defaultMonthsInternet?: number + defaultMonthsHome?: number + requireAgency?: boolean + autoCalcSettle?: boolean + allowWithdraw?: boolean +} + +type HomesPreference = { + manager?: ManagerInfo + loginBlock?: LoginBlock + accessIps?: AccessIps + contract?: ContractSetting + incentiveRate?: number + autoInvoice?: boolean +} + +const TAB_KEYS = [ + { key: 'manager', label: '담당자정보 설정' }, + { key: 'loginBlock', label: '로그인차단 설정' }, + { key: 'accessIp', label: '접속아이피 설정' }, + { key: 'contract', label: '계약 설정' }, +] as const + +type TabKey = (typeof TAB_KEYS)[number]['key'] + +const splitPhone = (phone?: string) => { + const digits = String(phone || '').replace(/\D/g, '') + if (digits.length >= 10) { + return { + p1: digits.slice(0, 3), + p2: digits.slice(3, digits.length - 4), + p3: digits.slice(-4) } + } + const parts = String(phone || '').split('-') + return { p1: parts[0] || '010', p2: parts[1] || '', p3: parts[2] || '' } +} + +function SectionHead({ title, desc }: { title: string; desc: string }) { + return ( +
+

{title}

+

{desc}

+
+ ) +} + +export default function SettingPreferencePage() { + const qc = useQueryClient() + const [tab, setTab] = useState('manager') + const [managerForm] = Form.useForm() + const [loginForm] = Form.useForm() + const [contractForm] = Form.useForm() + const [ipInput, setIpInput] = useState('') + const [localIps, setLocalIps] = useState([]) + const [ipEnabled, setIpEnabled] = useState(false) + const sectionRefs = useRef>({ + manager: null, + loginBlock: null, + accessIp: null, + contract: null }) + + const query = useQuery({ + queryKey: ['homes-preference'], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/homes/preference`) + return unwrap(data) + } }) + + useEffect(() => { + if (!query.data) return + const phone = splitPhone(query.data.manager?.phone) + managerForm.setFieldsValue({ + name: query.data.manager?.name || '', + phone1: phone.p1 || '010', + phone2: phone.p2, + phone3: phone.p3 }) + loginForm.setFieldsValue({ + mode: query.data.loginBlock?.mode || 'none' }) + setLocalIps(query.data.accessIps?.ips || []) + setIpEnabled(!!query.data.accessIps?.enabled) + contractForm.setFieldsValue({ + autoRegisterCustomer: query.data.contract?.autoRegisterCustomer ?? true, + defaultMonthsInternet: query.data.contract?.defaultMonthsInternet ?? 36, + defaultMonthsHome: query.data.contract?.defaultMonthsHome ?? 60, + requireAgency: query.data.contract?.requireAgency ?? false, + autoCalcSettle: query.data.contract?.autoCalcSettle ?? true, + allowWithdraw: query.data.contract?.allowWithdraw ?? true, + incentiveRate: query.data.incentiveRate ?? 5, + autoInvoice: query.data.autoInvoice ?? true }) + }, [query.data, managerForm, loginForm, contractForm]) + + const save = useMutation({ + mutationFn: async (patch: HomesPreference) => { + const { data } = await client.put>(`${apiPrefix()}/homes/preference`, patch) + return unwrap(data) + }, + onSuccess: (data) => { + qc.setQueryData(['homes-preference'], data) + message.success('적용했습니다.') + }, + onError: (e: Error) => message.error(e.message) }) + + const onTabChange = (key: string) => { + const k = key as TabKey + setTab(k) + sectionRefs.current[k]?.scrollIntoView({ behavior: 'smooth', block: 'start' }) + } + + const saveManager = async () => { + const v = await managerForm.validateFields() + const phone = [v.phone1, v.phone2, v.phone3].filter(Boolean).join('-') + save.mutate({ manager: { name: v.name, phone } }) + } + + const saveLoginBlock = async () => { + const v = await loginForm.validateFields() + save.mutate({ loginBlock: { mode: v.mode } }) + } + + const saveAccessIps = () => { + save.mutate({ accessIps: { enabled: ipEnabled, ips: localIps } }) + } + + const saveContract = async () => { + const v = await contractForm.validateFields() + save.mutate({ + contract: { + autoRegisterCustomer: v.autoRegisterCustomer, + defaultMonthsInternet: v.defaultMonthsInternet, + defaultMonthsHome: v.defaultMonthsHome, + requireAgency: v.requireAgency, + autoCalcSettle: v.autoCalcSettle, + allowWithdraw: v.allowWithdraw }, + incentiveRate: v.incentiveRate, + autoInvoice: v.autoInvoice }) + } + + const addIp = () => { + const ip = ipInput.trim() + if (!ip) { + message.warning('아이피를 입력하세요.') + return + } + if (localIps.includes(ip)) { + message.warning('이미 등록된 아이피입니다.') + return + } + setLocalIps((prev) => [...prev, ip]) + setIpInput('') + } + + return ( +
+
+
+

+ + 기본설정 +

+

매니저 이용에 필요한 기본적인 설정을 하실 수 있습니다.

+
+
+ + + ({ key: t.key, label: t.label }))} + /> + +
{ sectionRefs.current.manager = el }} + className="pref-section" + id="pref-manager" + > + +
+ + + + + + + + + + + + + + + + + +
구분내용
담당자명 + + + +
연락처 + + + + + + + + + + + +
+
+ +
+
+
+ +
{ sectionRefs.current.loginBlock = el }} + className="pref-section" + id="pref-loginBlock" + > + +
+ + + + + + + + + + + + + +
구분내용
로그인 차단권한 + + + +
+
+ +
+
+
+ +
{ sectionRefs.current.accessIp = el }} + className="pref-section" + id="pref-accessIp" + > + + + + + + + + + + + + + + + + + + +
구분내용
접속제한 + setIpEnabled(e.target.value)} + options={[ + { value: false, label: '미사용' }, + { value: true, label: '사용' }, + ]} + /> +
아이피 등록 + + setIpInput(e.target.value)} + onPressEnter={addIp} + /> + + +
+ ({ ip }))} + columns={[ + { + title: 'No', + width: 60, + align: 'center' as const, + render: (_: unknown, __: { ip: string }, i: number) => i + 1 }, + { title: '아이피', dataIndex: 'ip' }, + { + title: '관리', + width: 90, + align: 'center' as const, + render: (_: unknown, row: { ip: string }) => ( + + ) }, + ]} + locale={{ emptyText: '등록된 아이피가 없습니다.' }} + /> +
+ +
+
+ +
{ sectionRefs.current.contract = el }} + className="pref-section" + id="pref-contract" + > + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
구분내용
고객 자동등록 + + + +
기본 약정개월(인터넷) + + + + + 개월 + +
기본 약정개월(홈렌탈) + + + + + 개월 + +
거래처 필수 + + + +
정산금액 자동계산 + + + +
철회 허용 + + + +
인센티브율 + + + + + % + +
월말 자동정산서 + + + +
+
+ +
+
+
+
+
+ ) +} diff --git a/frontend/src/pages/care/SitemapPage.tsx b/frontend/src/pages/care/SitemapPage.tsx new file mode 100644 index 0000000..12ed120 --- /dev/null +++ b/frontend/src/pages/care/SitemapPage.tsx @@ -0,0 +1,263 @@ +import { useEffect, useMemo, useState } from 'react' +import { Link } from 'react-router-dom' +import { Card } from 'antd' +import { AppstoreOutlined } from '@ant-design/icons' +import { SITEMAP_TITLE_TO_ID } from '../../menu' +import { fetchMyMenus } from '../../api/auth' + +type SitemapChild = { + label: string + to?: string + children?: { label: string; to: string }[] +} + +type SitemapSection = { + letter: string + title: string + to?: string + items?: SitemapChild[] +} + +/** 비즈케어홈즈 원본 전체보기(사이트맵) 구성 */ +const SITEMAP_COLUMNS: SitemapSection[][] = [ + [ + { letter: 'A', title: '대시보드', to: '/care/main' }, + { + letter: 'B', + title: '상품관리', + items: [ + { + label: '인터넷상품', + to: '/care/product/internet', + children: [ + { label: '인터넷', to: '/care/product/internet?category=인터넷' }, + { label: 'TV', to: '/care/product/internet?category=TV' }, + { label: '전화', to: '/care/product/internet?category=전화' }, + { label: '와이파이', to: '/care/product/internet?category=와이파이' }, + { label: '스마트홈', to: '/care/product/internet?category=스마트홈' }, + { label: '기타', to: '/care/product/internet?category=기타' }, + ] }, + { + label: '홈렌탈상품', + to: '/care/product/home', + children: [ + { label: '정수기', to: '/care/product/home?category=정수기' }, + { label: '공기청정기', to: '/care/product/home?category=공기청정기' }, + { label: '비데', to: '/care/product/home?category=비데' }, + { label: '매트리스', to: '/care/product/home?category=매트리스' }, + { label: '가전', to: '/care/product/home?category=가전' }, + { label: '기타', to: '/care/product/home?category=기타' }, + ] }, + { label: '상품이력', to: '/care/product/log' }, + ] }, + ], + [ + { + letter: 'C', + title: '계약관리', + items: [ + { label: '인터넷접수', to: '/care/contract/internet' }, + { label: '홈렌탈접수', to: '/care/contract/home' }, + { label: '계약이력', to: '/care/contract/log' }, + ] }, + { + letter: 'R', + title: '접수관리', + items: [ + { label: '접수리스트', to: '/care/reception/list' }, + ] }, + { + letter: 'D', + title: '정산관리', + items: [ + { label: '정산서발행', to: '/care/invoice/publish' }, + { label: '나의정산서', to: '/care/invoice/my' }, + { label: '정산서이력', to: '/care/invoice/log' }, + ] }, + { + letter: 'E', + title: '일정관리', + items: [ + { label: '캘린더', to: '/care/pending/calendar' }, + { label: '일정관리', to: '/care/pending/pending' }, + { label: '일정이력', to: '/care/pending/log' }, + ] }, + { + letter: 'F', + title: '예약관리', + items: [ + { label: '예약관리', to: '/care/booking/booking' }, + { label: '예약이력', to: '/care/booking/log' }, + ] }, + ], + [ + { + letter: 'G', + title: '고객관리', + items: [ + { label: '고객관리', to: '/care/customer/customer' }, + { label: '고객이력', to: '/care/customer/log' }, + ] }, + { + letter: 'H', + title: '장부관리', + items: [ + { + label: '간편장부', + to: '/care/abooks/abooks-mon', + children: [ + { label: '월별 간편장부', to: '/care/abooks/abooks-mon?view=month' }, + { label: '상세 간편장부', to: '/care/abooks/abooks-mon?view=detail' }, + ] }, + { label: '장부이력', to: '/care/abooks/log' }, + ] }, + { + letter: 'I', + title: '통계관리', + items: [ + { label: '원클릭리포트', to: '/care/report/daily' }, + { label: '월간리포트', to: '/care/report/monthly' }, + { label: '인터넷통계', to: '/care/report/internet' }, + { label: '홈렌탈통계', to: '/care/report/home' }, + { label: '직원별통계', to: '/care/report/member' }, + { label: '거래처별통계', to: '/care/report/agency' }, + ] }, + ], + [ + { + letter: 'J', + title: '환경설정', + items: [ + { label: '거래처관리', to: '/care/setting/agency' }, + { label: '직원관리', to: '/care/setting/member' }, + { label: '기본설정', to: '/care/setting/preference' }, + { label: '권한설정', to: '/care/setting/level' }, + { label: '상부점관리', to: '/care/setting/partner1' }, + { label: '하부점관리', to: '/care/setting/partner2' }, + ] }, + { + letter: 'L', + title: '고객센터', + items: [ + { label: '공지사항', to: '/care/cs/notice' }, + { label: '사용문의', to: '/care/cs/question' }, + ] }, + { + letter: 'M', + title: '게시판', + items: [{ label: '사내게시판', to: '/care/bbs/bbs' }] }, + ], +] + +function SectionTitle({ section }: { section: SitemapSection }) { + const text = `${section.letter}. ${section.title}` + if (section.to) { + return ( +

+ {text} +

+ ) + } + return

{text}

+} + +function SitemapItem({ item }: { item: SitemapChild }) { + return ( +
  • + {item.to ? ( + + {item.label} + + ) : ( + {item.label} + )} + {item.children && item.children.length > 0 && ( +
      + {item.children.map((child) => ( +
    • + - + + {child.label} + +
    • + ))} +
    + )} +
  • + ) +} + +export default function SitemapPage() { + const [allowedMenus, setAllowedMenus] = useState(null) + + useEffect(() => { + let cancelled = false + fetchMyMenus() + .then((res) => { + if (!cancelled) setAllowedMenus(res.menus || []) + }) + .catch(() => { + if (cancelled) return + try { + const cached = localStorage.getItem('allowedMenus') + if (cached) setAllowedMenus(JSON.parse(cached) as string[]) + else setAllowedMenus(null) + } catch { + setAllowedMenus(null) + } + }) + return () => { cancelled = true } + }, []) + + const columns = useMemo(() => { + if (!allowedMenus) return SITEMAP_COLUMNS + const set = new Set(allowedMenus) + return SITEMAP_COLUMNS.map((col) => + col.filter((section) => { + const id = SITEMAP_TITLE_TO_ID[section.title] + return !id || set.has(id) + }), + ).filter((col) => col.length > 0) + }, [allowedMenus]) + + return ( +
    +
    +
    +

    + + 전체보기 +

    +

    비즈케어홈즈의 모든 서비스를 소개드립니다. 원하시는 메뉴를 클릭해주세요.

    +
    +
    + + +
    + {columns.map((col, idx) => ( +
    + {col.map((section) => ( +
    + + {section.items && section.items.length > 0 && ( +
      + {section.items.map((item) => ( + + ))} +
    + )} +
    + ))} +
    + ))} +
    +
    + +
    +

    (주) 비즈케어 | 사업자등록번호 : 000-00-00000

    +

    연락처 : 0000-0000

    +

    Copyright @ 2025 BizCare All Right Reversed.

    +
    +
    + ) +} diff --git a/frontend/src/pages/care/SmsAddressPage.tsx b/frontend/src/pages/care/SmsAddressPage.tsx new file mode 100644 index 0000000..eac5de8 --- /dev/null +++ b/frontend/src/pages/care/SmsAddressPage.tsx @@ -0,0 +1,546 @@ +import { useMemo, useState } from 'react' +import Modal from '../../components/DraggableModal' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useNavigate } from 'react-router-dom' +import { + Button, + Card, + Form, + Input, + Select, + Space, + Typography, + message } from 'antd' +import CareTable from '../../components/CareTable' +import { + DownloadOutlined, EditOutlined, PlusOutlined, SearchOutlined, SendOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' + +type GroupRow = { + id?: number | null + name: string + count: number + all?: boolean +} + +type ContactRow = { + id: number + addressGroupId?: number + groupName?: string + name?: string + phone?: string + phoneDisplay?: string +} + +type ContactData = { + total: number + list: ContactRow[] +} + +export default function SmsAddressPage() { + const qc = useQueryClient() + const navigate = useNavigate() + const [form] = Form.useForm() + const [contactForm] = Form.useForm() + const [groupForm] = Form.useForm() + const [changeForm] = Form.useForm() + const [bulkForm] = Form.useForm() + const [selectedGroupId, setSelectedGroupId] = useState(undefined) + const [selected, setSelected] = useState([]) + const [contactOpen, setContactOpen] = useState(false) + const [editingContact, setEditingContact] = useState(null) + const [groupOpen, setGroupOpen] = useState(false) + const [editingGroup, setEditingGroup] = useState(null) + const [changeOpen, setChangeOpen] = useState(false) + const [bulkOpen, setBulkOpen] = useState(false) + const [filters, setFilters] = useState>({ + searchType: '이름', + page: 0, + size: 15 }) + + const groupsQuery = useQuery({ + queryKey: ['sms-address-groups'], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/sms/address/groups`) + return unwrap(data) + } }) + + const contactsQuery = useQuery({ + queryKey: ['sms-address-contacts', filters, selectedGroupId], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/sms/address/contacts`, { + params: { + ...filters, + addressGroupId: selectedGroupId ?? undefined } }) + return unwrap(data) + } }) + + const groupOptions = useMemo( + () => (groupsQuery.data?.list ?? []) + .filter((g) => !g.all) + .map((g) => ({ value: g.id as number, label: g.name })), + [groupsQuery.data], + ) + + const refresh = () => { + setSelected([]) + qc.invalidateQueries({ queryKey: ['sms-address-groups'] }) + qc.invalidateQueries({ queryKey: ['sms-address-contacts'] }) + } + + const saveGroup = useMutation({ + mutationFn: async (values: { name: string }) => { + if (editingGroup?.id) { + const { data } = await client.put(`${apiPrefix()}/sms/address/groups/${editingGroup.id}`, values) + return unwrap(data) + } + const { data } = await client.post(`${apiPrefix()}/sms/address/groups`, values) + return unwrap(data) + }, + onSuccess: () => { + message.success(editingGroup ? '그룹을 수정했습니다.' : '그룹을 등록했습니다.') + setGroupOpen(false) + setEditingGroup(null) + groupForm.resetFields() + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const saveContact = useMutation({ + mutationFn: async (values: Record) => { + const phone = [values.phone1, values.phone2, values.phone3].filter(Boolean).join('') + const body = { + addressGroupId: values.addressGroupId, + name: values.name, + phone } + if (editingContact?.id) { + const { data } = await client.put(`${apiPrefix()}/sms/address/contacts/${editingContact.id}`, body) + return unwrap(data) + } + const { data } = await client.post(`${apiPrefix()}/sms/address/contacts`, body) + return unwrap(data) + }, + onSuccess: () => { + message.success(editingContact ? '연락처를 수정했습니다.' : '연락처를 등록했습니다.') + setContactOpen(false) + setEditingContact(null) + contactForm.resetFields() + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const deleteContacts = useMutation({ + mutationFn: async (ids: number[]) => { + const { data } = await client.post(`${apiPrefix()}/sms/address/contacts/delete`, { ids }) + return unwrap(data) + }, + onSuccess: () => { message.success('삭제했습니다.'); refresh() }, + onError: () => message.error('삭제에 실패했습니다.') }) + + const changeGroup = useMutation({ + mutationFn: async (addressGroupId: number) => { + const { data } = await client.post(`${apiPrefix()}/sms/address/contacts/change-group`, { + ids: selected, + addressGroupId }) + return unwrap(data) + }, + onSuccess: () => { + message.success('그룹을 변경했습니다.') + setChangeOpen(false) + changeForm.resetFields() + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const bulkCreate = useMutation({ + mutationFn: async (values: Record) => { + const { data } = await client.post(`${apiPrefix()}/sms/address/contacts/bulk`, values) + return unwrap(data) + }, + onSuccess: () => { + message.success('대량등록이 완료되었습니다.') + setBulkOpen(false) + bulkForm.resetFields() + refresh() + }, + onError: (e: Error) => message.error(e.message) }) + + const cartAdd = useMutation({ + mutationFn: async (phones: string[]) => { + const { data } = await client.post(`${apiPrefix()}/sms/cart`, { phones }) + return unwrap(data) as { count: number } + }, + onSuccess: (data) => { + message.success(`문자 장바구니에 담았습니다. (총 ${data.count}건)`) + navigate('/care/sms/history') + }, + onError: () => message.error('장바구니 담기에 실패했습니다.') }) + + const onSearch = (values: Record) => { + setFilters((prev) => ({ + ...prev, + searchType: values.searchType || '이름', + keyword: values.keyword, + page: 0, + size: Number(values.size ?? prev.size ?? 15) })) + } + + const onReset = () => { + form.setFieldsValue({ searchType: '이름', keyword: undefined, size: 15 }) + setSelectedGroupId(undefined) + setFilters({ searchType: '이름', page: 0, size: 15 }) + } + + const openContactCreate = () => { + setEditingContact(null) + contactForm.setFieldsValue({ + addressGroupId: selectedGroupId ?? undefined, + name: undefined, + phone1: '010', + phone2: '', + phone3: '' }) + setContactOpen(true) + } + + const openContactEdit = (row: ContactRow) => { + setEditingContact(row) + const digits = (row.phone || '').replace(/\D/g, '') + contactForm.setFieldsValue({ + addressGroupId: row.addressGroupId, + name: row.name, + phone1: digits.slice(0, 3) || '010', + phone2: digits.slice(3, 7), + phone3: digits.slice(7) }) + setContactOpen(true) + } + + const openGroupCreate = () => { + setEditingGroup(null) + groupForm.setFieldsValue({ name: undefined }) + setGroupOpen(true) + } + + const openGroupEdit = (row: GroupRow) => { + setEditingGroup(row) + groupForm.setFieldsValue({ name: row.name }) + setGroupOpen(true) + } + + const download = async () => { + const { data } = await client.get(`${apiPrefix()}/sms/address/contacts/export`, { + params: { ...filters, addressGroupId: selectedGroupId ?? undefined }, + responseType: 'blob' }) + const url = URL.createObjectURL(data) + const a = document.createElement('a') + a.href = url + a.download = '문자주소록.csv' + a.click() + URL.revokeObjectURL(url) + } + + const requireSelected = (action: () => void) => { + if (!selected.length) { + message.warning('항목을 선택하세요.') + return + } + action() + } + + const groupColumns = [ + { + title: '그룹명', + dataIndex: 'name', + render: (v: string, row: GroupRow) => ( + + ) }, + { title: '연락처', dataIndex: 'count', width: 80, align: 'center' as const }, + { + title: '관리', + key: 'manage', + width: 60, + align: 'center' as const, + render: (_: unknown, row: GroupRow) => ( + row.all ? null : + + + + + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + {!editingContact && ( + + )} +
    + + +
    +
    + + + + { setGroupOpen(false); setEditingGroup(null) }} + footer={null} + width={400} + destroyOnHidden + centered + > +
    saveGroup.mutate(values)}> + + + +
    + + +
    +
    +
    + + setChangeOpen(false)} + footer={null} + width={400} + destroyOnHidden + centered + > +
    changeGroup.mutate(Number(values.addressGroupId))} + > + + + + + + +
    + + +
    +
    +
    +
    + ) +} diff --git a/frontend/src/pages/care/SmsHistoryPage.tsx b/frontend/src/pages/care/SmsHistoryPage.tsx new file mode 100644 index 0000000..1b40654 --- /dev/null +++ b/frontend/src/pages/care/SmsHistoryPage.tsx @@ -0,0 +1,388 @@ +import { useMemo, useState } from 'react' +import Modal from '../../components/DraggableModal' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import dayjs, { type Dayjs } from 'dayjs' +import { + Button, + Card, + DatePicker, + Form, + Input, + Select, + Space, + Typography, + message } from 'antd' +import CareTable from '../../components/CareTable' +import { EditOutlined, SearchOutlined, SendOutlined } from '@ant-design/icons' +import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' + +const { RangePicker } = DatePicker +const { TextArea } = Input + +type SmsRow = { + id: number + sentAt?: string + msgType?: string + sendCount?: number + deductCount?: number + phone?: string + message?: string + senderNumber?: string + senderName?: string +} + +type SmsData = { + total: number + list: SmsRow[] + balance?: number +} + +const defaultRange = (): [Dayjs, Dayjs] => [dayjs().subtract(1, 'month').startOf('month'), dayjs()] + +const smsBytes = (text: string) => { + let bytes = 0 + for (let i = 0; i < text.length; i += 1) { + bytes += text.charCodeAt(i) <= 0x7f ? 1 : 2 + } + return bytes +} + +export default function SmsHistoryPage() { + const qc = useQueryClient() + const [form] = Form.useForm() + const [open, setOpen] = useState(false) + const [selected, setSelected] = useState([]) + const [phoneInput, setPhoneInput] = useState('') + const [phones, setPhones] = useState([]) + const [content, setContent] = useState('') + const [filters, setFilters] = useState>(() => { + const [from, to] = defaultRange() + return { + dateFrom: from.format('YYYY-MM-DD'), + dateTo: to.format('YYYY-MM-DD'), + searchType: '수신번호', + page: 0, + size: 15 } + }) + + const query = useQuery({ + queryKey: ['sms-history', filters], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/sms/history`, { params: filters }) + return unwrap(data) + } }) + + const balanceQuery = useQuery({ + queryKey: ['sms-balance'], + queryFn: async () => { + const { data } = await client.get>(`${apiPrefix()}/sms/balance`) + return unwrap(data) + } }) + + const balance = balanceQuery.data?.balance ?? query.data?.balance ?? 996 + const senderNumber = balanceQuery.data?.senderNumber ?? '1600-3903' + const bytes = smsBytes(content) + const msgType = bytes <= 90 ? '단문' : '장문' + const perDeduct = bytes <= 90 ? 1 : Math.max(3, Math.ceil(bytes / 90)) + const deduct = phones.length === 0 ? 0 : perDeduct * phones.length + + const sendMut = useMutation({ + mutationFn: async () => { + const { data } = await client.post(`${apiPrefix()}/sms/send`, { + phones, + message: content, + senderNumber }) + return unwrap(data) + }, + onSuccess: () => { + message.success('문자를 발송했습니다.') + setOpen(false) + setPhones([]) + setPhoneInput('') + setContent('') + qc.invalidateQueries({ queryKey: ['sms-history'] }) + qc.invalidateQueries({ queryKey: ['sms-balance'] }) + }, + onError: (e: Error) => message.error(e.message || '발송에 실패했습니다.') }) + + const deleteMut = useMutation({ + mutationFn: async (ids: number[]) => { + const { data } = await client.post(`${apiPrefix()}/sms/history/delete`, { ids }) + return unwrap(data) + }, + onSuccess: () => { + message.success('삭제했습니다.') + setSelected([]) + qc.invalidateQueries({ queryKey: ['sms-history'] }) + }, + onError: () => message.error('삭제에 실패했습니다.') }) + + const chargeMut = useMutation({ + mutationFn: async () => { + const { data } = await client.post(`${apiPrefix()}/sms/charge`, { amount: 100 }) + return unwrap(data) as { balance: number } + }, + onSuccess: (data) => { + message.success('SMS가 충전되었습니다.') + qc.setQueryData(['sms-balance'], (prev: { balance: number; senderNumber: string } | undefined) => ({ + balance: data.balance, + senderNumber: prev?.senderNumber ?? senderNumber })) + qc.invalidateQueries({ queryKey: ['sms-balance'] }) + } }) + + const onSearch = (values: Record) => { + const dates = values.dates as [Dayjs, Dayjs] | undefined + if (!dates?.[0] || !dates?.[1]) { + message.warning('날짜를 선택하세요.') + return + } + setFilters((prev) => ({ + ...prev, + dateFrom: dates[0].format('YYYY-MM-DD'), + dateTo: dates[1].format('YYYY-MM-DD'), + searchType: values.searchType || '수신번호', + keyword: values.keyword, + page: 0, + size: Number(values.size ?? prev.size ?? 15) })) + } + + const onReset = () => { + const [from, to] = defaultRange() + form.setFieldsValue({ + dates: [from, to], + searchType: '수신번호', + keyword: undefined, + size: 15 }) + setFilters({ + dateFrom: from.format('YYYY-MM-DD'), + dateTo: to.format('YYYY-MM-DD'), + searchType: '수신번호', + page: 0, + size: 15 }) + } + + const addPhone = () => { + const phone = phoneInput.replace(/[^\d]/g, '') + if (!phone) { + message.warning('연락처를 입력하세요.') + return + } + if (phones.includes(phone)) { + message.warning('이미 추가된 번호입니다.') + return + } + setPhones((prev) => [...prev, phone]) + setPhoneInput('') + } + + const openSend = () => { + setPhones([]) + setPhoneInput('') + setContent('') + setOpen(true) + balanceQuery.refetch() + } + + const columns = useMemo(() => [ + { title: '발송일', dataIndex: 'sentAt', width: 140, align: 'center' as const, sorter: true }, + { title: '구분', dataIndex: 'msgType', width: 70, align: 'center' as const }, + { title: '발송', dataIndex: 'sendCount', width: 60, align: 'center' as const }, + { title: '차감', dataIndex: 'deductCount', width: 60, align: 'center' as const }, + { title: '발송명단', dataIndex: 'phone', width: 140, ellipsis: true }, + { title: '내용', dataIndex: 'message', ellipsis: true }, + { title: '발신번호', dataIndex: 'senderNumber', width: 110, align: 'center' as const }, + { title: '발송자', dataIndex: 'senderName', width: 90, align: 'center' as const }, + ], []) + + return ( +
    +
    +
    +

    문자발송내역

    +

    발송하신 문자(SMS) 내역을 확인하실 수 있습니다.

    +
    + + + + +
    + + +
    +
    + + + + + + + + + + + + + + setPhoneInput(e.target.value)} + onPressEnter={addPhone} + placeholder="연락처 입력" + /> + + +
    + {phones.length + ? phones.map((p) => ( + + )) + : '받는 사람 연락처를 추가해주세요.'} +
    +
    + + 총 {phones.length} 건 +
    +
    +
    + +
    +
    발송내용 필수
    +
    +