BillCare 초기 소스 등록

비즈케어 홈즈 홈상품 운용관리(BillCare) 프론트/백엔드/Docker 구성을 exdev git에 등록합니다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Macbook
2026-07-12 05:05:17 +09:00
commit 03cd93e1f2
160 changed files with 47915 additions and 0 deletions
@@ -0,0 +1,13 @@
package com.bizcare.agent;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AgentApplication {
public static void main(String[] args) {
SpringApplication.run(AgentApplication.class, args);
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,587 @@
package com.bizcare.agent;
import jakarta.persistence.*;
import lombok.*;
import java.math.BigDecimal;
import java.time.*;
@MappedSuperclass @Getter @Setter @NoArgsConstructor @AllArgsConstructor
abstract class BaseEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) Long id;
String groupId;
Instant createdAt; Instant updatedAt;
@PrePersist void created() { createdAt = updatedAt = Instant.now(); }
@PreUpdate void updated() { updatedAt = Instant.now(); }
}
@Entity @Table(name="members") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class Member extends BaseEntity {
@Column(unique=true, nullable=false) String userid; String password; String name; String phone;
String role, staffPermission; Long companyId; boolean active = true;
Integer loginCount;
LocalDateTime lastLoginAt;
String plainPassword;
/** 직원 주소 (홈즈 직원관리) */
String address;
/** 접속제한 메모/IP 등 */
String accessLimit;
@Column(columnDefinition="TEXT") String menuFlags;
@Column(columnDefinition="TEXT") String assignedOutNames;
@Column(columnDefinition="TEXT") String memo;
}
@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class Company extends BaseEntity {
String type, name, managerName, phone, fax, mobile, telecom, bankAccount, businessName, businessNumber, ceo, address, email;
String channel, pcode, memo, salesperson, category;
@Column(columnDefinition="TEXT") String smsPhones; boolean active = true;
}
@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class Stock extends BaseEntity {
String gubun, telecom, telcompany, serial, serials, barcode, model, color, cond, pay, memo, state, outcode, userid, procid;
String petname, outCategory, salesperson, source, processor;
@Column(unique=true) String uid;
BigDecimal inprice; Long incompanyId, outcompanyId, openId; LocalDate indate, outdate, recalldate, returndate, lossdate, selldate, processDate;
}
@Entity @Table(name="open_records") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class OpenRecord extends BaseEntity {
Long outcompanyId, phoneStockId, usimStockId, incompanyId;
String gubun, telecom, telcompany, pserial, pmodel, pcolor, userial, umodel, name, openphone, openhow, plan, salehow, joinhow, usimhow, decltype, userid, dealermemo, memo1, memo2, memo3, state;
String salesperson, employee, outCategory, openStatus, supportType;
LocalDate opendate; Integer salemon, agreemon, openHour, openMinute;
BigDecimal joinprice, usimprice, inprice, netprice, sellprice, basicprice1, basicprice2, basicprice3, basicprice4, etcprice1, etcprice2, declprice1, declprice2, moveprice, planprice, preprice, memberpoint, dealermargin1, dealermargin2;
BigDecimal commonSupport, extraSupport, switchSupport, pointPay, gradePrice;
@Column(unique=true) String uid;
}
@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class SettlementMonth extends BaseEntity {
Long outcompanyId;
@Column(name = "ym") String yearMonth;
String status, excelUrl, memo, outCategory, inquiry, agreement, invoiceStatus, remittanceStatus, outcompanyName;
Integer openCount, afterSettleCount, homeProductCount;
BigDecimal openAmount, afterSettleAmount, homeProductAmount, realSettleAmount;
boolean issued;
}
@Entity @Table(name="after_balances") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class AfterBalance extends BaseEntity {
Long outcompanyId, incompanyId, openId;
LocalDate opendate, basedate;
String outcompanyName, customerName, openphone, gubun, reason, memo, telecom, outCategory;
BigDecimal amount;
}
@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class Farebox extends BaseEntity {
Long outcompanyId;
LocalDate faredate;
String telcompany, name, phone, how, paystate, receiptNo;
String outcompanyName, outCategory, detail, registrant;
BigDecimal cashprice, cardprice, paidAmount;
}
@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class FareboxPayment extends BaseEntity {
Long outcompanyId;
String outcompanyName, mode, memo, registrant, status, outCategory;
LocalDate paydate;
Integer payCount;
BigDecimal amount;
@Column(columnDefinition="TEXT") String fareboxIds;
}
@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class BbsPost extends BaseEntity {
String code, title, authorName, userid, state, targetGroup, fileName;
@Column(columnDefinition="TEXT") String contents, comments;
boolean notice;
Integer views, confirmCount, commentCount;
}
@Entity @Table(name="board_groups") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class BoardGroup extends BaseEntity {
String category, name;
@Column(columnDefinition="TEXT") String companyIds;
}
@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class ScanDoc extends BaseEntity {
Long outcompanyId, incompanyId;
String telcompany, gubun, name, memo, sendmsg, state, src, outcompanyName;
Integer filesCount;
LocalDateTime registeredAt;
}
@Entity @Table(name="indoc") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class IncompleteDoc extends BaseEntity {
Long outcompanyId, openId, incompanyId;
LocalDate opendate, deadline;
String customerName, openphone, missingDocs, deductReason, telecom, status;
BigDecimal deductAmount;
}
@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class UnreturnedPhone extends BaseEntity {
Long outcompanyId, openId, incompanyId;
LocalDate opendate, returndate;
String customerName, openphone, model, serial, telecom, returnStatus;
BigDecimal deductAmount;
}
@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class ScheduleItem extends BaseEntity { String userid, stime, contents; LocalDate sdate; }
@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class SmsMessage extends BaseEntity {
String userid, phone, status, msgType, senderNumber, senderName;
@Column(columnDefinition="TEXT") String message;
Integer sendCount, deductCount;
Instant sentAt;
}
@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class SmsTemplate extends BaseEntity { String title; @Column(columnDefinition="TEXT") String body; }
@Entity @Table(name="sms_address_groups") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class SmsAddressGroup extends BaseEntity {
String name;
}
@Entity @Table(name="sms_address_contacts") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class SmsAddressContact extends BaseEntity {
Long addressGroupId;
String name, phone;
}
@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class PlanMaster extends BaseEntity { String telecom, name; BigDecimal price; boolean active = true; }
@Entity @Table(name="unit_cost_tables") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class UnitCostTable extends BaseEntity {
Long incompanyId;
String telecom, incompanyName, memo, category;
Integer planGroupA, planGroupB, planGroupC, planGroupD, planGroupE, planGroupF, planGroupG, policyCount;
}
@Entity @Table(name="unit_cost_policies") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class UnitCostPolicy extends BaseEntity {
Long unitCostId;
String planGroup, model, planName;
BigDecimal amount;
}
@Entity @Table(name="home_sales") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomeSale extends BaseEntity {
Long outcompanyId, incompanyId;
LocalDate installDate, receiptDate;
String status, telecom, outCategory, outcompanyName, employee, customerName, phone, address, products, missingDocs, memo;
BigDecimal extraPolicy, settleAmount, margin;
}
@Entity @Table(name="home_indocs") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomeIncompleteDoc extends BaseEntity {
Long outcompanyId, incompanyId, homeSaleId;
LocalDate installDate, deadline;
String customerName, phone, missingDocs, deductReason, telecom, status, products, outcompanyName;
BigDecimal deductAmount;
}
@Entity @Table(name="open_exchanges") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class OpenExchange extends BaseEntity {
Long outcompanyId, openId;
LocalDate exchangeDate, openDate;
LocalDateTime processedAt;
String outcompanyName, customerName, openphone, kind, newModel, newColor, newSerial, oldModel, oldColor, oldSerial, processor, memo;
}
@Entity @Table(name="open_name_changes") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class OpenNameChange extends BaseEntity {
Long outcompanyId, openId;
LocalDate changeDate, openDate;
LocalDateTime processedAt;
String outcompanyName, customerName, openphone, oldOwner, oldPhone, oldUsim, processor, memo;
}
@Entity @Table(name="open_withdrawals") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class OpenWithdrawal extends BaseEntity {
Long outcompanyId, openId;
LocalDate withdrawDate, openDate;
LocalDateTime processedAt;
String outcompanyName, customerName, openphone, processor, reason;
}
@Entity @Table(name="open_settle_histories") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class OpenSettleHistory extends BaseEntity {
Long outcompanyId, openId;
LocalDate changeDate, openDate;
LocalDateTime processedAt;
String outcompanyName, customerName, openphone, processor, memo;
BigDecimal oldAmount, newAmount;
}
@Entity @Table(name="open_delete_histories") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class OpenDeleteHistory extends BaseEntity {
Long outcompanyId, openId;
LocalDate deleteDate, openDate;
LocalDateTime processedAt;
String outcompanyName, customerName, openphone, processor, memo;
}
@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class PhoneModel extends BaseEntity { String telecom, modelCode, modelName; @Column(columnDefinition="TEXT") String colorOptions; boolean active = true; }
@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class CsInquiry extends BaseEntity {
String userid, authorName, title, status;
@Column(columnDefinition="TEXT") String content, reply;
Integer replyCount;
}
@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class Alim extends BaseEntity { String userid, title; @Column(columnDefinition="TEXT") String content; boolean confirmed; }
@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class AccountEntry extends BaseEntity {
Long outcompanyId, dealerId;
LocalDate entryDate;
String type, category, memo;
@Column(name = "ym") String yearMonth;
BigDecimal amount;
}
@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class Setting extends BaseEntity {
@Column(name = "setting_key") String settingKey;
@Lob @Column(columnDefinition = "LONGTEXT") String value;
}
@Entity @Table(name="usim_ledger") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class UsimLedger extends BaseEntity {
Long outcompanyId;
LocalDate tradeDate;
String telecom, detail, registrant, status;
Integer qty;
BigDecimal amount;
}
@Entity @Table(name="stock_history") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class StockHistory extends BaseEntity {
Long stockId, incompanyId, outcompanyId;
LocalDate processDate;
String gubun, state, statusLabel, serial, model, color, cond, memo, processor, salesperson;
String companyLabel;
}
@Entity @Table(name="stock_notes") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class StockNote extends BaseEntity {
Long stockId;
LocalDate noteDate;
String contents, authorName;
}
@Entity @Table(name="care_contracts") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class CareContract extends BaseEntity {
LocalDate contractDate, startDate, endDate;
String status, customerName, phone, productName, telecom, employee, address, memo;
BigDecimal amount, monthlyFee;
}
@Entity @Table(name="care_customers") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class CareCustomer extends BaseEntity {
String name, phone, telecom, address, status, employee, memo;
}
@Entity @Table(name="care_products") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class CareProduct extends BaseEntity {
String category, name, telecom, plan, status, memo;
BigDecimal monthlyFee, price;
}
@Entity @Table(name="homes_products") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomesProduct extends BaseEntity {
String kind, category, company, name, model, memo;
Integer months, policyCount;
BigDecimal policyAmount;
}
@Entity @Table(name="homes_product_policies") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomesProductPolicy extends BaseEntity {
Long productId;
String name;
BigDecimal amount;
}
@Entity @Table(name="homes_contracts") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomesContract extends BaseEntity {
String kind, status, customerType, customerName, phone, address, products, agency, employee, payMethod, memo;
LocalDate contractDate, installDate;
Integer months;
BigDecimal policySum, settleAmount, margin;
String email, gender, nationality, customerGrade, birthDate;
String zipcode, addressDetail;
String installPhone, installZip, installAddress, installAddressDetail;
String feeReduction, invoiceType;
Boolean sameContact;
}
@Entity @Table(name="homes_balances") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomesBalance extends BaseEntity {
/** 정산차감 / 정산추가 (구: 차감/추가) */
String balanceType;
/** 직원 / 하부점 */
String targetType;
/** 대상명 (직원명 또는 하부점명) */
String targetName;
String customerName, phone, products, reason, agency, memo;
LocalDate baseDate;
BigDecimal amount;
Long contractId;
}
@Entity @Table(name="homes_invoices") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomesInvoice extends BaseEntity {
String scope, employee, agency, status, memo;
@Column(name = "ym") String yearMonth;
Integer contractCount;
BigDecimal amount;
/** 발행 스냅샷 */
Integer internetCount, homeCount, balanceCount;
BigDecimal internetAmount, homeAmount, balanceAmount, settleAmount;
Boolean issued;
String consent, taxInvoice, withdrawStatus;
}
@Entity @Table(name="homes_pendings") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomesPending extends BaseEntity {
/** 과목: 일반/약속1/약속2 ... */
String pendingType, pendingTime, customerName, phone, contents, employee, status;
LocalDate pendingDate;
/** 접수직원 (없으면 employee 사용) */
String receiveEmployee;
/** 처리직원 */
String processEmployee;
LocalDate processDate;
String memo;
}
@Entity @Table(name="homes_bookings") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomesBooking extends BaseEntity {
String status, customerName, phone, products, address, employee, memo;
LocalDate bookingDate;
/** 인터넷상품 (콤마 구분) */
String internetProducts;
/** 홈렌탈상품 (콤마 구분) */
String rentalProducts;
String internetNote;
String rentalNote;
String zipcode;
String addressDetail;
String fileName;
}
@Entity @Table(name="homes_customers") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomesCustomer extends BaseEntity {
/** 분류 (단골, 단골1 … / 분류없음) */
String grade;
/** 구분: 개인 / 사업자 */
String customerType;
String name, phone, phoneAlt, address, agency, employee, memo;
String zipcode, addressDetail, email, gender, nationality;
String birthYear, birthMonth, birthDay;
LocalDate registeredDate;
}
@Entity @Table(name="homes_abooks") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomesAbook extends BaseEntity {
/** 입금 / 출금 / 카드입금 / 카드출금 (구: 수입/지출) */
String entryType;
/** 과목 */
String category;
/** 품목 */
String item;
String agency, employee, memo;
LocalDate entryDate;
BigDecimal amount;
}
@Entity @Table(name="homes_agencies") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomesAgency extends BaseEntity {
String name, managerName, phone, fax, mobile, agencyType, address, memo;
boolean active = true;
LocalDate registeredDate;
}
@Entity @Table(name="homes_partners") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomesPartner extends BaseEntity {
/** 1=상부점, 2=하부점 */
String partnerLevel, name, parentName, managerName, phone, address, memo;
/** 상부점 연결 아이디 */
String userid;
/** 대기 / 승인 / 거절 */
String status;
LocalDate registeredDate;
}
@Entity @Table(name="homes_logs") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomesLog extends BaseEntity {
String domain, action, content, staff;
LocalDateTime processedAt;
}
@Entity @Table(name="homes_notices") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomesNotice extends BaseEntity {
String title, authorName;
@Column(columnDefinition="TEXT") String contents;
Integer views;
}
@Entity @Table(name="homes_questions") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomesQuestion extends BaseEntity {
String title, authorName, status;
@Column(columnDefinition="TEXT") String content, reply;
}
@Entity @Table(name="homes_bbs") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomesBbs extends BaseEntity {
String title, authorName;
@Column(columnDefinition="TEXT") String contents;
Integer views;
}
/** 접수관리 — billing_react orders 목록 필드를 Care Homes 멀티테넌트에 맞게 정리 */
@Entity @Table(name="homes_receptions") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomesReception extends BaseEntity {
/** 접수일시 */
LocalDateTime orderDate;
/** 상담자 */
String counselor;
/** 지점/영업점 */
String branch;
/** 영업점 그룹 */
String branchGroup;
/** 영업점 연락처 */
String branchPhone;
/** 통신사 */
String carrier;
/** 사은품분류 */
String giftCategory;
/** 상품분류 */
String productCategory;
/** 상품종류 e01~e04 */
String productType;
/** 상품명 */
String productName;
/** 고객유형 public/company/foreigner */
String customerType;
/** 고객명 */
String customerName;
/** 연락처(휴대) */
String phone;
/** 유선전화 */
String tel;
/** 이메일 */
String email;
/** 우편번호 */
String zipcode;
/** 주소 */
String address;
/** 상세주소 */
String addressDetail;
/** 지역(시도) */
String region;
/** 진행상황 */
String progressStatus;
/** 주문번호 */
String orderNumber;
/** 연결된 ISP id */
Long ispId;
/** ISP 표시명 */
String ispName;
/** 입고/물류 정보 */
String inboundInfo;
/** 처리자 */
String processor;
/** 세부진행 */
String detailProgress;
/** 지급구분(표시용) */
String paymentType;
/** 납부방식 transfer/card/paper/telephone */
String paymentMethod;
String paymentName;
String paymentSsid;
String paymentTel;
String paymentBank;
String paymentAccountNumber;
String paymentCardCompany;
String paymentCardYear;
String paymentCardMonth;
String paymentCardNumber;
/** 금액 */
BigDecimal amount;
/** 정산상태 */
String settleStatus;
/** 개통일 */
LocalDate openDate;
/** 해지일 */
LocalDate cancelDate;
/** 사은품 지급일 */
LocalDate giftDate;
/** 사은품 진행 */
String giftStatus;
/** 지급처 */
String giftPlace;
LocalDate giftRequested;
LocalDate giftSended;
String giftBank;
String giftAccountNumber;
String giftAccountName;
String giftSsid;
String giftGoods;
BigDecimal giftCashAmount;
BigDecimal giftGoodsPrice;
String deliveryCompany;
String trackingNumber;
/** 약정명 */
String agreementName;
/** 개통/계약번호 */
String contractNumber;
/** 설치일정 */
String installSchedule;
/** 고객아이디(통신사) */
String customerLoginId;
/** 신규전화번호 */
String newTel;
Boolean multiLine;
Boolean addPayer;
Boolean recordFlag;
/** 주민/식별번호 */
String identifyNumber;
/** 포스트잇 */
String postIt;
/** 추가메모 */
String postAdd;
@Column(columnDefinition="TEXT") String memo;
@Column(columnDefinition="TEXT") String adminMemo;
String file1;
String file2;
String file3;
String file4;
String file5;
String adminFile1;
String adminFile2;
String adminFile3;
}
/** 접수 진행 이력 */
@Entity @Table(name="homes_reception_progress_logs") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomesReceptionProgressLog extends BaseEntity {
Long receptionId;
String status;
String authorName;
LocalDateTime loggedAt;
@Column(columnDefinition="TEXT") String memo;
Boolean adminOnly;
}
/** 사은품 진행 이력 — billing_react order_gift_progress_logs */
@Entity @Table(name="homes_reception_gift_progress_logs") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomesReceptionGiftProgressLog extends BaseEntity {
Long receptionId;
String status;
String authorName;
LocalDateTime loggedAt;
@Column(columnDefinition="TEXT") String memo;
}
/** 신규신청 통신사(ISP) 마스터 — billing_react isp 대응 */
@Entity @Table(name="homes_reception_isps") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomesReceptionIsp extends BaseEntity {
String name;
String telecom;
Integer seq;
Boolean active;
Boolean isSktb;
}
/** ISP 상품 */
@Entity @Table(name="homes_reception_isp_products") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomesReceptionIspProduct extends BaseEntity {
Long ispId;
/** e01 인터넷 / e02 전화 / e03 TV / e04 렌탈 */
String productType;
String name;
Boolean active;
}
/** ISP 약정 */
@Entity @Table(name="homes_reception_isp_agreements") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
class HomesReceptionIspAgreement extends BaseEntity {
Long ispId;
String name;
Boolean active;
}
@@ -0,0 +1,85 @@
package com.bizcare.agent;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.*;
import java.util.UUID;
/** 공통 파일 저장소 — billing_react FileStorageService 이식. */
@Service
class FileStorageService {
private final Path baseDir;
FileStorageService(@Value("${app.upload-dir:./storage/uploads}") String uploadDir) {
this.baseDir = Paths.get(uploadDir).toAbsolutePath().normalize();
}
@PostConstruct
void init() {
try {
Files.createDirectories(baseDir);
} catch (IOException e) {
throw new IllegalStateException("업로드 디렉터리를 생성할 수 없습니다: " + baseDir, e);
}
}
String store(String subDir, MultipartFile file) {
if (file == null || file.isEmpty()) return null;
try {
Path dir = baseDir.resolve(subDir).normalize();
if (!dir.startsWith(baseDir)) throw new IllegalArgumentException("잘못된 저장 경로입니다.");
Files.createDirectories(dir);
String original = file.getOriginalFilename() == null ? "file" : file.getOriginalFilename();
String safe = original.replaceAll("[/\\\\]", "_").replaceAll("[\\r\\n\\t]", "");
if (safe.length() > 200) {
String ext = safe.contains(".") ? safe.substring(safe.lastIndexOf('.')) : "";
safe = UUID.randomUUID().toString().replace("-", "") + ext;
}
Path target = dir.resolve(safe);
if (Files.exists(target)) {
safe = UUID.randomUUID().toString().substring(0, 8) + "_" + safe;
target = dir.resolve(safe);
}
file.transferTo(target);
return subDir + "/" + safe;
} catch (IOException e) {
throw new IllegalStateException("파일 저장 중 오류가 발생하였습니다.", e);
}
}
Resource loadAsResource(String relativePath) {
try {
Path file = baseDir.resolve(relativePath).normalize();
if (!file.startsWith(baseDir)) throw new IllegalArgumentException("잘못된 파일 경로입니다.");
Resource resource = new UrlResource(file.toUri());
if (!resource.exists() || !resource.isReadable()) {
throw new IllegalArgumentException("파일을 찾을 수 없습니다.");
}
return resource;
} catch (java.net.MalformedURLException e) {
throw new IllegalArgumentException("파일을 찾을 수 없습니다.");
}
}
void delete(String relativePath) {
if (relativePath == null || relativePath.isBlank()) return;
try {
Path file = baseDir.resolve(relativePath).normalize();
if (file.startsWith(baseDir)) Files.deleteIfExists(file);
} catch (IOException ignored) {
}
}
static String displayName(String relativePath) {
if (relativePath == null) return "";
int idx = relativePath.lastIndexOf('/');
return idx >= 0 ? relativePath.substring(idx + 1) : relativePath;
}
}
@@ -0,0 +1,14 @@
package com.bizcare.agent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
class HealthController {
@GetMapping("/api/health")
Map<String, Object> health() {
return Map.of("status", "UP");
}
}
@@ -0,0 +1,111 @@
package com.bizcare.agent;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.*;
import lombok.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.*;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.*;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.*;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.stereotype.Component;
import org.springframework.web.cors.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
@Configuration @EnableWebSecurity
class SecurityConfig {
@Value("${app.cors.origins}")
private String corsOrigins;
@Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
@Bean SecurityFilterChain filterChain(HttpSecurity http, JwtFilter filter) throws Exception {
return http.csrf(c -> c.disable())
.cors(c -> c.configurationSource(cors()))
.httpBasic(b -> b.disable())
.formLogin(f -> f.disable())
.logout(l -> l.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(a -> a
.requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers("/api/auth/**", "/api/health").permitAll()
.requestMatchers("/api/agent/**").hasRole("AGENT")
.requestMatchers("/api/shop/**").hasAnyRole("SHOP", "AGENT")
.anyRequest().authenticated())
.exceptionHandling(e -> e
.authenticationEntryPoint((req, res, ex) -> {
res.setStatus(401);
res.setContentType("application/json;charset=UTF-8");
res.getWriter().write("{\"result\":false,\"msg\":\"로그인이 필요합니다.\",\"data\":null}");
})
.accessDeniedHandler((req, res, ex) -> {
res.setStatus(403);
res.setContentType("application/json;charset=UTF-8");
res.getWriter().write("{\"result\":false,\"msg\":\"권한이 없습니다.\",\"data\":null}");
}))
.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean CorsConfigurationSource cors() {
CorsConfiguration c = new CorsConfiguration();
List<String> origins = Arrays.stream(corsOrigins.split(",")).map(String::trim).filter(s -> !s.isEmpty()).toList();
if (origins.isEmpty() || origins.contains("*")) {
c.setAllowedOriginPatterns(List.of("*"));
} else {
// 로컬/사설망 개발 접근을 허용하면서도 credentials 유지
List<String> patterns = new ArrayList<>(origins);
patterns.add("http://localhost:*");
patterns.add("http://127.0.0.1:*");
patterns.add("http://192.168.*.*:*");
patterns.add("http://10.*.*.*:*");
c.setAllowedOriginPatterns(patterns);
}
c.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
c.setAllowedHeaders(List.of("*"));
c.setExposedHeaders(List.of("*"));
c.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", c);
return source;
}
}
@Component
class JwtService {
private final byte[] secret; private final long expiration;
JwtService(@Value("${app.jwt.secret}") String secret, @Value("${app.jwt.expiration-ms}") long expiration) { this.secret=secret.getBytes(StandardCharsets.UTF_8); this.expiration=expiration; }
String create(Member m) { return Jwts.builder().subject(m.getUserid()).claim("role",m.getRole()).claim("groupId",m.getGroupId()).claim("companyId",m.getCompanyId()).issuedAt(new Date()).expiration(new Date(System.currentTimeMillis()+expiration)).signWith(Keys.hmacShaKeyFor(secret)).compact(); }
Claims claims(String token) { return Jwts.parser().verifyWith(Keys.hmacShaKeyFor(secret)).build().parseSignedClaims(token).getPayload(); }
}
@Component @RequiredArgsConstructor
class JwtFilter extends OncePerRequestFilter {
private final JwtService jwt;
@Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, jakarta.servlet.ServletException {
String header=req.getHeader(HttpHeaders.AUTHORIZATION);
if(header != null && header.startsWith("Bearer ")) try {
Claims c=jwt.claims(header.substring(7)); JwtUser user=new JwtUser(c.getSubject(), c.get("role",String.class), c.get("groupId",String.class), c.get("companyId",Long.class));
var auth=new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth);
} catch (JwtException | IllegalArgumentException ignored) { SecurityContextHolder.clearContext(); }
chain.doFilter(req,res);
}
}
@Getter @AllArgsConstructor
class JwtUser implements UserDetails {
private String userid, role, groupId; private Long companyId;
@Override public Collection<? extends GrantedAuthority> getAuthorities() { return List.of(new SimpleGrantedAuthority("ROLE_"+role)); }
@Override public String getPassword() { return ""; } @Override public String getUsername() { return userid; }
@Override public boolean isAccountNonExpired(){return true;} @Override public boolean isAccountNonLocked(){return true;} @Override public boolean isCredentialsNonExpired(){return true;} @Override public boolean isEnabled(){return true;}
}
@@ -0,0 +1,32 @@
package com.bizcare.agent;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController @RequestMapping("/api/shop") @RequiredArgsConstructor
class ShopController {
private final CrudService crud;
@GetMapping({"/bbs","/stocks","/opens","/settlements","/fareboxes","/indocs","/returns","/scans"})
ApiResponse list(HttpServletRequest r,@AuthenticationPrincipal JwtUser u,@RequestParam Map<String,String> f,@RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="30") int size) {
Map<String,String> scoped=new HashMap<>(f); Class<? extends BaseEntity> type=type(r.getRequestURI());
if(type!=BbsPost.class && u.getCompanyId()!=null) scoped.put("outcompanyId",u.getCompanyId().toString());
return ApiResponse.ok(crud.list(type,u,scoped,page,size));
}
@GetMapping("/company") ApiResponse company(@AuthenticationPrincipal JwtUser u) {
if(u.getCompanyId()==null) return ApiResponse.fail("소속 매장이 없습니다."); return ApiResponse.ok(crud.get(Company.class,u.getCompanyId(),u));
}
@PutMapping("/company") ApiResponse companyPut(@AuthenticationPrincipal JwtUser u,@RequestBody Map<String,Object> body) {
if(u.getCompanyId()==null) return ApiResponse.fail("소속 매장이 없습니다."); return ApiResponse.ok(crud.update(Company.class,u.getCompanyId(),body,u));
}
@PostMapping("/scans") ApiResponse scan(@AuthenticationPrincipal JwtUser u,@RequestBody Map<String,Object> body) {
body.put("outcompanyId",u.getCompanyId()); return ApiResponse.ok(crud.create(ScanDoc.class,body,u));
}
@PutMapping("/scans/{id}") ApiResponse scanPut(@AuthenticationPrincipal JwtUser u,@PathVariable Long id,@RequestBody Map<String,Object> body) { return ApiResponse.ok(crud.update(ScanDoc.class,id,body,u)); }
@SuppressWarnings("unchecked") private <T extends BaseEntity> Class<T> type(String uri) {
if(uri.contains("stocks"))return (Class<T>)Stock.class;if(uri.contains("opens"))return (Class<T>)OpenRecord.class;if(uri.contains("settlements"))return (Class<T>)SettlementMonth.class;
if(uri.contains("fareboxes"))return (Class<T>)Farebox.class;if(uri.contains("indocs"))return (Class<T>)IncompleteDoc.class;if(uri.contains("returns"))return (Class<T>)UnreturnedPhone.class;if(uri.contains("scans"))return (Class<T>)ScanDoc.class;return (Class<T>)BbsPost.class;
}
}
@@ -0,0 +1 @@
spring.application.name=agent
@@ -0,0 +1,30 @@
spring:
datasource:
url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3308/billcare?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8}
username: ${SPRING_DATASOURCE_USERNAME:billcare}
password: ${SPRING_DATASOURCE_PASSWORD:billcare}
jpa:
hibernate:
ddl-auto: update
open-in-view: false
properties:
hibernate:
format_sql: true
jackson:
time-zone: Asia/Seoul
servlet:
multipart:
max-file-size: 20MB
max-request-size: 25MB
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration
server:
port: 8080
app:
jwt:
secret: ${APP_JWT_SECRET:billcare-bizcare-homes-secret-key-must-be-long-enough-256bits}
expiration-ms: ${APP_JWT_EXPIRATION_MS:86400000}
cors:
origins: ${APP_CORS_ORIGINS:http://localhost:5174,http://localhost:82,http://127.0.0.1:82}
upload-dir: ${APP_UPLOAD_DIR:./storage/uploads}
@@ -0,0 +1,13 @@
package com.bizcare.agent;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class AgentApplicationTests {
@Test
void contextLoads() {
}
}