기능수정
This commit is contained in:
Vendored
+3
-3
@@ -19,16 +19,16 @@ pipeline {
|
||||
sh '''
|
||||
set -euo pipefail
|
||||
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:$PATH"
|
||||
echo "=== BillCare Jenkins Build #${BUILD_NUMBER} ==="
|
||||
echo "=== BizCare Jenkins Build #${BUILD_NUMBER} ==="
|
||||
echo "시각: $(date)"
|
||||
SERVICE_NAME=${SERVICE_NAME} /Volumes/ADATA/git/deploy-billcare.sh
|
||||
SERVICE_NAME=${SERVICE_NAME} /Volumes/ADATA/git/deploy-bizcare.sh
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
sh 'tail -40 /Volumes/ADATA/logs/deploy/billcare-deploy.log || true'
|
||||
sh 'tail -40 /Volumes/ADATA/logs/deploy/bizcare-deploy.log || true'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 빌링 케어 (비즈케어 홈즈)
|
||||
# BizCare (비즈케어 홈즈)
|
||||
|
||||
[비즈케어 홈즈](https://h.bizcare.co.kr/) 홈상품(인터넷/홈렌탈) 운용관리 프로그램을
|
||||
billAgent와 동일한 레이아웃·테마 구조로 재구현한 **독립 프로젝트**입니다.
|
||||
@@ -13,7 +13,7 @@ docker compose up --build -d
|
||||
|
||||
- UI: http://localhost:82
|
||||
- API: http://localhost:8082
|
||||
- MySQL: localhost:3308 (`billcare`)
|
||||
- MySQL: localhost:3308 (`bizcare`)
|
||||
|
||||
데모 계정: `demo` / `demo123`
|
||||
|
||||
@@ -21,10 +21,10 @@ docker compose up --build -d
|
||||
|
||||
| 구분 | 내용 |
|
||||
|------|------|
|
||||
| 브랜드 | 빌링 케어 / Bizcare Homes |
|
||||
| 브랜드 | BizCare / Bizcare Homes |
|
||||
| 레이아웃/테마 | `AgentShell` + light/dark/blue |
|
||||
| 메뉴 | 상품·계약·정산·약속·예약·고객·장부·통계·설정·CS·게시판 |
|
||||
| API | Spring Boot JWT (`/api/agent/homes/**`) |
|
||||
| 컨테이너 | `billcare-frontend` / `billcare-backend` / `billcare-mysql` |
|
||||
| 컨테이너 | `bizcare-frontend` / `bizcare-backend` / `bizcare-mysql` |
|
||||
|
||||
billAgent와 동시에 실행할 수 있도록 포트·컨테이너·DB명을 분리했습니다.
|
||||
|
||||
@@ -814,7 +814,7 @@ class CareReceptionController {
|
||||
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 processor = blankToNull(first(f, "processor", "processors", "centerNames"));
|
||||
String region = blankToNull(first(f, "region", "customerSido"));
|
||||
String giftCategory = blankToNull(first(f, "giftCategory", "giftType"));
|
||||
String productCategory = blankToNull(first(f, "productCategory", "productType"));
|
||||
@@ -832,8 +832,29 @@ class CareReceptionController {
|
||||
String giftPlace = blankToNull(f.get("giftPlace"));
|
||||
String branchEmployee = blankToNull(first(f, "branchEmployee"));
|
||||
String branchName = blankToNull(f.get("branchName"));
|
||||
String centerName = blankToNull(first(f, "centerName", "processorName"));
|
||||
String branchGroup = blankToNull(first(f, "branchGroup", "branchGroupName", "branchGroupId"));
|
||||
String paymentTransferSsid = blankToNull(first(f, "paymentTransferSsid", "paymentSsid"));
|
||||
String paymentTransferNumber = blankToNull(first(f, "paymentTransferNumber", "paymentAccountNumber"));
|
||||
String centerStatus = blankToNull(first(f, "centerStatus"));
|
||||
String ispIdCsv = blankToNull(first(f, "ispId", "ispIds"));
|
||||
String productNameCsv = blankToNull(first(f, "ispProductId", "productNames", "productNameExact"));
|
||||
String sorting = blankToNull(f.get("sorting"));
|
||||
|
||||
Set<String> progressSet = csvSet(progress);
|
||||
Set<String> settleSet = csvSet(settle);
|
||||
Set<String> giftStatusSet = csvSet(giftStatus);
|
||||
Set<String> centerStatusSet = csvSet(centerStatus);
|
||||
Set<String> carrierSet = csvSet(carrier);
|
||||
Set<String> branchSet = csvSet(branch);
|
||||
Set<String> processorSet = csvSet(processor);
|
||||
Set<String> regionSet = csvSet(region);
|
||||
Set<String> giftCatSet = csvSet(giftCategory);
|
||||
Set<String> productCatSet = csvSet(productCategory);
|
||||
Set<String> ispIdSet = csvSet(ispIdCsv);
|
||||
Set<String> productExactSet = csvSet(productNameCsv);
|
||||
Set<String> branchGroupSet = csvSet(branchGroup);
|
||||
|
||||
String progressF = progress;
|
||||
List<HomesReception> result = all.stream().filter(r -> {
|
||||
LocalDate od = r.getOrderDate() == null ? null : r.getOrderDate().toLocalDate();
|
||||
@@ -846,34 +867,95 @@ class CareReceptionController {
|
||||
LocalDate gd = r.getGiftSended() != null ? r.getGiftSended() : r.getGiftDate();
|
||||
if (gd == null || !gd.equals(giftSended)) return false;
|
||||
}
|
||||
if (progressF != null) {
|
||||
if (!progressSet.isEmpty()) {
|
||||
String rp = ReceptionStatuses.normalizeProgress(r.getProgressStatus());
|
||||
boolean hit = progressSet.stream().anyMatch(fp -> {
|
||||
String n = ReceptionStatuses.normalizeProgress(fp);
|
||||
return n.equals(rp) || fp.equals(r.getProgressStatus());
|
||||
});
|
||||
if (!hit) return false;
|
||||
} else if (progressF != null) {
|
||||
String rp = ReceptionStatuses.normalizeProgress(r.getProgressStatus());
|
||||
String fp = ReceptionStatuses.normalizeProgress(progressF);
|
||||
if (!fp.equals(rp) && !progressF.equals(r.getProgressStatus())) return false;
|
||||
}
|
||||
if (settle != null) {
|
||||
if (!settleSet.isEmpty()) {
|
||||
String rs = ReceptionStatuses.normalizeCenter(r.getSettleStatus());
|
||||
boolean hit = settleSet.stream().anyMatch(fs -> {
|
||||
String n = ReceptionStatuses.normalizeCenter(fs);
|
||||
return n.equals(rs) || fs.equals(r.getSettleStatus());
|
||||
});
|
||||
if (!hit) return false;
|
||||
} else if (settle != null) {
|
||||
String rs = ReceptionStatuses.normalizeCenter(r.getSettleStatus());
|
||||
String fs = ReceptionStatuses.normalizeCenter(settle);
|
||||
if (!fs.equals(rs) && !settle.equals(r.getSettleStatus())) return false;
|
||||
}
|
||||
if (giftStatus != null) {
|
||||
if (!centerStatusSet.isEmpty()) {
|
||||
String rs = ReceptionStatuses.normalizeCenter(r.getSettleStatus());
|
||||
boolean hit = centerStatusSet.stream().anyMatch(fs -> {
|
||||
String n = ReceptionStatuses.normalizeCenter(fs);
|
||||
return n.equals(rs) || fs.equals(r.getSettleStatus());
|
||||
});
|
||||
if (!hit) return false;
|
||||
}
|
||||
if (!giftStatusSet.isEmpty()) {
|
||||
String rg = ReceptionStatuses.normalizeGift(r.getGiftStatus());
|
||||
boolean hit = giftStatusSet.stream().anyMatch(fg -> {
|
||||
String n = ReceptionStatuses.normalizeGift(fg);
|
||||
return n.equals(rg) || fg.equals(r.getGiftStatus());
|
||||
});
|
||||
if (!hit) return false;
|
||||
} else if (giftStatus != null) {
|
||||
String rg = ReceptionStatuses.normalizeGift(r.getGiftStatus());
|
||||
String fg = ReceptionStatuses.normalizeGift(giftStatus);
|
||||
if (!fg.equals(rg) && !giftStatus.equals(r.getGiftStatus())) return false;
|
||||
}
|
||||
if (carrier != null && !carrier.equals(r.getCarrier()) && !carrier.equals(r.getIspName())) return false;
|
||||
if (!carrierSet.isEmpty()) {
|
||||
if (!carrierSet.contains(nullToEmpty(r.getCarrier())) && !carrierSet.contains(nullToEmpty(r.getIspName()))) {
|
||||
if (r.getIspId() == null || !carrierSet.contains(String.valueOf(r.getIspId()))) return false;
|
||||
}
|
||||
} else if (carrier != null && !carrier.equals(r.getCarrier()) && !carrier.equals(r.getIspName())) return false;
|
||||
if (!ispIdSet.isEmpty()) {
|
||||
if (r.getIspId() == null || !ispIdSet.contains(String.valueOf(r.getIspId()))) {
|
||||
if (!ispIdSet.contains(nullToEmpty(r.getIspName())) && !ispIdSet.contains(nullToEmpty(r.getCarrier()))) 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 (!branchSet.isEmpty()) {
|
||||
if (!branchSet.contains(nullToEmpty(r.getBranch()))) return false;
|
||||
} else if (branch != null && !branch.equals(r.getBranch())) return false;
|
||||
if (branchName != null && !nullToEmpty(r.getBranch()).contains(branchName)) return false;
|
||||
if (!branchGroupSet.isEmpty()) {
|
||||
String bg = nullToEmpty(r.getBranchGroup());
|
||||
boolean hit = branchGroupSet.stream().anyMatch(g -> bg.contains(g) || bg.equals(g) || String.valueOf(r.getBranchGroup()).equals(g));
|
||||
if (!hit) return false;
|
||||
} else if (branchGroup != null && !nullToEmpty(r.getBranchGroup()).contains(branchGroup)
|
||||
&& !String.valueOf(r.getBranchGroup()).equals(branchGroup)) 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
|
||||
if (centerName != null) {
|
||||
String cn = blank(r.getProcessor()) ? "본사" : r.getProcessor();
|
||||
if (!cn.contains(centerName)) return false;
|
||||
}
|
||||
if (!processorSet.isEmpty()) {
|
||||
String cn = blank(r.getProcessor()) ? "본사" : nullToEmpty(r.getProcessor());
|
||||
if (!processorSet.contains(cn) && !processorSet.contains(nullToEmpty(r.getProcessor()))) return false;
|
||||
} else if (processor != null && !processor.equals(r.getProcessor())) return false;
|
||||
if (!regionSet.isEmpty()) {
|
||||
if (!regionSet.contains(nullToEmpty(r.getRegion()))) return false;
|
||||
} else if (region != null && !region.equals(r.getRegion())) return false;
|
||||
if (!giftCatSet.isEmpty()) {
|
||||
boolean hit = giftCatSet.stream().anyMatch(g -> nullToEmpty(r.getGiftCategory()).contains(g));
|
||||
if (!hit) return false;
|
||||
} else if (giftCategory != null && !nullToEmpty(r.getGiftCategory()).contains(giftCategory)) return false;
|
||||
if (!productCatSet.isEmpty()) {
|
||||
if (!productCatSet.contains(nullToEmpty(r.getProductCategory()))
|
||||
&& !productCatSet.contains(nullToEmpty(r.getProductType()))) return false;
|
||||
} else if (productCategory != null
|
||||
&& !productCategory.equals(r.getProductCategory())
|
||||
&& !productCategory.equals(r.getProductType())) return false;
|
||||
if (giftPlace != null && !giftPlace.equals(r.getGiftPlace())) return false;
|
||||
@@ -888,7 +970,13 @@ class CareReceptionController {
|
||||
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 (paymentTransferSsid != null && !nullToEmpty(r.getPaymentSsid()).contains(paymentTransferSsid)) return false;
|
||||
if (paymentTransferNumber != null
|
||||
&& !nullToEmpty(r.getPaymentAccountNumber()).contains(paymentTransferNumber)
|
||||
&& !nullToEmpty(r.getPaymentCardNumber()).contains(paymentTransferNumber)) return false;
|
||||
if (!productExactSet.isEmpty()) {
|
||||
if (!productExactSet.contains(nullToEmpty(r.getProductName()))) return false;
|
||||
} else 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()) + " "
|
||||
@@ -909,6 +997,14 @@ class CareReceptionController {
|
||||
return result;
|
||||
}
|
||||
|
||||
private Set<String> csvSet(String csv) {
|
||||
if (csv == null || csv.isBlank()) return Set.of();
|
||||
return Arrays.stream(csv.split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
private void apply(HomesReception r, Map<String, Object> d) {
|
||||
if (d.containsKey("orderDate")) r.setOrderDate(parseDateTime(str(d.get("orderDate"))));
|
||||
if (d.containsKey("counselor")) r.setCounselor(str(d.get("counselor")));
|
||||
|
||||
@@ -732,7 +732,7 @@ class HomesBranch extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 영업점 수수료 시트 — billing branch_fees / branch_group_fees 축소.
|
||||
* BillCare는 영업점명을 문자열로 쓰므로 branchName 키.
|
||||
* BizCare는 영업점명을 문자열로 쓰므로 branchName 키.
|
||||
*/
|
||||
@Entity @Table(name = "homes_branch_fees") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
|
||||
class HomesBranchFee extends BaseEntity {
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.bizcare.agent;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* billing_react AppConstant 의 접수/사은품/입력처/정산 상태·전이를 BillCare 에 이식.
|
||||
* billing_react AppConstant 의 접수/사은품/입력처/정산 상태·전이를 BizCare 에 이식.
|
||||
*/
|
||||
public final class ReceptionStatuses {
|
||||
private ReceptionStatuses() {}
|
||||
@@ -43,7 +43,7 @@ public final class ReceptionStatuses {
|
||||
public static final Map<String, String> CENTER_COST_TYPE = map(
|
||||
"k01", "정산", "k02", "위면해지", "k03", "직권해지", "k04", "유보중단환수", "k05", "해지", "k06", "voc차감");
|
||||
|
||||
/** 구 BillCare 한글 라벨 → 코드 */
|
||||
/** 구 BizCare 한글 라벨 → 코드 */
|
||||
public static final Map<String, String> LEGACY_PROGRESS = Map.ofEntries(
|
||||
Map.entry("1.접수완료", "f04"),
|
||||
Map.entry("2.고객통화", "f03"),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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}
|
||||
url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3308/bizcare?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8}
|
||||
username: ${SPRING_DATASOURCE_USERNAME:bizcare}
|
||||
password: ${SPRING_DATASOURCE_PASSWORD:bizcare}
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
@@ -23,8 +23,8 @@ server:
|
||||
port: 8080
|
||||
app:
|
||||
jwt:
|
||||
secret: ${APP_JWT_SECRET:billcare-bizcare-homes-secret-key-must-be-long-enough-256bits}
|
||||
secret: ${APP_JWT_SECRET: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,http://localhost:1420,https://billcare.exdev.co.kr,tauri://localhost,https://tauri.localhost,http://tauri.localhost}
|
||||
origins: ${APP_CORS_ORIGINS:http://localhost:5174,http://localhost:82,http://127.0.0.1:82,http://localhost:1420,https://bizcare.exdev.co.kr,tauri://localhost,https://tauri.localhost,http://tauri.localhost}
|
||||
upload-dir: ${APP_UPLOAD_DIR:./storage/uploads}
|
||||
|
||||
+12
-12
@@ -1,21 +1,21 @@
|
||||
# BillCare 서버 배포 가이드 (exdev.co.kr + Jenkins)
|
||||
# BizCare 서버 배포 가이드 (exdev.co.kr + Jenkins)
|
||||
|
||||
## 개요
|
||||
|
||||
| 트랙 | 트리거 | 동작 |
|
||||
|------|--------|------|
|
||||
| Track A | `git push origin main` | `post-receive` → `deploy-billcare.sh` |
|
||||
| Track B | Jenkins `BillCare-Pipeline` | `deploy-billcare.sh` |
|
||||
| Track A | `git push origin main` | `post-receive` → `deploy-bizcare.sh` |
|
||||
| Track B | Jenkins `BizCare-Pipeline` | `deploy-bizcare.sh` |
|
||||
|
||||
## 경로
|
||||
|
||||
| 항목 | 경로 |
|
||||
|------|------|
|
||||
| bare repo | `/Volumes/ADATA/git/BillCare.git` |
|
||||
| work-tree | `/Users/aidev/apps/BillCare` |
|
||||
| 배포 스크립트 | `/Volumes/ADATA/git/deploy-billcare.sh` |
|
||||
| 로그 | `/Volumes/ADATA/logs/deploy/billcare-deploy.log` |
|
||||
| HTTPS | https://billcare.exdev.co.kr |
|
||||
| bare repo | `/Volumes/ADATA/git/BizCare.git` |
|
||||
| work-tree | `/Users/aidev/apps/BizCare` |
|
||||
| 배포 스크립트 | `/Volumes/ADATA/git/deploy-bizcare.sh` |
|
||||
| 로그 | `/Volumes/ADATA/logs/deploy/bizcare-deploy.log` |
|
||||
| HTTPS | https://bizcare.exdev.co.kr |
|
||||
|
||||
## 호스트 포트
|
||||
|
||||
@@ -36,12 +36,12 @@ git push origin main
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| URL | https://billcare.exdev.co.kr |
|
||||
| URL | https://bizcare.exdev.co.kr |
|
||||
| Caddy | `ex-chatbot-caddy` → host `:83` |
|
||||
| 인증서 | LE 전용 (`billcare.exdev.co.kr`) |
|
||||
| cert 경로 | `.../nginx/certs/billcare/{fullchain,privkey}.pem` |
|
||||
| 인증서 | LE 전용 (`bizcare.exdev.co.kr`) |
|
||||
| cert 경로 | `.../nginx/certs/bizcare/{fullchain,privkey}.pem` |
|
||||
| 발급 | `acme.sh` + HTTP-01 (`gc-frontend` `/var/www/certbot`) |
|
||||
| 갱신 reload | `docker exec ex-chatbot-caddy caddy reload --config /etc/caddy/Caddyfile` |
|
||||
|
||||
공유 인증서(`stock`/`chatbot`) SAN에 billcare가 없어 별도 발급했습니다.
|
||||
공유 인증서(`stock`/`chatbot`) SAN에 bizcare가 없어 별도 발급했습니다.
|
||||
`gc-frontend`에 acme-webroot가 마운트되어 있어야 갱신이 됩니다.
|
||||
|
||||
+14
-14
@@ -1,17 +1,17 @@
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: billcare-mysql
|
||||
container_name: bizcare-mysql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_DATABASE: billcare
|
||||
MYSQL_USER: billcare
|
||||
MYSQL_PASSWORD: billcare
|
||||
MYSQL_DATABASE: bizcare
|
||||
MYSQL_USER: bizcare
|
||||
MYSQL_PASSWORD: bizcare
|
||||
ports:
|
||||
- "${MYSQL_PORT:-3308}:3306"
|
||||
volumes:
|
||||
- billcare_mysql:/var/lib/mysql
|
||||
- bizcare_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"]
|
||||
@@ -24,19 +24,19 @@ services:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: billcare-backend
|
||||
container_name: bizcare-backend
|
||||
restart: unless-stopped
|
||||
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: ${APP_CORS_ORIGINS:-http://localhost:82,http://127.0.0.1:82,http://localhost:5174,http://localhost:1420,https://billcare.exdev.co.kr,tauri://localhost,https://tauri.localhost,http://tauri.localhost}
|
||||
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/bizcare?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
|
||||
SPRING_DATASOURCE_USERNAME: bizcare
|
||||
SPRING_DATASOURCE_PASSWORD: bizcare
|
||||
APP_CORS_ORIGINS: ${APP_CORS_ORIGINS:-http://localhost:82,http://127.0.0.1:82,http://localhost:5174,http://localhost:1420,https://bizcare.exdev.co.kr,tauri://localhost,https://tauri.localhost,http://tauri.localhost}
|
||||
APP_UPLOAD_DIR: /app/storage/uploads
|
||||
volumes:
|
||||
- billcare_uploads:/app/storage/uploads
|
||||
- bizcare_uploads:/app/storage/uploads
|
||||
ports:
|
||||
- "${BACKEND_PORT:-8082}:8080"
|
||||
healthcheck:
|
||||
@@ -50,7 +50,7 @@ services:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
container_name: billcare-frontend
|
||||
container_name: bizcare-frontend
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
backend:
|
||||
@@ -59,5 +59,5 @@ services:
|
||||
- "${FRONTEND_PORT:-82}:80"
|
||||
|
||||
volumes:
|
||||
billcare_mysql:
|
||||
billcare_uploads:
|
||||
bizcare_mysql:
|
||||
bizcare_uploads:
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>비즈케어</title>
|
||||
<title>BizCare</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
AppstoreOutlined, CalendarOutlined, ShoppingCartOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import SchedulePanel from './SchedulePanel'
|
||||
import BillCareLogo from './BillCareLogo'
|
||||
import BizCareLogo from './BizCareLogo'
|
||||
import AccountPopover from './AccountPopover'
|
||||
import { careMenuSections, filterMenuSections, resolveSelectedKey, sectionContainsPath, shopMenuSections } from '../menu'
|
||||
import { fetchMyMenus } from '../api/auth'
|
||||
@@ -143,7 +143,7 @@ export default function AgentShell({ shop = false }: Props) {
|
||||
<div className={`topbar-brand${collapsed ? ' is-collapsed' : ''}`} style={collapsed ? undefined : { width: panelWidth }}>
|
||||
{!collapsed && (
|
||||
<div className="brand-inline">
|
||||
<BillCareLogo title="BillCare" size="header" />
|
||||
<BizCareLogo title="BizCare" size="header" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+11
-11
@@ -3,7 +3,7 @@ import logoMark from '../assets/logo_mark_light.png'
|
||||
import { useTheme } from '../themeContext'
|
||||
import type { Theme } from '../theme'
|
||||
|
||||
interface BillCareLogoProps {
|
||||
interface BizCareLogoProps {
|
||||
className?: string
|
||||
title?: string
|
||||
size?: 'header' | 'login'
|
||||
@@ -18,25 +18,25 @@ function isDarkTheme(theme: Theme) {
|
||||
* logo_image.png — 라이트 헤더 가로 로고
|
||||
* logo_mark_light.png / logo_image_theme.png — 마크 (로그인·다크 헤더)
|
||||
*/
|
||||
export default function BillCareLogo({
|
||||
export default function BizCareLogo({
|
||||
className = '',
|
||||
title = 'BillCare',
|
||||
title = 'BizCare',
|
||||
size = 'header',
|
||||
forceTheme,
|
||||
}: BillCareLogoProps) {
|
||||
}: BizCareLogoProps) {
|
||||
const { theme } = useTheme()
|
||||
const active = forceTheme ?? theme
|
||||
const dark = isDarkTheme(active)
|
||||
const root = ['billcare-logo', `billcare-logo--${size}`, `billcare-logo--${active}`, className]
|
||||
const root = ['bizcare-logo', `bizcare-logo--${size}`, `bizcare-logo--${active}`, className]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
// 로그인: 항상 마크 + BillCare (색상은 CSS로 고정)
|
||||
// 로그인: 항상 마크 + BizCare (색상은 CSS로 고정)
|
||||
if (size === 'login') {
|
||||
return (
|
||||
<div className={root} title={title}>
|
||||
<img src={logoMark} alt="" className="billcare-logo-mark" draggable={false} />
|
||||
<span className="billcare-logo-title">{title}</span>
|
||||
<img src={logoMark} alt="" className="bizcare-logo-mark" draggable={false} />
|
||||
<span className="bizcare-logo-title">{title}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -54,11 +54,11 @@ export default function BillCareLogo({
|
||||
)
|
||||
}
|
||||
|
||||
// 다크/블루 헤더: 마크 + BillCare
|
||||
// 다크/블루 헤더: 마크 + BizCare
|
||||
return (
|
||||
<div className={root} title={title}>
|
||||
<img src={logoMark} alt="" className="billcare-logo-mark" draggable={false} />
|
||||
<span className="billcare-logo-title">{title}</span>
|
||||
<img src={logoMark} alt="" className="bizcare-logo-mark" draggable={false} />
|
||||
<span className="bizcare-logo-title">{title}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
export type MultiSelectOption = [string, string]
|
||||
|
||||
type Props = {
|
||||
options: MultiSelectOption[]
|
||||
value: string[]
|
||||
onChange: (v: string[]) => void
|
||||
placeholder?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
/** 클릭 시 펼쳐지는 체크박스 다중선택 (billing_react MultiSelect 이식) */
|
||||
export default function MultiSelect({ options, value, onChange, placeholder, className }: Props) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [panelPos, setPanelPos] = useState<{ top: number; left: number; width: number; maxHeight: number } | null>(null)
|
||||
const rootRef = useRef<HTMLDivElement>(null)
|
||||
const toggleRef = useRef<HTMLButtonElement>(null)
|
||||
const panelRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const updatePanelPos = () => {
|
||||
const el = toggleRef.current
|
||||
if (!el) return
|
||||
const r = el.getBoundingClientRect()
|
||||
setPanelPos({
|
||||
top: r.bottom + 2,
|
||||
left: r.left,
|
||||
width: Math.max(r.width, 160),
|
||||
maxHeight: Math.min(300, Math.max(120, window.innerHeight - r.bottom - 12)),
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
updatePanelPos()
|
||||
const onScrollOrResize = () => updatePanelPos()
|
||||
window.addEventListener('resize', onScrollOrResize)
|
||||
window.addEventListener('scroll', onScrollOrResize, true)
|
||||
return () => {
|
||||
window.removeEventListener('resize', onScrollOrResize)
|
||||
window.removeEventListener('scroll', onScrollOrResize, true)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
useEffect(() => {
|
||||
const h = (e: MouseEvent) => {
|
||||
const t = e.target as Node
|
||||
if (rootRef.current?.contains(t) || panelRef.current?.contains(t)) return
|
||||
setOpen(false)
|
||||
}
|
||||
document.addEventListener('mousedown', h)
|
||||
return () => document.removeEventListener('mousedown', h)
|
||||
}, [])
|
||||
|
||||
const toggle = (k: string) => onChange(value.includes(k) ? value.filter((x) => x !== k) : [...value, k])
|
||||
const selectAll = () => onChange(options.map(([k]) => k))
|
||||
const selectNone = () => onChange([])
|
||||
const text = value.length === 0
|
||||
? (placeholder || '전체')
|
||||
: options.filter(([k]) => value.includes(k)).map(([, v]) => v).join(', ')
|
||||
|
||||
return (
|
||||
<div className={`ms${open ? ' is-open' : ''}${className ? ` ${className}` : ''}`} ref={rootRef}>
|
||||
<button
|
||||
ref={toggleRef}
|
||||
type="button"
|
||||
className="ms-toggle"
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
title={text}
|
||||
>
|
||||
<span className="ms-text" style={value.length ? undefined : { color: 'var(--muted)' }}>{text}</span>
|
||||
<span className="ms-caret">▾</span>
|
||||
</button>
|
||||
{open && panelPos && createPortal(
|
||||
<div
|
||||
ref={panelRef}
|
||||
className="ms-panel ms-panel-portal"
|
||||
style={{ top: panelPos.top, left: panelPos.left, width: panelPos.width, maxHeight: panelPos.maxHeight }}
|
||||
>
|
||||
<div className="ms-actions">
|
||||
<button type="button" className="ms-action" onClick={selectAll}>전체 선택</button>
|
||||
<button type="button" className="ms-action ms-action-muted" onClick={selectNone}>전체 해제</button>
|
||||
</div>
|
||||
{options.map(([k, v]) => (
|
||||
<label key={k} className="ms-item">
|
||||
<input type="checkbox" checked={value.includes(k)} onChange={() => toggle(k)} /> {v}
|
||||
</label>
|
||||
))}
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function mapOptions(items: { id: number; name: string }[]): MultiSelectOption[] {
|
||||
return items.map((x) => [String(x.id), x.name])
|
||||
}
|
||||
|
||||
export function entriesOptions(map: Record<string, string> | undefined): MultiSelectOption[] {
|
||||
return map ? Object.entries(map) : []
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type FormEvent, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { login } from '../api/auth'
|
||||
import BillCareLogo from '../components/BillCareLogo'
|
||||
import BizCareLogo from '../components/BizCareLogo'
|
||||
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigate()
|
||||
@@ -34,7 +34,7 @@ export default function LoginPage() {
|
||||
<div className="login-wrap">
|
||||
<form className="login-box" onSubmit={submit}>
|
||||
<div className="login-brand">
|
||||
<BillCareLogo title="BillCare" size="login" />
|
||||
<BizCareLogo title="BizCare" size="login" />
|
||||
<p>통합 영업·정산 관리 시스템</p>
|
||||
</div>
|
||||
<div className="field">
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import ReceptionApplyModal from '../../components/ReceptionApplyModal'
|
||||
import ReceptionBulkModal from '../../components/ReceptionBulkModal'
|
||||
import ReceptionProgressModal from '../../components/ReceptionProgressModal'
|
||||
import MultiSelect, { entriesOptions, mapOptions, type MultiSelectOption } from '../../components/MultiSelect'
|
||||
import { DownOutlined } from '@ant-design/icons'
|
||||
import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client'
|
||||
import { options } from './homesShared'
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
PROGRESS_STATUS,
|
||||
GIFT_STATUS,
|
||||
CENTER_STATUS,
|
||||
GIFT_PLACE,
|
||||
} from './receptionStatuses'
|
||||
|
||||
type ReceptionRow = {
|
||||
@@ -96,11 +98,20 @@ type ReceptionResult = {
|
||||
productCategories?: string[]
|
||||
}
|
||||
|
||||
type IspProduct = { id: number; name: string; type?: string }
|
||||
|
||||
const SIDO_LIST = [
|
||||
'서울', '경기', '인천', '강원', '충북', '충남', '대전', '전북', '전남', '광주',
|
||||
'경북', '대구', '경남', '울산', '부산', '제주',
|
||||
]
|
||||
|
||||
const PRODUCT_TYPE_OPTS: MultiSelectOption[] = [
|
||||
['e01', '인터넷'],
|
||||
['e02', '전화'],
|
||||
['e03', 'TV'],
|
||||
['e04', '렌탈'],
|
||||
]
|
||||
|
||||
const initialFilters = () => ({
|
||||
page: 0,
|
||||
size: 50,
|
||||
@@ -118,19 +129,69 @@ const initialFilters = () => ({
|
||||
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,
|
||||
centerName: '',
|
||||
paymentTransferSsid: '',
|
||||
paymentTransferNumber: '',
|
||||
giftType: [] as string[],
|
||||
productType: [] as string[],
|
||||
progressStatus: [] as string[],
|
||||
giftStatus: [] as string[],
|
||||
centerStatus: [] as string[],
|
||||
customerSido: [] as string[],
|
||||
ispId: [] as string[],
|
||||
productNames: [] as string[],
|
||||
productIds: [] as string[],
|
||||
branches: [] as string[],
|
||||
branchIds: [] as string[],
|
||||
processors: [] as string[],
|
||||
centerNames: [] as string[],
|
||||
branchGroupIds: [] as string[],
|
||||
giftPlace: '',
|
||||
opended: undefined as string | undefined,
|
||||
giftSended: undefined as string | undefined,
|
||||
})
|
||||
|
||||
type Filters = ReturnType<typeof initialFilters>
|
||||
|
||||
const csv = (arr: string[]) => (arr.length ? arr.join(',') : undefined)
|
||||
|
||||
/** API 쿼리 파라미터 — 배열은 comma join */
|
||||
const toSearchParams = (f: Filters, opts?: { omitPaging?: boolean }) => ({
|
||||
...(opts?.omitPaging ? {} : { page: f.page, size: f.size }),
|
||||
orderDateStart: f.orderDateStart || undefined,
|
||||
orderDateEnd: f.orderDateEnd || undefined,
|
||||
sorting: f.sorting,
|
||||
postIt: f.postIt || undefined,
|
||||
agreementName: f.agreementName || undefined,
|
||||
customerSsid: f.customerSsid || undefined,
|
||||
customerEmail: f.customerEmail || undefined,
|
||||
customerName: f.customerName || undefined,
|
||||
customerPhone: f.customerPhone || undefined,
|
||||
contractNumber: f.contractNumber || undefined,
|
||||
productName: f.productName || undefined,
|
||||
ispName: f.ispName || undefined,
|
||||
branchEmployee: f.branchEmployee || undefined,
|
||||
branchName: f.branchName || undefined,
|
||||
centerName: f.centerName || undefined,
|
||||
paymentTransferSsid: f.paymentTransferSsid || undefined,
|
||||
paymentTransferNumber: f.paymentTransferNumber || undefined,
|
||||
giftType: csv(f.giftType),
|
||||
productType: csv(f.productType),
|
||||
progressStatus: csv(f.progressStatus),
|
||||
giftStatus: csv(f.giftStatus),
|
||||
centerStatus: csv(f.centerStatus),
|
||||
customerSido: csv(f.customerSido),
|
||||
ispId: csv(f.ispId),
|
||||
productNames: csv(f.productNames),
|
||||
ispProductId: csv(f.productNames.length ? f.productNames : f.productIds),
|
||||
branch: csv(f.branches.length ? f.branches : f.branchIds),
|
||||
processor: csv(f.processors.length ? f.processors : f.centerNames),
|
||||
branchGroupId: csv(f.branchGroupIds),
|
||||
giftPlace: f.giftPlace || undefined,
|
||||
opended: f.opended || undefined,
|
||||
giftSended: f.giftSended || undefined,
|
||||
})
|
||||
|
||||
const num = (n?: number | null) => (n == null ? '' : Number(n).toLocaleString())
|
||||
|
||||
const giftPlaceLabel = (v?: string) => placeLabel(v)
|
||||
@@ -164,39 +225,57 @@ export default function ReceptionListPage() {
|
||||
},
|
||||
})
|
||||
|
||||
const branchGroupsQuery = useQuery({
|
||||
queryKey: ['homes-branch-groups'],
|
||||
queryFn: async () => {
|
||||
const { data } = await client.get<ApiResponse<{ id: number; name: string }[]>>(
|
||||
`${apiPrefix()}/homes/branch-groups`,
|
||||
)
|
||||
return unwrap(data) || []
|
||||
},
|
||||
})
|
||||
|
||||
const inputCentersQuery = useQuery({
|
||||
queryKey: ['homes-input-centers'],
|
||||
queryFn: async () => {
|
||||
const { data } = await client.get<ApiResponse<{ id: number; name?: string }[]>>(
|
||||
`${apiPrefix()}/homes/input-centers`,
|
||||
)
|
||||
return unwrap(data) || []
|
||||
},
|
||||
})
|
||||
|
||||
const branchesQuery = useQuery({
|
||||
queryKey: ['homes-branches-office'],
|
||||
queryFn: async () => {
|
||||
const { data } = await client.get<ApiResponse<{ id: number; name?: string; officeName?: string }[]>>(
|
||||
`${apiPrefix()}/homes/branches`,
|
||||
)
|
||||
return unwrap(data) || []
|
||||
},
|
||||
})
|
||||
|
||||
const productsQuery = useQuery({
|
||||
queryKey: ['homes-reception-products-multi', filters.ispId],
|
||||
queryFn: async () => {
|
||||
const results = await Promise.all(
|
||||
filters.ispId.map(async (id) => {
|
||||
const { data } = await client.get<ApiResponse<IspProduct[]>>(
|
||||
`${apiPrefix()}/homes/reception-isps/${id}/products`,
|
||||
)
|
||||
return unwrap(data) || []
|
||||
}),
|
||||
)
|
||||
return results.flat()
|
||||
},
|
||||
enabled: filters.ispId.length > 0,
|
||||
})
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ['homes-receptions', filters],
|
||||
queryFn: async () => {
|
||||
const { data } = await client.get<ApiResponse<ReceptionResult>>(`${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,
|
||||
},
|
||||
params: toSearchParams(filters),
|
||||
})
|
||||
return unwrap(data)
|
||||
},
|
||||
@@ -218,8 +297,52 @@ export default function ReceptionListPage() {
|
||||
const giftSelectOpts = statusOptions(giftLabels as Record<string, string>)
|
||||
const settleSelectOpts = statusOptions(settleLabels as Record<string, string>)
|
||||
|
||||
const setD = (patch: Partial<ReturnType<typeof initialFilters>>) => setDraft((p) => ({ ...p, ...patch }))
|
||||
const applyF = (patch: Partial<ReturnType<typeof initialFilters>>) => {
|
||||
const giftTypeOpts: MultiSelectOption[] = (
|
||||
query.data?.giftCategories || ['상품권', '가전', '캐시백', '없음']
|
||||
).map((x) => [x, x])
|
||||
|
||||
const progressMsOpts = entriesOptions(progressLabels as Record<string, string>)
|
||||
const giftMsOpts = entriesOptions(giftLabels as Record<string, string>)
|
||||
const centerStatusMsOpts = entriesOptions(settleLabels as Record<string, string>)
|
||||
const sidoMsOpts: MultiSelectOption[] = SIDO_LIST.map((s) => [s, s])
|
||||
const ispMsOpts = mapOptions(ispQuery.data || [])
|
||||
const branchGroupMsOpts = mapOptions(branchGroupsQuery.data || [])
|
||||
|
||||
const processorMsOpts: MultiSelectOption[] = useMemo(() => {
|
||||
const names = new Set<string>()
|
||||
for (const p of query.data?.processors || []) if (p) names.add(p)
|
||||
for (const c of inputCentersQuery.data || []) if (c.name) names.add(c.name)
|
||||
names.add('본사')
|
||||
return Array.from(names).sort().map((n) => [n, n] as MultiSelectOption)
|
||||
}, [query.data?.processors, inputCentersQuery.data])
|
||||
|
||||
const branchMsOpts: MultiSelectOption[] = useMemo(() => {
|
||||
const names = new Set<string>()
|
||||
for (const b of query.data?.branches || []) if (b) names.add(b)
|
||||
for (const b of branchesQuery.data || []) {
|
||||
const n = b.officeName || b.name
|
||||
if (n) names.add(n)
|
||||
}
|
||||
return Array.from(names).sort().map((n) => [n, n] as MultiSelectOption)
|
||||
}, [query.data?.branches, branchesQuery.data])
|
||||
|
||||
const productMsOpts: MultiSelectOption[] = useMemo(() => {
|
||||
let items = productsQuery.data || []
|
||||
if (filters.productType.length) {
|
||||
items = items.filter((p) => filters.productType.includes(p.type || ''))
|
||||
}
|
||||
const seen = new Set<string>()
|
||||
const opts: MultiSelectOption[] = []
|
||||
for (const p of items) {
|
||||
if (!p.name || seen.has(p.name)) continue
|
||||
seen.add(p.name)
|
||||
opts.push([p.name, p.name])
|
||||
}
|
||||
return opts
|
||||
}, [productsQuery.data, filters.productType])
|
||||
|
||||
const setD = (patch: Partial<Filters>) => setDraft((p) => ({ ...p, ...patch }))
|
||||
const applyF = (patch: Partial<Filters>) => {
|
||||
const next = { ...draft, ...patch, page: 0 }
|
||||
setDraft(next)
|
||||
setFilters(next)
|
||||
@@ -325,7 +448,7 @@ export default function ReceptionListPage() {
|
||||
const downloadExcel = async () => {
|
||||
try {
|
||||
const { data } = await client.get(`${apiPrefix()}/homes/receptions/excel`, {
|
||||
params: { ...filters, page: undefined, size: undefined },
|
||||
params: toSearchParams(filters, { omitPaging: true }),
|
||||
responseType: 'blob',
|
||||
})
|
||||
const url = URL.createObjectURL(data)
|
||||
@@ -405,11 +528,6 @@ export default function ReceptionListPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="toolbar order-top-buttons">
|
||||
<Button type="primary" onClick={search}>검색</Button>
|
||||
<Button onClick={reset}>검색 초기화</Button>
|
||||
</div>
|
||||
|
||||
<div className="order-filter">
|
||||
<div className="order-date-row">
|
||||
<DatePicker
|
||||
@@ -447,28 +565,27 @@ export default function ReceptionListPage() {
|
||||
/>
|
||||
</label>
|
||||
<div className="order-attach-buttons">
|
||||
<Button type="primary" className="order-search-btn" onClick={search}>검색</Button>
|
||||
<Button onClick={reset}>검색 초기화</Button>
|
||||
<Button onClick={downloadExcel}>Excel 다운로드</Button>
|
||||
<Button onClick={downloadExcel}>사은품 Excel 다운로드</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="order-result-row">
|
||||
<span className="order-result muted">검색 결과: 총 {(query.data?.total ?? 0).toLocaleString()} 건</span>
|
||||
</div>
|
||||
|
||||
<div className="order-search-fields">
|
||||
<Select
|
||||
allowClear
|
||||
<MultiSelect
|
||||
className="ff"
|
||||
placeholder="사은품"
|
||||
options={giftTypeOpts}
|
||||
value={draft.giftType}
|
||||
onChange={(v) => setD({ giftType: v })}
|
||||
options={options(query.data?.giftCategories || ['상품권', '가전', '캐시백', '없음'])}
|
||||
/>
|
||||
<input className="ff-i" placeholder="포스트잇" value={draft.postIt} onChange={(e) => setD({ postIt: e.target.value })} />
|
||||
<input className="ff-i" placeholder="약정" value={draft.agreementName} onChange={(e) => setD({ agreementName: e.target.value })} />
|
||||
<input className="ff-i" placeholder="주민번호" value={draft.customerSsid} onChange={(e) => setD({ customerSsid: e.target.value })} />
|
||||
<input className="ff-i" placeholder="이메일" value={draft.customerEmail} onChange={(e) => setD({ customerEmail: e.target.value })} />
|
||||
<input className="ff-i" placeholder="납입자주민" value={draft.paymentTransferSsid} onChange={(e) => setD({ paymentTransferSsid: e.target.value })} />
|
||||
<input className="ff-i" placeholder="납입계좌번호" value={draft.paymentTransferNumber} onChange={(e) => setD({ paymentTransferNumber: e.target.value })} />
|
||||
<label className="ff-d">
|
||||
개통일자
|
||||
<DatePicker
|
||||
@@ -484,9 +601,18 @@ export default function ReceptionListPage() {
|
||||
onChange={(d) => setD({ giftSended: d ? d.format('YYYY-MM-DD') : undefined })}
|
||||
/>
|
||||
</label>
|
||||
<input className="ff-i" placeholder="입력처명" value={draft.centerName} onChange={(e) => setD({ centerName: e.target.value })} />
|
||||
<input className="ff-i" placeholder="영업점명" value={draft.branchName} onChange={(e) => setD({ branchName: e.target.value })} />
|
||||
<MultiSelect
|
||||
className="ff"
|
||||
placeholder="영업점 그룹"
|
||||
options={branchGroupMsOpts}
|
||||
value={draft.branchGroupIds}
|
||||
onChange={(v) => setD({ branchGroupIds: v })}
|
||||
/>
|
||||
<input className="ff-i" placeholder="통신사명" value={draft.ispName} onChange={(e) => setD({ ispName: e.target.value })} />
|
||||
<input className="ff-i" placeholder="상품명" value={draft.productName} onChange={(e) => setD({ productName: e.target.value })} />
|
||||
<span className="order-result muted">검색 결과: 총 {(query.data?.total ?? 0).toLocaleString()} 건</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -523,50 +649,45 @@ export default function ReceptionListPage() {
|
||||
</select>
|
||||
</th>
|
||||
<th className="col-center">
|
||||
<Select
|
||||
allowClear
|
||||
size="small"
|
||||
<MultiSelect
|
||||
placeholder="입력처"
|
||||
value={draft.settleStatus}
|
||||
onChange={(v) => applyF({ settleStatus: v })}
|
||||
options={settleSelectOpts}
|
||||
style={{ width: '100%' }}
|
||||
options={processorMsOpts}
|
||||
value={draft.processors}
|
||||
onChange={(v) => applyF({ processors: v, centerNames: v })}
|
||||
/>
|
||||
</th>
|
||||
<th className="col-branch">
|
||||
<Select
|
||||
allowClear
|
||||
size="small"
|
||||
<MultiSelect
|
||||
placeholder="영업점"
|
||||
value={draft.branch}
|
||||
onChange={(v) => applyF({ branch: v })}
|
||||
options={options(query.data?.branches || [])}
|
||||
style={{ width: '100%' }}
|
||||
options={branchMsOpts}
|
||||
value={draft.branches}
|
||||
onChange={(v) => applyF({ branches: v, branchIds: v })}
|
||||
/>
|
||||
</th>
|
||||
<th className="col-isp">
|
||||
<Select
|
||||
allowClear
|
||||
size="small"
|
||||
<MultiSelect
|
||||
placeholder="통신사"
|
||||
value={draft.carrier}
|
||||
onChange={(v) => applyF({ carrier: v })}
|
||||
options={options(query.data?.carriers || [])}
|
||||
style={{ width: '100%' }}
|
||||
options={ispMsOpts}
|
||||
value={draft.ispId}
|
||||
onChange={(v) => applyF({ ispId: v, productNames: [], productIds: [] })}
|
||||
/>
|
||||
</th>
|
||||
<th className="col-ptype">
|
||||
<Select
|
||||
allowClear
|
||||
size="small"
|
||||
<MultiSelect
|
||||
placeholder="상품종류"
|
||||
options={PRODUCT_TYPE_OPTS}
|
||||
value={draft.productType}
|
||||
onChange={(v) => applyF({ productType: v })}
|
||||
options={options(query.data?.productCategories || ['인터넷', '전화', 'TV', '렌탈'])}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</th>
|
||||
<th className="col-product"> </th>
|
||||
<th className="col-product">
|
||||
<MultiSelect
|
||||
placeholder="상품"
|
||||
options={productMsOpts}
|
||||
value={draft.productNames}
|
||||
onChange={(v) => applyF({ productNames: v, productIds: v })}
|
||||
/>
|
||||
</th>
|
||||
<th className="col-customer">
|
||||
<input
|
||||
placeholder="고객명"
|
||||
@@ -576,14 +697,11 @@ export default function ReceptionListPage() {
|
||||
/>
|
||||
</th>
|
||||
<th className="col-region">
|
||||
<Select
|
||||
allowClear
|
||||
size="small"
|
||||
<MultiSelect
|
||||
placeholder="지역"
|
||||
options={sidoMsOpts}
|
||||
value={draft.customerSido}
|
||||
onChange={(v) => applyF({ customerSido: v })}
|
||||
options={options(SIDO_LIST)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</th>
|
||||
<th className="col-idnum">
|
||||
@@ -595,14 +713,11 @@ export default function ReceptionListPage() {
|
||||
/>
|
||||
</th>
|
||||
<th className="col-status">
|
||||
<Select
|
||||
allowClear
|
||||
size="small"
|
||||
<MultiSelect
|
||||
placeholder="진행현황"
|
||||
options={progressMsOpts}
|
||||
value={draft.progressStatus}
|
||||
onChange={(v) => applyF({ progressStatus: v })}
|
||||
options={progressSelectOpts}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</th>
|
||||
<th className="col-contract">
|
||||
@@ -614,33 +729,29 @@ export default function ReceptionListPage() {
|
||||
/>
|
||||
</th>
|
||||
<th className="col-giftplace">
|
||||
<Select
|
||||
allowClear
|
||||
size="small"
|
||||
placeholder="지급처"
|
||||
value={draft.giftPlace}
|
||||
onChange={(v) => applyF({ giftPlace: v })}
|
||||
options={[
|
||||
{ value: 'b01', label: '본사요청' },
|
||||
{ value: 'b02', label: '자체발송' },
|
||||
{ value: '본사', label: '본사' },
|
||||
{ value: '영업점', label: '영업점' },
|
||||
]}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<select value={draft.giftPlace} onChange={(e) => applyF({ giftPlace: e.target.value })}>
|
||||
<option value="">모두</option>
|
||||
{Object.entries(GIFT_PLACE).map(([code, label]) => (
|
||||
<option key={code} value={code}>{label}</option>
|
||||
))}
|
||||
</select>
|
||||
</th>
|
||||
<th className="col-gift">
|
||||
<Select
|
||||
allowClear
|
||||
size="small"
|
||||
placeholder="사은품진행"
|
||||
<MultiSelect
|
||||
placeholder="사은품진행현황"
|
||||
options={giftMsOpts}
|
||||
value={draft.giftStatus}
|
||||
onChange={(v) => applyF({ giftStatus: v })}
|
||||
options={giftSelectOpts}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</th>
|
||||
<th className="col-cstatus"> </th>
|
||||
<th className="col-cstatus">
|
||||
<MultiSelect
|
||||
placeholder="입력처진행현황"
|
||||
options={centerStatusMsOpts}
|
||||
value={draft.centerStatus}
|
||||
onChange={(v) => applyF({ centerStatus: v })}
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
+182
-31
@@ -43,14 +43,14 @@ body { margin: 0; background: var(--bg); color: var(--text); }
|
||||
text-align: center;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
.login-box .login-brand .billcare-logo {
|
||||
.login-box .login-brand .bizcare-logo {
|
||||
margin: 0 auto;
|
||||
}
|
||||
.login-box .login-brand .billcare-logo-mark {
|
||||
.login-box .login-brand .bizcare-logo-mark {
|
||||
height: 64px;
|
||||
width: auto;
|
||||
}
|
||||
.login-box .login-brand .billcare-logo-title {
|
||||
.login-box .login-brand .bizcare-logo-title {
|
||||
font-size: 28px;
|
||||
color: var(--login-text) !important;
|
||||
}
|
||||
@@ -108,7 +108,7 @@ body { margin: 0; background: var(--bg); color: var(--text); }
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.billcare-logo {
|
||||
.bizcare-logo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -116,39 +116,39 @@ body { margin: 0; background: var(--bg); color: var(--text); }
|
||||
max-width: 100%;
|
||||
background: transparent !important;
|
||||
}
|
||||
.billcare-logo--login {
|
||||
.bizcare-logo--login {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.billcare-logo--header.billcare-logo--dark,
|
||||
.billcare-logo--header.billcare-logo--blue {
|
||||
.bizcare-logo--header.bizcare-logo--dark,
|
||||
.bizcare-logo--header.bizcare-logo--blue {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
img.billcare-logo,
|
||||
.billcare-logo img,
|
||||
.billcare-logo-mark {
|
||||
img.bizcare-logo,
|
||||
.bizcare-logo img,
|
||||
.bizcare-logo-mark {
|
||||
display: block;
|
||||
background: transparent !important;
|
||||
object-fit: contain;
|
||||
}
|
||||
img.billcare-logo--header {
|
||||
img.bizcare-logo--header {
|
||||
height: 28px;
|
||||
width: auto;
|
||||
max-width: min(150px, 100%);
|
||||
object-position: center;
|
||||
}
|
||||
.billcare-logo--header .billcare-logo-mark {
|
||||
.bizcare-logo--header .bizcare-logo-mark {
|
||||
height: 30px;
|
||||
width: auto;
|
||||
}
|
||||
.billcare-logo--login .billcare-logo-mark {
|
||||
.bizcare-logo--login .bizcare-logo-mark {
|
||||
height: 64px;
|
||||
width: auto;
|
||||
}
|
||||
.billcare-logo-title {
|
||||
.bizcare-logo-title {
|
||||
font-family: 'Pretendard', -apple-system, 'Malgun Gothic', sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 15px;
|
||||
@@ -157,14 +157,14 @@ img.billcare-logo--header {
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.billcare-logo--header .billcare-logo-title {
|
||||
.bizcare-logo--header .bizcare-logo-title {
|
||||
font-size: 19px;
|
||||
transform: translateY(2px);
|
||||
}
|
||||
.billcare-logo--login .billcare-logo-title {
|
||||
.bizcare-logo--login .bizcare-logo-title {
|
||||
font-size: 28px;
|
||||
}
|
||||
.billcare-logo--blue .billcare-logo-title {
|
||||
.bizcare-logo--blue .bizcare-logo-title {
|
||||
color: #e8f4ff;
|
||||
}
|
||||
.brand-inline {
|
||||
@@ -177,7 +177,7 @@ img.billcare-logo--header {
|
||||
overflow: hidden;
|
||||
padding: 0 8px;
|
||||
}
|
||||
.brand-inline .billcare-logo {
|
||||
.brand-inline .bizcare-logo {
|
||||
max-width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -4251,7 +4251,7 @@ html.theme-light .reception-order-page {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
.order-result { margin-left: auto; font-size: 12px; }
|
||||
.order-result { margin-left: auto; font-size: 12px; white-space: nowrap; }
|
||||
.order-attach-buttons {
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
@@ -4259,15 +4259,10 @@ html.theme-light .reception-order-page {
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.order-result-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.order-result-row .order-result {
|
||||
margin-left: 0;
|
||||
.order-attach-buttons .order-search-btn.ant-btn-primary,
|
||||
.order-attach-buttons .order-search-btn.ant-btn-primary:hover,
|
||||
.order-attach-buttons .order-search-btn.ant-btn-primary:focus {
|
||||
color: #000 !important;
|
||||
}
|
||||
.order-search-fields {
|
||||
display: flex;
|
||||
@@ -4352,12 +4347,168 @@ table.order-list tr.order-col-filter select {
|
||||
width: 100%;
|
||||
font-size: 10px;
|
||||
padding: 2px 4px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface) !important;
|
||||
color: var(--text) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
table.order-list tr.order-col-filter select option {
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
table.order-list tr.order-col-filter .ms-toggle {
|
||||
padding: 2px 4px;
|
||||
font-size: 10px;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
table.order-list tr.order-col-filter .ms-panel,
|
||||
table.order-list tr.order-col-filter .ms-panel-portal {
|
||||
min-width: 140px;
|
||||
}
|
||||
.order-search-fields .ff .ms-toggle {
|
||||
width: 100%;
|
||||
font-size: 11px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
/* MultiSelect — 테마 토큰 기반 */
|
||||
.ms { position: relative; display: inline-block; width: 100%; min-width: 0; }
|
||||
.ms-toggle {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
text-align: left;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
background: var(--surface) !important;
|
||||
color: var(--text) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 4px;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.ms-toggle:hover {
|
||||
border-color: color-mix(in srgb, var(--primary) 45%, var(--border)) !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
.ms.is-open .ms-toggle,
|
||||
.ms-toggle:focus-visible {
|
||||
border-color: var(--primary) !important;
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 25%, transparent) !important;
|
||||
outline: none;
|
||||
}
|
||||
.ms-toggle .ms-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
color: inherit;
|
||||
}
|
||||
.ms-toggle .ms-caret { color: var(--muted); font-size: 10px; flex-shrink: 0; }
|
||||
|
||||
.ms-panel,
|
||||
.ms-panel-portal {
|
||||
background: var(--elevated, var(--surface)) !important;
|
||||
color: var(--text) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 6px;
|
||||
box-shadow: var(--shadow-lg, var(--shadow)) !important;
|
||||
padding: 4px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: color-mix(in srgb, var(--primary) 45%, var(--muted)) var(--surface-2);
|
||||
}
|
||||
.ms-panel {
|
||||
position: absolute;
|
||||
z-index: 60;
|
||||
top: calc(100% + 2px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-width: 160px;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.ms-panel-portal {
|
||||
position: fixed;
|
||||
z-index: 5000;
|
||||
right: auto;
|
||||
min-width: 160px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.ms-panel::-webkit-scrollbar,
|
||||
.ms-panel-portal::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
.ms-panel::-webkit-scrollbar-track,
|
||||
.ms-panel-portal::-webkit-scrollbar-track {
|
||||
background: var(--surface-2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.ms-panel::-webkit-scrollbar-thumb,
|
||||
.ms-panel-portal::-webkit-scrollbar-thumb {
|
||||
background: color-mix(in srgb, var(--primary) 40%, var(--muted));
|
||||
border-radius: 4px;
|
||||
border: 2px solid var(--surface-2);
|
||||
}
|
||||
.ms-panel::-webkit-scrollbar-thumb:hover,
|
||||
.ms-panel-portal::-webkit-scrollbar-thumb:hover {
|
||||
background: color-mix(in srgb, var(--primary) 60%, var(--muted));
|
||||
}
|
||||
|
||||
.ms-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 2px 2px 6px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 4px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--elevated, var(--surface));
|
||||
z-index: 1;
|
||||
}
|
||||
.ms-action {
|
||||
padding: 2px 6px;
|
||||
font-size: 11px;
|
||||
background: transparent !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 3px;
|
||||
color: var(--text) !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ms-action:hover {
|
||||
background: color-mix(in srgb, var(--primary) 14%, transparent) !important;
|
||||
border-color: color-mix(in srgb, var(--primary) 40%, var(--border)) !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
.ms-action-muted { color: var(--muted) !important; }
|
||||
|
||||
.ms-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 6px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
border-radius: 4px;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
.ms-item:hover {
|
||||
background: color-mix(in srgb, var(--primary) 14%, transparent) !important;
|
||||
}
|
||||
.ms-item:has(input:checked) {
|
||||
background: color-mix(in srgb, var(--primary) 18%, transparent);
|
||||
}
|
||||
.ms-item input[type='checkbox'] {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
accent-color: var(--primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
table.order-list tbody tr { cursor: pointer; }
|
||||
table.order-list tbody tr:hover td { background: color-mix(in srgb, var(--primary) 8%, var(--surface)); }
|
||||
table.order-list .c-name { color: var(--c-name); }
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# BillCare — exdev Caddy 서브도메인 스니펫
|
||||
# host 포트 83 → billcare-frontend
|
||||
# 인증서: acme.sh 로 발급한 billcare 전용 LE cert
|
||||
# /Users/aidev/dev/exAIChatbot2.0/docker/nginx/certs/billcare/{fullchain,privkey}.pem
|
||||
# (Caddy 컨테이너 마운트: /etc/caddy/certs/billcare/)
|
||||
|
||||
# BillCare
|
||||
https://{$BILLCARE_DOMAIN:billcare.exdev.co.kr} {
|
||||
tls /etc/caddy/certs/billcare/fullchain.pem /etc/caddy/certs/billcare/privkey.pem
|
||||
handle {
|
||||
reverse_proxy host.docker.internal:{$BILLCARE_HOST_PORT:83} {
|
||||
flush_interval -1
|
||||
transport http {
|
||||
read_timeout 120s
|
||||
write_timeout 120s
|
||||
versions 1.1 2
|
||||
}
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# BizCare — exdev Caddy 서브도메인 스니펫
|
||||
# host 포트 83 → bizcare-frontend
|
||||
# 인증서: acme.sh 로 발급한 bizcare 전용 LE cert
|
||||
# /Users/aidev/dev/exAIChatbot2.0/docker/nginx/certs/bizcare/{fullchain,privkey}.pem
|
||||
# (Caddy 컨테이너 마운트: /etc/caddy/certs/bizcare/)
|
||||
|
||||
# BizCare
|
||||
https://{$BILLCARE_DOMAIN:bizcare.exdev.co.kr} {
|
||||
tls /etc/caddy/certs/bizcare/fullchain.pem /etc/caddy/certs/bizcare/privkey.pem
|
||||
handle {
|
||||
reverse_proxy host.docker.internal:{$BILLCARE_HOST_PORT:83} {
|
||||
flush_interval -1
|
||||
transport http {
|
||||
read_timeout 120s
|
||||
write_timeout 120s
|
||||
versions 1.1 2
|
||||
}
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-10
@@ -1,17 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# BillCare 배포 스크립트 — Docker Compose
|
||||
# BizCare 배포 스크립트 — Docker Compose
|
||||
# 호출: Git post-receive (Track A) | Jenkins (Track B)
|
||||
# 서버 설치: /Volumes/ADATA/git/deploy-billcare.sh
|
||||
# 서버 설치: /Volumes/ADATA/git/deploy-bizcare.sh
|
||||
# ==============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
GIT_DIR="${GIT_DIR:-/Volumes/ADATA/git/BillCare.git}"
|
||||
WORK_TREE="${WORK_TREE:-/Users/aidev/apps/BillCare}"
|
||||
GIT_DIR="${GIT_DIR:-/Volumes/ADATA/git/BizCare.git}"
|
||||
WORK_TREE="${WORK_TREE:-/Users/aidev/apps/BizCare}"
|
||||
BRANCH="${BRANCH:-main}"
|
||||
DOCKER_COMPOSE_FILE="${DOCKER_COMPOSE_FILE:-${WORK_TREE}/docker-compose.yml}"
|
||||
DEPLOY_LOG="${DEPLOY_LOG:-/Volumes/ADATA/logs/deploy/billcare-deploy.log}"
|
||||
LOG_TAG="[billcare-deploy]"
|
||||
DEPLOY_LOG="${DEPLOY_LOG:-/Volumes/ADATA/logs/deploy/bizcare-deploy.log}"
|
||||
LOG_TAG="[bizcare-deploy]"
|
||||
SERVICE_NAME="${SERVICE_NAME:-all}"
|
||||
DOCKER_NO_CACHE="${DOCKER_NO_CACHE:-0}"
|
||||
BACKEND_HEALTH_URL="${BACKEND_HEALTH_URL:-http://127.0.0.1:18082/api/health}"
|
||||
@@ -22,7 +22,7 @@ export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:${PATH:-/usr/bi
|
||||
export FRONTEND_PORT="${FRONTEND_PORT:-83}"
|
||||
export BACKEND_PORT="${BACKEND_PORT:-18082}"
|
||||
export MYSQL_PORT="${MYSQL_PORT:-3308}"
|
||||
export APP_CORS_ORIGINS="${APP_CORS_ORIGINS:-https://billcare.exdev.co.kr,http://billcare.exdev.co.kr,http://localhost:83,http://127.0.0.1:83}"
|
||||
export APP_CORS_ORIGINS="${APP_CORS_ORIGINS:-https://bizcare.exdev.co.kr,http://bizcare.exdev.co.kr,http://localhost:83,http://127.0.0.1:83}"
|
||||
|
||||
mkdir -p "$(dirname "$DEPLOY_LOG")"
|
||||
exec >> "$DEPLOY_LOG" 2>&1
|
||||
@@ -33,7 +33,7 @@ warn() { log "[WARN] $*"; }
|
||||
err() { log "[ERROR] $*"; exit 1; }
|
||||
|
||||
log "======================================================"
|
||||
log " BillCare 배포 시작"
|
||||
log " BizCare 배포 시작"
|
||||
log " 서비스 : $SERVICE_NAME"
|
||||
log " 브랜치 : $BRANCH"
|
||||
log " 작업트리: $WORK_TREE"
|
||||
@@ -41,7 +41,7 @@ log " compose : $DOCKER_COMPOSE_FILE"
|
||||
log " 실행자 : $(whoami)"
|
||||
log "======================================================"
|
||||
|
||||
LOCK_FILE="/tmp/billcare_deploy.lock"
|
||||
LOCK_FILE="/tmp/bizcare_deploy.lock"
|
||||
if [ -f "$LOCK_FILE" ]; then
|
||||
LOCK_PID=$(cat "$LOCK_FILE" 2>/dev/null || echo "")
|
||||
if [ -n "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2>/dev/null; then
|
||||
@@ -141,7 +141,7 @@ esac
|
||||
log "======================================================"
|
||||
log " 배포 완료: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
log " 커밋: $(git --git-dir="$GIT_DIR" log --oneline -1 2>/dev/null || echo N/A)"
|
||||
log " URL: https://billcare.exdev.co.kr"
|
||||
log " URL: https://bizcare.exdev.co.kr"
|
||||
log "======================================================"
|
||||
|
||||
docker compose -f "$DOCKER_COMPOSE_FILE" ps 2>/dev/null || docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# BillCare post-receive — Track A (git push main → deploy-billcare.sh)
|
||||
# 설치: cp scripts/post-receive /Volumes/ADATA/git/BillCare.git/hooks/post-receive && chmod +x ...
|
||||
# BizCare post-receive — Track A (git push main → deploy-bizcare.sh)
|
||||
# 설치: cp scripts/post-receive /Volumes/ADATA/git/BizCare.git/hooks/post-receive && chmod +x ...
|
||||
set -euo pipefail
|
||||
|
||||
GIT_DIR="/Volumes/ADATA/git/BillCare.git"
|
||||
WORK_TREE="${WORK_TREE:-/Users/aidev/apps/BillCare}"
|
||||
DEPLOY_SCRIPT="/Volumes/ADATA/git/deploy-billcare.sh"
|
||||
GIT_DIR="/Volumes/ADATA/git/BizCare.git"
|
||||
WORK_TREE="${WORK_TREE:-/Users/aidev/apps/BizCare}"
|
||||
DEPLOY_SCRIPT="/Volumes/ADATA/git/deploy-bizcare.sh"
|
||||
SUMMARY_SCRIPT="/Volumes/ADATA/git/git-push-summary.sh"
|
||||
DEPLOY_LOG="/Volumes/ADATA/logs/deploy/billcare-deploy.log"
|
||||
DEPLOY_LOG="/Volumes/ADATA/logs/deploy/bizcare-deploy.log"
|
||||
SUMMARY_DIR="/Volumes/ADATA/logs/deploy/push-summaries"
|
||||
HOOK_TAG="[billcare-post-receive]"
|
||||
HOOK_TAG="[bizcare-post-receive]"
|
||||
|
||||
mkdir -p "$(dirname "$DEPLOY_LOG")" "$SUMMARY_DIR"
|
||||
|
||||
@@ -31,7 +31,7 @@ while read -r OLDREV NEWREV REFNAME; do
|
||||
fi
|
||||
|
||||
if [ "$BRANCH_NAME" = "main" ]; then
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG main → deploy-billcare.sh 실행"
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG main → deploy-bizcare.sh 실행"
|
||||
export SERVICE_NAME="${SERVICE_NAME:-all}"
|
||||
if [ ! -x "$DEPLOY_SCRIPT" ]; then
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG [ERROR] 배포 스크립트 없음: $DEPLOY_SCRIPT"
|
||||
@@ -43,7 +43,7 @@ while read -r OLDREV NEWREV REFNAME; do
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG [ERROR] 배포 실패 (exit: $EXIT_CODE)"
|
||||
exit "$EXIT_CODE"
|
||||
fi
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG 배포 완료 → https://billcare.exdev.co.kr"
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG 배포 완료 → https://bizcare.exdev.co.kr"
|
||||
else
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG $BRANCH_NAME — 배포 생략 (main만 자동 배포)"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user