기능수정
This commit is contained in:
Vendored
+3
-3
@@ -19,16 +19,16 @@ pipeline {
|
|||||||
sh '''
|
sh '''
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:$PATH"
|
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)"
|
echo "시각: $(date)"
|
||||||
SERVICE_NAME=${SERVICE_NAME} /Volumes/ADATA/git/deploy-billcare.sh
|
SERVICE_NAME=${SERVICE_NAME} /Volumes/ADATA/git/deploy-bizcare.sh
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
post {
|
post {
|
||||||
always {
|
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/) 홈상품(인터넷/홈렌탈) 운용관리 프로그램을
|
[비즈케어 홈즈](https://h.bizcare.co.kr/) 홈상품(인터넷/홈렌탈) 운용관리 프로그램을
|
||||||
billAgent와 동일한 레이아웃·테마 구조로 재구현한 **독립 프로젝트**입니다.
|
billAgent와 동일한 레이아웃·테마 구조로 재구현한 **독립 프로젝트**입니다.
|
||||||
@@ -13,7 +13,7 @@ docker compose up --build -d
|
|||||||
|
|
||||||
- UI: http://localhost:82
|
- UI: http://localhost:82
|
||||||
- API: http://localhost:8082
|
- API: http://localhost:8082
|
||||||
- MySQL: localhost:3308 (`billcare`)
|
- MySQL: localhost:3308 (`bizcare`)
|
||||||
|
|
||||||
데모 계정: `demo` / `demo123`
|
데모 계정: `demo` / `demo123`
|
||||||
|
|
||||||
@@ -21,10 +21,10 @@ docker compose up --build -d
|
|||||||
|
|
||||||
| 구분 | 내용 |
|
| 구분 | 내용 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 브랜드 | 빌링 케어 / Bizcare Homes |
|
| 브랜드 | BizCare / Bizcare Homes |
|
||||||
| 레이아웃/테마 | `AgentShell` + light/dark/blue |
|
| 레이아웃/테마 | `AgentShell` + light/dark/blue |
|
||||||
| 메뉴 | 상품·계약·정산·약속·예약·고객·장부·통계·설정·CS·게시판 |
|
| 메뉴 | 상품·계약·정산·약속·예약·고객·장부·통계·설정·CS·게시판 |
|
||||||
| API | Spring Boot JWT (`/api/agent/homes/**`) |
|
| API | Spring Boot JWT (`/api/agent/homes/**`) |
|
||||||
| 컨테이너 | `billcare-frontend` / `billcare-backend` / `billcare-mysql` |
|
| 컨테이너 | `bizcare-frontend` / `bizcare-backend` / `bizcare-mysql` |
|
||||||
|
|
||||||
billAgent와 동시에 실행할 수 있도록 포트·컨테이너·DB명을 분리했습니다.
|
billAgent와 동시에 실행할 수 있도록 포트·컨테이너·DB명을 분리했습니다.
|
||||||
|
|||||||
@@ -814,7 +814,7 @@ class CareReceptionController {
|
|||||||
String carrier = blankToNull(f.get("carrier"));
|
String carrier = blankToNull(f.get("carrier"));
|
||||||
String branch = blankToNull(f.get("branch"));
|
String branch = blankToNull(f.get("branch"));
|
||||||
String counselor = blankToNull(f.get("counselor"));
|
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 region = blankToNull(first(f, "region", "customerSido"));
|
||||||
String giftCategory = blankToNull(first(f, "giftCategory", "giftType"));
|
String giftCategory = blankToNull(first(f, "giftCategory", "giftType"));
|
||||||
String productCategory = blankToNull(first(f, "productCategory", "productType"));
|
String productCategory = blankToNull(first(f, "productCategory", "productType"));
|
||||||
@@ -832,8 +832,29 @@ class CareReceptionController {
|
|||||||
String giftPlace = blankToNull(f.get("giftPlace"));
|
String giftPlace = blankToNull(f.get("giftPlace"));
|
||||||
String branchEmployee = blankToNull(first(f, "branchEmployee"));
|
String branchEmployee = blankToNull(first(f, "branchEmployee"));
|
||||||
String branchName = blankToNull(f.get("branchName"));
|
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"));
|
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;
|
String progressF = progress;
|
||||||
List<HomesReception> result = all.stream().filter(r -> {
|
List<HomesReception> result = all.stream().filter(r -> {
|
||||||
LocalDate od = r.getOrderDate() == null ? null : r.getOrderDate().toLocalDate();
|
LocalDate od = r.getOrderDate() == null ? null : r.getOrderDate().toLocalDate();
|
||||||
@@ -846,34 +867,95 @@ class CareReceptionController {
|
|||||||
LocalDate gd = r.getGiftSended() != null ? r.getGiftSended() : r.getGiftDate();
|
LocalDate gd = r.getGiftSended() != null ? r.getGiftSended() : r.getGiftDate();
|
||||||
if (gd == null || !gd.equals(giftSended)) return false;
|
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 rp = ReceptionStatuses.normalizeProgress(r.getProgressStatus());
|
||||||
String fp = ReceptionStatuses.normalizeProgress(progressF);
|
String fp = ReceptionStatuses.normalizeProgress(progressF);
|
||||||
if (!fp.equals(rp) && !progressF.equals(r.getProgressStatus())) return false;
|
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 rs = ReceptionStatuses.normalizeCenter(r.getSettleStatus());
|
||||||
String fs = ReceptionStatuses.normalizeCenter(settle);
|
String fs = ReceptionStatuses.normalizeCenter(settle);
|
||||||
if (!fs.equals(rs) && !settle.equals(r.getSettleStatus())) return false;
|
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 rg = ReceptionStatuses.normalizeGift(r.getGiftStatus());
|
||||||
String fg = ReceptionStatuses.normalizeGift(giftStatus);
|
String fg = ReceptionStatuses.normalizeGift(giftStatus);
|
||||||
if (!fg.equals(rg) && !giftStatus.equals(r.getGiftStatus())) return false;
|
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) {
|
if (ispName != null) {
|
||||||
String hay = (nullToEmpty(r.getIspName()) + " " + nullToEmpty(r.getCarrier())).toLowerCase();
|
String hay = (nullToEmpty(r.getIspName()) + " " + nullToEmpty(r.getCarrier())).toLowerCase();
|
||||||
if (!hay.contains(ispName.toLowerCase())) return false;
|
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 (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 (counselor != null && !counselor.equals(r.getCounselor())) return false;
|
||||||
if (branchEmployee != null && !nullToEmpty(r.getCounselor()).contains(branchEmployee)) return false;
|
if (branchEmployee != null && !nullToEmpty(r.getCounselor()).contains(branchEmployee)) return false;
|
||||||
if (processor != null && !processor.equals(r.getProcessor())) return false;
|
if (centerName != null) {
|
||||||
if (region != null && !region.equals(r.getRegion())) return false;
|
String cn = blank(r.getProcessor()) ? "본사" : r.getProcessor();
|
||||||
if (giftCategory != null && !nullToEmpty(r.getGiftCategory()).contains(giftCategory)) return false;
|
if (!cn.contains(centerName)) return false;
|
||||||
if (productCategory != null
|
}
|
||||||
|
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.getProductCategory())
|
||||||
&& !productCategory.equals(r.getProductType())) return false;
|
&& !productCategory.equals(r.getProductType())) return false;
|
||||||
if (giftPlace != null && !giftPlace.equals(r.getGiftPlace())) 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 (agreementName != null && !nullToEmpty(r.getAgreementName()).contains(agreementName)) return false;
|
||||||
if (customerSsid != null && !nullToEmpty(r.getIdentifyNumber()).contains(customerSsid)) return false;
|
if (customerSsid != null && !nullToEmpty(r.getIdentifyNumber()).contains(customerSsid)) return false;
|
||||||
if (customerEmail != null && !nullToEmpty(r.getEmail()).contains(customerEmail)) 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 (contractNumber != null && !nullToEmpty(r.getContractNumber()).contains(contractNumber)) return false;
|
||||||
if (q != null) {
|
if (q != null) {
|
||||||
String hay = (nullToEmpty(r.getCustomerName()) + " " + nullToEmpty(r.getPhone()) + " "
|
String hay = (nullToEmpty(r.getCustomerName()) + " " + nullToEmpty(r.getPhone()) + " "
|
||||||
@@ -909,6 +997,14 @@ class CareReceptionController {
|
|||||||
return result;
|
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) {
|
private void apply(HomesReception r, Map<String, Object> d) {
|
||||||
if (d.containsKey("orderDate")) r.setOrderDate(parseDateTime(str(d.get("orderDate"))));
|
if (d.containsKey("orderDate")) r.setOrderDate(parseDateTime(str(d.get("orderDate"))));
|
||||||
if (d.containsKey("counselor")) r.setCounselor(str(d.get("counselor")));
|
if (d.containsKey("counselor")) r.setCounselor(str(d.get("counselor")));
|
||||||
|
|||||||
@@ -732,7 +732,7 @@ class HomesBranch extends BaseEntity {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 영업점 수수료 시트 — billing branch_fees / branch_group_fees 축소.
|
* 영업점 수수료 시트 — billing branch_fees / branch_group_fees 축소.
|
||||||
* BillCare는 영업점명을 문자열로 쓰므로 branchName 키.
|
* BizCare는 영업점명을 문자열로 쓰므로 branchName 키.
|
||||||
*/
|
*/
|
||||||
@Entity @Table(name = "homes_branch_fees") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
|
@Entity @Table(name = "homes_branch_fees") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
|
||||||
class HomesBranchFee extends BaseEntity {
|
class HomesBranchFee extends BaseEntity {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package com.bizcare.agent;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* billing_react AppConstant 의 접수/사은품/입력처/정산 상태·전이를 BillCare 에 이식.
|
* billing_react AppConstant 의 접수/사은품/입력처/정산 상태·전이를 BizCare 에 이식.
|
||||||
*/
|
*/
|
||||||
public final class ReceptionStatuses {
|
public final class ReceptionStatuses {
|
||||||
private ReceptionStatuses() {}
|
private ReceptionStatuses() {}
|
||||||
@@ -43,7 +43,7 @@ public final class ReceptionStatuses {
|
|||||||
public static final Map<String, String> CENTER_COST_TYPE = map(
|
public static final Map<String, String> CENTER_COST_TYPE = map(
|
||||||
"k01", "정산", "k02", "위면해지", "k03", "직권해지", "k04", "유보중단환수", "k05", "해지", "k06", "voc차감");
|
"k01", "정산", "k02", "위면해지", "k03", "직권해지", "k04", "유보중단환수", "k05", "해지", "k06", "voc차감");
|
||||||
|
|
||||||
/** 구 BillCare 한글 라벨 → 코드 */
|
/** 구 BizCare 한글 라벨 → 코드 */
|
||||||
public static final Map<String, String> LEGACY_PROGRESS = Map.ofEntries(
|
public static final Map<String, String> LEGACY_PROGRESS = Map.ofEntries(
|
||||||
Map.entry("1.접수완료", "f04"),
|
Map.entry("1.접수완료", "f04"),
|
||||||
Map.entry("2.고객통화", "f03"),
|
Map.entry("2.고객통화", "f03"),
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3308/billcare?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8}
|
url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3308/bizcare?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8}
|
||||||
username: ${SPRING_DATASOURCE_USERNAME:billcare}
|
username: ${SPRING_DATASOURCE_USERNAME:bizcare}
|
||||||
password: ${SPRING_DATASOURCE_PASSWORD:billcare}
|
password: ${SPRING_DATASOURCE_PASSWORD:bizcare}
|
||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: update
|
ddl-auto: update
|
||||||
@@ -23,8 +23,8 @@ server:
|
|||||||
port: 8080
|
port: 8080
|
||||||
app:
|
app:
|
||||||
jwt:
|
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}
|
expiration-ms: ${APP_JWT_EXPIRATION_MS:86400000}
|
||||||
cors:
|
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}
|
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 A | `git push origin main` | `post-receive` → `deploy-bizcare.sh` |
|
||||||
| Track B | Jenkins `BillCare-Pipeline` | `deploy-billcare.sh` |
|
| Track B | Jenkins `BizCare-Pipeline` | `deploy-bizcare.sh` |
|
||||||
|
|
||||||
## 경로
|
## 경로
|
||||||
|
|
||||||
| 항목 | 경로 |
|
| 항목 | 경로 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| bare repo | `/Volumes/ADATA/git/BillCare.git` |
|
| bare repo | `/Volumes/ADATA/git/BizCare.git` |
|
||||||
| work-tree | `/Users/aidev/apps/BillCare` |
|
| work-tree | `/Users/aidev/apps/BizCare` |
|
||||||
| 배포 스크립트 | `/Volumes/ADATA/git/deploy-billcare.sh` |
|
| 배포 스크립트 | `/Volumes/ADATA/git/deploy-bizcare.sh` |
|
||||||
| 로그 | `/Volumes/ADATA/logs/deploy/billcare-deploy.log` |
|
| 로그 | `/Volumes/ADATA/logs/deploy/bizcare-deploy.log` |
|
||||||
| HTTPS | https://billcare.exdev.co.kr |
|
| 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` |
|
| Caddy | `ex-chatbot-caddy` → host `:83` |
|
||||||
| 인증서 | LE 전용 (`billcare.exdev.co.kr`) |
|
| 인증서 | LE 전용 (`bizcare.exdev.co.kr`) |
|
||||||
| cert 경로 | `.../nginx/certs/billcare/{fullchain,privkey}.pem` |
|
| cert 경로 | `.../nginx/certs/bizcare/{fullchain,privkey}.pem` |
|
||||||
| 발급 | `acme.sh` + HTTP-01 (`gc-frontend` `/var/www/certbot`) |
|
| 발급 | `acme.sh` + HTTP-01 (`gc-frontend` `/var/www/certbot`) |
|
||||||
| 갱신 reload | `docker exec ex-chatbot-caddy caddy reload --config /etc/caddy/Caddyfile` |
|
| 갱신 reload | `docker exec ex-chatbot-caddy caddy reload --config /etc/caddy/Caddyfile` |
|
||||||
|
|
||||||
공유 인증서(`stock`/`chatbot`) SAN에 billcare가 없어 별도 발급했습니다.
|
공유 인증서(`stock`/`chatbot`) SAN에 bizcare가 없어 별도 발급했습니다.
|
||||||
`gc-frontend`에 acme-webroot가 마운트되어 있어야 갱신이 됩니다.
|
`gc-frontend`에 acme-webroot가 마운트되어 있어야 갱신이 됩니다.
|
||||||
|
|||||||
+14
-14
@@ -1,17 +1,17 @@
|
|||||||
services:
|
services:
|
||||||
mysql:
|
mysql:
|
||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
container_name: billcare-mysql
|
container_name: bizcare-mysql
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: root
|
MYSQL_ROOT_PASSWORD: root
|
||||||
MYSQL_DATABASE: billcare
|
MYSQL_DATABASE: bizcare
|
||||||
MYSQL_USER: billcare
|
MYSQL_USER: bizcare
|
||||||
MYSQL_PASSWORD: billcare
|
MYSQL_PASSWORD: bizcare
|
||||||
ports:
|
ports:
|
||||||
- "${MYSQL_PORT:-3308}:3306"
|
- "${MYSQL_PORT:-3308}:3306"
|
||||||
volumes:
|
volumes:
|
||||||
- billcare_mysql:/var/lib/mysql
|
- bizcare_mysql:/var/lib/mysql
|
||||||
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"]
|
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"]
|
||||||
@@ -24,19 +24,19 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: billcare-backend
|
container_name: bizcare-backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
mysql:
|
mysql:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/billcare?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
|
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/bizcare?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
|
||||||
SPRING_DATASOURCE_USERNAME: billcare
|
SPRING_DATASOURCE_USERNAME: bizcare
|
||||||
SPRING_DATASOURCE_PASSWORD: billcare
|
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://billcare.exdev.co.kr,tauri://localhost,https://tauri.localhost,http://tauri.localhost}
|
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
|
APP_UPLOAD_DIR: /app/storage/uploads
|
||||||
volumes:
|
volumes:
|
||||||
- billcare_uploads:/app/storage/uploads
|
- bizcare_uploads:/app/storage/uploads
|
||||||
ports:
|
ports:
|
||||||
- "${BACKEND_PORT:-8082}:8080"
|
- "${BACKEND_PORT:-8082}:8080"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -50,7 +50,7 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: billcare-frontend
|
container_name: bizcare-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
backend:
|
backend:
|
||||||
@@ -59,5 +59,5 @@ services:
|
|||||||
- "${FRONTEND_PORT:-82}:80"
|
- "${FRONTEND_PORT:-82}:80"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
billcare_mysql:
|
bizcare_mysql:
|
||||||
billcare_uploads:
|
bizcare_uploads:
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>비즈케어</title>
|
<title>BizCare</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
AppstoreOutlined, CalendarOutlined, ShoppingCartOutlined,
|
AppstoreOutlined, CalendarOutlined, ShoppingCartOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import SchedulePanel from './SchedulePanel'
|
import SchedulePanel from './SchedulePanel'
|
||||||
import BillCareLogo from './BillCareLogo'
|
import BizCareLogo from './BizCareLogo'
|
||||||
import AccountPopover from './AccountPopover'
|
import AccountPopover from './AccountPopover'
|
||||||
import { careMenuSections, filterMenuSections, resolveSelectedKey, sectionContainsPath, shopMenuSections } from '../menu'
|
import { careMenuSections, filterMenuSections, resolveSelectedKey, sectionContainsPath, shopMenuSections } from '../menu'
|
||||||
import { fetchMyMenus } from '../api/auth'
|
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 }}>
|
<div className={`topbar-brand${collapsed ? ' is-collapsed' : ''}`} style={collapsed ? undefined : { width: panelWidth }}>
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<div className="brand-inline">
|
<div className="brand-inline">
|
||||||
<BillCareLogo title="BillCare" size="header" />
|
<BizCareLogo title="BizCare" size="header" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+11
-11
@@ -3,7 +3,7 @@ import logoMark from '../assets/logo_mark_light.png'
|
|||||||
import { useTheme } from '../themeContext'
|
import { useTheme } from '../themeContext'
|
||||||
import type { Theme } from '../theme'
|
import type { Theme } from '../theme'
|
||||||
|
|
||||||
interface BillCareLogoProps {
|
interface BizCareLogoProps {
|
||||||
className?: string
|
className?: string
|
||||||
title?: string
|
title?: string
|
||||||
size?: 'header' | 'login'
|
size?: 'header' | 'login'
|
||||||
@@ -18,25 +18,25 @@ function isDarkTheme(theme: Theme) {
|
|||||||
* logo_image.png — 라이트 헤더 가로 로고
|
* logo_image.png — 라이트 헤더 가로 로고
|
||||||
* logo_mark_light.png / logo_image_theme.png — 마크 (로그인·다크 헤더)
|
* logo_mark_light.png / logo_image_theme.png — 마크 (로그인·다크 헤더)
|
||||||
*/
|
*/
|
||||||
export default function BillCareLogo({
|
export default function BizCareLogo({
|
||||||
className = '',
|
className = '',
|
||||||
title = 'BillCare',
|
title = 'BizCare',
|
||||||
size = 'header',
|
size = 'header',
|
||||||
forceTheme,
|
forceTheme,
|
||||||
}: BillCareLogoProps) {
|
}: BizCareLogoProps) {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const active = forceTheme ?? theme
|
const active = forceTheme ?? theme
|
||||||
const dark = isDarkTheme(active)
|
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)
|
.filter(Boolean)
|
||||||
.join(' ')
|
.join(' ')
|
||||||
|
|
||||||
// 로그인: 항상 마크 + BillCare (색상은 CSS로 고정)
|
// 로그인: 항상 마크 + BizCare (색상은 CSS로 고정)
|
||||||
if (size === 'login') {
|
if (size === 'login') {
|
||||||
return (
|
return (
|
||||||
<div className={root} title={title}>
|
<div className={root} title={title}>
|
||||||
<img src={logoMark} alt="" className="billcare-logo-mark" draggable={false} />
|
<img src={logoMark} alt="" className="bizcare-logo-mark" draggable={false} />
|
||||||
<span className="billcare-logo-title">{title}</span>
|
<span className="bizcare-logo-title">{title}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -54,11 +54,11 @@ export default function BillCareLogo({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 다크/블루 헤더: 마크 + BillCare
|
// 다크/블루 헤더: 마크 + BizCare
|
||||||
return (
|
return (
|
||||||
<div className={root} title={title}>
|
<div className={root} title={title}>
|
||||||
<img src={logoMark} alt="" className="billcare-logo-mark" draggable={false} />
|
<img src={logoMark} alt="" className="bizcare-logo-mark" draggable={false} />
|
||||||
<span className="billcare-logo-title">{title}</span>
|
<span className="bizcare-logo-title">{title}</span>
|
||||||
</div>
|
</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 { type FormEvent, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { login } from '../api/auth'
|
import { login } from '../api/auth'
|
||||||
import BillCareLogo from '../components/BillCareLogo'
|
import BizCareLogo from '../components/BizCareLogo'
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -34,7 +34,7 @@ export default function LoginPage() {
|
|||||||
<div className="login-wrap">
|
<div className="login-wrap">
|
||||||
<form className="login-box" onSubmit={submit}>
|
<form className="login-box" onSubmit={submit}>
|
||||||
<div className="login-brand">
|
<div className="login-brand">
|
||||||
<BillCareLogo title="BillCare" size="login" />
|
<BizCareLogo title="BizCare" size="login" />
|
||||||
<p>통합 영업·정산 관리 시스템</p>
|
<p>통합 영업·정산 관리 시스템</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import ReceptionApplyModal from '../../components/ReceptionApplyModal'
|
import ReceptionApplyModal from '../../components/ReceptionApplyModal'
|
||||||
import ReceptionBulkModal from '../../components/ReceptionBulkModal'
|
import ReceptionBulkModal from '../../components/ReceptionBulkModal'
|
||||||
import ReceptionProgressModal from '../../components/ReceptionProgressModal'
|
import ReceptionProgressModal from '../../components/ReceptionProgressModal'
|
||||||
|
import MultiSelect, { entriesOptions, mapOptions, type MultiSelectOption } from '../../components/MultiSelect'
|
||||||
import { DownOutlined } from '@ant-design/icons'
|
import { DownOutlined } from '@ant-design/icons'
|
||||||
import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client'
|
import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client'
|
||||||
import { options } from './homesShared'
|
import { options } from './homesShared'
|
||||||
@@ -29,6 +30,7 @@ import {
|
|||||||
PROGRESS_STATUS,
|
PROGRESS_STATUS,
|
||||||
GIFT_STATUS,
|
GIFT_STATUS,
|
||||||
CENTER_STATUS,
|
CENTER_STATUS,
|
||||||
|
GIFT_PLACE,
|
||||||
} from './receptionStatuses'
|
} from './receptionStatuses'
|
||||||
|
|
||||||
type ReceptionRow = {
|
type ReceptionRow = {
|
||||||
@@ -96,11 +98,20 @@ type ReceptionResult = {
|
|||||||
productCategories?: string[]
|
productCategories?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IspProduct = { id: number; name: string; type?: string }
|
||||||
|
|
||||||
const SIDO_LIST = [
|
const SIDO_LIST = [
|
||||||
'서울', '경기', '인천', '강원', '충북', '충남', '대전', '전북', '전남', '광주',
|
'서울', '경기', '인천', '강원', '충북', '충남', '대전', '전북', '전남', '광주',
|
||||||
'경북', '대구', '경남', '울산', '부산', '제주',
|
'경북', '대구', '경남', '울산', '부산', '제주',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const PRODUCT_TYPE_OPTS: MultiSelectOption[] = [
|
||||||
|
['e01', '인터넷'],
|
||||||
|
['e02', '전화'],
|
||||||
|
['e03', 'TV'],
|
||||||
|
['e04', '렌탈'],
|
||||||
|
]
|
||||||
|
|
||||||
const initialFilters = () => ({
|
const initialFilters = () => ({
|
||||||
page: 0,
|
page: 0,
|
||||||
size: 50,
|
size: 50,
|
||||||
@@ -118,19 +129,69 @@ const initialFilters = () => ({
|
|||||||
ispName: '',
|
ispName: '',
|
||||||
branchEmployee: '',
|
branchEmployee: '',
|
||||||
branchName: '',
|
branchName: '',
|
||||||
giftType: undefined as string | undefined,
|
centerName: '',
|
||||||
productType: undefined as string | undefined,
|
paymentTransferSsid: '',
|
||||||
progressStatus: undefined as string | undefined,
|
paymentTransferNumber: '',
|
||||||
giftStatus: undefined as string | undefined,
|
giftType: [] as string[],
|
||||||
giftPlace: undefined as string | undefined,
|
productType: [] as string[],
|
||||||
customerSido: undefined as string | undefined,
|
progressStatus: [] as string[],
|
||||||
carrier: undefined as string | undefined,
|
giftStatus: [] as string[],
|
||||||
branch: undefined as string | undefined,
|
centerStatus: [] as string[],
|
||||||
settleStatus: undefined as string | undefined,
|
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,
|
opended: undefined as string | undefined,
|
||||||
giftSended: 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 num = (n?: number | null) => (n == null ? '' : Number(n).toLocaleString())
|
||||||
|
|
||||||
const giftPlaceLabel = (v?: string) => placeLabel(v)
|
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({
|
const query = useQuery({
|
||||||
queryKey: ['homes-receptions', filters],
|
queryKey: ['homes-receptions', filters],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await client.get<ApiResponse<ReceptionResult>>(`${apiPrefix()}/homes/receptions`, {
|
const { data } = await client.get<ApiResponse<ReceptionResult>>(`${apiPrefix()}/homes/receptions`, {
|
||||||
params: {
|
params: toSearchParams(filters),
|
||||||
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)
|
return unwrap(data)
|
||||||
},
|
},
|
||||||
@@ -218,8 +297,52 @@ export default function ReceptionListPage() {
|
|||||||
const giftSelectOpts = statusOptions(giftLabels as Record<string, string>)
|
const giftSelectOpts = statusOptions(giftLabels as Record<string, string>)
|
||||||
const settleSelectOpts = statusOptions(settleLabels as Record<string, string>)
|
const settleSelectOpts = statusOptions(settleLabels as Record<string, string>)
|
||||||
|
|
||||||
const setD = (patch: Partial<ReturnType<typeof initialFilters>>) => setDraft((p) => ({ ...p, ...patch }))
|
const giftTypeOpts: MultiSelectOption[] = (
|
||||||
const applyF = (patch: Partial<ReturnType<typeof initialFilters>>) => {
|
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 }
|
const next = { ...draft, ...patch, page: 0 }
|
||||||
setDraft(next)
|
setDraft(next)
|
||||||
setFilters(next)
|
setFilters(next)
|
||||||
@@ -325,7 +448,7 @@ export default function ReceptionListPage() {
|
|||||||
const downloadExcel = async () => {
|
const downloadExcel = async () => {
|
||||||
try {
|
try {
|
||||||
const { data } = await client.get(`${apiPrefix()}/homes/receptions/excel`, {
|
const { data } = await client.get(`${apiPrefix()}/homes/receptions/excel`, {
|
||||||
params: { ...filters, page: undefined, size: undefined },
|
params: toSearchParams(filters, { omitPaging: true }),
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
})
|
})
|
||||||
const url = URL.createObjectURL(data)
|
const url = URL.createObjectURL(data)
|
||||||
@@ -405,11 +528,6 @@ export default function ReceptionListPage() {
|
|||||||
</div>
|
</div>
|
||||||
</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-filter">
|
||||||
<div className="order-date-row">
|
<div className="order-date-row">
|
||||||
<DatePicker
|
<DatePicker
|
||||||
@@ -447,28 +565,27 @@ export default function ReceptionListPage() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div className="order-attach-buttons">
|
<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>
|
||||||
<Button onClick={downloadExcel}>사은품 Excel 다운로드</Button>
|
<Button onClick={downloadExcel}>사은품 Excel 다운로드</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="order-result-row">
|
|
||||||
<span className="order-result muted">검색 결과: 총 {(query.data?.total ?? 0).toLocaleString()} 건</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="order-search-fields">
|
<div className="order-search-fields">
|
||||||
<Select
|
<MultiSelect
|
||||||
allowClear
|
|
||||||
className="ff"
|
className="ff"
|
||||||
placeholder="사은품"
|
placeholder="사은품"
|
||||||
|
options={giftTypeOpts}
|
||||||
value={draft.giftType}
|
value={draft.giftType}
|
||||||
onChange={(v) => setD({ giftType: v })}
|
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.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.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.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.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">
|
<label className="ff-d">
|
||||||
개통일자
|
개통일자
|
||||||
<DatePicker
|
<DatePicker
|
||||||
@@ -484,9 +601,18 @@ export default function ReceptionListPage() {
|
|||||||
onChange={(d) => setD({ giftSended: d ? d.format('YYYY-MM-DD') : undefined })}
|
onChange={(d) => setD({ giftSended: d ? d.format('YYYY-MM-DD') : undefined })}
|
||||||
/>
|
/>
|
||||||
</label>
|
</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 })} />
|
<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.ispName} onChange={(e) => setD({ ispName: e.target.value })} />
|
||||||
<input className="ff-i" placeholder="상품명" value={draft.productName} onChange={(e) => setD({ productName: 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -523,50 +649,45 @@ export default function ReceptionListPage() {
|
|||||||
</select>
|
</select>
|
||||||
</th>
|
</th>
|
||||||
<th className="col-center">
|
<th className="col-center">
|
||||||
<Select
|
<MultiSelect
|
||||||
allowClear
|
|
||||||
size="small"
|
|
||||||
placeholder="입력처"
|
placeholder="입력처"
|
||||||
value={draft.settleStatus}
|
options={processorMsOpts}
|
||||||
onChange={(v) => applyF({ settleStatus: v })}
|
value={draft.processors}
|
||||||
options={settleSelectOpts}
|
onChange={(v) => applyF({ processors: v, centerNames: v })}
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th className="col-branch">
|
<th className="col-branch">
|
||||||
<Select
|
<MultiSelect
|
||||||
allowClear
|
|
||||||
size="small"
|
|
||||||
placeholder="영업점"
|
placeholder="영업점"
|
||||||
value={draft.branch}
|
options={branchMsOpts}
|
||||||
onChange={(v) => applyF({ branch: v })}
|
value={draft.branches}
|
||||||
options={options(query.data?.branches || [])}
|
onChange={(v) => applyF({ branches: v, branchIds: v })}
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th className="col-isp">
|
<th className="col-isp">
|
||||||
<Select
|
<MultiSelect
|
||||||
allowClear
|
|
||||||
size="small"
|
|
||||||
placeholder="통신사"
|
placeholder="통신사"
|
||||||
value={draft.carrier}
|
options={ispMsOpts}
|
||||||
onChange={(v) => applyF({ carrier: v })}
|
value={draft.ispId}
|
||||||
options={options(query.data?.carriers || [])}
|
onChange={(v) => applyF({ ispId: v, productNames: [], productIds: [] })}
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th className="col-ptype">
|
<th className="col-ptype">
|
||||||
<Select
|
<MultiSelect
|
||||||
allowClear
|
|
||||||
size="small"
|
|
||||||
placeholder="상품종류"
|
placeholder="상품종류"
|
||||||
|
options={PRODUCT_TYPE_OPTS}
|
||||||
value={draft.productType}
|
value={draft.productType}
|
||||||
onChange={(v) => applyF({ productType: v })}
|
onChange={(v) => applyF({ productType: v })}
|
||||||
options={options(query.data?.productCategories || ['인터넷', '전화', 'TV', '렌탈'])}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
/>
|
||||||
</th>
|
</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">
|
<th className="col-customer">
|
||||||
<input
|
<input
|
||||||
placeholder="고객명"
|
placeholder="고객명"
|
||||||
@@ -576,14 +697,11 @@ export default function ReceptionListPage() {
|
|||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th className="col-region">
|
<th className="col-region">
|
||||||
<Select
|
<MultiSelect
|
||||||
allowClear
|
|
||||||
size="small"
|
|
||||||
placeholder="지역"
|
placeholder="지역"
|
||||||
|
options={sidoMsOpts}
|
||||||
value={draft.customerSido}
|
value={draft.customerSido}
|
||||||
onChange={(v) => applyF({ customerSido: v })}
|
onChange={(v) => applyF({ customerSido: v })}
|
||||||
options={options(SIDO_LIST)}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th className="col-idnum">
|
<th className="col-idnum">
|
||||||
@@ -595,14 +713,11 @@ export default function ReceptionListPage() {
|
|||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th className="col-status">
|
<th className="col-status">
|
||||||
<Select
|
<MultiSelect
|
||||||
allowClear
|
|
||||||
size="small"
|
|
||||||
placeholder="진행현황"
|
placeholder="진행현황"
|
||||||
|
options={progressMsOpts}
|
||||||
value={draft.progressStatus}
|
value={draft.progressStatus}
|
||||||
onChange={(v) => applyF({ progressStatus: v })}
|
onChange={(v) => applyF({ progressStatus: v })}
|
||||||
options={progressSelectOpts}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th className="col-contract">
|
<th className="col-contract">
|
||||||
@@ -614,33 +729,29 @@ export default function ReceptionListPage() {
|
|||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th className="col-giftplace">
|
<th className="col-giftplace">
|
||||||
<Select
|
<select value={draft.giftPlace} onChange={(e) => applyF({ giftPlace: e.target.value })}>
|
||||||
allowClear
|
<option value="">모두</option>
|
||||||
size="small"
|
{Object.entries(GIFT_PLACE).map(([code, label]) => (
|
||||||
placeholder="지급처"
|
<option key={code} value={code}>{label}</option>
|
||||||
value={draft.giftPlace}
|
))}
|
||||||
onChange={(v) => applyF({ giftPlace: v })}
|
</select>
|
||||||
options={[
|
|
||||||
{ value: 'b01', label: '본사요청' },
|
|
||||||
{ value: 'b02', label: '자체발송' },
|
|
||||||
{ value: '본사', label: '본사' },
|
|
||||||
{ value: '영업점', label: '영업점' },
|
|
||||||
]}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
</th>
|
</th>
|
||||||
<th className="col-gift">
|
<th className="col-gift">
|
||||||
<Select
|
<MultiSelect
|
||||||
allowClear
|
placeholder="사은품진행현황"
|
||||||
size="small"
|
options={giftMsOpts}
|
||||||
placeholder="사은품진행"
|
|
||||||
value={draft.giftStatus}
|
value={draft.giftStatus}
|
||||||
onChange={(v) => applyF({ giftStatus: v })}
|
onChange={(v) => applyF({ giftStatus: v })}
|
||||||
options={giftSelectOpts}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th className="col-cstatus"> </th>
|
<th className="col-cstatus">
|
||||||
|
<MultiSelect
|
||||||
|
placeholder="입력처진행현황"
|
||||||
|
options={centerStatusMsOpts}
|
||||||
|
value={draft.centerStatus}
|
||||||
|
onChange={(v) => applyF({ centerStatus: v })}
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
+182
-31
@@ -43,14 +43,14 @@ body { margin: 0; background: var(--bg); color: var(--text); }
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 22px;
|
margin-bottom: 22px;
|
||||||
}
|
}
|
||||||
.login-box .login-brand .billcare-logo {
|
.login-box .login-brand .bizcare-logo {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
.login-box .login-brand .billcare-logo-mark {
|
.login-box .login-brand .bizcare-logo-mark {
|
||||||
height: 64px;
|
height: 64px;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
.login-box .login-brand .billcare-logo-title {
|
.login-box .login-brand .bizcare-logo-title {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
color: var(--login-text) !important;
|
color: var(--login-text) !important;
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ body { margin: 0; background: var(--bg); color: var(--text); }
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.billcare-logo {
|
.bizcare-logo {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -116,39 +116,39 @@ body { margin: 0; background: var(--bg); color: var(--text); }
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
.billcare-logo--login {
|
.bizcare-logo--login {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
.billcare-logo--header.billcare-logo--dark,
|
.bizcare-logo--header.bizcare-logo--dark,
|
||||||
.billcare-logo--header.billcare-logo--blue {
|
.bizcare-logo--header.bizcare-logo--blue {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
img.billcare-logo,
|
img.bizcare-logo,
|
||||||
.billcare-logo img,
|
.bizcare-logo img,
|
||||||
.billcare-logo-mark {
|
.bizcare-logo-mark {
|
||||||
display: block;
|
display: block;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
img.billcare-logo--header {
|
img.bizcare-logo--header {
|
||||||
height: 28px;
|
height: 28px;
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: min(150px, 100%);
|
max-width: min(150px, 100%);
|
||||||
object-position: center;
|
object-position: center;
|
||||||
}
|
}
|
||||||
.billcare-logo--header .billcare-logo-mark {
|
.bizcare-logo--header .bizcare-logo-mark {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
.billcare-logo--login .billcare-logo-mark {
|
.bizcare-logo--login .bizcare-logo-mark {
|
||||||
height: 64px;
|
height: 64px;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
.billcare-logo-title {
|
.bizcare-logo-title {
|
||||||
font-family: 'Pretendard', -apple-system, 'Malgun Gothic', sans-serif;
|
font-family: 'Pretendard', -apple-system, 'Malgun Gothic', sans-serif;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
@@ -157,14 +157,14 @@ img.billcare-logo--header {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.billcare-logo--header .billcare-logo-title {
|
.bizcare-logo--header .bizcare-logo-title {
|
||||||
font-size: 19px;
|
font-size: 19px;
|
||||||
transform: translateY(2px);
|
transform: translateY(2px);
|
||||||
}
|
}
|
||||||
.billcare-logo--login .billcare-logo-title {
|
.bizcare-logo--login .bizcare-logo-title {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
}
|
}
|
||||||
.billcare-logo--blue .billcare-logo-title {
|
.bizcare-logo--blue .bizcare-logo-title {
|
||||||
color: #e8f4ff;
|
color: #e8f4ff;
|
||||||
}
|
}
|
||||||
.brand-inline {
|
.brand-inline {
|
||||||
@@ -177,7 +177,7 @@ img.billcare-logo--header {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
}
|
}
|
||||||
.brand-inline .billcare-logo {
|
.brand-inline .bizcare-logo {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -4251,7 +4251,7 @@ html.theme-light .reception-order-page {
|
|||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--primary);
|
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 {
|
.order-attach-buttons {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -4259,15 +4259,10 @@ html.theme-light .reception-order-page {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.order-result-row {
|
.order-attach-buttons .order-search-btn.ant-btn-primary,
|
||||||
display: flex;
|
.order-attach-buttons .order-search-btn.ant-btn-primary:hover,
|
||||||
align-items: center;
|
.order-attach-buttons .order-search-btn.ant-btn-primary:focus {
|
||||||
margin-top: 8px;
|
color: #000 !important;
|
||||||
padding-top: 8px;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
.order-result-row .order-result {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
}
|
||||||
.order-search-fields {
|
.order-search-fields {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -4352,12 +4347,168 @@ table.order-list tr.order-col-filter select {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
background: var(--surface);
|
background: var(--surface) !important;
|
||||||
color: var(--text);
|
color: var(--text) !important;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border) !important;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-sizing: border-box;
|
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 { cursor: pointer; }
|
||||||
table.order-list tbody tr:hover td { background: color-mix(in srgb, var(--primary) 8%, var(--surface)); }
|
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); }
|
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
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# BillCare 배포 스크립트 — Docker Compose
|
# BizCare 배포 스크립트 — Docker Compose
|
||||||
# 호출: Git post-receive (Track A) | Jenkins (Track B)
|
# 호출: Git post-receive (Track A) | Jenkins (Track B)
|
||||||
# 서버 설치: /Volumes/ADATA/git/deploy-billcare.sh
|
# 서버 설치: /Volumes/ADATA/git/deploy-bizcare.sh
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
GIT_DIR="${GIT_DIR:-/Volumes/ADATA/git/BillCare.git}"
|
GIT_DIR="${GIT_DIR:-/Volumes/ADATA/git/BizCare.git}"
|
||||||
WORK_TREE="${WORK_TREE:-/Users/aidev/apps/BillCare}"
|
WORK_TREE="${WORK_TREE:-/Users/aidev/apps/BizCare}"
|
||||||
BRANCH="${BRANCH:-main}"
|
BRANCH="${BRANCH:-main}"
|
||||||
DOCKER_COMPOSE_FILE="${DOCKER_COMPOSE_FILE:-${WORK_TREE}/docker-compose.yml}"
|
DOCKER_COMPOSE_FILE="${DOCKER_COMPOSE_FILE:-${WORK_TREE}/docker-compose.yml}"
|
||||||
DEPLOY_LOG="${DEPLOY_LOG:-/Volumes/ADATA/logs/deploy/billcare-deploy.log}"
|
DEPLOY_LOG="${DEPLOY_LOG:-/Volumes/ADATA/logs/deploy/bizcare-deploy.log}"
|
||||||
LOG_TAG="[billcare-deploy]"
|
LOG_TAG="[bizcare-deploy]"
|
||||||
SERVICE_NAME="${SERVICE_NAME:-all}"
|
SERVICE_NAME="${SERVICE_NAME:-all}"
|
||||||
DOCKER_NO_CACHE="${DOCKER_NO_CACHE:-0}"
|
DOCKER_NO_CACHE="${DOCKER_NO_CACHE:-0}"
|
||||||
BACKEND_HEALTH_URL="${BACKEND_HEALTH_URL:-http://127.0.0.1:18082/api/health}"
|
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 FRONTEND_PORT="${FRONTEND_PORT:-83}"
|
||||||
export BACKEND_PORT="${BACKEND_PORT:-18082}"
|
export BACKEND_PORT="${BACKEND_PORT:-18082}"
|
||||||
export MYSQL_PORT="${MYSQL_PORT:-3308}"
|
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")"
|
mkdir -p "$(dirname "$DEPLOY_LOG")"
|
||||||
exec >> "$DEPLOY_LOG" 2>&1
|
exec >> "$DEPLOY_LOG" 2>&1
|
||||||
@@ -33,7 +33,7 @@ warn() { log "[WARN] $*"; }
|
|||||||
err() { log "[ERROR] $*"; exit 1; }
|
err() { log "[ERROR] $*"; exit 1; }
|
||||||
|
|
||||||
log "======================================================"
|
log "======================================================"
|
||||||
log " BillCare 배포 시작"
|
log " BizCare 배포 시작"
|
||||||
log " 서비스 : $SERVICE_NAME"
|
log " 서비스 : $SERVICE_NAME"
|
||||||
log " 브랜치 : $BRANCH"
|
log " 브랜치 : $BRANCH"
|
||||||
log " 작업트리: $WORK_TREE"
|
log " 작업트리: $WORK_TREE"
|
||||||
@@ -41,7 +41,7 @@ log " compose : $DOCKER_COMPOSE_FILE"
|
|||||||
log " 실행자 : $(whoami)"
|
log " 실행자 : $(whoami)"
|
||||||
log "======================================================"
|
log "======================================================"
|
||||||
|
|
||||||
LOCK_FILE="/tmp/billcare_deploy.lock"
|
LOCK_FILE="/tmp/bizcare_deploy.lock"
|
||||||
if [ -f "$LOCK_FILE" ]; then
|
if [ -f "$LOCK_FILE" ]; then
|
||||||
LOCK_PID=$(cat "$LOCK_FILE" 2>/dev/null || echo "")
|
LOCK_PID=$(cat "$LOCK_FILE" 2>/dev/null || echo "")
|
||||||
if [ -n "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2>/dev/null; then
|
if [ -n "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2>/dev/null; then
|
||||||
@@ -141,7 +141,7 @@ esac
|
|||||||
log "======================================================"
|
log "======================================================"
|
||||||
log " 배포 완료: $(date '+%Y-%m-%d %H:%M:%S')"
|
log " 배포 완료: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
log " 커밋: $(git --git-dir="$GIT_DIR" log --oneline -1 2>/dev/null || echo N/A)"
|
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 "======================================================"
|
log "======================================================"
|
||||||
|
|
||||||
docker compose -f "$DOCKER_COMPOSE_FILE" ps 2>/dev/null || docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
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
|
#!/usr/bin/env bash
|
||||||
# BillCare post-receive — Track A (git push main → deploy-billcare.sh)
|
# BizCare post-receive — Track A (git push main → deploy-bizcare.sh)
|
||||||
# 설치: cp scripts/post-receive /Volumes/ADATA/git/BillCare.git/hooks/post-receive && chmod +x ...
|
# 설치: cp scripts/post-receive /Volumes/ADATA/git/BizCare.git/hooks/post-receive && chmod +x ...
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
GIT_DIR="/Volumes/ADATA/git/BillCare.git"
|
GIT_DIR="/Volumes/ADATA/git/BizCare.git"
|
||||||
WORK_TREE="${WORK_TREE:-/Users/aidev/apps/BillCare}"
|
WORK_TREE="${WORK_TREE:-/Users/aidev/apps/BizCare}"
|
||||||
DEPLOY_SCRIPT="/Volumes/ADATA/git/deploy-billcare.sh"
|
DEPLOY_SCRIPT="/Volumes/ADATA/git/deploy-bizcare.sh"
|
||||||
SUMMARY_SCRIPT="/Volumes/ADATA/git/git-push-summary.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"
|
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"
|
mkdir -p "$(dirname "$DEPLOY_LOG")" "$SUMMARY_DIR"
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ while read -r OLDREV NEWREV REFNAME; do
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$BRANCH_NAME" = "main" ]; then
|
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}"
|
export SERVICE_NAME="${SERVICE_NAME:-all}"
|
||||||
if [ ! -x "$DEPLOY_SCRIPT" ]; then
|
if [ ! -x "$DEPLOY_SCRIPT" ]; then
|
||||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG [ERROR] 배포 스크립트 없음: $DEPLOY_SCRIPT"
|
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)"
|
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG [ERROR] 배포 실패 (exit: $EXIT_CODE)"
|
||||||
exit "$EXIT_CODE"
|
exit "$EXIT_CODE"
|
||||||
fi
|
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
|
else
|
||||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG $BRANCH_NAME — 배포 생략 (main만 자동 배포)"
|
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG $BRANCH_NAME — 배포 생략 (main만 자동 배포)"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user