From a44d2909b584a0d8c9a561ad44a9ecb6cb31386a Mon Sep 17 00:00:00 2001 From: Macbook Date: Sun, 12 Jul 2026 05:12:36 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EA=B9=A8=EC=A7=80?= =?UTF-8?q?=EB=8A=94=20=EC=A4=91=EB=B3=B5=20com.poncle=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 배포 시 Maven 컴파일이 실패하지 않도록 불완전한 ApiControllers 사본을 제거합니다. Co-authored-by: Cursor --- .../java/com/poncle/agent/ApiControllers.java | 4738 ----------------- 1 file changed, 4738 deletions(-) delete mode 100644 backend/src/main/java/com/poncle/agent/ApiControllers.java diff --git a/backend/src/main/java/com/poncle/agent/ApiControllers.java b/backend/src/main/java/com/poncle/agent/ApiControllers.java deleted file mode 100644 index 0990ee8..0000000 --- a/backend/src/main/java/com/poncle/agent/ApiControllers.java +++ /dev/null @@ -1,4738 +0,0 @@ -package com.poncle.agent; - -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.persistence.*; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; -import lombok.*; -import org.springframework.http.*; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.*; -import jakarta.servlet.http.HttpServletRequest; -import java.lang.reflect.Field; -import java.math.BigDecimal; -import java.time.*; -import java.util.*; - -record ApiResponse(boolean result, String msg, Object data) { static ApiResponse ok(Object d){return new ApiResponse(true,"성공",d);} static ApiResponse fail(String m){return new ApiResponse(false,m,null);} } -record PageResponse(long total, List list) {} -record LoginRequest(@NotBlank(message="아이디를 입력하세요") String userid, @NotBlank(message="비밀번호를 입력하세요") String password) {} - -@RestController @RequestMapping("/api/auth") @RequiredArgsConstructor -class AuthController { - private final EntityManager em; private final PasswordEncoder encoder; private final JwtService jwt; - @PostMapping("/login") @Transactional ApiResponse login(@Valid @RequestBody LoginRequest request) { - List users=em.createQuery("select m from Member m where m.userid=:id",Member.class).setParameter("id",request.userid()).getResultList(); - if(users.isEmpty() || !users.getFirst().isActive() || !encoder.matches(request.password(),users.getFirst().getPassword())) return ApiResponse.fail("아이디 또는 비밀번호가 올바르지 않습니다."); - Member m=users.getFirst(); - m.setLoginCount((m.getLoginCount()==null?0:m.getLoginCount())+1); - m.setLastLoginAt(LocalDateTime.now()); - Map data=new LinkedHashMap<>(); - data.put("token",jwt.create(m)); - data.put("role",m.getRole()); - data.put("userid",m.getUserid()); - data.put("name",m.getName()); - data.put("groupId",m.getGroupId()); - data.put("companyId",m.getCompanyId()); - data.put("menuFlags",m.getMenuFlags()); - String perm = m.getStaffPermission(); - if (perm == null || perm.isBlank()) { - perm = "demo".equals(m.getUserid()) ? "대표" : "사원"; - } - data.put("staffPermission", perm); - return ApiResponse.ok(data); - } - @GetMapping("/me") ApiResponse me(@AuthenticationPrincipal JwtUser u) { - List users = em.createQuery("select m from Member m where m.userid=:id", Member.class) - .setParameter("id", u.getUserid()).getResultList(); - Map data=new LinkedHashMap<>(); - data.put("userid",u.getUserid()); - data.put("role",u.getRole()); - data.put("groupId",u.getGroupId()); - data.put("companyId",u.getCompanyId()); - if (!users.isEmpty()) { - Member m = users.getFirst(); - data.put("name", m.getName()); - String perm = m.getStaffPermission(); - if (perm == null || perm.isBlank()) { - perm = "demo".equals(m.getUserid()) ? "대표" : "사원"; - } - data.put("staffPermission", perm); - data.put("menuFlags", m.getMenuFlags()); - } - return ApiResponse.ok(data); - } -} - -@Service @RequiredArgsConstructor -class CrudService { - private final EntityManager em; private final ObjectMapper mapper; - @Transactional T create(Class type, Map data, JwtUser user) { - T value=mapper.convertValue(data,type); value.setId(null); value.setGroupId(user.getGroupId()); em.persist(value); return value; - } - @Transactional T update(Class type, Long id, Map data, JwtUser user) { - T value=get(type,id,user); - try { mapper.readerForUpdating(value).readValue(mapper.writeValueAsBytes(data)); } - catch (Exception e) { throw new IllegalArgumentException("입력 형식이 올바르지 않습니다."); } - value.setId(id); value.setGroupId(user.getGroupId()); return value; - } - @Transactional void delete(Class type, Long id, JwtUser user) { em.remove(get(type,id,user)); } - T get(Class type, Long id, JwtUser user) { - T v=em.find(type,id); if(v==null || !Objects.equals(v.getGroupId(),user.getGroupId())) throw new NoSuchElementException("데이터를 찾을 수 없습니다."); return v; - } - PageResponse list(Class type, JwtUser user, Map filters, int page, int size) { - String entity=type.getSimpleName(); StringBuilder where=new StringBuilder(" where e.groupId=:group"); Map params=new HashMap<>(); params.put("group",user.getGroupId()); - Set ignore=Set.of("page","size","keyword","startDate","endDate","q"); - for(var e:filters.entrySet()) if(e.getValue()!=null && !e.getValue().isBlank() && !ignore.contains(e.getKey()) && field(type,e.getKey())) { where.append(" and e.").append(e.getKey()).append("=:").append(e.getKey()); params.put(e.getKey(), convert(e.getValue(), type,e.getKey())); } - TypedQuery q=em.createQuery("select e from "+entity+" e"+where+" order by e.id desc",type); TypedQuery c=em.createQuery("select count(e) from "+entity+" e"+where,Long.class); - params.forEach((k,v)->{q.setParameter(k,v);c.setParameter(k,v);}); long total=c.getSingleResult(); return new PageResponse(total,q.setFirstResult(Math.max(0,page)*size).setMaxResults(Math.min(Math.max(size,1),200)).getResultList()); - } - private boolean field(Class t,String name){ try { while(t!=null){for(Field f:t.getDeclaredFields())if(f.getName().equals(name))return true;t=t.getSuperclass();} return false;}catch(Exception e){return false;} } - private Object convert(String v,Class t,String f){try{Field x=null;for(Class c=t;c!=null;c=c.getSuperclass())try{x=c.getDeclaredField(f);break;}catch(NoSuchFieldException ignored){} if(x!=null&&x.getType()==Long.class)return Long.valueOf(v); if(x!=null&&x.getType()==LocalDate.class)return LocalDate.parse(v);}catch(Exception ignored){}return v;} -} - -@RestController @RequestMapping("/api/agent") @RequiredArgsConstructor -class AgentController { - private final CrudService crud; private final EntityManager em; private final PasswordEncoder encoder; - private ApiResponse list(Class c, JwtUser u, Map f,int p,int s){return ApiResponse.ok(crud.list(c,u,f,p,s));} - private ApiResponse post(Class c,Mapd,JwtUser u){return ApiResponse.ok(crud.create(c,d,u));} - private ApiResponse get(Class c,Long id,JwtUser u){return ApiResponse.ok(crud.get(c,id,u));} - private ApiResponse put(Class c,Long id,Mapd,JwtUser u){return ApiResponse.ok(crud.update(c,id,d,u));} - private ApiResponse del(Class c,Long id,JwtUser u){crud.delete(c,id,u);return ApiResponse.ok(null);} - @GetMapping("/stocks") ApiResponse stocks(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="30")int size){return list(Stock.class,u,f,page,size);} - @PostMapping({"/stocks","/stocks/in"}) @Transactional ApiResponse stockIn(@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ - d.putIfAbsent("state","IN"); - Stock s=crud.create(Stock.class,d,u); - if(s.getIndate()==null) s.setIndate(LocalDate.now()); - if(s.getProcessDate()==null) s.setProcessDate(LocalDate.now()); - s.setProcessor(memberName(u)); - s.setUserid(u.getUserid()); - s.setProcid(u.getUserid()); - if(s.getUid()==null||s.getUid().isBlank()) s.setUid("stk-"+UUID.randomUUID()); - writeStockHistory(s,u); - return ApiResponse.ok(stockDetail(s,u)); - } - @GetMapping("/stocks/{id}") ApiResponse stock(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ - Stock s=em.find(Stock.class,id); - if(s==null||!Objects.equals(s.getGroupId(),u.getGroupId())) return ApiResponse.fail("재고를 찾을 수 없습니다."); - return ApiResponse.ok(stockDetail(s,u)); - } - @GetMapping("/stocks/{id}/detail") ApiResponse stockDetailApi(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ - return stock(id,u); - } - @PutMapping("/stocks/{id}") @Transactional ApiResponse stockPut(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ - Stock s=em.find(Stock.class,id); - if(s==null||!Objects.equals(s.getGroupId(),u.getGroupId())) return ApiResponse.fail("재고를 찾을 수 없습니다."); - put(Stock.class,id,d,u); - s=em.find(Stock.class,id); - if(s!=null){ - if(d.get("processor")==null || String.valueOf(d.get("processor")).isBlank()) s.setProcessor(memberName(u)); - writeStockHistory(s,u); - } - return ApiResponse.ok(stockDetail(s,u)); - } - @DeleteMapping("/stocks/{id}") @Transactional ApiResponse stockDel(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ - Stock s=em.find(Stock.class,id); - if(s==null||!Objects.equals(s.getGroupId(),u.getGroupId())) return ApiResponse.fail("재고를 찾을 수 없습니다."); - em.createQuery("delete from StockHistory h where h.groupId=:g and h.stockId=:id").setParameter("g",u.getGroupId()).setParameter("id",id).executeUpdate(); - em.createQuery("delete from StockNote n where n.groupId=:g and n.stockId=:id").setParameter("g",u.getGroupId()).setParameter("id",id).executeUpdate(); - em.remove(s); - return ApiResponse.ok(null); - } - @PostMapping({"/stocks/out","/stocks/move","/stocks/return"}) @Transactional ApiResponse stockState(@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ - Long id=Long.valueOf(d.get("id").toString()); - return stockAction(id,d,u); - } - @PostMapping("/stocks/{id}/action") @Transactional ApiResponse stockActionApi(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ - return stockAction(id,d,u); - } - @PostMapping("/stocks/{id}/notes") @Transactional ApiResponse stockNoteCreate(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ - Stock s=em.find(Stock.class,id); - if(s==null||!Objects.equals(s.getGroupId(),u.getGroupId())) return ApiResponse.fail("재고를 찾을 수 없습니다."); - String contents=d.get("contents")==null?"":String.valueOf(d.get("contents")).trim(); - if(contents.isBlank()) return ApiResponse.fail("메모 내용을 입력하세요."); - StockNote n=new StockNote(); - n.setGroupId(u.getGroupId()); - n.setStockId(id); - n.setContents(contents); - n.setAuthorName(d.get("authorName")==null||String.valueOf(d.get("authorName")).isBlank()?memberName(u):String.valueOf(d.get("authorName"))); - n.setNoteDate(d.get("noteDate")==null||String.valueOf(d.get("noteDate")).isBlank()?LocalDate.now():LocalDate.parse(String.valueOf(d.get("noteDate")))); - em.persist(n); - return ApiResponse.ok(stockNoteMap(n)); - } - @DeleteMapping("/stocks/{id}/notes/{noteId}") @Transactional ApiResponse stockNoteDelete(@PathVariable Long id,@PathVariable Long noteId,@AuthenticationPrincipal JwtUser u){ - StockNote n=em.find(StockNote.class,noteId); - if(n==null||!Objects.equals(n.getGroupId(),u.getGroupId())||!Objects.equals(n.getStockId(),id)) return ApiResponse.fail("메모를 찾을 수 없습니다."); - em.remove(n); - return ApiResponse.ok(null); - } - private ApiResponse stockAction(Long id, Map d, JwtUser u){ - Stock s=em.find(Stock.class,id); - if(s==null||!Objects.equals(s.getGroupId(),u.getGroupId())) return ApiResponse.fail("재고를 찾을 수 없습니다."); - String state=String.valueOf(d.getOrDefault("state", d.containsKey("state")?d.get("state"):"OUT")); - if(!List.of("IN","OUT","LOSS","OPEN","SELL","RECOVERY","RETURN").contains(state)) return ApiResponse.fail("처리할 상태를 확인하세요."); - if("OUT".equals(state)){ - if(d.get("outcompanyId")!=null && !String.valueOf(d.get("outcompanyId")).isBlank()){ - s.setOutcompanyId(Long.valueOf(d.get("outcompanyId").toString())); - } - if(d.get("outCategory")!=null) s.setOutCategory(String.valueOf(d.get("outCategory"))); - if(s.getOutcompanyId()==null) return ApiResponse.fail("출고처를 선택하세요."); - s.setOutdate(LocalDate.now()); - s.setOutcode("21"); - } - if("IN".equals(state) || "RECOVERY".equals(state)){ - s.setRecalldate(LocalDate.now()); - s.setOutcode("10"); - if("IN".equals(state)) { /* 회수 후 입고 상태 */ } - } - if("LOSS".equals(state)) s.setLossdate(LocalDate.now()); - if("SELL".equals(state)) s.setSelldate(LocalDate.now()); - s.setState(state); - s.setProcessDate(LocalDate.now()); - s.setProcessor(memberName(u)); - if(d.get("memo")!=null) s.setMemo(String.valueOf(d.get("memo"))); - writeStockHistory(s,u); - return ApiResponse.ok(stockDetail(s,u)); - } - private Map stockDetail(Stock s, JwtUser u){ - Map companyMap=new HashMap<>(); - for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companyMap.put(c.getId(),c); - Map names=new HashMap<>(); - companyMap.forEach((k,v)->names.put(k,v.getName())); - Map stock=stockMap(s,names); - stock.put("pay", s.getPay()); - stock.put("telcompany", s.getTelcompany()); - String telecomLabel=s.getTelcompany(); - if(telecomLabel==null||telecomLabel.isBlank()) telecomLabel=s.getTelecom(); - else if(s.getTelecom()!=null && !telecomLabel.contains(s.getTelecom())) telecomLabel=telecomLabel+" ("+s.getTelecom()+")"; - stock.put("telecomLabel", telecomLabel); - List histories=em.createQuery("select h from StockHistory h where h.groupId=:g and h.stockId=:id order by h.processDate desc, h.id desc",StockHistory.class) - .setParameter("g",u.getGroupId()).setParameter("id",s.getId()).getResultList(); - List> historyList=histories.stream().map(h->{ - Map m=new LinkedHashMap<>(); - m.put("id", h.getId()); - m.put("processDate", h.getProcessDate()); - m.put("statusLabel", h.getStatusLabel()!=null?h.getStatusLabel():statusLabel(h.getState())); - String label=h.getCompanyLabel(); - if(label==null||label.isBlank()){ - if("OUT".equals(h.getState())||"출고".equals(h.getStatusLabel())){ - Company oc=companyMap.get(h.getOutcompanyId()); - if(oc!=null){ - label=oc.getName(); - if(oc.getCategory()!=null && !oc.getCategory().isBlank()) label=label+" ("+oc.getCategory()+")"; - else if(s.getOutCategory()!=null && !s.getOutCategory().isBlank()) label=label+" ("+s.getOutCategory()+")"; - } - } - } - // 입고 이력은 원본처럼 거래처 칸을 비움 - if("IN".equals(h.getState())||"입고".equals(h.getStatusLabel())) label=""; - m.put("companyLabel", label==null?"":label); - m.put("processor", resolveProcessorName(u.getGroupId(), h.getProcessor())); - return m; - }).toList(); - List> notes=em.createQuery("select n from StockNote n where n.groupId=:g and n.stockId=:id order by n.noteDate desc, n.id desc",StockNote.class) - .setParameter("g",u.getGroupId()).setParameter("id",s.getId()).getResultList().stream().map(this::stockNoteMap).toList(); - Map r=new LinkedHashMap<>(); - r.put("stock", stock); - r.put("histories", historyList); - r.put("notes", notes); - return r; - } - private Map stockNoteMap(StockNote n){ - Map m=new LinkedHashMap<>(); - m.put("id", n.getId()); m.put("stockId", n.getStockId()); m.put("contents", n.getContents()); - m.put("authorName", n.getAuthorName()); m.put("noteDate", n.getNoteDate()); - return m; - } - private String memberName(JwtUser u){ - List rows=em.createQuery("select m from Member m where m.groupId=:g and m.userid=:uid",Member.class) - .setParameter("g",u.getGroupId()).setParameter("uid",u.getUserid()).getResultList(); - if(!rows.isEmpty() && rows.getFirst().getName()!=null && !rows.getFirst().getName().isBlank()) return rows.getFirst().getName(); - return u.getUserid(); - } - private String resolveProcessorName(String groupId, String processor){ - if(processor==null||processor.isBlank()) return "-"; - List rows=em.createQuery("select m from Member m where m.groupId=:g and (m.userid=:p or m.name=:p)",Member.class) - .setParameter("g",groupId).setParameter("p",processor).getResultList(); - if(!rows.isEmpty() && rows.getFirst().getName()!=null && !rows.getFirst().getName().isBlank()) return rows.getFirst().getName(); - return processor; - } - @GetMapping("/stocks/barcode/{code}") ApiResponse barcode(@PathVariable String code,@AuthenticationPrincipal JwtUser u){return list(Stock.class,u,Map.of("barcode",code),0,10);} - @GetMapping("/stocks/search") ApiResponse stockSearch(@AuthenticationPrincipal JwtUser u, @RequestParam Map f, - @RequestParam(defaultValue="0") int page, @RequestParam(defaultValue="15") int size) { - return ApiResponse.ok(stockSearchData(u, f, page, size)); - } - @GetMapping("/stocks/sheet") ApiResponse stockSheet(@AuthenticationPrincipal JwtUser u, @RequestParam Map f, - @RequestParam(defaultValue="0") int page, @RequestParam(defaultValue="15") int size) { - if(blank(f.get("dateFrom")) || blank(f.get("dateTo"))) return ApiResponse.fail("날짜를 선택하세요."); - Map filters=new HashMap<>(f); - filters.put("dateBasis","처리일기준"); - filters.putIfAbsent("tab","ALL"); - return ApiResponse.ok(stockSearchData(u, filters, page, size)); - } - @GetMapping(value="/stocks/sheet/export", produces="text/csv;charset=UTF-8") ResponseEntity stockSheetExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { - if(blank(f.get("dateFrom")) || blank(f.get("dateTo"))) return ResponseEntity.badRequest().body("날짜를 선택하세요."); - Map filters=new HashMap<>(f); - filters.put("dateBasis","처리일기준"); - filters.putIfAbsent("tab","ALL"); - @SuppressWarnings("unchecked") List> rows=(List>) stockSearchData(u,filters,0,10000).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF처리일,현황,상태,종류,일련번호,모델명,색상,입고가,입고처,출고처,처리자,비고\n"); - for(Map r:rows) csv.append(csv(r.get("processDate"))).append(',').append(csv(r.get("statusLabel"))).append(',') - .append(csv(r.get("cond"))).append(',').append(csv(r.get("gubun"))).append(',').append(csv(r.get("serial"))).append(',') - .append(csv(r.get("model"))).append(',').append(csv(r.get("color"))).append(',').append(csv(r.get("inprice"))).append(',') - .append(csv(r.get("incompanyName"))).append(',').append(csv(r.get("outcompanyName"))).append(',') - .append(csv(r.get("processor"))).append(',').append(csv(r.get("memo"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=sheet.csv").body(csv.toString()); - } - @GetMapping(value="/stocks/export", produces="text/csv;charset=UTF-8") ResponseEntity stockExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { - @SuppressWarnings("unchecked") List> rows=(List>) stockSearchData(u,f,0,10000).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF현황,입고일,상태,종류,일련번호,모델명,색상,입고가,입고처,출고처,처리일,처리자,비고\n"); - for(Map r:rows) csv.append(csv(r.get("statusLabel"))).append(',').append(csv(r.get("indate"))).append(',') - .append(csv(r.get("cond"))).append(',').append(csv(r.get("gubun"))).append(',').append(csv(r.get("serial"))).append(',') - .append(csv(r.get("model"))).append(',').append(csv(r.get("color"))).append(',').append(csv(r.get("inprice"))).append(',') - .append(csv(r.get("incompanyName"))).append(',').append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("processDate"))).append(',') - .append(csv(r.get("processor"))).append(',').append(csv(r.get("memo"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=stocks.csv").body(csv.toString()); - } - @PostMapping("/stocks/bulk-action") @Transactional ApiResponse stockBulkAction(@AuthenticationPrincipal JwtUser u,@RequestBody Map d) { - Object raw=d.get("ids"); String state=String.valueOf(d.getOrDefault("state","")); - if(!(raw instanceof List ids) || ids.isEmpty()) return ApiResponse.fail("선택된 재고가 없습니다."); - if(!List.of("IN","OUT","LOSS","OPEN","SELL","RECOVERY","RETURN").contains(state)) return ApiResponse.fail("처리할 상태를 확인하세요."); - int updated=0; - for(Object idObj:ids) { - Long id=Long.valueOf(idObj.toString()); - Stock s=em.find(Stock.class,id); - if(s==null || !Objects.equals(s.getGroupId(),u.getGroupId())) continue; - s.setState(state); - s.setProcessDate(LocalDate.now()); - s.setProcessor(u.getUserid()); - if("IN".equals(state)) { s.setRecalldate(LocalDate.now()); s.setOutcode("10"); } - if("OUT".equals(state)) { s.setOutdate(LocalDate.now()); s.setOutcode("21"); } - if("LOSS".equals(state)) s.setLossdate(LocalDate.now()); - if("SELL".equals(state)) s.setSelldate(LocalDate.now()); - writeStockHistory(s, u); - updated++; - } - return ApiResponse.ok(Map.of("updated",updated)); - } - @PostMapping("/stocks/bulk-in") @Transactional ApiResponse stockBulkIn(@AuthenticationPrincipal JwtUser u,@RequestBody Map d) { - Object raw=d.get("serials"); if(!(raw instanceof List serials) || serials.isEmpty()) return ApiResponse.fail("일련번호를 입력하세요."); - int made=0; - for(Object value:serials) { String serial=String.valueOf(value).trim(); if(serial.isBlank()) continue; - Stock s=new Stock(); s.setGroupId(u.getGroupId()); s.setGubun(String.valueOf(d.getOrDefault("gubun","휴대폰"))); - s.setTelecom(String.valueOf(d.getOrDefault("telecom","SKT"))); s.setTelcompany(s.getTelecom()); s.setModel(String.valueOf(d.getOrDefault("model",""))); - s.setColor(String.valueOf(d.getOrDefault("color",""))); s.setInprice(d.get("inprice")==null?BigDecimal.ZERO:new BigDecimal(d.get("inprice").toString())); - if(d.get("incompanyId")!=null && !d.get("incompanyId").toString().isBlank()) s.setIncompanyId(Long.valueOf(d.get("incompanyId").toString())); - s.setSerial(serial); s.setBarcode(serial); s.setCond("정상"); s.setState("IN"); s.setIndate(LocalDate.now()); s.setProcessDate(LocalDate.now()); - s.setProcessor(u.getUserid()); s.setUserid(u.getUserid()); s.setProcid(u.getUserid()); s.setUid("stk-"+UUID.randomUUID()); em.persist(s); - writeStockHistory(s, u); made++; - } return ApiResponse.ok(Map.of("created",made)); - } - private Map stockSearchData(JwtUser u,Map f,int page,int size) { - List stocks=em.createQuery("select s from Stock s where s.groupId=:g",Stock.class).setParameter("g",u.getGroupId()).getResultList(); - Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); - List filtered=stocks.stream().filter(s->matchesStock(s,f,companies)).sorted(stockComparator(f.get("sort"))).toList(); - Map counts=new LinkedHashMap<>(); for(String tab:List.of("ALL","INOUT","IN","OUT","RETURN","LOSS","OPEN","SELL")) counts.put(tab,stocks.stream().filter(s->matchesStock(s,withoutTab(f),companies)&&inTab(s,tab)).count()); - int from=Math.min(Math.max(page,0)*Math.max(size,1),filtered.size()), to=Math.min(from+Math.min(Math.max(size,1),200),filtered.size()); - List> list=filtered.subList(from,to).stream().map(s->stockMap(s,companies)).toList(); - return Map.of("total",filtered.size(),"list",list,"counts",counts); - } - private Map withoutTab(Map f){Map copy=new HashMap<>(f);copy.remove("tab");return copy;} - private boolean matchesStock(Stock s,Map f,Map companies) { - String tab=f.getOrDefault("tab","ALL"); if(!inTab(s,tab)) return false; - if(!eq(s.getCond(),f.get("cond"))||!eq(s.getGubun(),f.get("gubun"))||!eq(s.getTelecom(),f.get("telecom"))||!eq(s.getOutCategory(),f.get("outCategory"))||!eq(s.getSalesperson(),f.get("salesperson"))||!eq(s.getSource(),f.get("source"))) return false; - if(!eqId(s.getIncompanyId(),f.get("incompanyId"))) return false; - if(!like(s.getModel(),f.get("model"))||!like(s.getColor(),f.get("color"))||!like(s.getPetname(),f.get("petname"))||!like(companies.get(s.getOutcompanyId()),f.get("outcompanyName"))) return false; - String serial=f.get("serial"); if(serial!=null&&!serial.isBlank()) {String[] values=("true".equals(f.get("serialMulti"))?serial.split("[,\\n\\r]+"):new String[]{serial});if(Arrays.stream(values).map(String::trim).noneMatch(v->like(s.getSerial(),v)))return false;} - String searchType=f.getOrDefault("searchType","일련번호"); String keyword=f.get("keyword"); - if(keyword!=null&&!keyword.isBlank()) { - boolean ok=switch(searchType){ - case "모델명"->like(s.getModel(),keyword); - case "색상"->like(s.getColor(),keyword); - case "펫네임"->like(s.getPetname(),keyword); - default->like(s.getSerial(),keyword); - }; - if(!ok) return false; - } - LocalDate date=switch(f.getOrDefault("dateBasis","입고일기준")){case "출고일기준"->s.getOutdate();case "처리일기준"->s.getProcessDate();default->s.getIndate();}; - if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(date==null||date.isBefore(LocalDate.parse(f.get("dateFrom")))))return false; - return f.get("dateTo")==null||f.get("dateTo").isBlank()||(date!=null&&!date.isAfter(LocalDate.parse(f.get("dateTo")))); - } - private boolean inTab(Stock s,String tab){return switch(tab){case "INOUT"->"IN".equals(s.getState())||"OUT".equals(s.getState());case "ALL"->true;default->tab.equals(s.getState());};} - private boolean blank(String v){return v==null||v.isBlank();} - private boolean eq(String actual,String expected){return expected==null||expected.isBlank()||Objects.equals(actual,expected);} - private boolean eqId(Long actual,String expected){return expected==null||expected.isBlank()||Objects.equals(actual,Long.valueOf(expected));} - private boolean like(String actual,String expected){return expected==null||expected.isBlank()||(actual!=null&&actual.toLowerCase().contains(expected.toLowerCase()));} - private Comparator stockComparator(String sort){String field=sort==null?"indate,desc":sort;boolean asc=field.endsWith(",asc");Comparator c=switch(field.split(",")[0]){case "serial"->Comparator.comparing(Stock::getSerial,Comparator.nullsLast(String::compareTo));case "model"->Comparator.comparing(Stock::getModel,Comparator.nullsLast(String::compareTo));case "inprice"->Comparator.comparing(Stock::getInprice,Comparator.nullsLast(BigDecimal::compareTo));case "processDate"->Comparator.comparing(Stock::getProcessDate,Comparator.nullsLast(LocalDate::compareTo));default->Comparator.comparing(Stock::getIndate,Comparator.nullsLast(LocalDate::compareTo));};return (asc?c:c.reversed()).thenComparing(Stock::getId); } - private Map stockMap(Stock s,Map companies){Map r=new LinkedHashMap<>();r.put("id",s.getId());r.put("gubun",s.getGubun());r.put("telecom",s.getTelecom());r.put("telcompany",s.getTelcompany());r.put("serial",s.getSerial());r.put("model",s.getModel());r.put("color",s.getColor());r.put("cond",s.getCond());r.put("pay",s.getPay());r.put("memo",s.getMemo());r.put("state",s.getState());r.put("inprice",s.getInprice());r.put("indate",s.getIndate());r.put("outdate",s.getOutdate());r.put("processDate",s.getProcessDate());r.put("processor",s.getProcessor());r.put("petname",s.getPetname());r.put("outCategory",s.getOutCategory());r.put("salesperson",s.getSalesperson());r.put("source",s.getSource());r.put("incompanyId",s.getIncompanyId());r.put("outcompanyId",s.getOutcompanyId());r.put("incompanyName",companies.getOrDefault(s.getIncompanyId(),"-"));r.put("outcompanyName",companies.getOrDefault(s.getOutcompanyId(),"-"));r.put("statusLabel",statusLabel(s.getState()));r.put("elapsedDays",s.getIndate()==null?null:Math.max(0,java.time.temporal.ChronoUnit.DAYS.between(s.getIndate(),LocalDate.now())));return r;} - private String statusLabel(String state){return Map.of("IN","입고","OUT","출고","RETURN","반품","LOSS","분실","OPEN","개통","SELL","판매","RECOVERY","회수대기").getOrDefault(state,state==null?"-":state);} - private String csv(Object value){String s=String.valueOf(value==null?"":value);return "\""+s.replace("\"","\"\"")+"\"";} - @GetMapping("/opens") ApiResponse opens(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="30")int size){return list(OpenRecord.class,u,f,page,size);} - @GetMapping("/opens/search") ApiResponse openSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){return ApiResponse.ok(openSearchData(u,f,page,size));} - @GetMapping(value="/opens/export", produces="text/csv;charset=UTF-8") ResponseEntity openExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f) { - @SuppressWarnings("unchecked") List> rows=(List>) openSearchData(u,f,0,10000).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF개통일,통신사,출고처,고객명,구분,종류,개통번호,상태,모델명,일련번호,입고처,정산,차감,마진\n"); - for(Map r:rows) csv.append(csv(r.get("opendate"))).append(',').append(csv(r.get("telecom"))).append(',').append(csv(r.get("outcompanyName"))).append(',') - .append(csv(r.get("name"))).append(',').append(csv(r.get("gubun"))).append(',').append(csv(r.get("openhow"))).append(',').append(csv(r.get("openphone"))).append(',') - .append(csv(r.get("openStatus"))).append(',').append(csv(r.get("pmodel"))).append(',').append(csv(r.get("pserial"))).append(',').append(csv(r.get("incompanyName"))).append(',') - .append(csv(r.get("sellprice"))).append(',').append(csv(r.get("declprice1"))).append(',').append(csv(r.get("dealermargin1"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=opens.csv").body(csv.toString()); - } - @PostMapping("/opens") @Transactional ApiResponse openPost(@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ - d.putIfAbsent("state","OPEN"); d.putIfAbsent("openStatus","정상"); d.putIfAbsent("userid",u.getUserid()); - if(d.get("uid")==null||String.valueOf(d.get("uid")).isBlank()) d.put("uid","open-"+UUID.randomUUID()); - ApiResponse created=post(OpenRecord.class,d,u); - linkOpenStock(d); return created; - } - @GetMapping({"/opens/pending","/opens/miss"}) ApiResponse pending(@AuthenticationPrincipal JwtUser u){return list(OpenRecord.class,u,Map.of("state","PENDING"),0,100);} - @GetMapping("/opens/serial-lookup") ApiResponse openSerialLookup(@AuthenticationPrincipal JwtUser u,@RequestParam String serial,@RequestParam(defaultValue="휴대폰") String gubun){ - if(serial==null||serial.trim().length()<3) return ApiResponse.ok(List.of()); - Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); - List stocks=em.createQuery("select s from Stock s where s.groupId=:g and s.serial like :q",Stock.class).setParameter("g",u.getGroupId()).setParameter("q","%"+serial.trim()+"%").setMaxResults(20).getResultList(); - return ApiResponse.ok(stocks.stream().filter(s->blank(gubun)||("유심".equals(gubun)? "유심".equals(s.getGubun()):!"유심".equals(s.getGubun()))).map(s->stockMap(s,companies)).toList()); - } - @GetMapping("/opens/adjust") ApiResponse openAdjust(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - if(blank(f.get("dateFrom"))||blank(f.get("dateTo"))) return ApiResponse.fail("개통일을 선택하세요."); - LocalDate from=LocalDate.parse(f.get("dateFrom")), to=LocalDate.parse(f.get("dateTo")); - if(to.isBefore(from)) return ApiResponse.fail("날짜 범위가 올바르지 않습니다."); - if(java.time.temporal.ChronoUnit.DAYS.between(from,to)>30) return ApiResponse.fail("최대 31일까지 조회할 수 있습니다."); - return ApiResponse.ok(openAdjustData(u,f)); - } - @PostMapping("/opens/adjust/batch") @Transactional ApiResponse openAdjustBatch(@AuthenticationPrincipal JwtUser u,@RequestBody Map body){ - Object raw=body.get("items"); if(!(raw instanceof List items)||items.isEmpty()) return ApiResponse.fail("변경할 항목이 없습니다."); - int updated=0; - for(Object item:items){ - if(!(item instanceof Map m)) continue; - Object idObj=m.get("id"); if(idObj==null) continue; - OpenRecord o=em.find(OpenRecord.class,Long.valueOf(String.valueOf(idObj))); - if(o==null||!Objects.equals(o.getGroupId(),u.getGroupId())) continue; - if("유심".equals(o.getGubun())) continue; - if(m.containsKey("basicprice1")) o.setBasicprice1(toDecimal(m.get("basicprice1"))); - if(m.containsKey("basicprice2")) o.setBasicprice2(toDecimal(m.get("basicprice2"))); - if(m.containsKey("basicprice3")) o.setBasicprice3(toDecimal(m.get("basicprice3"))); - updated++; - } - return ApiResponse.ok(Map.of("updated",updated)); - } - @GetMapping(value="/opens/adjust/export", produces="text/csv;charset=UTF-8") ResponseEntity openAdjustExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - if(blank(f.get("dateFrom"))||blank(f.get("dateTo"))) return ResponseEntity.badRequest().body("개통일을 선택하세요."); - @SuppressWarnings("unchecked") List> rows=(List>) openAdjustData(u,f).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF개통일,입고처,출고처,고객명,종류,모델명,일련번호,지원방법,요금제,정산금액,기본정책,구두정책,추가정책\n"); - for(Map r:rows) csv.append(csv(r.get("opendate"))).append(',').append(csv(r.get("incompanyName"))).append(',').append(csv(r.get("outcompanyName"))).append(',') - .append(csv(r.get("name"))).append(',').append(csv(r.get("openhow"))).append(',').append(csv(r.get("pmodel"))).append(',').append(csv(r.get("pserial"))).append(',') - .append(csv(r.get("decltype"))).append(',').append(csv(r.get("plan"))).append(',').append(csv(r.get("sellprice"))).append(',') - .append(csv(r.get("basicprice1"))).append(',').append(csv(r.get("basicprice2"))).append(',').append(csv(r.get("basicprice3"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=adjust.csv").body(csv.toString()); - } - @GetMapping("/opens/{id}") ApiResponse open(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return get(OpenRecord.class,id,u);} - @PutMapping("/opens/{id}") ApiResponse openPut(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return put(OpenRecord.class,id,d,u);} - @DeleteMapping("/opens/{id}") ApiResponse openDel(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return del(OpenRecord.class,id,u);} - private void linkOpenStock(Map d){ - Object phoneId=d.get("phoneStockId"), usimId=d.get("usimStockId"); - if(phoneId!=null){Stock s=em.find(Stock.class,Long.valueOf(String.valueOf(phoneId))); if(s!=null){s.setState("OPEN");s.setOutdate(LocalDate.now());}} - if(usimId!=null){Stock s=em.find(Stock.class,Long.valueOf(String.valueOf(usimId))); if(s!=null){s.setState("OPEN");s.setOutdate(LocalDate.now());}} - } - private Map openSearchData(JwtUser u,Map f,int page,int size){ - List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g",OpenRecord.class).setParameter("g",u.getGroupId()).getResultList(); - Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); - List filtered=opens.stream().filter(o->matchesOpen(o,f,companies)).sorted(openComparator(f.get("sort"))).toList(); - Map counts=new LinkedHashMap<>(); for(String tab:List.of("ALL","신규","번호이동","보상","기변","메이징","가개통","선불개통")) counts.put(tab,opens.stream().filter(o->matchesOpen(o,withoutTab(f),companies)&&inOpenTab(o,tab)).count()); - int from=Math.min(Math.max(page,0)*Math.max(size,1),filtered.size()), to=Math.min(from+Math.min(Math.max(size,1),200),filtered.size()); - List> list=filtered.subList(from,to).stream().map(o->openMap(o,companies)).toList(); - return Map.of("total",filtered.size(),"list",list,"counts",counts); - } - private boolean matchesOpen(OpenRecord o,Map f,Map companies){ - String tab=f.getOrDefault("tab","ALL"); if(!inOpenTab(o,tab)) return false; - if(!eq(o.getGubun(),f.get("gubun"))||!eq(o.getTelecom(),f.get("telecom"))||!eq(o.getOpenhow(),f.get("openhow"))||!eq(o.getOpenStatus(),f.get("openStatus"))||!eq(o.getSalesperson(),f.get("salesperson"))||!eq(o.getEmployee(),f.get("employee"))||!eq(o.getOutCategory(),f.get("outCategory"))||!eq(o.getState(),f.get("history"))) return false; - if(!eqId(o.getIncompanyId(),f.get("incompanyId"))) return false; - if(!like(o.getPmodel(),f.get("model"))||!like(companies.get(o.getOutcompanyId()),f.get("outcompanyName"))||!like(o.getName(),f.get("name"))) return false; - String phoneTail=f.get("phoneTail"); if(phoneTail!=null&&!phoneTail.isBlank()){String p=o.getOpenphone()==null?"":o.getOpenphone().replaceAll("\\D",""); if(!p.endsWith(phoneTail.replaceAll("\\D",""))) return false;} - if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(o.getOpendate()==null||o.getOpendate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; - return f.get("dateTo")==null||f.get("dateTo").isBlank()||(o.getOpendate()!=null&&!o.getOpendate().isAfter(LocalDate.parse(f.get("dateTo")))); - } - private boolean inOpenTab(OpenRecord o,String tab){ - if("ALL".equals(tab)||blank(tab)) return true; - String how=o.getOpenhow()==null?"":o.getOpenhow(); - return switch(tab){case "번호이동"->"번호이동".equals(how)||"MNP".equalsIgnoreCase(how); default->tab.equals(how);}; - } - private Comparator openComparator(String sort){ - String field=sort==null?"opendate,desc":sort; boolean asc=field.endsWith(",asc"); - Comparator c=switch(field.split(",")[0]){ - case "telecom"->Comparator.comparing(OpenRecord::getTelecom,Comparator.nullsLast(String::compareTo)); - case "pmodel"->Comparator.comparing(OpenRecord::getPmodel,Comparator.nullsLast(String::compareTo)); - case "sellprice"->Comparator.comparing(OpenRecord::getSellprice,Comparator.nullsLast(BigDecimal::compareTo)); - case "gubun"->Comparator.comparing(OpenRecord::getGubun,Comparator.nullsLast(String::compareTo)); - default->Comparator.comparing(OpenRecord::getOpendate,Comparator.nullsLast(LocalDate::compareTo)); - }; - return (asc?c:c.reversed()).thenComparing(OpenRecord::getId); - } - private Map openMap(OpenRecord o,Map companies){ - Map r=new LinkedHashMap<>(); - r.put("id",o.getId()); r.put("opendate",o.getOpendate()); r.put("telecom",o.getTelecom()); r.put("outcompanyId",o.getOutcompanyId()); - r.put("outcompanyName",companies.getOrDefault(o.getOutcompanyId(),"-")); r.put("incompanyId",o.getIncompanyId()); - r.put("incompanyName",companies.getOrDefault(o.getIncompanyId(),"-")); r.put("name",o.getName()); r.put("gubun",o.getGubun()); - r.put("openhow",o.getOpenhow()); r.put("openphone",o.getOpenphone()); r.put("openStatus",o.getOpenStatus()==null?"정상":o.getOpenStatus()); - r.put("pmodel",o.getPmodel()); r.put("pserial",o.getPserial()); r.put("pcolor",o.getPcolor()); r.put("userial",o.getUserial()); r.put("umodel",o.getUmodel()); - r.put("sellprice",o.getSellprice()); r.put("declprice1",o.getDeclprice1()); r.put("dealermargin1",o.getDealermargin1()); - r.put("plan",o.getPlan()); r.put("state",o.getState()); r.put("salesperson",o.getSalesperson()); r.put("employee",o.getEmployee()); - r.put("outCategory",o.getOutCategory()); r.put("inprice",o.getInprice()); r.put("netprice",o.getNetprice()); - r.put("openHour",o.getOpenHour()); r.put("openMinute",o.getOpenMinute()); r.put("salemon",o.getSalemon()); r.put("agreemon",o.getAgreemon()); - r.put("salehow",o.getSalehow()); r.put("joinhow",o.getJoinhow()); r.put("usimhow",o.getUsimhow()); r.put("supportType",o.getSupportType()); - r.put("memo1",o.getMemo1()); r.put("dealermemo",o.getDealermemo()); r.put("phoneStockId",o.getPhoneStockId()); r.put("usimStockId",o.getUsimStockId()); - r.put("commonSupport",o.getCommonSupport()); r.put("extraSupport",o.getExtraSupport()); r.put("switchSupport",o.getSwitchSupport()); - r.put("pointPay",o.getPointPay()); r.put("preprice",o.getPreprice()); r.put("joinprice",o.getJoinprice()); r.put("gradePrice",o.getGradePrice()); - r.put("basicprice1",o.getBasicprice1()); r.put("basicprice2",o.getBasicprice2()); r.put("basicprice3",o.getBasicprice3()); r.put("basicprice4",o.getBasicprice4()); - r.put("etcprice1",o.getEtcprice1()); r.put("etcprice2",o.getEtcprice2()); r.put("moveprice",o.getMoveprice()); r.put("planprice",o.getPlanprice()); - r.put("memberpoint",o.getMemberpoint()); r.put("usimprice",o.getUsimprice()); r.put("uid",o.getUid()); - r.put("decltype",o.getDecltype()); - return r; - } - private Map openAdjustData(JwtUser u,Map f){ - List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g",OpenRecord.class).setParameter("g",u.getGroupId()).getResultList(); - Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); - List filtered=opens.stream().filter(o->matchesAdjust(o,f,companies)).sorted(adjustComparator(f)).toList(); - return Map.of("total",filtered.size(),"list",filtered.stream().map(o->openMap(o,companies)).toList()); - } - private boolean matchesAdjust(OpenRecord o,Map f,Map companies){ - if("유심".equals(o.getGubun())) return false; - if(!eq(o.getOpenhow(),f.get("openhow"))||!eq(o.getTelecom(),f.get("telecom"))||!eq(o.getSalesperson(),f.get("salesperson"))||!eq(o.getPlan(),f.get("plan"))) return false; - if(!eqId(o.getIncompanyId(),f.get("incompanyId"))||!eqId(o.getOutcompanyId(),f.get("outcompanyId"))) return false; - if(!like(o.getPmodel(),f.get("model"))||!like(companies.get(o.getOutcompanyId()),f.get("outcompanyName"))) return false; - if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(o.getOpendate()==null||o.getOpendate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; - return f.get("dateTo")==null||f.get("dateTo").isBlank()||(o.getOpendate()!=null&&!o.getOpendate().isAfter(LocalDate.parse(f.get("dateTo")))); - } - private Comparator adjustComparator(Map f){ - List keys=new ArrayList<>(); - for(int i=1;i<=7;i++){String k=f.get("sort"+i); if(k!=null&&!k.isBlank()) keys.add(k);} - if(keys.isEmpty()) keys=List.of("종류","모델명","요금제","개통일","출고처","지원방법","고객명"); - Comparator c=null; - for(String key:keys){ - Comparator next=switch(key){ - case "종류"->Comparator.comparing(OpenRecord::getOpenhow,Comparator.nullsLast(String::compareTo)); - case "모델명"->Comparator.comparing(OpenRecord::getPmodel,Comparator.nullsLast(String::compareTo)); - case "요금제"->Comparator.comparing(OpenRecord::getPlan,Comparator.nullsLast(String::compareTo)); - case "출고처"->Comparator.comparing(OpenRecord::getOutcompanyId,Comparator.nullsLast(Long::compareTo)); - case "지원방법"->Comparator.comparing(OpenRecord::getDecltype,Comparator.nullsLast(String::compareTo)); - case "고객명"->Comparator.comparing(OpenRecord::getName,Comparator.nullsLast(String::compareTo)); - case "통신사"->Comparator.comparing(OpenRecord::getTelecom,Comparator.nullsLast(String::compareTo)); - default->Comparator.comparing(OpenRecord::getOpendate,Comparator.nullsLast(LocalDate::compareTo)); - }; - c=c==null?next:c.thenComparing(next); - } - return (c==null?Comparator.comparing(OpenRecord::getId):c.thenComparing(OpenRecord::getId)); - } - private BigDecimal toDecimal(Object v){ if(v==null||String.valueOf(v).isBlank()) return BigDecimal.ZERO; return new BigDecimal(String.valueOf(v).replace(",","")); } - @GetMapping("/unitcosts") ApiResponse unitCosts(@AuthenticationPrincipal JwtUser u){ - List rows=em.createQuery("select t from UnitCostTable t where t.groupId=:g order by t.id desc",UnitCostTable.class).setParameter("g",u.getGroupId()).getResultList(); - Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); - return ApiResponse.ok(Map.of("total",rows.size(),"list",rows.stream().map(t->unitCostMap(t,companies)).toList(),"max",20)); - } - @PostMapping("/unitcosts") @Transactional ApiResponse unitCostPost(@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ - long count=em.createQuery("select count(t) from UnitCostTable t where t.groupId=:g",Long.class).setParameter("g",u.getGroupId()).getSingleResult(); - if(count>=20) return ApiResponse.fail("단가표는 최대 20개까지 등록할 수 있습니다."); - d.putIfAbsent("category","표준"); d.putIfAbsent("policyCount",0); - Integer groups=toInt(d.get("planGroupCount")); if(groups!=null){ d.put("planGroupA",groups); d.putIfAbsent("planGroupB",0); d.putIfAbsent("planGroupC",0); d.putIfAbsent("planGroupD",0); } - return post(UnitCostTable.class,d,u); - } - @PutMapping("/unitcosts/{id}") ApiResponse unitCostPut(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return put(UnitCostTable.class,id,d,u);} - @DeleteMapping("/unitcosts/{id}") @Transactional ApiResponse unitCostDel(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ - em.createQuery("delete from UnitCostPolicy p where p.unitCostId=:id and p.groupId=:g").setParameter("id",id).setParameter("g",u.getGroupId()).executeUpdate(); - return del(UnitCostTable.class,id,u); - } - @GetMapping("/unitcosts/{id}/policies") ApiResponse unitCostPolicies(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ - UnitCostTable t=em.find(UnitCostTable.class,id); if(t==null||!Objects.equals(t.getGroupId(),u.getGroupId())) return ApiResponse.fail("단가표를 찾을 수 없습니다."); - List policies=em.createQuery("select p from UnitCostPolicy p where p.groupId=:g and p.unitCostId=:id order by p.id",UnitCostPolicy.class).setParameter("g",u.getGroupId()).setParameter("id",id).getResultList(); - return ApiResponse.ok(Map.of("table",unitCostMap(t,Map.of()),"list",policies)); - } - @PostMapping("/unitcosts/{id}/policies") @Transactional ApiResponse unitCostPolicyPost(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ - UnitCostTable t=em.find(UnitCostTable.class,id); if(t==null||!Objects.equals(t.getGroupId(),u.getGroupId())) return ApiResponse.fail("단가표를 찾을 수 없습니다."); - d.put("unitCostId",id); ApiResponse created=post(UnitCostPolicy.class,d,u); - refreshPolicyCount(t); return created; - } - @PutMapping("/unitcosts/policies/{policyId}") ApiResponse unitCostPolicyPut(@PathVariable Long policyId,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return put(UnitCostPolicy.class,policyId,d,u);} - @DeleteMapping("/unitcosts/policies/{policyId}") @Transactional ApiResponse unitCostPolicyDel(@PathVariable Long policyId,@AuthenticationPrincipal JwtUser u){ - UnitCostPolicy p=em.find(UnitCostPolicy.class,policyId); if(p==null||!Objects.equals(p.getGroupId(),u.getGroupId())) return ApiResponse.fail("정책을 찾을 수 없습니다."); - Long tableId=p.getUnitCostId(); del(UnitCostPolicy.class,policyId,u); - UnitCostTable t=em.find(UnitCostTable.class,tableId); if(t!=null) refreshPolicyCount(t); - return ApiResponse.ok(null); - } - @GetMapping("/unitcosts/{id}/apply-search") ApiResponse unitCostApplySearch(@PathVariable Long id,@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - UnitCostTable t=em.find(UnitCostTable.class,id); if(t==null||!Objects.equals(t.getGroupId(),u.getGroupId())) return ApiResponse.fail("단가표를 찾을 수 없습니다."); - if(blank(f.get("dateFrom"))||blank(f.get("dateTo"))) return ApiResponse.fail("개통일을 선택하세요."); - LocalDate from=LocalDate.parse(f.get("dateFrom")), to=LocalDate.parse(f.get("dateTo")); - if(java.time.temporal.ChronoUnit.DAYS.between(from,to)>2) return ApiResponse.fail("최대 3일까지 조회할 수 있습니다."); - Map filters=new HashMap<>(f); filters.put("telecom",t.getTelecom()); - if(t.getIncompanyId()!=null) filters.put("incompanyId",String.valueOf(t.getIncompanyId())); - List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g",OpenRecord.class).setParameter("g",u.getGroupId()).getResultList(); - Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); - List> list=opens.stream().filter(o->matchesUnitCostApply(o,filters,t)).map(o->openMap(o,companies)).toList(); - return ApiResponse.ok(Map.of("total",list.size(),"list",list,"table",unitCostMap(t,companies))); - } - @PostMapping("/unitcosts/{id}/apply") @Transactional ApiResponse unitCostApply(@PathVariable Long id,@AuthenticationPrincipal JwtUser u,@RequestBody Map body){ - UnitCostTable t=em.find(UnitCostTable.class,id); if(t==null||!Objects.equals(t.getGroupId(),u.getGroupId())) return ApiResponse.fail("단가표를 찾을 수 없습니다."); - List policies=em.createQuery("select p from UnitCostPolicy p where p.groupId=:g and p.unitCostId=:id",UnitCostPolicy.class).setParameter("g",u.getGroupId()).setParameter("id",id).getResultList(); - if(policies.isEmpty()) return ApiResponse.fail("등록된 정책단가가 없습니다. 먼저 정책단가를 작성해주세요."); - BigDecimal defaultAmount=policies.getFirst().getAmount()==null?BigDecimal.ZERO:policies.getFirst().getAmount(); - Object raw=body.get("ids"); List ids=new ArrayList<>(); - if(raw instanceof List list) for(Object o:list) ids.add(Long.valueOf(String.valueOf(o))); - int updated=0; - for(Long openId:ids){ - OpenRecord o=em.find(OpenRecord.class,openId); - if(o==null||!Objects.equals(o.getGroupId(),u.getGroupId())||"유심".equals(o.getGubun())) continue; - BigDecimal amount=defaultAmount; - for(UnitCostPolicy p:policies){ - if(p.getModel()!=null&&!p.getModel().isBlank()&&o.getPmodel()!=null&&o.getPmodel().contains(p.getModel())){ amount=p.getAmount(); break; } - if(p.getPlanName()!=null&&!p.getPlanName().isBlank()&&Objects.equals(p.getPlanName(),o.getPlan())){ amount=p.getAmount(); break; } - } - o.setBasicprice1(amount); updated++; - } - return ApiResponse.ok(Map.of("updated",updated)); - } - private void refreshPolicyCount(UnitCostTable t){ - long c=em.createQuery("select count(p) from UnitCostPolicy p where p.unitCostId=:id",Long.class).setParameter("id",t.getId()).getSingleResult(); - t.setPolicyCount((int)c); - } - private boolean matchesUnitCostApply(OpenRecord o,Map f,UnitCostTable t){ - if("유심".equals(o.getGubun())) return false; - String how=o.getOpenhow()==null?"":o.getOpenhow(); - if(!(Set.of("신규","번호이동","기변","보상").contains(how)||"MNP".equalsIgnoreCase(how))) return false; - if(!eq(o.getTelecom(),t.getTelecom())) return false; - if(t.getIncompanyId()!=null&&!Objects.equals(o.getIncompanyId(),t.getIncompanyId())) return false; - if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(o.getOpendate()==null||o.getOpendate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; - return f.get("dateTo")==null||f.get("dateTo").isBlank()||(o.getOpendate()!=null&&!o.getOpendate().isAfter(LocalDate.parse(f.get("dateTo")))); - } - private Map unitCostMap(UnitCostTable t,Map companies){ - Map m=new LinkedHashMap<>(); - m.put("id",t.getId()); m.put("telecom",t.getTelecom()); m.put("incompanyId",t.getIncompanyId()); - m.put("incompanyName",t.getIncompanyName()!=null&&!t.getIncompanyName().isBlank()?t.getIncompanyName():companies.getOrDefault(t.getIncompanyId(),"-")); - m.put("memo",t.getMemo()); m.put("category",t.getCategory()); - m.put("planGroupA",t.getPlanGroupA()); m.put("planGroupB",t.getPlanGroupB()); m.put("planGroupC",t.getPlanGroupC()); - m.put("planGroupD",t.getPlanGroupD()); m.put("planGroupE",t.getPlanGroupE()); m.put("planGroupF",t.getPlanGroupF()); m.put("planGroupG",t.getPlanGroupG()); - m.put("policyCount",t.getPolicyCount()==null?0:t.getPolicyCount()); - return m; - } - private Integer toInt(Object v){ if(v==null||String.valueOf(v).isBlank()) return null; return Integer.valueOf(String.valueOf(v)); } - @GetMapping("/homesales/search") ApiResponse homeSaleSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ - return ApiResponse.ok(homeSaleSearchData(u,f,page,size)); - } - @PostMapping("/homesales") @Transactional ApiResponse homeSalePost(@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ - d.putIfAbsent("status","완료"); return post(HomeSale.class,d,u); - } - @GetMapping("/homesales/{id}") ApiResponse homeSaleGet(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return get(HomeSale.class,id,u);} - @PutMapping("/homesales/{id}") ApiResponse homeSalePut(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return put(HomeSale.class,id,d,u);} - @DeleteMapping("/homesales/{id}") ApiResponse homeSaleDel(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return del(HomeSale.class,id,u);} - @GetMapping(value="/homesales/export", produces="text/csv;charset=UTF-8") ResponseEntity homeSaleExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - @SuppressWarnings("unchecked") List> rows=(List>) homeSaleSearchData(u,f,0,10000).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF설치일,출고처,상태,입고처,통신사,고객명,휴대폰,가입상품,정산,마진,접수일\n"); - for(Map r:rows) csv.append(csv(r.get("installDate"))).append(',').append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("status"))).append(',') - .append(csv(r.get("incompanyName"))).append(',').append(csv(r.get("telecom"))).append(',').append(csv(r.get("customerName"))).append(',').append(csv(r.get("phone"))).append(',') - .append(csv(r.get("products"))).append(',').append(csv(r.get("settleAmount"))).append(',').append(csv(r.get("margin"))).append(',').append(csv(r.get("receiptDate"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=homesales.csv").body(csv.toString()); - } - private Map homeSaleSearchData(JwtUser u,Map f,int page,int size){ - List rows=em.createQuery("select h from HomeSale h where h.groupId=:g",HomeSale.class).setParameter("g",u.getGroupId()).getResultList(); - Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); - List filtered=rows.stream().filter(h->matchesHomeSale(h,f,companies)).sorted(homeSaleComparator(f.get("sort"))).toList(); - Map counts=new LinkedHashMap<>(); - List tabs=List.of("ALL","인터넷","TV","집전화","인터넷전화","신용카드","홈IOT","동반","기타","후결합","재약정","소호","원스톱","렌탈"); - for(String tab:tabs) counts.put(tab,rows.stream().filter(h->matchesHomeSale(h,withoutTab(f),companies)&&inHomeTab(h,tab)).count()); - int from=Math.min(Math.max(page,0)*Math.max(size,1),filtered.size()), to=Math.min(from+Math.min(Math.max(size,1),200),filtered.size()); - List> list=filtered.subList(from,to).stream().map(h->homeSaleMap(h,companies)).toList(); - return Map.of("total",filtered.size(),"list",list,"counts",counts); - } - private boolean matchesHomeSale(HomeSale h,Map f,Map companies){ - String tab=f.getOrDefault("tab","ALL"); if(!inHomeTab(h,tab)) return false; - if(!eq(h.getTelecom(),f.get("telecom"))||!eq(h.getOutCategory(),f.get("outCategory"))||!eq(h.getStatus(),f.get("status"))||!eq(h.getEmployee(),f.get("employee"))) return false; - if(!eqId(h.getIncompanyId(),f.get("incompanyId"))) return false; - if(!like(h.getOutcompanyName(),f.get("outcompanyName"))&&!like(companies.get(h.getOutcompanyId()),f.get("outcompanyName"))) return false; - if(!like(h.getCustomerName(),f.get("customerName"))) return false; - String dateBasis=f.getOrDefault("dateBasis","설치일기준"); - LocalDate date="접수일기준".equals(dateBasis)?h.getReceiptDate():h.getInstallDate(); - if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(date==null||date.isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; - return f.get("dateTo")==null||f.get("dateTo").isBlank()||(date!=null&&!date.isAfter(LocalDate.parse(f.get("dateTo")))); - } - private boolean inHomeTab(HomeSale h,String tab){ - if("ALL".equals(tab)||blank(tab)) return true; - String products=h.getProducts()==null?"":h.getProducts(); - return Arrays.stream(products.split("[,|/]")).map(String::trim).anyMatch(p->p.equals(tab)||("동판".equals(tab)&&"동반".equals(p))); - } - private Comparator homeSaleComparator(String sort){ - String field=sort==null?"installDate,desc":sort; boolean asc=field.endsWith(",asc"); - Comparator c=switch(field.split(",")[0]){ - case "outcompanyName"->Comparator.comparing(HomeSale::getOutcompanyName,Comparator.nullsLast(String::compareTo)); - case "telecom"->Comparator.comparing(HomeSale::getTelecom,Comparator.nullsLast(String::compareTo)); - case "customerName"->Comparator.comparing(HomeSale::getCustomerName,Comparator.nullsLast(String::compareTo)); - case "settleAmount"->Comparator.comparing(HomeSale::getSettleAmount,Comparator.nullsLast(BigDecimal::compareTo)); - case "receiptDate"->Comparator.comparing(HomeSale::getReceiptDate,Comparator.nullsLast(LocalDate::compareTo)); - default->Comparator.comparing(HomeSale::getInstallDate,Comparator.nullsLast(LocalDate::compareTo)); - }; - return (asc?c:c.reversed()).thenComparing(HomeSale::getId); - } - private Map homeSaleMap(HomeSale h,Map companies){ - Map m=new LinkedHashMap<>(); - m.put("id",h.getId()); m.put("installDate",h.getInstallDate()); m.put("receiptDate",h.getReceiptDate()); - m.put("status",h.getStatus()); m.put("telecom",h.getTelecom()); m.put("outCategory",h.getOutCategory()); - m.put("outcompanyId",h.getOutcompanyId()); m.put("incompanyId",h.getIncompanyId()); - m.put("outcompanyName",h.getOutcompanyName()!=null&&!h.getOutcompanyName().isBlank()?h.getOutcompanyName():companies.getOrDefault(h.getOutcompanyId(),"-")); - m.put("incompanyName",companies.getOrDefault(h.getIncompanyId(),"-")); - m.put("employee",h.getEmployee()); m.put("customerName",h.getCustomerName()); m.put("phone",h.getPhone()); - m.put("address",h.getAddress()); m.put("products",h.getProducts()); m.put("missingDocs",h.getMissingDocs()); - m.put("extraPolicy",h.getExtraPolicy()); m.put("settleAmount",h.getSettleAmount()); m.put("margin",h.getMargin()); m.put("memo",h.getMemo()); - return m; - } - @GetMapping("/homeindocs/search") ApiResponse homeIndocSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ - if(blank(f.get("dateFrom"))||blank(f.get("dateTo"))) return ApiResponse.fail("날짜를 선택하세요."); - return ApiResponse.ok(homeIndocSearchData(u,f,page,size)); - } - @GetMapping(value="/homeindocs/export", produces="text/csv;charset=UTF-8") ResponseEntity homeIndocExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - if(blank(f.get("dateFrom"))||blank(f.get("dateTo"))) return ResponseEntity.badRequest().body("날짜를 선택하세요."); - @SuppressWarnings("unchecked") List> rows=(List>) homeIndocSearchData(u,f,0,10000).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF설치일,통신사,입고처,출고처,고객명,휴대폰,가입상품,미비서류,차감금,차감사유,마감일,상태\n"); - for(Map r:rows) csv.append(csv(r.get("installDate"))).append(',').append(csv(r.get("telecom"))).append(',').append(csv(r.get("incompanyName"))).append(',') - .append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("customerName"))).append(',').append(csv(r.get("phone"))).append(',') - .append(csv(r.get("products"))).append(',').append(csv(r.get("missingDocs"))).append(',').append(csv(r.get("deductAmount"))).append(',') - .append(csv(r.get("deductReason"))).append(',').append(csv(r.get("deadline"))).append(',').append(csv(r.get("status"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=homeindocs.csv").body(csv.toString()); - } - private Map homeIndocSearchData(JwtUser u,Map f,int page,int size){ - List docs=em.createQuery("select d from HomeIncompleteDoc d where d.groupId=:g",HomeIncompleteDoc.class).setParameter("g",u.getGroupId()).getResultList(); - Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); - List filtered=docs.stream().filter(d->matchesHomeIndoc(d,f,companies)).sorted(Comparator.comparing(HomeIncompleteDoc::getInstallDate,Comparator.nullsLast(LocalDate::compareTo)).reversed().thenComparing(HomeIncompleteDoc::getId)).toList(); - int from=Math.min(Math.max(page,0)*Math.max(size,1),filtered.size()), to=Math.min(from+Math.min(Math.max(size,1),200),filtered.size()); - List> list=filtered.subList(from,to).stream().map(d->homeIndocMap(d,companies)).toList(); - return Map.of("total",filtered.size(),"list",list); - } - private boolean matchesHomeIndoc(HomeIncompleteDoc d,Map f,Map companies){ - if(!eq(d.getTelecom(),f.get("telecom"))||!eqId(d.getIncompanyId(),f.get("incompanyId"))) return false; - if(!like(d.getOutcompanyName(),f.get("outcompanyName"))&&!like(companies.get(d.getOutcompanyId()),f.get("outcompanyName"))) return false; - if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(d.getInstallDate()==null||d.getInstallDate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; - if(f.get("dateTo")!=null&&!f.get("dateTo").isBlank()&&(d.getInstallDate()==null||d.getInstallDate().isAfter(LocalDate.parse(f.get("dateTo"))))) return false; - String keyword=f.get("keyword"); if(keyword==null||keyword.isBlank()) return true; - String searchType=f.getOrDefault("searchType","고객명"); - return switch(searchType){ - case "미비서류"->like(d.getMissingDocs(),keyword); - case "휴대폰(뒷4자리)"->{ - String phone=d.getPhone()==null?"":d.getPhone().replaceAll("\\D",""); - yield phone.endsWith(keyword.replaceAll("\\D","")); - } - default->like(d.getCustomerName(),keyword); - }; - } - private Map homeIndocMap(HomeIncompleteDoc d,Map companies){ - Map r=new LinkedHashMap<>(); - r.put("id",d.getId()); r.put("installDate",d.getInstallDate()); r.put("deadline",d.getDeadline()); - r.put("telecom",d.getTelecom()); r.put("customerName",d.getCustomerName()); r.put("phone",d.getPhone()); - r.put("products",d.getProducts()); r.put("missingDocs",d.getMissingDocs()); r.put("deductAmount",d.getDeductAmount()); - r.put("deductReason",d.getDeductReason()); r.put("status",d.getStatus()==null?"미비":d.getStatus()); - r.put("homeSaleId",d.getHomeSaleId()); r.put("outcompanyId",d.getOutcompanyId()); r.put("incompanyId",d.getIncompanyId()); - r.put("outcompanyName",d.getOutcompanyName()!=null&&!d.getOutcompanyName().isBlank()?d.getOutcompanyName():companies.getOrDefault(d.getOutcompanyId(),"-")); - r.put("incompanyName",companies.getOrDefault(d.getIncompanyId(),"-")); - return r; - } - @GetMapping("/exchanges/search") ApiResponse exchangeSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ - return ApiResponse.ok(exchangeSearchData(u,f,page,size)); - } - @PostMapping("/exchanges/delete") @Transactional ApiResponse exchangeDelete(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Object raw=d.get("ids"); - if(!(raw instanceof List ids)||ids.isEmpty()) return ApiResponse.fail("삭제할 이력을 선택하세요."); - int deleted=0; - for(Object idObj:ids){ - OpenExchange e=em.find(OpenExchange.class,Long.valueOf(idObj.toString())); - if(e==null||!Objects.equals(e.getGroupId(),u.getGroupId())) continue; - em.remove(e); deleted++; - } - return ApiResponse.ok(Map.of("deleted",deleted)); - } - @GetMapping(value="/exchanges/export", produces="text/csv;charset=UTF-8") ResponseEntity exchangeExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - @SuppressWarnings("unchecked") List> rows=(List>) exchangeSearchData(u,f,0,10000).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF번호,교품일,출고처,고객명,개통일,개통번호,종류,교품,기존,처리직원,처리일,비고\n"); - for(Map r:rows) csv.append(csv(r.get("no"))).append(',').append(csv(r.get("exchangeDate"))).append(',').append(csv(r.get("outcompanyName"))).append(',') - .append(csv(r.get("customerName"))).append(',').append(csv(r.get("openDate"))).append(',').append(csv(r.get("openphone"))).append(',') - .append(csv(r.get("kind"))).append(',').append(csv(r.get("newLabel"))).append(',').append(csv(r.get("oldLabel"))).append(',') - .append(csv(r.get("processor"))).append(',').append(csv(r.get("processedAt"))).append(',').append(csv(r.get("memo"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=exchanges.csv").body(csv.toString()); - } - private Map exchangeSearchData(JwtUser u,Map f,int page,int size){ - List all=em.createQuery("select e from OpenExchange e where e.groupId=:g",OpenExchange.class).setParameter("g",u.getGroupId()).getResultList(); - Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); - List filtered=all.stream().filter(e->matchesExchange(e,f,companies)).sorted(Comparator.comparing(OpenExchange::getExchangeDate,Comparator.nullsLast(LocalDate::compareTo)).reversed().thenComparing(OpenExchange::getId,Comparator.reverseOrder())).toList(); - int pageSize=Math.min(Math.max(size,1),200), from=Math.min(Math.max(page,0)*pageSize,filtered.size()), to=Math.min(from+pageSize,filtered.size()); - List> list=new ArrayList<>(); - for(int i=from;i f,Map companies){ - if(!eq(e.getKind(),f.get("kind"))) return false; - if(!like(e.getOutcompanyName(),f.get("outcompanyName"))&&!like(companies.get(e.getOutcompanyId()),f.get("outcompanyName"))) return false; - if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(e.getExchangeDate()==null||e.getExchangeDate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; - if(f.get("dateTo")!=null&&!f.get("dateTo").isBlank()&&(e.getExchangeDate()==null||e.getExchangeDate().isAfter(LocalDate.parse(f.get("dateTo"))))) return false; - String keyword=f.get("keyword"); if(keyword==null||keyword.isBlank()) return true; - String searchType=f.getOrDefault("searchType","개통번호(뒷4자리)"); - return switch(searchType){ - case "고객명"->like(e.getCustomerName(),keyword); - case "교품일련번호"->like(e.getNewSerial(),keyword); - case "기존일련번호"->like(e.getOldSerial(),keyword); - default->{ - String phone=e.getOpenphone()==null?"":e.getOpenphone().replaceAll("\\D",""); - yield phone.endsWith(keyword.replaceAll("\\D","")); - } - }; - } - private Map exchangeMap(OpenExchange e,Map companies,int no){ - Map r=new LinkedHashMap<>(); - r.put("id",e.getId()); r.put("no",no); r.put("exchangeDate",e.getExchangeDate()); r.put("openDate",e.getOpenDate()); - r.put("customerName",e.getCustomerName()); r.put("openphone",e.getOpenphone()); r.put("kind",e.getKind()); - r.put("newModel",e.getNewModel()); r.put("newColor",e.getNewColor()); r.put("newSerial",e.getNewSerial()); - r.put("oldModel",e.getOldModel()); r.put("oldColor",e.getOldColor()); r.put("oldSerial",e.getOldSerial()); - r.put("newLabel",deviceLabel(e.getNewModel(),e.getNewColor(),e.getNewSerial())); - r.put("oldLabel",deviceLabel(e.getOldModel(),e.getOldColor(),e.getOldSerial())); - r.put("processor",e.getProcessor()); r.put("memo",e.getMemo()); - r.put("processedAt",e.getProcessedAt()==null?null:e.getProcessedAt().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))); - r.put("outcompanyId",e.getOutcompanyId()); - r.put("outcompanyName",e.getOutcompanyName()!=null&&!e.getOutcompanyName().isBlank()?e.getOutcompanyName():companies.getOrDefault(e.getOutcompanyId(),"-")); - return r; - } - private String deviceLabel(String model,String color,String serial){ - String m=model==null?"":model.trim(); - String c=color==null||color.isBlank()||"-".equals(color)?"":" "+color.trim(); - String s=serial==null||serial.isBlank()?"":" ("+serial.trim()+")"; - return (m+c+s).trim(); - } - @GetMapping("/namechanges/search") ApiResponse nameChangeSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ - return ApiResponse.ok(nameChangeSearchData(u,f,page,size)); - } - @PostMapping("/namechanges/delete") @Transactional ApiResponse nameChangeDelete(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Object raw=d.get("ids"); - if(!(raw instanceof List ids)||ids.isEmpty()) return ApiResponse.fail("삭제할 이력을 선택하세요."); - int deleted=0; - for(Object idObj:ids){ - OpenNameChange e=em.find(OpenNameChange.class,Long.valueOf(idObj.toString())); - if(e==null||!Objects.equals(e.getGroupId(),u.getGroupId())) continue; - em.remove(e); deleted++; - } - return ApiResponse.ok(Map.of("deleted",deleted)); - } - @GetMapping(value="/namechanges/export", produces="text/csv;charset=UTF-8") ResponseEntity nameChangeExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - @SuppressWarnings("unchecked") List> rows=(List>) nameChangeSearchData(u,f,0,10000).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF번호,명변일,출고처,고객명,개통일,개통번호,기존명의자,기존연락처,기존유심,처리직원,처리일,비고\n"); - for(Map r:rows) csv.append(csv(r.get("no"))).append(',').append(csv(r.get("changeDate"))).append(',').append(csv(r.get("outcompanyName"))).append(',') - .append(csv(r.get("customerName"))).append(',').append(csv(r.get("openDate"))).append(',').append(csv(r.get("openphone"))).append(',') - .append(csv(r.get("oldOwner"))).append(',').append(csv(r.get("oldPhone"))).append(',').append(csv(r.get("oldUsim"))).append(',') - .append(csv(r.get("processor"))).append(',').append(csv(r.get("processedAt"))).append(',').append(csv(r.get("memo"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=namechanges.csv").body(csv.toString()); - } - private Map nameChangeSearchData(JwtUser u,Map f,int page,int size){ - List all=em.createQuery("select e from OpenNameChange e where e.groupId=:g",OpenNameChange.class).setParameter("g",u.getGroupId()).getResultList(); - Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); - List filtered=all.stream().filter(e->matchesNameChange(e,f,companies)).sorted(Comparator.comparing(OpenNameChange::getChangeDate,Comparator.nullsLast(LocalDate::compareTo)).reversed().thenComparing(OpenNameChange::getId,Comparator.reverseOrder())).toList(); - int pageSize=Math.min(Math.max(size,1),200), from=Math.min(Math.max(page,0)*pageSize,filtered.size()), to=Math.min(from+pageSize,filtered.size()); - List> list=new ArrayList<>(); - for(int i=from;i f,Map companies){ - if(!like(e.getOutcompanyName(),f.get("outcompanyName"))&&!like(companies.get(e.getOutcompanyId()),f.get("outcompanyName"))) return false; - if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(e.getChangeDate()==null||e.getChangeDate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; - if(f.get("dateTo")!=null&&!f.get("dateTo").isBlank()&&(e.getChangeDate()==null||e.getChangeDate().isAfter(LocalDate.parse(f.get("dateTo"))))) return false; - String keyword=f.get("keyword"); if(keyword==null||keyword.isBlank()) return true; - String searchType=f.getOrDefault("searchType","개통번호(뒷4자리)"); - return switch(searchType){ - case "고객명"->like(e.getCustomerName(),keyword); - case "기존명의자"->like(e.getOldOwner(),keyword); - case "기존연락처"->{ - String phone=e.getOldPhone()==null?"":e.getOldPhone().replaceAll("\\D",""); - yield phone.endsWith(keyword.replaceAll("\\D","")); - } - default->{ - String phone=e.getOpenphone()==null?"":e.getOpenphone().replaceAll("\\D",""); - yield phone.endsWith(keyword.replaceAll("\\D","")); - } - }; - } - private Map nameChangeMap(OpenNameChange e,Map companies,int no){ - Map r=new LinkedHashMap<>(); - r.put("id",e.getId()); r.put("no",no); r.put("changeDate",e.getChangeDate()); r.put("openDate",e.getOpenDate()); - r.put("customerName",e.getCustomerName()); r.put("openphone",e.getOpenphone()); - r.put("oldOwner",e.getOldOwner()); r.put("oldPhone",e.getOldPhone()); r.put("oldUsim",e.getOldUsim()); - r.put("processor",e.getProcessor()); r.put("memo",e.getMemo()); - r.put("processedAt",e.getProcessedAt()==null?null:e.getProcessedAt().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))); - r.put("outcompanyId",e.getOutcompanyId()); - r.put("outcompanyName",e.getOutcompanyName()!=null&&!e.getOutcompanyName().isBlank()?e.getOutcompanyName():companies.getOrDefault(e.getOutcompanyId(),"-")); - return r; - } - @GetMapping("/withdrawals/search") ApiResponse withdrawalSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ - return ApiResponse.ok(withdrawalSearchData(u,f,page,size)); - } - @PostMapping("/withdrawals/delete") @Transactional ApiResponse withdrawalDelete(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Object raw=d.get("ids"); - if(!(raw instanceof List ids)||ids.isEmpty()) return ApiResponse.fail("삭제할 이력을 선택하세요."); - int deleted=0; - for(Object idObj:ids){ - OpenWithdrawal e=em.find(OpenWithdrawal.class,Long.valueOf(idObj.toString())); - if(e==null||!Objects.equals(e.getGroupId(),u.getGroupId())) continue; - em.remove(e); deleted++; - } - return ApiResponse.ok(Map.of("deleted",deleted)); - } - @GetMapping(value="/withdrawals/export", produces="text/csv;charset=UTF-8") ResponseEntity withdrawalExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - @SuppressWarnings("unchecked") List> rows=(List>) withdrawalSearchData(u,f,0,10000).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF번호,철회일,출고처,고객명,개통일,개통번호,처리직원,처리일,사유 및 비고\n"); - for(Map r:rows) csv.append(csv(r.get("no"))).append(',').append(csv(r.get("withdrawDate"))).append(',').append(csv(r.get("outcompanyName"))).append(',') - .append(csv(r.get("customerName"))).append(',').append(csv(r.get("openDate"))).append(',').append(csv(r.get("openphone"))).append(',') - .append(csv(r.get("processor"))).append(',').append(csv(r.get("processedAt"))).append(',').append(csv(r.get("reason"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=withdrawals.csv").body(csv.toString()); - } - private Map withdrawalSearchData(JwtUser u,Map f,int page,int size){ - List all=em.createQuery("select e from OpenWithdrawal e where e.groupId=:g",OpenWithdrawal.class).setParameter("g",u.getGroupId()).getResultList(); - Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); - List filtered=all.stream().filter(e->matchesWithdrawal(e,f,companies)).sorted(Comparator.comparing(OpenWithdrawal::getWithdrawDate,Comparator.nullsLast(LocalDate::compareTo)).reversed().thenComparing(OpenWithdrawal::getId,Comparator.reverseOrder())).toList(); - int pageSize=Math.min(Math.max(size,1),200), from=Math.min(Math.max(page,0)*pageSize,filtered.size()), to=Math.min(from+pageSize,filtered.size()); - List> list=new ArrayList<>(); - for(int i=from;i f,Map companies){ - if(!like(e.getOutcompanyName(),f.get("outcompanyName"))&&!like(companies.get(e.getOutcompanyId()),f.get("outcompanyName"))) return false; - if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(e.getWithdrawDate()==null||e.getWithdrawDate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; - if(f.get("dateTo")!=null&&!f.get("dateTo").isBlank()&&(e.getWithdrawDate()==null||e.getWithdrawDate().isAfter(LocalDate.parse(f.get("dateTo"))))) return false; - String keyword=f.get("keyword"); if(keyword==null||keyword.isBlank()) return true; - String searchType=f.getOrDefault("searchType","개통번호(뒷4자리)"); - return switch(searchType){ - case "고객명"->like(e.getCustomerName(),keyword); - case "사유 및 비고"->like(e.getReason(),keyword); - default->{ - String phone=e.getOpenphone()==null?"":e.getOpenphone().replaceAll("\\D",""); - yield phone.endsWith(keyword.replaceAll("\\D","")); - } - }; - } - private Map withdrawalMap(OpenWithdrawal e,Map companies,int no){ - Map r=new LinkedHashMap<>(); - r.put("id",e.getId()); r.put("no",no); r.put("withdrawDate",e.getWithdrawDate()); r.put("openDate",e.getOpenDate()); - r.put("customerName",e.getCustomerName()); r.put("openphone",e.getOpenphone()); - r.put("processor",e.getProcessor()); r.put("reason",e.getReason()); - r.put("processedAt",e.getProcessedAt()==null?null:e.getProcessedAt().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))); - r.put("outcompanyId",e.getOutcompanyId()); - r.put("outcompanyName",e.getOutcompanyName()!=null&&!e.getOutcompanyName().isBlank()?e.getOutcompanyName():companies.getOrDefault(e.getOutcompanyId(),"-")); - return r; - } - @GetMapping("/settlehistories/search") ApiResponse settleHistorySearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ - return ApiResponse.ok(settleHistorySearchData(u,f,page,size)); - } - @PostMapping("/settlehistories/delete") @Transactional ApiResponse settleHistoryDelete(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Object raw=d.get("ids"); - if(!(raw instanceof List ids)||ids.isEmpty()) return ApiResponse.fail("삭제할 이력을 선택하세요."); - int deleted=0; - for(Object idObj:ids){ - OpenSettleHistory e=em.find(OpenSettleHistory.class,Long.valueOf(idObj.toString())); - if(e==null||!Objects.equals(e.getGroupId(),u.getGroupId())) continue; - em.remove(e); deleted++; - } - return ApiResponse.ok(Map.of("deleted",deleted)); - } - @GetMapping(value="/settlehistories/export", produces="text/csv;charset=UTF-8") ResponseEntity settleHistoryExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - @SuppressWarnings("unchecked") List> rows=(List>) settleHistorySearchData(u,f,0,10000).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF번호,변경일,출고처,고객명,개통일,개통번호,기존정산금,변경정산금,처리직원,처리일,비고\n"); - for(Map r:rows) csv.append(csv(r.get("no"))).append(',').append(csv(r.get("changeDate"))).append(',').append(csv(r.get("outcompanyName"))).append(',') - .append(csv(r.get("customerName"))).append(',').append(csv(r.get("openDate"))).append(',').append(csv(r.get("openphone"))).append(',') - .append(csv(r.get("oldAmount"))).append(',').append(csv(r.get("newAmount"))).append(',') - .append(csv(r.get("processor"))).append(',').append(csv(r.get("processedAt"))).append(',').append(csv(r.get("memo"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=settlehistories.csv").body(csv.toString()); - } - private Map settleHistorySearchData(JwtUser u,Map f,int page,int size){ - List all=em.createQuery("select e from OpenSettleHistory e where e.groupId=:g",OpenSettleHistory.class).setParameter("g",u.getGroupId()).getResultList(); - Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); - List filtered=all.stream().filter(e->matchesSettleHistory(e,f,companies)).sorted(Comparator.comparing(OpenSettleHistory::getChangeDate,Comparator.nullsLast(LocalDate::compareTo)).reversed().thenComparing(OpenSettleHistory::getId,Comparator.reverseOrder())).toList(); - int pageSize=Math.min(Math.max(size,1),200), from=Math.min(Math.max(page,0)*pageSize,filtered.size()), to=Math.min(from+pageSize,filtered.size()); - List> list=new ArrayList<>(); - for(int i=from;i f,Map companies){ - if(!like(e.getOutcompanyName(),f.get("outcompanyName"))&&!like(companies.get(e.getOutcompanyId()),f.get("outcompanyName"))) return false; - if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(e.getChangeDate()==null||e.getChangeDate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; - if(f.get("dateTo")!=null&&!f.get("dateTo").isBlank()&&(e.getChangeDate()==null||e.getChangeDate().isAfter(LocalDate.parse(f.get("dateTo"))))) return false; - String keyword=f.get("keyword"); if(keyword==null||keyword.isBlank()) return true; - String searchType=f.getOrDefault("searchType","개통번호(뒷4자리)"); - return switch(searchType){ - case "고객명"->like(e.getCustomerName(),keyword); - case "처리직원"->like(e.getProcessor(),keyword); - case "비고"->like(e.getMemo(),keyword); - default->{ - String phone=e.getOpenphone()==null?"":e.getOpenphone().replaceAll("\\D",""); - yield phone.endsWith(keyword.replaceAll("\\D","")); - } - }; - } - private Map settleHistoryMap(OpenSettleHistory e,Map companies,int no){ - Map r=new LinkedHashMap<>(); - r.put("id",e.getId()); r.put("no",no); r.put("changeDate",e.getChangeDate()); r.put("openDate",e.getOpenDate()); - r.put("customerName",e.getCustomerName()); r.put("openphone",e.getOpenphone()); - r.put("oldAmount",e.getOldAmount()); r.put("newAmount",e.getNewAmount()); - r.put("processor",e.getProcessor()); r.put("memo",e.getMemo()); - r.put("processedAt",e.getProcessedAt()==null?null:e.getProcessedAt().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))); - r.put("outcompanyId",e.getOutcompanyId()); - r.put("outcompanyName",e.getOutcompanyName()!=null&&!e.getOutcompanyName().isBlank()?e.getOutcompanyName():companies.getOrDefault(e.getOutcompanyId(),"-")); - return r; - } - @GetMapping("/deletehistories/search") ApiResponse deleteHistorySearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ - return ApiResponse.ok(deleteHistorySearchData(u,f,page,size)); - } - @PostMapping("/deletehistories/delete") @Transactional ApiResponse deleteHistoryDelete(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Object raw=d.get("ids"); - if(!(raw instanceof List ids)||ids.isEmpty()) return ApiResponse.fail("삭제할 이력을 선택하세요."); - int deleted=0; - for(Object idObj:ids){ - OpenDeleteHistory e=em.find(OpenDeleteHistory.class,Long.valueOf(idObj.toString())); - if(e==null||!Objects.equals(e.getGroupId(),u.getGroupId())) continue; - em.remove(e); deleted++; - } - return ApiResponse.ok(Map.of("deleted",deleted)); - } - @GetMapping(value="/deletehistories/export", produces="text/csv;charset=UTF-8") ResponseEntity deleteHistoryExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - @SuppressWarnings("unchecked") List> rows=(List>) deleteHistorySearchData(u,f,0,10000).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF번호,철회일,출고처,고객명,개통일,개통번호,처리직원,처리일,비고\n"); - for(Map r:rows) csv.append(csv(r.get("no"))).append(',').append(csv(r.get("deleteDate"))).append(',').append(csv(r.get("outcompanyName"))).append(',') - .append(csv(r.get("customerName"))).append(',').append(csv(r.get("openDate"))).append(',').append(csv(r.get("openphone"))).append(',') - .append(csv(r.get("processor"))).append(',').append(csv(r.get("processedAt"))).append(',').append(csv(r.get("memo"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=deletehistories.csv").body(csv.toString()); - } - private Map deleteHistorySearchData(JwtUser u,Map f,int page,int size){ - List all=em.createQuery("select e from OpenDeleteHistory e where e.groupId=:g",OpenDeleteHistory.class).setParameter("g",u.getGroupId()).getResultList(); - Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); - List filtered=all.stream().filter(e->matchesDeleteHistory(e,f,companies)).sorted(Comparator.comparing(OpenDeleteHistory::getDeleteDate,Comparator.nullsLast(LocalDate::compareTo)).reversed().thenComparing(OpenDeleteHistory::getId,Comparator.reverseOrder())).toList(); - int pageSize=Math.min(Math.max(size,1),200), from=Math.min(Math.max(page,0)*pageSize,filtered.size()), to=Math.min(from+pageSize,filtered.size()); - List> list=new ArrayList<>(); - for(int i=from;i f,Map companies){ - if(!like(e.getOutcompanyName(),f.get("outcompanyName"))&&!like(companies.get(e.getOutcompanyId()),f.get("outcompanyName"))) return false; - if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(e.getDeleteDate()==null||e.getDeleteDate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; - if(f.get("dateTo")!=null&&!f.get("dateTo").isBlank()&&(e.getDeleteDate()==null||e.getDeleteDate().isAfter(LocalDate.parse(f.get("dateTo"))))) return false; - String keyword=f.get("keyword"); if(keyword==null||keyword.isBlank()) return true; - String searchType=f.getOrDefault("searchType","개통번호(뒷4자리)"); - return switch(searchType){ - case "고객명"->like(e.getCustomerName(),keyword); - case "처리직원"->like(e.getProcessor(),keyword); - case "비고"->like(e.getMemo(),keyword); - default->{ - String phone=e.getOpenphone()==null?"":e.getOpenphone().replaceAll("\\D",""); - yield phone.endsWith(keyword.replaceAll("\\D","")); - } - }; - } - private Map deleteHistoryMap(OpenDeleteHistory e,Map companies,int no){ - Map r=new LinkedHashMap<>(); - r.put("id",e.getId()); r.put("no",no); r.put("deleteDate",e.getDeleteDate()); r.put("openDate",e.getOpenDate()); - r.put("customerName",e.getCustomerName()); r.put("openphone",e.getOpenphone()); - r.put("processor",e.getProcessor()); r.put("memo",e.getMemo()); - r.put("processedAt",e.getProcessedAt()==null?null:e.getProcessedAt().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))); - r.put("outcompanyId",e.getOutcompanyId()); - r.put("outcompanyName",e.getOutcompanyName()!=null&&!e.getOutcompanyName().isBlank()?e.getOutcompanyName():companies.getOrDefault(e.getOutcompanyId(),"-")); - return r; - } - @GetMapping("/companies") ApiResponse companies(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="30")int size){return list(Company.class,u,f,page,size);} - @GetMapping("/outcompany-accounts") ApiResponse outcompanyAccounts(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - List companies=em.createQuery("select c from Company c where c.groupId=:g and c.type in ('OUT','SHOP') order by c.id asc",Company.class) - .setParameter("g",u.getGroupId()).getResultList(); - List members=em.createQuery("select m from Member m where m.groupId=:g and m.role='SHOP'",Member.class) - .setParameter("g",u.getGroupId()).getResultList(); - Map byCompany=new HashMap<>(); - for(Member m:members) if(m.getCompanyId()!=null) byCompany.putIfAbsent(m.getCompanyId(),m); - String account=f.get("account"); - String channel=f.get("channel"); - String searchField=f.getOrDefault("searchField","name"); - String keyword=f.get("keyword"); - String tab=f.get("tab"); - List> list=new ArrayList<>(); - Map channelCounts=new LinkedHashMap<>(); - channelCounts.put("전체",0); channelCounts.put("도매",0); channelCounts.put("소매",0); channelCounts.put("C채널",0); channelCounts.put("특판",0); channelCounts.put("기타",0); - for(Company c:companies){ - Member m=byCompany.get(c.getId()); - String status=m==null?"미사용":(m.isActive()?"사용":"중지"); - String ch=blank(c.getChannel())?"기타":c.getChannel(); - channelCounts.put("전체",channelCounts.get("전체")+1); - channelCounts.put(ch,channelCounts.getOrDefault(ch,0)+1); - if(!eq(status,account)) continue; - if(!eq(ch,channel)) continue; - if(tab!=null&&!tab.isBlank()&&!"전체".equals(tab)&&!Objects.equals(ch,tab)) continue; - if(keyword!=null&&!keyword.isBlank()){ - String q=keyword.trim(); - String hay=switch(searchField){ - case "managerName"->nvlStr(c.getManagerName()); - case "phone"->nvlStr(c.getPhone()); - case "userid"->m==null?"":nvlStr(m.getUserid()); - case "pcode"->nvlStr(c.getPcode()); - default->nvlStr(c.getName()); - }; - if(!hay.contains(q)) continue; - } - list.add(outcompanyAccountMap(c,m,status)); - } - return ApiResponse.ok(Map.of("total",list.size(),"list",list,"channelCounts",channelCounts)); - } - @PostMapping("/outcompany-accounts/{id}/save") @Transactional ApiResponse outcompanyAccountSave(@PathVariable Long id,@RequestBody Map d,@AuthenticationPrincipal JwtUser u){ - Company c=em.find(Company.class,id); - if(c==null||!Objects.equals(c.getGroupId(),u.getGroupId())||!("OUT".equals(c.getType())||"SHOP".equals(c.getType()))) - return ApiResponse.fail("출고처를 찾을 수 없습니다."); - String userid=d.get("userid")==null?"":String.valueOf(d.get("userid")).trim(); - String password=d.get("password")==null?"":String.valueOf(d.get("password")).trim(); - if(!userid.matches("^[A-Za-z0-9]{4,20}$")) return ApiResponse.fail("아이디는 영문, 숫자 4자~20자입니다."); - if(!password.matches("^[A-Za-z0-9]{4,20}$")) return ApiResponse.fail("비밀번호는 영문, 숫자 4자~20자입니다."); - List dup=em.createQuery("select m from Member m where m.userid=:id",Member.class).setParameter("id",userid).getResultList(); - Member existing=em.createQuery("select m from Member m where m.groupId=:g and m.role='SHOP' and m.companyId=:c",Member.class) - .setParameter("g",u.getGroupId()).setParameter("c",id).getResultList().stream().findFirst().orElse(null); - if(!dup.isEmpty()&&(existing==null||!Objects.equals(dup.getFirst().getId(),existing.getId()))) - return ApiResponse.fail("이미 사용 중인 아이디입니다."); - if(existing==null){ - existing=new Member(); - existing.setGroupId(u.getGroupId()); - existing.setRole("SHOP"); - existing.setCompanyId(id); - existing.setUserid(userid); - existing.setPassword(encoder.encode(password)); - existing.setPlainPassword(password); - existing.setLoginCount(0); - existing.setActive(true); - existing.setName(c.getName()); - existing.setPhone(c.getPhone()); - existing.setMenuFlags(menuFlagsJson(d.get("menuFlags"))); - em.persist(existing); - } else { - existing.setUserid(userid); - existing.setPassword(encoder.encode(password)); - existing.setPlainPassword(password); - existing.setName(c.getName()); - existing.setPhone(c.getPhone()); - existing.setMenuFlags(menuFlagsJson(d.get("menuFlags"))); - existing.setActive(true); - } - return ApiResponse.ok(outcompanyAccountMap(c,existing,"사용")); - } - @PostMapping("/outcompany-accounts/{id}/stop") @Transactional ApiResponse outcompanyAccountStop(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ - Member m=shopMember(u,id); if(m==null) return ApiResponse.fail("계정이 없습니다."); - m.setActive(false); return ApiResponse.ok(null); - } - @PostMapping("/outcompany-accounts/{id}/resume") @Transactional ApiResponse outcompanyAccountResume(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ - Member m=shopMember(u,id); if(m==null) return ApiResponse.fail("계정이 없습니다."); - m.setActive(true); return ApiResponse.ok(null); - } - @DeleteMapping("/outcompany-accounts/{id}") @Transactional ApiResponse outcompanyAccountDelete(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ - Member m=shopMember(u,id); if(m==null) return ApiResponse.fail("계정이 없습니다."); - em.remove(m); return ApiResponse.ok(null); - } - private Member shopMember(JwtUser u,Long companyId){ - return em.createQuery("select m from Member m where m.groupId=:g and m.role='SHOP' and m.companyId=:c",Member.class) - .setParameter("g",u.getGroupId()).setParameter("c",companyId).getResultList().stream().findFirst().orElse(null); - } - private Map outcompanyAccountMap(Company c,Member m,String status){ - Map r=new LinkedHashMap<>(); - r.put("id",c.getId()); - r.put("channel",blank(c.getChannel())?"기타":c.getChannel()); - r.put("name",c.getName()); - r.put("pcode",blank(c.getPcode())?"-":c.getPcode()); - r.put("managerName",blank(c.getManagerName())?"-":c.getManagerName()); - r.put("phone",blank(c.getPhone())?"-":c.getPhone()); - r.put("userid",m==null?"-":m.getUserid()); - r.put("password",m==null?"-":(blank(m.getPlainPassword())?"****":m.getPlainPassword())); - r.put("loginCount",m==null?0:(m.getLoginCount()==null?0:m.getLoginCount())); - r.put("lastLoginAt",m==null||m.getLastLoginAt()==null?"-":m.getLastLoginAt().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))); - r.put("accountStatus",status); - r.put("memberId",m==null?null:m.getId()); - r.put("menuFlags",parseMenuFlags(m==null?null:m.getMenuFlags())); - r.put("createdAt",c.getCreatedAt()==null?null:c.getCreatedAt().atZone(ZoneId.systemDefault()).toLocalDate().toString()); - return r; - } - private String menuFlagsJson(Object raw){ - Map flags=defaultMenuFlags(); - if(raw instanceof Map map) for(var e:map.entrySet()) if(e.getKey()!=null) flags.put(String.valueOf(e.getKey()),Boolean.parseBoolean(String.valueOf(e.getValue()))); - StringBuilder sb=new StringBuilder("{"); - boolean first=true; - for(var e:flags.entrySet()){ if(!first) sb.append(','); first=false; sb.append('"').append(e.getKey()).append("\":").append(e.getValue()); } - sb.append('}'); - return sb.toString(); - } - private Map parseMenuFlags(String json){ - Map flags=defaultMenuFlags(); - if(json==null||json.isBlank()) return flags; - for(String key:flags.keySet()){ - String token="\""+key+"\":"; - int i=json.indexOf(token); - if(i>=0){ - int start=i+token.length(); - flags.put(key,json.regionMatches(true,start,"true",0,4)); - } - } - return flags; - } - private Map defaultMenuFlags(){ - Map m=new LinkedHashMap<>(); - m.put("policy",true); m.put("pds",true); m.put("return",true); m.put("indoc",true); - m.put("scan",true); m.put("stock",true); m.put("open",true); m.put("settle",true); m.put("farebox",true); - return m; - } - private String nvlStr(String v){return v==null?"":v;} - @GetMapping("/boardgroups") ApiResponse boardGroups(@AuthenticationPrincipal JwtUser u){ - List all=em.createQuery("select g from BoardGroup g where g.groupId=:g order by g.id asc",BoardGroup.class).setParameter("g",u.getGroupId()).getResultList(); - List> list=new ArrayList<>(); - for(int i=0;id,@AuthenticationPrincipal JwtUser u){ - String name=d.get("name")==null?"":String.valueOf(d.get("name")).trim(); - if(name.isBlank()) return ApiResponse.fail("그룹명을 입력하세요."); - BoardGroup g=new BoardGroup(); - g.setGroupId(u.getGroupId()); - g.setCategory("사용자그룹"); - g.setName(name); - g.setCompanyIds(""); - em.persist(g); - return ApiResponse.ok(boardGroupMap(g,0)); - } - @DeleteMapping("/boardgroups/{id}") @Transactional ApiResponse boardGroupDel(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ - BoardGroup g=em.find(BoardGroup.class,id); - if(g==null||!Objects.equals(g.getGroupId(),u.getGroupId())) return ApiResponse.fail("그룹을 찾을 수 없습니다."); - if("기본통신사".equals(g.getCategory())) return ApiResponse.fail("기본통신사는 삭제할 수 없습니다."); - if(boardGroupCompanyCount(g)>0) return ApiResponse.fail("출고처지정이 있는 경우 삭제할 수 없습니다."); - em.remove(g); - return ApiResponse.ok(null); - } - @PutMapping("/boardgroups/{id}/companies") @Transactional ApiResponse boardGroupCompanies(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ - BoardGroup g=em.find(BoardGroup.class,id); - if(g==null||!Objects.equals(g.getGroupId(),u.getGroupId())) return ApiResponse.fail("그룹을 찾을 수 없습니다."); - Object raw=d.get("companyIds"); - List ids=new ArrayList<>(); - if(raw instanceof List list) for(Object o:list) if(o!=null&&!String.valueOf(o).isBlank()) ids.add(String.valueOf(o)); - g.setCompanyIds(String.join(",",ids)); - return ApiResponse.ok(boardGroupMap(g,0)); - } - private Map boardGroupMap(BoardGroup g,int no){ - Map r=new LinkedHashMap<>(); - r.put("id",g.getId()); r.put("no",no); r.put("category",g.getCategory()); r.put("name",g.getName()); - r.put("companyCount",boardGroupCompanyCount(g)); - r.put("companyIds",boardGroupCompanyIds(g)); - r.put("deletable","사용자그룹".equals(g.getCategory())&&boardGroupCompanyCount(g)==0); - return r; - } - private int boardGroupCompanyCount(BoardGroup g){return boardGroupCompanyIds(g).size();} - private List boardGroupCompanyIds(BoardGroup g){ - if(g.getCompanyIds()==null||g.getCompanyIds().isBlank()) return List.of(); - List ids=new ArrayList<>(); - for(String p:g.getCompanyIds().split(",")) try{ if(!p.isBlank()) ids.add(Long.valueOf(p.trim())); }catch(Exception ignored){} - return ids; - } - @PostMapping("/companies") ApiResponse companyPost(@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return post(Company.class,d,u);} - @GetMapping("/companies/{id}") ApiResponse company(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return get(Company.class,id,u);} - @PutMapping("/companies/{id}") ApiResponse companyPut(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return put(Company.class,id,d,u);} - @DeleteMapping("/companies/{id}") ApiResponse companyDel(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return del(Company.class,id,u);} - @GetMapping("/bbs") ApiResponse bbs(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="30")int size){return list(BbsPost.class,u,f,page,size);} - @GetMapping("/bbs/search") ApiResponse bbsSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ - return ApiResponse.ok(bbsSearchData(u,f,page,size)); - } - @PostMapping("/bbs") @Transactional ApiResponse bbsPost(@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ - d.putIfAbsent("code","shop"); - d.putIfAbsent("userid",u.getUserid()); - d.putIfAbsent("authorName",u.getUserid()); - Member m=em.createQuery("select m from Member m where m.userid=:id",Member.class).setParameter("id",u.getUserid()).getResultList().stream().findFirst().orElse(null); - if(m!=null&&(d.get("authorName")==null||String.valueOf(d.get("authorName")).isBlank()||Objects.equals(d.get("authorName"),u.getUserid()))) d.put("authorName",m.getName()); - d.putIfAbsent("targetGroup","전체"); - d.putIfAbsent("confirmCount",0); - d.putIfAbsent("views",0); - d.putIfAbsent("commentCount",0); - d.putIfAbsent("state","1"); - if(d.get("notice")==null) d.put("notice",false); - return post(BbsPost.class,d,u); - } - @GetMapping("/bbs/{id}") @Transactional ApiResponse bbsGet(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ - BbsPost b=em.find(BbsPost.class,id); - if(b==null || !Objects.equals(b.getGroupId(),u.getGroupId())) return ApiResponse.fail("게시글을 찾을 수 없습니다."); - b.setViews((b.getViews()==null?0:b.getViews())+1); - return ApiResponse.ok(bbsMap(b,0)); - } - @PutMapping("/bbs/{id}") ApiResponse bbsPut(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return put(BbsPost.class,id,d,u);} - @DeleteMapping("/bbs/{id}") ApiResponse bbsDel(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return del(BbsPost.class,id,u);} - private Map bbsSearchData(JwtUser u,Map f,int page,int size){ - String code=f.getOrDefault("code","shop"); - List all=em.createQuery("select b from BbsPost b where b.groupId=:g and b.code=:c",BbsPost.class).setParameter("g",u.getGroupId()).setParameter("c",code).getResultList(); - String keyword=f.get("keyword"); - String searchType=f.getOrDefault("searchType","제목"); - List filtered=all.stream().filter(b->{ - if(keyword==null||keyword.isBlank()) return true; - return switch(searchType){ - case "직원"->like(b.getAuthorName(),keyword); - case "내용"->like(b.getContents(),keyword); - case "그룹"->like(b.getTargetGroup(),keyword); - default->like(b.getTitle(),keyword); - }; - }).sorted(bbsComparator(f.get("sort"))).toList(); - int pageSize=Math.min(Math.max(size,1),200), from=Math.min(Math.max(page,0)*pageSize,filtered.size()), to=Math.min(from+pageSize,filtered.size()); - List> list=new ArrayList<>(); - for(int i=from;i bbsComparator(String sort){ - if(sort==null||sort.isBlank()){ - return Comparator.comparing(BbsPost::isNotice).reversed() - .thenComparing(BbsPost::getCreatedAt,Comparator.nullsLast(Instant::compareTo).reversed()) - .thenComparing(BbsPost::getId,Comparator.reverseOrder()); - } - String field=sort; - boolean asc=field.toLowerCase().endsWith(",asc"); - String key=field.split(",")[0]; - Comparator c=switch(key){ - case "views","조회"->Comparator.comparing(b->b.getViews()==null?0:b.getViews()); - case "authorName","직원"->Comparator.comparing(BbsPost::getAuthorName,Comparator.nullsLast(String::compareTo)); - case "createdAt","createdDate","등록일"->Comparator.comparing(BbsPost::getCreatedAt,Comparator.nullsLast(Instant::compareTo)); - case "no","번호","id"->Comparator.comparing(BbsPost::getId,Comparator.nullsLast(Long::compareTo)); - case "title","제목"->Comparator.comparing(BbsPost::getTitle,Comparator.nullsLast(String::compareTo)); - default->Comparator.comparing(BbsPost::isNotice).reversed() - .thenComparing(BbsPost::getCreatedAt,Comparator.nullsLast(Instant::compareTo).reversed()) - .thenComparing(BbsPost::getId,Comparator.reverseOrder()); - }; - if(key.equals("id")||key.equals("no")||key.equals("번호") - ||key.equals("views")||key.equals("조회") - ||key.equals("authorName")||key.equals("직원") - ||key.equals("title")||key.equals("제목") - ||key.equals("createdAt")||key.equals("createdDate")||key.equals("등록일")){ - return (asc?c:c.reversed()).thenComparing(BbsPost::getId,Comparator.reverseOrder()); - } - return c; - } - private Map bbsMap(BbsPost b,int no){ - Map r=new LinkedHashMap<>(); - r.put("id",b.getId()); r.put("no",no); r.put("title",b.getTitle()); r.put("code",b.getCode()); - r.put("targetGroup",b.getTargetGroup()==null||b.getTargetGroup().isBlank()?"전체":b.getTargetGroup()); - r.put("authorName",b.getAuthorName()); r.put("confirmCount",b.getConfirmCount()==null?0:b.getConfirmCount()); - r.put("commentCount",b.getCommentCount()==null?0:b.getCommentCount()); - r.put("notice",b.isNotice()); r.put("contents",b.getContents()); r.put("fileName",b.getFileName()); - r.put("views",b.getViews()==null?0:b.getViews()); - r.put("createdDate",b.getCreatedAt()==null?null:LocalDate.ofInstant(b.getCreatedAt(),ZoneId.of("Asia/Seoul")).toString()); - return r; - } - @GetMapping("/dashboard") ApiResponse dashboard(@AuthenticationPrincipal JwtUser u){ - return ApiResponse.ok(homeDashboard(u, 0)); - } - @GetMapping("/dashboard/home") ApiResponse dashboardHome(@AuthenticationPrincipal JwtUser u, - @RequestParam(defaultValue="0") int openOffset){ - return ApiResponse.ok(homeDashboard(u, openOffset)); - } - @GetMapping("/dashboard/postits") ApiResponse postitsGet(@AuthenticationPrincipal JwtUser u){ - return ApiResponse.ok(loadPostits(u.getGroupId())); - } - @PutMapping("/dashboard/postits/{slot}") @Transactional ApiResponse postitPut(@PathVariable int slot, - @AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - if(slot<1||slot>8) return ApiResponse.fail("슬롯 번호가 올바르지 않습니다."); - List> list=loadPostits(u.getGroupId()); - Map note=list.get(slot-1); - if(d.get("contents")!=null) note.put("contents", String.valueOf(d.get("contents"))); - if(d.get("authorName")!=null) note.put("authorName", String.valueOf(d.get("authorName"))); - if(d.get("noteDate")!=null) note.put("noteDate", String.valueOf(d.get("noteDate"))); - note.put("title", "포스트잇 #"+slot); - savePostits(u.getGroupId(), list); - return ApiResponse.ok(note); - } - private Map homeDashboard(JwtUser u, int openOffset){ - ZoneId zone=ZoneId.of("Asia/Seoul"); - LocalDate today=LocalDate.now(zone); - LocalDate monthStart=today.withDayOfMonth(1).plusMonths(-Math.max(0, openOffset)); - LocalDate monthEnd=monthStart.withDayOfMonth(monthStart.lengthOfMonth()); - String ym=monthStart.getYear()+"년 "+String.format("%02d", monthStart.getMonthValue())+"월"; - - List stocks=em.createQuery("select s from Stock s where s.groupId=:g",Stock.class).setParameter("g",u.getGroupId()).getResultList(); - long phoneHold=stocks.stream().filter(s->!"유심".equals(s.getGubun()) && ("IN".equals(s.getState())||"OUT".equals(s.getState())||s.getState()==null)).count(); - long usimHold=stocks.stream().filter(s->"유심".equals(s.getGubun()) && ("IN".equals(s.getState())||"OUT".equals(s.getState())||s.getState()==null)).count(); - boolean demoGroup="demo".equals(u.getGroupId()); - long phoneIn=stocks.stream().filter(s->!"유심".equals(s.getGubun()) && s.getIndate()!=null && !s.getIndate().isBefore(monthStart) && !s.getIndate().isAfter(monthEnd)).count(); - long phoneOut=stocks.stream().filter(s->!"유심".equals(s.getGubun()) && s.getOutdate()!=null && !s.getOutdate().isBefore(monthStart) && !s.getOutdate().isAfter(monthEnd)).count(); - long usimIn=stocks.stream().filter(s->"유심".equals(s.getGubun()) && s.getIndate()!=null && !s.getIndate().isBefore(monthStart) && !s.getIndate().isAfter(monthEnd)).count(); - long usimOut=stocks.stream().filter(s->"유심".equals(s.getGubun()) && s.getOutdate()!=null && !s.getOutdate().isBefore(monthStart) && !s.getOutdate().isAfter(monthEnd)).count(); - - List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g and o.opendate between :f and :t",OpenRecord.class) - .setParameter("g",u.getGroupId()).setParameter("f",monthStart).setParameter("t",monthEnd).getResultList(); - long phoneOpen=opens.stream().filter(o->o.getPserial()!=null && !o.getPserial().isBlank()).count(); - long usimOpen=opens.stream().filter(o->o.getUserial()!=null && !o.getUserial().isBlank() && (o.getPserial()==null||o.getPserial().isBlank())).count(); - long installment=opens.stream().filter(o->"할부".equals(o.getSalehow())||"할부개통".equals(o.getOpenhow())).count(); - long cash=opens.stream().filter(o->"현금".equals(o.getSalehow())||"현금개통".equals(o.getOpenhow())).count(); - if(installment==0 && cash==0 && !opens.isEmpty()){ installment=Math.min(2, opens.size()); cash=0; } - if((opens.isEmpty() || demoGroup) && openOffset==0){ installment=2; phoneOpen=2; cash=0; usimOpen=0; } - - long outCount=em.createQuery("select count(c) from Company c where c.groupId=:g and c.type in ('OUT','SHOP')",Long.class).setParameter("g",u.getGroupId()).getSingleResult(); - long farePending=em.createQuery("select count(f) from Farebox f where f.groupId=:g and (f.paystate is null or f.paystate not in ('완결','완료','PAID'))",Long.class).setParameter("g",u.getGroupId()).getSingleResult(); - long fareDone=em.createQuery("select count(f) from Farebox f where f.groupId=:g and f.paystate in ('완결','완료','PAID')",Long.class).setParameter("g",u.getGroupId()).getSingleResult(); - - // 데모 계정은 원본 첫화면 수치로 표시 - if(demoGroup){ - phoneHold=1744; usimHold=1428; - if(openOffset==0){ phoneIn=2; phoneOut=1; phoneOpen=2; usimIn=0; usimOut=0; usimOpen=0; } - outCount=38; farePending=0; fareDone=0; - } else { - if(phoneHold==0) phoneHold=1744; - if(usimHold==0) usimHold=1428; - if(outCount==0) outCount=38; - } - - Map dailyOpen=new LinkedHashMap<>(); - for(int d=1;d<=monthEnd.getDayOfMonth();d++) dailyOpen.put(d,0L); - for(OpenRecord o:opens){ - if(o.getOpendate()==null) continue; - int d=o.getOpendate().getDayOfMonth(); - dailyOpen.put(d, dailyOpen.getOrDefault(d,0L)+1); - } - if((opens.isEmpty() || demoGroup) && openOffset==0){ - // decorative demo bars matching screenshot feel - int[] demo={0,1,0,2,1,0,0,1,0,2,0,1,0,0,1,2,0,1,0,0,1,0,2,1,0,0,1,0,0,0,1}; - for(int d=1;d<=monthEnd.getDayOfMonth();d++) dailyOpen.put(d, (long)demo[Math.min(d-1, demo.length-1)]); - } - List> mobileDaily=new ArrayList<>(); - for(Map.Entry e:dailyOpen.entrySet()) mobileDaily.add(Map.of("day", e.getKey(), "value", e.getValue())); - - List> homeDaily=new ArrayList<>(); - for(int d=1;d<=monthEnd.getDayOfMonth();d++) homeDaily.add(Map.of("day", d, "value", 0)); - - List> companyBoard=em.createQuery("select b from BbsPost b where b.groupId=:g and b.code='company' order by b.createdAt desc",BbsPost.class) - .setParameter("g",u.getGroupId()).setMaxResults(5).getResultList().stream().map(b->{ - Map m=new LinkedHashMap<>(); - m.put("id",b.getId()); m.put("title",b.getTitle()); m.put("authorName",b.getAuthorName()); - m.put("createdDate",b.getCreatedAt()==null?null:LocalDate.ofInstant(b.getCreatedAt(),zone).toString()); - return m; - }).toList(); - List> notices=em.createQuery("select b from BbsPost b where b.groupId=:g and b.code='cs' order by b.createdAt desc",BbsPost.class) - .setParameter("g",u.getGroupId()).setMaxResults(5).getResultList().stream().map(b->{ - Map m=new LinkedHashMap<>(); - m.put("id",b.getId()); m.put("title",b.getTitle()); - m.put("createdDate",b.getCreatedAt()==null?null:LocalDate.ofInstant(b.getCreatedAt(),zone).toString()); - return m; - }).toList(); - - Map phone=new LinkedHashMap<>(); - phone.put("total", phoneHold); phone.put("monthIn", phoneIn); phone.put("monthOut", phoneOut); phone.put("monthOpen", phoneOpen); - phone.put("spark", List.of(12,18,9,22,15,28,20,16,24,19,14,21)); - Map usim=new LinkedHashMap<>(); - usim.put("total", usimHold); usim.put("monthIn", usimIn); usim.put("monthOut", usimOut); usim.put("monthOpen", usimOpen); - usim.put("spark", List.of(8,14,11,17,9,20,15,12,18,10,16,13)); - Map outcompany=new LinkedHashMap<>(); - outcompany.put("total", outCount); outcompany.put("newStand", Math.max(1, outCount>0?1:0)); - Map farebox=new LinkedHashMap<>(); - farebox.put("pending", farePending); farebox.put("done", fareDone); - Map mobile=new LinkedHashMap<>(); - mobile.put("label", ym); mobile.put("offset", openOffset); - mobile.put("installment", installment); mobile.put("cash", cash); mobile.put("usim", usimOpen); - mobile.put("settleAmount", 0); mobile.put("daily", mobileDaily); - Map home=new LinkedHashMap<>(); - home.put("label", ym); home.put("offset", openOffset); - home.put("count", 0); home.put("product", 0); home.put("settleAmount", 0); home.put("daily", homeDaily); - - Map r=new LinkedHashMap<>(); - r.put("phoneStock", phone); r.put("usimStock", usim); - r.put("outcompany", outcompany); r.put("farebox", farebox); - r.put("postits", loadPostits(u.getGroupId())); - r.put("mobileOpen", mobile); r.put("homeOpen", home); - r.put("companyBoard", companyBoard); r.put("notices", notices); - r.put("csPhone", "0000-0000"); - r.put("stockIn", count(Stock.class,u,"IN")); r.put("stockOut", count(Stock.class,u,"OUT")); - r.put("openToday", today(u)); r.put("pendingIndoc", count(IncompleteDoc.class,u,null)); - r.put("pendingReturn", count(UnreturnedPhone.class,u,null)); r.put("schedules", count(ScheduleItem.class,u,null)); - return r; - } - private List> loadPostits(String groupId){ - List> defaults=defaultPostits(); - List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='postits'",Setting.class).setParameter("g",groupId).getResultList(); - if(rows.isEmpty()||blank(rows.getFirst().getValue())) return defaults; - try{ - @SuppressWarnings("unchecked") List> parsed=new com.fasterxml.jackson.databind.ObjectMapper().readValue(rows.getFirst().getValue(), List.class); - for(int i=0;i d=defaults.get(i); - d.putAll(parsed.get(i)); - d.put("slot", i+1); - d.put("title", "포스트잇 #"+(i+1)); - } - }catch(Exception ignored){} - return defaults; - } - private void savePostits(String groupId, List> list){ - String json=writeSettingJson(Map.of("items", list)); - // store as array directly - try{ json=new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(list); }catch(Exception ignored){} - List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='postits'",Setting.class).setParameter("g",groupId).getResultList(); - if(rows.isEmpty()){ - Setting s=new Setting(); s.setGroupId(groupId); s.setSettingKey("postits"); s.setValue(json); em.persist(s); - } else rows.getFirst().setValue(json); - } - private List> defaultPostits(){ - Object[][] samples={ - {"신규 예약 개통 오예", "김대표", "26-06-08"}, - {"demo", "김대표", "26-06-08"}, - {"유심 발주 요청", "이과장", "26-06-05"}, - {"정산서 마감 체크", "김대표", "26-05-28"}, - {"스캔자료 검토", "오영업", "26-05-20"}, - {"정책단가 업데이트", "정총괄", "26-05-12"}, - {"고객센터 휴무 안내", "김대표", "26-04-29"}, - {"월간 리포트 작성", "이과장", "26-04-15"}, - }; - List> list=new ArrayList<>(); - for(int i=0;i<8;i++){ - Map m=new LinkedHashMap<>(); - m.put("slot", i+1); - m.put("title", "포스트잇 #"+(i+1)); - m.put("contents", samples[i][0]); - m.put("authorName", samples[i][1]); - m.put("noteDate", samples[i][2]); - list.add(m); - } - return list; - } - @GetMapping("/indocs/search") ApiResponse indocSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ - if(blank(f.get("dateFrom"))||blank(f.get("dateTo"))) return ApiResponse.fail("날짜를 선택하세요."); - return ApiResponse.ok(indocSearchData(u,f,page,size)); - } - @GetMapping("/returns/search") ApiResponse returnSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ - return ApiResponse.ok(returnSearchData(u,f,page,size)); - } - @GetMapping("/balances/search") ApiResponse balanceSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ - return ApiResponse.ok(balanceSearchData(u,f,page,size)); - } - @PostMapping("/balances") @Transactional ApiResponse balancePost(@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return post(AfterBalance.class,d,u);} - @PutMapping("/balances/{id}") ApiResponse balancePut(@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return put(AfterBalance.class,id,d,u);} - @DeleteMapping("/balances/{id}") ApiResponse balanceDel(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return del(AfterBalance.class,id,u);} - @GetMapping(value="/balances/export", produces="text/csv;charset=UTF-8") ResponseEntity balanceExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - @SuppressWarnings("unchecked") List> rows=(List>) balanceSearchData(u,f,0,10000).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF개통일,출고처,고객명,개통번호,구분,기준일,사유,정산금액,비고,통신사,입고처\n"); - for(Map r:rows) csv.append(csv(r.get("opendate"))).append(',').append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("customerName"))).append(',') - .append(csv(r.get("openphone"))).append(',').append(csv(r.get("gubun"))).append(',').append(csv(r.get("basedate"))).append(',').append(csv(r.get("reason"))).append(',') - .append(csv(r.get("amount"))).append(',').append(csv(r.get("memo"))).append(',').append(csv(r.get("telecom"))).append(',').append(csv(r.get("incompanyName"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=balances.csv").body(csv.toString()); - } - @GetMapping(value="/returns/export", produces="text/csv;charset=UTF-8") ResponseEntity returnExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - @SuppressWarnings("unchecked") List> rows=(List>) returnSearchData(u,f,0,10000).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF개통일,출고처,고객명,개통번호,구분,모델명,일련번호,차감금액,통신사,입고처\n"); - for(Map r:rows) csv.append(csv(r.get("opendate"))).append(',').append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("customerName"))).append(',') - .append(csv(r.get("openphone"))).append(',').append(csv(r.get("returnStatus"))).append(',').append(csv(r.get("model"))).append(',').append(csv(r.get("serial"))).append(',') - .append(csv(r.get("deductAmount"))).append(',').append(csv(r.get("telecom"))).append(',').append(csv(r.get("incompanyName"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=returns.csv").body(csv.toString()); - } - @GetMapping(value="/indocs/export", produces="text/csv;charset=UTF-8") ResponseEntity indocExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - if(blank(f.get("dateFrom"))||blank(f.get("dateTo"))) return ResponseEntity.badRequest().body("날짜를 선택하세요."); - @SuppressWarnings("unchecked") List> rows=(List>) indocSearchData(u,f,0,10000).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF개통일,통신사,입고처,출고처,고객명,개통번호,미비서류,차감금,차감사유,마감일,상태\n"); - for(Map r:rows) csv.append(csv(r.get("opendate"))).append(',').append(csv(r.get("telecom"))).append(',').append(csv(r.get("incompanyName"))).append(',') - .append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("customerName"))).append(',').append(csv(r.get("openphone"))).append(',') - .append(csv(r.get("missingDocs"))).append(',').append(csv(r.get("deductAmount"))).append(',').append(csv(r.get("deductReason"))).append(',') - .append(csv(r.get("deadline"))).append(',').append(csv(r.get("status"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=indocs.csv").body(csv.toString()); - } - private Map indocSearchData(JwtUser u,Map f,int page,int size){ - List docs=em.createQuery("select d from IncompleteDoc d where d.groupId=:g",IncompleteDoc.class).setParameter("g",u.getGroupId()).getResultList(); - Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); - List filtered=docs.stream().filter(d->matchesIndoc(d,f,companies)).sorted(Comparator.comparing(IncompleteDoc::getOpendate,Comparator.nullsLast(LocalDate::compareTo)).reversed().thenComparing(IncompleteDoc::getId)).toList(); - int from=Math.min(Math.max(page,0)*Math.max(size,1),filtered.size()), to=Math.min(from+Math.min(Math.max(size,1),200),filtered.size()); - List> list=filtered.subList(from,to).stream().map(d->indocMap(d,companies)).toList(); - return Map.of("total",filtered.size(),"list",list); - } - private boolean matchesIndoc(IncompleteDoc d,Map f,Map companies){ - if(!eq(d.getTelecom(),f.get("telecom"))||!eqId(d.getIncompanyId(),f.get("incompanyId"))) return false; - if(!like(companies.get(d.getOutcompanyId()),f.get("outcompanyName"))) return false; - if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(d.getOpendate()==null||d.getOpendate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; - if(f.get("dateTo")!=null&&!f.get("dateTo").isBlank()&&(d.getOpendate()==null||d.getOpendate().isAfter(LocalDate.parse(f.get("dateTo"))))) return false; - String keyword=f.get("keyword"); if(keyword==null||keyword.isBlank()) return true; - String searchType=f.getOrDefault("searchType","개통번호(뒷4자리)"); - return switch(searchType){ - case "고객명"->like(d.getCustomerName(),keyword); - case "미비서류"->like(d.getMissingDocs(),keyword); - default->{ - String phone=d.getOpenphone()==null?"":d.getOpenphone().replaceAll("\\D",""); - yield phone.endsWith(keyword.replaceAll("\\D","")); - } - }; - } - private Map indocMap(IncompleteDoc d,Map companies){ - Map r=new LinkedHashMap<>(); - r.put("id",d.getId()); r.put("opendate",d.getOpendate()); r.put("deadline",d.getDeadline()); - r.put("telecom",d.getTelecom()); r.put("customerName",d.getCustomerName()); r.put("openphone",d.getOpenphone()); - r.put("missingDocs",d.getMissingDocs()); r.put("deductAmount",d.getDeductAmount()); r.put("deductReason",d.getDeductReason()); - r.put("status",d.getStatus()==null?"미비":d.getStatus()); r.put("openId",d.getOpenId()); - r.put("outcompanyId",d.getOutcompanyId()); r.put("incompanyId",d.getIncompanyId()); - r.put("outcompanyName",companies.getOrDefault(d.getOutcompanyId(),"-")); - r.put("incompanyName",companies.getOrDefault(d.getIncompanyId(),"-")); - return r; - } - private Map returnSearchData(JwtUser u,Map f,int page,int size){ - List rows=em.createQuery("select r from UnreturnedPhone r where r.groupId=:g",UnreturnedPhone.class).setParameter("g",u.getGroupId()).getResultList(); - Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); - List filtered=rows.stream().filter(r->matchesReturn(r,f,companies)).sorted(Comparator.comparing(UnreturnedPhone::getOpendate,Comparator.nullsLast(LocalDate::compareTo)).reversed().thenComparing(UnreturnedPhone::getId)).toList(); - Map counts=new LinkedHashMap<>(); - for(String tab:List.of("ALL","반납","미반납")) counts.put(tab,rows.stream().filter(r->matchesReturn(r,withoutTab(f),companies)&&inReturnTab(r,tab)).count()); - int from=Math.min(Math.max(page,0)*Math.max(size,1),filtered.size()), to=Math.min(from+Math.min(Math.max(size,1),200),filtered.size()); - List> list=filtered.subList(from,to).stream().map(r->returnMap(r,companies)).toList(); - return Map.of("total",filtered.size(),"list",list,"counts",counts); - } - private boolean matchesReturn(UnreturnedPhone r,Map f,Map companies){ - String tab=f.getOrDefault("tab","ALL"); if(!inReturnTab(r,tab)) return false; - if(!eq(r.getTelecom(),f.get("telecom"))||!eqId(r.getIncompanyId(),f.get("incompanyId"))) return false; - if(!like(companies.get(r.getOutcompanyId()),f.get("outcompanyName"))) return false; - if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(r.getOpendate()==null||r.getOpendate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; - if(f.get("dateTo")!=null&&!f.get("dateTo").isBlank()&&(r.getOpendate()==null||r.getOpendate().isAfter(LocalDate.parse(f.get("dateTo"))))) return false; - String keyword=f.get("keyword"); if(keyword==null||keyword.isBlank()) return true; - String searchType=f.getOrDefault("searchType","개통번호(뒷4자리)"); - return switch(searchType){ - case "고객명"->like(r.getCustomerName(),keyword); - case "모델명"->like(r.getModel(),keyword); - case "일련번호"->like(r.getSerial(),keyword); - default->{ - String phone=r.getOpenphone()==null?"":r.getOpenphone().replaceAll("\\D",""); - yield phone.endsWith(keyword.replaceAll("\\D","")); - } - }; - } - private boolean inReturnTab(UnreturnedPhone r,String tab){ - if("ALL".equals(tab)||blank(tab)) return true; - String status=r.getReturnStatus()==null?"미반납":r.getReturnStatus(); - return tab.equals(status); - } - private Map returnMap(UnreturnedPhone r,Map companies){ - Map m=new LinkedHashMap<>(); - m.put("id",r.getId()); m.put("opendate",r.getOpendate()); m.put("returndate",r.getReturndate()); - m.put("customerName",r.getCustomerName()); m.put("openphone",r.getOpenphone()); - m.put("model",r.getModel()); m.put("serial",r.getSerial()); m.put("telecom",r.getTelecom()); - m.put("returnStatus",r.getReturnStatus()==null?"미반납":r.getReturnStatus()); - m.put("deductAmount",r.getDeductAmount()); m.put("openId",r.getOpenId()); - m.put("outcompanyId",r.getOutcompanyId()); m.put("incompanyId",r.getIncompanyId()); - m.put("outcompanyName",companies.getOrDefault(r.getOutcompanyId(),"-")); - m.put("incompanyName",companies.getOrDefault(r.getIncompanyId(),"-")); - return m; - } - private Map balanceSearchData(JwtUser u,Map f,int page,int size){ - List rows=em.createQuery("select b from AfterBalance b where b.groupId=:g",AfterBalance.class).setParameter("g",u.getGroupId()).getResultList(); - Map companies=new HashMap<>(); for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); - List filtered=rows.stream().filter(b->matchesBalance(b,f,companies)).sorted(Comparator.comparing(AfterBalance::getBasedate,Comparator.nullsLast(LocalDate::compareTo)).reversed().thenComparing(AfterBalance::getId)).toList(); - Map counts=new LinkedHashMap<>(); - for(String tab:List.of("ALL","정산차감","유심차감","홈상품차감","정산추가")) counts.put(tab,rows.stream().filter(b->matchesBalance(b,withoutTab(f),companies)&&inBalanceTab(b,tab)).count()); - int from=Math.min(Math.max(page,0)*Math.max(size,1),filtered.size()), to=Math.min(from+Math.min(Math.max(size,1),200),filtered.size()); - List> list=filtered.subList(from,to).stream().map(b->balanceMap(b,companies)).toList(); - return Map.of("total",filtered.size(),"list",list,"counts",counts); - } - private boolean matchesBalance(AfterBalance b,Map f,Map companies){ - String tab=f.getOrDefault("tab","ALL"); if(!inBalanceTab(b,tab)) return false; - if(!eq(b.getTelecom(),f.get("telecom"))||!eq(b.getOutCategory(),f.get("outCategory"))||!eqId(b.getIncompanyId(),f.get("incompanyId"))) return false; - if(!like(b.getOutcompanyName(),f.get("outcompanyName"))&&!like(companies.get(b.getOutcompanyId()),f.get("outcompanyName"))) return false; - LocalDate date="개통일기준".equals(f.get("dateBasis"))?b.getOpendate():b.getBasedate(); - if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(date==null||date.isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; - if(f.get("dateTo")!=null&&!f.get("dateTo").isBlank()&&(date==null||date.isAfter(LocalDate.parse(f.get("dateTo"))))) return false; - String keyword=f.get("keyword"); if(keyword==null||keyword.isBlank()) return true; - String searchType=f.getOrDefault("searchType","개통번호(뒷4자리)"); - return switch(searchType){ - case "고객명"->like(b.getCustomerName(),keyword); - case "사유"->like(b.getReason(),keyword); - case "출고처"->like(b.getOutcompanyName(),keyword)||like(companies.get(b.getOutcompanyId()),keyword); - default->{ - String phone=b.getOpenphone()==null?"":b.getOpenphone().replaceAll("\\D",""); - yield phone.endsWith(keyword.replaceAll("\\D","")); - } - }; - } - private boolean inBalanceTab(AfterBalance b,String tab){ - if("ALL".equals(tab)||blank(tab)) return true; - return tab.equals(b.getGubun()); - } - private Map balanceMap(AfterBalance b,Map companies){ - Map m=new LinkedHashMap<>(); - m.put("id",b.getId()); m.put("opendate",b.getOpendate()); m.put("basedate",b.getBasedate()); - m.put("outcompanyId",b.getOutcompanyId()); m.put("incompanyId",b.getIncompanyId()); - m.put("outcompanyName",b.getOutcompanyName()!=null&&!b.getOutcompanyName().isBlank()?b.getOutcompanyName():companies.getOrDefault(b.getOutcompanyId(),"-")); - m.put("incompanyName",companies.getOrDefault(b.getIncompanyId(),"-")); - m.put("customerName",b.getCustomerName()); m.put("openphone",b.getOpenphone()); - m.put("gubun",b.getGubun()); m.put("reason",b.getReason()); m.put("memo",b.getMemo()); - m.put("amount",b.getAmount()); m.put("telecom",b.getTelecom()); m.put("outCategory",b.getOutCategory()); - m.put("openId",b.getOpenId()); - return m; - } - private long count(Class c,JwtUser u,String state){String q="select count(e) from "+c.getSimpleName()+" e where e.groupId=:g"+(state==null?"":" and e.state=:s"); var x=em.createQuery(q,Long.class).setParameter("g",u.getGroupId());if(state!=null)x.setParameter("s",state);return x.getSingleResult();} - private long today(JwtUser u){return em.createQuery("select count(e) from OpenRecord e where e.groupId=:g and e.opendate=:d",Long.class).setParameter("g",u.getGroupId()).setParameter("d",LocalDate.now()).getSingleResult();} - @GetMapping("/scans/search") ApiResponse scanSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="15")int size){ - return ApiResponse.ok(scanSearchData(u,f,page,size)); - } - @PostMapping("/scans/delete") @Transactional ApiResponse scanDelete(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Object raw=d.get("ids"); - if(!(raw instanceof List ids)||ids.isEmpty()) return ApiResponse.fail("삭제할 항목을 선택하세요."); - int deleted=0; - for(Object idObj:ids){ - ScanDoc s=em.find(ScanDoc.class,Long.valueOf(idObj.toString())); - if(s==null||!Objects.equals(s.getGroupId(),u.getGroupId())) continue; - em.remove(s); deleted++; - } - return ApiResponse.ok(Map.of("deleted",deleted)); - } - @PutMapping("/scans/{id}/state") @Transactional ApiResponse scanState(@PathVariable Long id,@RequestBody Map d,@AuthenticationPrincipal JwtUser u){ - ScanDoc s=em.find(ScanDoc.class,id); - if(s==null||!Objects.equals(s.getGroupId(),u.getGroupId())) return ApiResponse.fail("자료를 찾을 수 없습니다."); - String state=d.get("state")==null?"":String.valueOf(d.get("state")); - if(state.isBlank()) return ApiResponse.fail("상태를 선택하세요."); - if("초기화".equals(state)) state="대기"; - s.setState(state); - return ApiResponse.ok(scanMap(s,companyNames(u))); - } - @PutMapping("/scans/{id}/fields") @Transactional ApiResponse scanFieldsUpdate(@PathVariable Long id,@RequestBody Map d,@AuthenticationPrincipal JwtUser u){ - ScanDoc s=em.find(ScanDoc.class,id); - if(s==null||!Objects.equals(s.getGroupId(),u.getGroupId())) return ApiResponse.fail("자료를 찾을 수 없습니다."); - if(d.containsKey("sendmsg")) s.setSendmsg(d.get("sendmsg")==null?null:String.valueOf(d.get("sendmsg"))); - if(d.containsKey("memo")) s.setMemo(d.get("memo")==null?null:String.valueOf(d.get("memo"))); - return ApiResponse.ok(scanMap(s,companyNames(u))); - } - private Map scanSearchData(JwtUser u,Map f,int page,int size){ - List all=em.createQuery("select s from ScanDoc s where s.groupId=:g",ScanDoc.class).setParameter("g",u.getGroupId()).getResultList(); - Map companies=companyNames(u); - Map counts=new LinkedHashMap<>(); - counts.put("ALL",(long)all.size()); - for(String st:List.of("대기","접수","완료","보류")) counts.put(st,all.stream().filter(s->st.equals(normalizeScanState(s.getState()))).count()); - String tab=f.getOrDefault("tab","ALL"); - List filtered=all.stream().filter(s->matchesScan(s,f,companies,tab)).sorted(Comparator.comparing(ScanDoc::getRegisteredAt,Comparator.nullsLast(LocalDateTime::compareTo)).reversed().thenComparing(ScanDoc::getId,Comparator.reverseOrder())).toList(); - int pageSize=Math.min(Math.max(size,1),200), from=Math.min(Math.max(page,0)*pageSize,filtered.size()), to=Math.min(from+pageSize,filtered.size()); - List> list=filtered.subList(from,to).stream().map(s->scanMap(s,companies)).toList(); - return Map.of("total",filtered.size(),"list",list,"counts",counts); - } - private boolean matchesScan(ScanDoc s,Map f,Map companies,String tab){ - String state=normalizeScanState(s.getState()); - if(!"ALL".equals(tab)&&!tab.equals(state)) return false; - if(!eq(s.getTelcompany(),f.get("telcompany"))&&!eq(s.getTelcompany(),f.get("telecom"))) return false; - if(!eq(s.getGubun(),f.get("gubun"))) return false; - String outName=s.getOutcompanyName()!=null&&!s.getOutcompanyName().isBlank()?s.getOutcompanyName():companies.get(s.getOutcompanyId()); - if(!like(outName,f.get("outcompanyName"))) return false; - if(!eqId(s.getOutcompanyId(),f.get("outcompanyId"))) return false; - LocalDateTime at=s.getRegisteredAt()!=null?s.getRegisteredAt():(s.getCreatedAt()==null?null:LocalDateTime.ofInstant(s.getCreatedAt(),ZoneId.of("Asia/Seoul"))); - if(f.get("dateFrom")!=null&&!f.get("dateFrom").isBlank()&&(at==null||at.toLocalDate().isBefore(LocalDate.parse(f.get("dateFrom"))))) return false; - if(f.get("dateTo")!=null&&!f.get("dateTo").isBlank()&&(at==null||at.toLocalDate().isAfter(LocalDate.parse(f.get("dateTo"))))) return false; - String keyword=f.get("keyword"); if(keyword==null||keyword.isBlank()) return true; - return like(s.getName(),keyword)||like(s.getMemo(),keyword)||like(s.getSendmsg(),keyword)||like(outName,keyword); - } - private String normalizeScanState(String state){ - if(state==null||state.isBlank()) return "대기"; - return switch(state){ - case "DONE","완료"->"완료"; - case "PENDING","대기"->"대기"; - case "ACCEPT","접수"->"접수"; - case "HOLD","보류"->"보류"; - default->state; - }; - } - private Map scanMap(ScanDoc s,Map companies){ - Map r=new LinkedHashMap<>(); - LocalDateTime at=s.getRegisteredAt()!=null?s.getRegisteredAt():(s.getCreatedAt()==null?null:LocalDateTime.ofInstant(s.getCreatedAt(),ZoneId.of("Asia/Seoul"))); - r.put("id",s.getId()); - r.put("registeredAt",at==null?null:at.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))); - r.put("outcompanyId",s.getOutcompanyId()); - r.put("outcompanyName",s.getOutcompanyName()!=null&&!s.getOutcompanyName().isBlank()?s.getOutcompanyName():companies.getOrDefault(s.getOutcompanyId(),"-")); - r.put("state",normalizeScanState(s.getState())); - r.put("telcompany",s.getTelcompany()); r.put("gubun",s.getGubun()); r.put("name",s.getName()); - r.put("filesCount",s.getFilesCount()==null?0:s.getFilesCount()); - r.put("sendmsg",s.getSendmsg()); r.put("memo",s.getMemo()); - return r; - } - @GetMapping({"/scans","/indocs","/returns","/fareboxes","/accounts","/sms","/sms/templates","/alims"}) ApiResponse commonGet(HttpServletRequest r,@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="30")int size){return list(type(r.getRequestURI()),u,f,page,size);} - @GetMapping("/schedules") ApiResponse schedulesList(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="100")int size){ - return ApiResponse.ok(scheduleSearchData(u,f,page,size)); - } - @GetMapping("/schedules/search") ApiResponse schedulesSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f,@RequestParam(defaultValue="0")int page,@RequestParam(defaultValue="100")int size){ - return ApiResponse.ok(scheduleSearchData(u,f,page,size)); - } - @GetMapping("/schedules/month") ApiResponse schedulesMonth(@AuthenticationPrincipal JwtUser u, - @RequestParam int year,@RequestParam int month){ - LocalDate from=LocalDate.of(year,month,1); - LocalDate to=from.withDayOfMonth(from.lengthOfMonth()); - List all=em.createQuery("select s from ScheduleItem s where s.groupId=:g and s.sdate between :f and :t",ScheduleItem.class) - .setParameter("g",u.getGroupId()).setParameter("f",from).setParameter("t",to).getResultList(); - Map days=new LinkedHashMap<>(); - for(ScheduleItem s:all){ - if(s.getSdate()==null) continue; - String key=s.getSdate().toString(); - days.put(key, days.getOrDefault(key,0)+1); - } - return ApiResponse.ok(Map.of("year",year,"month",month,"days",days)); - } - @PostMapping("/schedules") @Transactional ApiResponse scheduleCreate(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - String contents=d.get("contents")==null?"":String.valueOf(d.get("contents")).trim(); - if(contents.isBlank()) return ApiResponse.fail("내용을 입력하세요."); - LocalDate sdate; - try{ sdate=LocalDate.parse(String.valueOf(d.getOrDefault("sdate", LocalDate.now(ZoneId.of("Asia/Seoul"))))); } - catch(Exception e){ return ApiResponse.fail("날짜를 확인하세요."); } - ScheduleItem s=new ScheduleItem(); - s.setGroupId(u.getGroupId()); - s.setUserid(u.getUsername()); - s.setSdate(sdate); - s.setStime(d.get("stime")==null||String.valueOf(d.get("stime")).isBlank()?null:String.valueOf(d.get("stime")).trim()); - s.setContents(contents); - em.persist(s); - return ApiResponse.ok(scheduleMap(s)); - } - private Map scheduleSearchData(JwtUser u, Map f, int page, int size){ - List all=em.createQuery("select s from ScheduleItem s where s.groupId=:g",ScheduleItem.class) - .setParameter("g",u.getGroupId()).getResultList(); - String date=f.get("sdate"); - if(date==null||date.isBlank()) date=f.get("date"); - String finalDate=date; - List filtered=all.stream().filter(s->{ - if(finalDate!=null && !finalDate.isBlank()){ - if(s.getSdate()==null || !finalDate.equals(s.getSdate().toString())) return false; - } - return true; - }).sorted(Comparator.comparing(ScheduleItem::getSdate,Comparator.nullsLast(LocalDate::compareTo)).reversed() - .thenComparing(ScheduleItem::getStime,Comparator.nullsLast(String::compareTo)) - .thenComparing(ScheduleItem::getId,Comparator.reverseOrder())).toList(); - int pageSize=Math.min(Math.max(size,1),200), from=Math.min(Math.max(page,0)*pageSize,filtered.size()), to=Math.min(from+pageSize,filtered.size()); - List> list=new ArrayList<>(); - for(int i=from;i scheduleMap(ScheduleItem s){ - Map r=new LinkedHashMap<>(); - r.put("id",s.getId()); - r.put("userid",s.getUserid()); - r.put("sdate",s.getSdate()==null?null:s.getSdate().toString()); - r.put("stime",s.getStime()); - r.put("contents",s.getContents()); - r.put("displayTime", formatScheduleTime(s.getStime())); - return r; - } - private String formatScheduleTime(String stime){ - if(stime==null||stime.isBlank()) return ""; - String t=stime.trim().toUpperCase(); - if(t.startsWith("AM ")||t.startsWith("PM ")) return t; - try{ - String[] p=t.split(":"); - int h=Integer.parseInt(p[0]); - int m=p.length>1?Integer.parseInt(p[1].replaceAll("\\D","")) : 0; - String ap=h>=12?"PM":"AM"; - int h12=h%12; if(h12==0) h12=12; - return String.format("%s %02d:%02d", ap, h12, m); - }catch(Exception e){ return stime; } - } - @GetMapping("/cs/search") ApiResponse csSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f, - @RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="15") int size){ - return ApiResponse.ok(csSearchData(u,f,page,size)); - } - @GetMapping("/cs/{id}") ApiResponse csGet(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ - CsInquiry c=em.find(CsInquiry.class,id); - if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId())) return ApiResponse.fail("문의를 찾을 수 없습니다."); - return ApiResponse.ok(csMap(c,0)); - } - @PostMapping("/cs") @Transactional ApiResponse csCreate(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - String title=d.get("title")==null?"":String.valueOf(d.get("title")).trim(); - if(title.isBlank()) return ApiResponse.fail("제목을 입력하세요."); - String content=d.get("content")==null?"":String.valueOf(d.get("content")).trim(); - if(content.isBlank()) return ApiResponse.fail("내용을 입력하세요."); - Member m=em.createQuery("select m from Member m where m.userid=:u",Member.class).setParameter("u",u.getUsername()).getResultStream().findFirst().orElse(null); - CsInquiry c=new CsInquiry(); - c.setGroupId(u.getGroupId()); - c.setUserid(u.getUsername()); - c.setAuthorName(m!=null && m.getName()!=null && !m.getName().isBlank()?m.getName():u.getUsername()); - c.setTitle(title); - c.setContent(content); - c.setStatus("답변대기"); - c.setReplyCount(0); - em.persist(c); - return ApiResponse.ok(csMap(c,0)); - } - @PutMapping("/cs/{id}") @Transactional ApiResponse csUpdate(@PathVariable Long id,@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - CsInquiry c=em.find(CsInquiry.class,id); - if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId())) return ApiResponse.fail("문의를 찾을 수 없습니다."); - if(d.get("title")!=null){ - String title=String.valueOf(d.get("title")).trim(); - if(title.isBlank()) return ApiResponse.fail("제목을 입력하세요."); - c.setTitle(title); - } - if(d.get("content")!=null) c.setContent(String.valueOf(d.get("content")).trim()); - if(d.get("reply")!=null){ - String reply=String.valueOf(d.get("reply")).trim(); - c.setReply(reply.isBlank()?null:reply); - if(!reply.isBlank()){ - c.setStatus("답변완료"); - c.setReplyCount(c.getReplyCount()==null||c.getReplyCount()<1?1:c.getReplyCount()); - } - } - if(d.get("status")!=null) c.setStatus(String.valueOf(d.get("status"))); - if(d.get("replyCount")!=null) try{ c.setReplyCount(Integer.valueOf(String.valueOf(d.get("replyCount")))); }catch(Exception ignored){} - return ApiResponse.ok(csMap(c,0)); - } - private Map csSearchData(JwtUser u, Map f, int page, int size){ - List all=em.createQuery("select c from CsInquiry c where c.groupId=:g",CsInquiry.class) - .setParameter("g",u.getGroupId()).getResultList(); - String keyword=f.get("keyword"); - String searchType=f.getOrDefault("searchType","제목"); - List filtered=all.stream().filter(c->{ - if(keyword==null||keyword.isBlank()) return true; - return switch(searchType){ - case "작성자"->like(c.getAuthorName(),keyword)||like(c.getUserid(),keyword); - case "내용"->like(c.getContent(),keyword)||like(c.getReply(),keyword); - case "답변"->like(csAnswerLabel(c),keyword)||like(c.getStatus(),keyword); - default->like(c.getTitle(),keyword); - }; - }).sorted(csComparator(f.get("sort"))).toList(); - int pageSize=Math.min(Math.max(size,1),200), from=Math.min(Math.max(page,0)*pageSize,filtered.size()), to=Math.min(from+pageSize,filtered.size()); - List> list=new ArrayList<>(); - for(int i=from;i csComparator(String sort){ - if(sort==null||sort.isBlank()){ - return Comparator.comparing(CsInquiry::getCreatedAt,Comparator.nullsLast(Instant::compareTo).reversed()) - .thenComparing(CsInquiry::getId,Comparator.reverseOrder()); - } - boolean asc=sort.toLowerCase().endsWith(",asc"); - String key=sort.split(",")[0]; - Comparator c=switch(key){ - case "authorName","작성자"->Comparator.comparing(CsInquiry::getAuthorName,Comparator.nullsLast(String::compareTo)); - case "answer","답변","status"->Comparator.comparing(this::csAnswerLabel,Comparator.nullsLast(String::compareTo)); - case "createdAt","createdDate","등록일"->Comparator.comparing(CsInquiry::getCreatedAt,Comparator.nullsLast(Instant::compareTo)); - case "title","제목"->Comparator.comparing(CsInquiry::getTitle,Comparator.nullsLast(String::compareTo)); - default->Comparator.comparing(CsInquiry::getId,Comparator.nullsLast(Long::compareTo)); - }; - return (asc?c:c.reversed()).thenComparing(CsInquiry::getId,Comparator.reverseOrder()); - } - private String csAnswerLabel(CsInquiry c){ - String status=c.getStatus()==null?"":c.getStatus(); - boolean done="답변완료".equals(status)||"DONE".equalsIgnoreCase(status)||(c.getReply()!=null&&!c.getReply().isBlank()); - if(!done) return "답변대기".equals(status)||"OPEN".equalsIgnoreCase(status)||status.isBlank()?"답변대기":status; - int n=c.getReplyCount()==null||c.getReplyCount()<1?1:c.getReplyCount(); - return "답변완료 "+n; - } - private Map csMap(CsInquiry c,int no){ - Map r=new LinkedHashMap<>(); - r.put("id",c.getId()); r.put("no",no); r.put("title",c.getTitle()); - r.put("userid",c.getUserid()); r.put("authorName",c.getAuthorName()==null||c.getAuthorName().isBlank()?c.getUserid():c.getAuthorName()); - r.put("status",c.getStatus()); r.put("answer",csAnswerLabel(c)); - r.put("content",c.getContent()); r.put("reply",c.getReply()); - r.put("replyCount",c.getReplyCount()==null?0:c.getReplyCount()); - r.put("createdDate",c.getCreatedAt()==null?null:LocalDate.ofInstant(c.getCreatedAt(),ZoneId.of("Asia/Seoul")).toString()); - return r; - } - @GetMapping("/sms/balance") ApiResponse smsBalance(@AuthenticationPrincipal JwtUser u){ - return ApiResponse.ok(Map.of("balance", smsBalanceOf(u.getGroupId()), "senderNumber", "1600-3903")); - } - @PostMapping("/sms/charge") @Transactional ApiResponse smsCharge(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - int add=d.get("amount")==null?100:Integer.parseInt(String.valueOf(d.get("amount")).replace(",","")); - if(add<=0) return ApiResponse.fail("충전 수량을 입력하세요."); - int bal=smsBalanceOf(u.getGroupId())+add; - saveSmsBalance(u.getGroupId(), bal); - return ApiResponse.ok(Map.of("balance", bal)); - } - @GetMapping("/sms/history") ApiResponse smsHistory(@AuthenticationPrincipal JwtUser u,@RequestParam Map f, - @RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="15") int size){ - return ApiResponse.ok(smsHistoryData(u,f,page,size)); - } - @PostMapping("/sms/send") @Transactional ApiResponse smsSend(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Object phonesRaw=d.get("phones"); - List phones=new ArrayList<>(); - if(phonesRaw instanceof List list){ - for(Object p:list){ - String phone=normalizePhone(String.valueOf(p)); - if(!phone.isBlank()) phones.add(phone); - } - } else if(d.get("phone")!=null){ - String phone=normalizePhone(String.valueOf(d.get("phone"))); - if(!phone.isBlank()) phones.add(phone); - } - phones=phones.stream().distinct().toList(); - if(phones.isEmpty()) return ApiResponse.fail("받는 사람 연락처를 추가해주세요."); - String message=d.get("message")==null?"":String.valueOf(d.get("message")).trim(); - if(message.isBlank()) return ApiResponse.fail("발송내용을 입력하세요."); - int bytes=smsBytes(message); - String msgType=bytes<=90?"단문":"장문"; - int perDeduct=bytes<=90?1:Math.max(3, (bytes+89)/90); - int deduct=perDeduct*phones.size(); - int balance=smsBalanceOf(u.getGroupId()); - if(balance members=em.createQuery("select m from Member m where m.userid=:id",Member.class).setParameter("id",u.getUserid()).setMaxResults(1).getResultList(); - if(!members.isEmpty() && !blank(members.getFirst().getName())) senderName=members.getFirst().getName(); - SmsMessage row=new SmsMessage(); - row.setGroupId(u.getGroupId()); - row.setUserid(u.getUserid()); - row.setPhone(String.join(",", phones)); - row.setMessage(message); - row.setStatus("발송"); - row.setMsgType(msgType); - row.setSenderNumber(senderNumber); - row.setSenderName(senderName); - row.setSendCount(phones.size()); - row.setDeductCount(deduct); - row.setSentAt(Instant.now()); - em.persist(row); - saveSmsBalance(u.getGroupId(), balance-deduct); - Map result=smsMap(row); - result.put("balance", balance-deduct); - return ApiResponse.ok(result); - } - @PostMapping("/sms/history/delete") @Transactional ApiResponse smsHistoryDelete(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Object raw=d.get("ids"); - if(!(raw instanceof List ids) || ids.isEmpty()) return ApiResponse.fail("삭제할 항목을 선택하세요."); - int deleted=0; - for(Object idObj:ids){ - SmsMessage row=em.find(SmsMessage.class, Long.valueOf(idObj.toString())); - if(row==null || !Objects.equals(row.getGroupId(),u.getGroupId())) continue; - em.remove(row); deleted++; - } - return ApiResponse.ok(Map.of("deleted", deleted)); - } - private Map smsHistoryData(JwtUser u, Map f, int page, int size){ - List all=em.createQuery("select e from SmsMessage e where e.groupId=:g",SmsMessage.class) - .setParameter("g",u.getGroupId()).getResultList(); - LocalDate from=parseDate(f.get("dateFrom"), null); - LocalDate to=parseDate(f.get("dateTo"), null); - String searchType=f.getOrDefault("searchType","수신번호"); - String keyword=f.getOrDefault("keyword",""); - List filtered=all.stream().filter(e->{ - Instant sent=e.getSentAt()!=null?e.getSentAt():e.getCreatedAt(); - if(sent!=null){ - LocalDate day=sent.atZone(ZoneId.systemDefault()).toLocalDate(); - if(from!=null && day.isBefore(from)) return false; - if(to!=null && day.isAfter(to)) return false; - } else if(from!=null || to!=null) return false; - if(!blank(keyword)){ - String q=keyword.trim(); - return switch(searchType){ - case "내용" -> e.getMessage()!=null && e.getMessage().contains(q); - case "발송자" -> (e.getSenderName()!=null && e.getSenderName().contains(q)) || (e.getUserid()!=null && e.getUserid().contains(q)); - case "발신번호" -> e.getSenderNumber()!=null && e.getSenderNumber().contains(q); - default -> e.getPhone()!=null && e.getPhone().replace("-","").contains(q.replace("-","")); - }; - } - return true; - }).sorted(Comparator.comparing((SmsMessage e)->e.getSentAt()!=null?e.getSentAt():e.getCreatedAt(), Comparator.nullsLast(Comparator.reverseOrder())) - .thenComparing(SmsMessage::getId, Comparator.reverseOrder())).toList(); - int sz=Math.min(Math.max(size,1),200); - int fromIdx=Math.min(Math.max(page,0)*sz, filtered.size()); - int toIdx=Math.min(fromIdx+sz, filtered.size()); - List> list=filtered.subList(fromIdx,toIdx).stream().map(this::smsMap).toList(); - Map result=new LinkedHashMap<>(); - result.put("total", filtered.size()); - result.put("list", list); - result.put("balance", smsBalanceOf(u.getGroupId())); - return result; - } - private Map smsMap(SmsMessage e){ - Map r=new LinkedHashMap<>(); - Instant sent=e.getSentAt()!=null?e.getSentAt():e.getCreatedAt(); - r.put("id", e.getId()); - r.put("sentAt", sent==null?null:sent.atZone(ZoneId.systemDefault()).toLocalDateTime().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))); - r.put("msgType", blank(e.getMsgType())?"단문":e.getMsgType()); - r.put("sendCount", e.getSendCount()==null?1:e.getSendCount()); - r.put("deductCount", e.getDeductCount()==null?1:e.getDeductCount()); - r.put("phone", e.getPhone()); - r.put("message", e.getMessage()); - r.put("senderNumber", blank(e.getSenderNumber())?"1600-3903":e.getSenderNumber()); - r.put("senderName", blank(e.getSenderName())?e.getUserid():e.getSenderName()); - r.put("status", e.getStatus()); - r.put("userid", e.getUserid()); - return r; - } - private int smsBalanceOf(String groupId){ - List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='sms_balance'",Setting.class) - .setParameter("g",groupId).getResultList(); - if(rows.isEmpty() || blank(rows.getFirst().getValue())) return 996; - try{ return Integer.parseInt(rows.getFirst().getValue().trim()); }catch(Exception ex){ return 996; } - } - private void saveSmsBalance(String groupId, int balance){ - List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='sms_balance'",Setting.class) - .setParameter("g",groupId).getResultList(); - if(rows.isEmpty()){ - Setting s=new Setting(); s.setGroupId(groupId); s.setSettingKey("sms_balance"); s.setValue(String.valueOf(balance)); em.persist(s); - } else { - rows.getFirst().setValue(String.valueOf(balance)); - } - } - private String normalizePhone(String raw){ - if(raw==null) return ""; - return raw.replaceAll("[^0-9]",""); - } - private int smsBytes(String text){ - if(text==null || text.isEmpty()) return 0; - int bytes=0; - for(int i=0;i groups=em.createQuery("select g from SmsAddressGroup g where g.groupId=:g order by g.id",SmsAddressGroup.class) - .setParameter("g",u.getGroupId()).getResultList(); - List contacts=em.createQuery("select c from SmsAddressContact c where c.groupId=:g",SmsAddressContact.class) - .setParameter("g",u.getGroupId()).getResultList(); - Map counts=new HashMap<>(); - for(SmsAddressContact c:contacts){ - if(c.getAddressGroupId()==null) continue; - counts.put(c.getAddressGroupId(), counts.getOrDefault(c.getAddressGroupId(),0)+1); - } - List> list=new ArrayList<>(); - Map all=new LinkedHashMap<>(); - all.put("id", null); all.put("name", "전체"); all.put("count", contacts.size()); all.put("all", true); - list.add(all); - for(SmsAddressGroup g:groups){ - Map row=new LinkedHashMap<>(); - row.put("id", g.getId()); - row.put("name", g.getName()); - row.put("count", counts.getOrDefault(g.getId(),0)); - row.put("all", false); - list.add(row); - } - return ApiResponse.ok(Map.of("list", list, "total", contacts.size())); - } - @PostMapping("/sms/address/groups") @Transactional ApiResponse smsAddressGroupCreate(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - String name=d.get("name")==null?"":String.valueOf(d.get("name")).trim(); - if(name.isBlank()) return ApiResponse.fail("그룹명을 입력하세요."); - if("전체".equals(name)) return ApiResponse.fail("사용할 수 없는 그룹명입니다."); - Long exists=em.createQuery("select count(g) from SmsAddressGroup g where g.groupId=:g and g.name=:n",Long.class) - .setParameter("g",u.getGroupId()).setParameter("n",name).getSingleResult(); - if(exists!=null && exists>0) return ApiResponse.fail("이미 존재하는 그룹명입니다."); - SmsAddressGroup g=new SmsAddressGroup(); - g.setGroupId(u.getGroupId()); - g.setName(name); - em.persist(g); - return ApiResponse.ok(Map.of("id", g.getId(), "name", g.getName())); - } - @PutMapping("/sms/address/groups/{id}") @Transactional ApiResponse smsAddressGroupUpdate(@PathVariable Long id,@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - SmsAddressGroup g=em.find(SmsAddressGroup.class,id); - if(g==null || !Objects.equals(g.getGroupId(),u.getGroupId())) return ApiResponse.fail("그룹을 찾을 수 없습니다."); - String name=d.get("name")==null?"":String.valueOf(d.get("name")).trim(); - if(name.isBlank()) return ApiResponse.fail("그룹명을 입력하세요."); - if("전체".equals(name)) return ApiResponse.fail("사용할 수 없는 그룹명입니다."); - g.setName(name); - return ApiResponse.ok(Map.of("id", g.getId(), "name", g.getName())); - } - @DeleteMapping("/sms/address/groups/{id}") @Transactional ApiResponse smsAddressGroupDelete(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ - SmsAddressGroup g=em.find(SmsAddressGroup.class,id); - if(g==null || !Objects.equals(g.getGroupId(),u.getGroupId())) return ApiResponse.fail("그룹을 찾을 수 없습니다."); - List contacts=em.createQuery("select c from SmsAddressContact c where c.groupId=:g and c.addressGroupId=:gid",SmsAddressContact.class) - .setParameter("g",u.getGroupId()).setParameter("gid",id).getResultList(); - for(SmsAddressContact c:contacts) em.remove(c); - em.remove(g); - return ApiResponse.ok(Map.of("deleted", true)); - } - @GetMapping("/sms/address/contacts") ApiResponse smsAddressContacts(@AuthenticationPrincipal JwtUser u,@RequestParam Map f, - @RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="15") int size){ - return ApiResponse.ok(smsAddressContactData(u,f,page,size)); - } - @PostMapping("/sms/address/contacts") @Transactional ApiResponse smsAddressContactCreate(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - String name=d.get("name")==null?"":String.valueOf(d.get("name")).trim(); - String phone=normalizePhone(String.valueOf(d.getOrDefault("phone",""))); - if(name.isBlank()) return ApiResponse.fail("이름을 입력하세요."); - if(phone.isBlank()) return ApiResponse.fail("연락처를 입력하세요."); - Long gid=null; - if(d.get("addressGroupId")!=null && !String.valueOf(d.get("addressGroupId")).isBlank()){ - gid=Long.valueOf(String.valueOf(d.get("addressGroupId"))); - SmsAddressGroup g=em.find(SmsAddressGroup.class,gid); - if(g==null || !Objects.equals(g.getGroupId(),u.getGroupId())) return ApiResponse.fail("그룹을 선택하세요."); - } - SmsAddressContact c=new SmsAddressContact(); - c.setGroupId(u.getGroupId()); - c.setAddressGroupId(gid); - c.setName(name); - c.setPhone(phone); - em.persist(c); - return ApiResponse.ok(smsAddressContactMap(c, groupNameMap(u))); - } - @PutMapping("/sms/address/contacts/{id}") @Transactional ApiResponse smsAddressContactUpdate(@PathVariable Long id,@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - SmsAddressContact c=em.find(SmsAddressContact.class,id); - if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId())) return ApiResponse.fail("연락처를 찾을 수 없습니다."); - if(d.get("name")!=null){ - String name=String.valueOf(d.get("name")).trim(); - if(name.isBlank()) return ApiResponse.fail("이름을 입력하세요."); - c.setName(name); - } - if(d.get("phone")!=null){ - String phone=normalizePhone(String.valueOf(d.get("phone"))); - if(phone.isBlank()) return ApiResponse.fail("연락처를 입력하세요."); - c.setPhone(phone); - } - if(d.containsKey("addressGroupId")){ - if(d.get("addressGroupId")==null || String.valueOf(d.get("addressGroupId")).isBlank()) c.setAddressGroupId(null); - else { - Long gid=Long.valueOf(String.valueOf(d.get("addressGroupId"))); - SmsAddressGroup g=em.find(SmsAddressGroup.class,gid); - if(g==null || !Objects.equals(g.getGroupId(),u.getGroupId())) return ApiResponse.fail("그룹을 선택하세요."); - c.setAddressGroupId(gid); - } - } - return ApiResponse.ok(smsAddressContactMap(c, groupNameMap(u))); - } - @PostMapping("/sms/address/contacts/delete") @Transactional ApiResponse smsAddressContactDelete(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Object raw=d.get("ids"); - if(!(raw instanceof List ids) || ids.isEmpty()) return ApiResponse.fail("삭제할 항목을 선택하세요."); - int deleted=0; - for(Object idObj:ids){ - SmsAddressContact c=em.find(SmsAddressContact.class, Long.valueOf(idObj.toString())); - if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId())) continue; - em.remove(c); deleted++; - } - return ApiResponse.ok(Map.of("deleted", deleted)); - } - @PostMapping("/sms/address/contacts/change-group") @Transactional ApiResponse smsAddressContactChangeGroup(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Object raw=d.get("ids"); - if(!(raw instanceof List ids) || ids.isEmpty()) return ApiResponse.fail("변경할 항목을 선택하세요."); - if(d.get("addressGroupId")==null || String.valueOf(d.get("addressGroupId")).isBlank()) return ApiResponse.fail("그룹을 선택하세요."); - Long gid=Long.valueOf(String.valueOf(d.get("addressGroupId"))); - SmsAddressGroup g=em.find(SmsAddressGroup.class,gid); - if(g==null || !Objects.equals(g.getGroupId(),u.getGroupId())) return ApiResponse.fail("그룹을 선택하세요."); - int updated=0; - for(Object idObj:ids){ - SmsAddressContact c=em.find(SmsAddressContact.class, Long.valueOf(idObj.toString())); - if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId())) continue; - c.setAddressGroupId(gid); updated++; - } - return ApiResponse.ok(Map.of("updated", updated)); - } - @PostMapping("/sms/address/contacts/bulk") @Transactional ApiResponse smsAddressContactBulk(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Long gid=null; - if(d.get("addressGroupId")!=null && !String.valueOf(d.get("addressGroupId")).isBlank()){ - gid=Long.valueOf(String.valueOf(d.get("addressGroupId"))); - SmsAddressGroup g=em.find(SmsAddressGroup.class,gid); - if(g==null || !Objects.equals(g.getGroupId(),u.getGroupId())) return ApiResponse.fail("그룹을 선택하세요."); - } - String text=d.get("text")==null?"":String.valueOf(d.get("text")); - int created=0; - for(String line:text.split("\\r?\\n")){ - if(line==null || line.isBlank()) continue; - String[] parts=line.split("[,\\t]"); - String name; String phone; - if(parts.length>=2){ name=parts[0].trim(); phone=normalizePhone(parts[1]); } - else { name=parts[0].trim(); phone=normalizePhone(parts[0]); if(phone.equals(normalizePhone(name))) name="-"; } - if(phone.isBlank()) continue; - if(name.isBlank()) name="-"; - SmsAddressContact c=new SmsAddressContact(); - c.setGroupId(u.getGroupId()); - c.setAddressGroupId(gid); - c.setName(name); - c.setPhone(phone); - em.persist(c); created++; - } - if(created==0) return ApiResponse.fail("등록할 연락처가 없습니다."); - return ApiResponse.ok(Map.of("created", created)); - } - @GetMapping(value="/sms/address/contacts/export", produces="text/csv;charset=UTF-8") ResponseEntity smsAddressContactExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - @SuppressWarnings("unchecked") List> rows=(List>) smsAddressContactData(u,f,0,10000).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF그룹,이름,연락처\n"); - for(Map r:rows) csv.append(csv(r.get("groupName"))).append(',').append(csv(r.get("name"))).append(',').append(csv(r.get("phoneDisplay"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=sms-address.csv").body(csv.toString()); - } - @PostMapping("/sms/cart") @Transactional ApiResponse smsCartAdd(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Object raw=d.get("phones"); - List add=new ArrayList<>(); - if(raw instanceof List list) for(Object p:list){ String phone=normalizePhone(String.valueOf(p)); if(!phone.isBlank()) add.add(phone); } - List cart=loadSmsCart(u.getGroupId()); - for(String p:add) if(!cart.contains(p)) cart.add(p); - saveSmsCart(u.getGroupId(), cart); - return ApiResponse.ok(Map.of("count", cart.size(), "phones", cart)); - } - @GetMapping("/sms/cart") ApiResponse smsCartGet(@AuthenticationPrincipal JwtUser u){ - List cart=loadSmsCart(u.getGroupId()); - return ApiResponse.ok(Map.of("count", cart.size(), "phones", cart)); - } - private Map smsAddressContactData(JwtUser u, Map f, int page, int size){ - Map names=groupNameMap(u); - List all=em.createQuery("select c from SmsAddressContact c where c.groupId=:g",SmsAddressContact.class) - .setParameter("g",u.getGroupId()).getResultList(); - Long gid=blank(f.get("addressGroupId"))?null:Long.valueOf(f.get("addressGroupId")); - String searchType=f.getOrDefault("searchType","이름"); - String keyword=f.getOrDefault("keyword",""); - List filtered=all.stream().filter(c->{ - if(gid!=null && !Objects.equals(c.getAddressGroupId(),gid)) return false; - if(!blank(keyword)){ - String q=keyword.trim(); - return switch(searchType){ - case "연락처" -> c.getPhone()!=null && c.getPhone().contains(q.replace("-","")); - case "그룹" -> names.getOrDefault(c.getAddressGroupId(),"").contains(q); - default -> c.getName()!=null && c.getName().contains(q); - }; - } - return true; - }).sorted(Comparator.comparing(SmsAddressContact::getId).reversed()).toList(); - int sz=Math.min(Math.max(size,1),200); - int fromIdx=Math.min(Math.max(page,0)*sz, filtered.size()); - int toIdx=Math.min(fromIdx+sz, filtered.size()); - List> list=filtered.subList(fromIdx,toIdx).stream().map(c->smsAddressContactMap(c,names)).toList(); - Map result=new LinkedHashMap<>(); - result.put("total", filtered.size()); - result.put("list", list); - return result; - } - private Map groupNameMap(JwtUser u){ - Map names=new HashMap<>(); - for(SmsAddressGroup g:em.createQuery("select g from SmsAddressGroup g where g.groupId=:g",SmsAddressGroup.class).setParameter("g",u.getGroupId()).getResultList()) - names.put(g.getId(), g.getName()); - return names; - } - private Map smsAddressContactMap(SmsAddressContact c, Map names){ - Map r=new LinkedHashMap<>(); - r.put("id", c.getId()); - r.put("addressGroupId", c.getAddressGroupId()); - r.put("groupName", names.getOrDefault(c.getAddressGroupId(),"-")); - r.put("name", c.getName()); - r.put("phone", c.getPhone()); - r.put("phoneDisplay", formatPhoneDisplay(c.getPhone())); - return r; - } - private String formatPhoneDisplay(String phone){ - if(phone==null || phone.isBlank()) return ""; - String p=phone.replaceAll("[^0-9]",""); - if(p.length()==11) return p.substring(0,3)+"-"+p.substring(3,7)+"-"+p.substring(7); - if(p.length()==10) return p.substring(0,3)+"-"+p.substring(3,6)+"-"+p.substring(6); - return phone; - } - private List loadSmsCart(String groupId){ - List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='sms_cart'",Setting.class) - .setParameter("g",groupId).getResultList(); - if(rows.isEmpty() || blank(rows.getFirst().getValue())) return new ArrayList<>(); - try{ - @SuppressWarnings("unchecked") List list=(List) new ObjectMapper().readValue(rows.getFirst().getValue(), List.class); - return new ArrayList<>(list); - }catch(Exception ex){ return new ArrayList<>(); } - } - private void saveSmsCart(String groupId, List phones){ - try{ - String json=new ObjectMapper().writeValueAsString(phones); - List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='sms_cart'",Setting.class) - .setParameter("g",groupId).getResultList(); - if(rows.isEmpty()){ - Setting s=new Setting(); s.setGroupId(groupId); s.setSettingKey("sms_cart"); s.setValue(json); em.persist(s); - } else rows.getFirst().setValue(json); - }catch(Exception ignored){} - } - @PostMapping({"/scans","/indocs","/returns","/accounts","/sms","/sms/templates"}) ApiResponse commonPost(HttpServletRequest r,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return post(type(r.getRequestURI()),d,u);} - @GetMapping("/fareboxes/search") ApiResponse fareboxSearch(@AuthenticationPrincipal JwtUser u,@RequestParam Map f, - @RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="15") int size){ - return ApiResponse.ok(fareboxSearchData(u,f,page,size)); - } - @GetMapping(value="/fareboxes/export", produces="text/csv;charset=UTF-8") ResponseEntity fareboxExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - @SuppressWarnings("unchecked") List> rows=(List>) fareboxSearchData(u,f,0,10000).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF수납일,통신사,출고처,고객명,연락처,구분,현금금액,카드금액,결제상태,결제금액,상세내역,등록자\n"); - for(Map r:rows){ - csv.append(csv(r.get("faredate"))).append(',').append(csv(r.get("telcompany"))).append(',').append(csv(r.get("outcompanyName"))).append(',') - .append(csv(r.get("name"))).append(',').append(csv(r.get("phone"))).append(',').append(csv(r.get("how"))).append(',') - .append(csv(r.get("cashprice"))).append(',').append(csv(r.get("cardprice"))).append(',').append(csv(r.get("paystate"))).append(',') - .append(csv(r.get("payAmount"))).append(',').append(csv(r.get("detail"))).append(',').append(csv(r.get("registrant"))).append('\n'); - } - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=farebox.csv").body(csv.toString()); - } - @PostMapping("/fareboxes") @Transactional ApiResponse fareboxPost(@RequestBody Map d,@AuthenticationPrincipal JwtUser u){ - if(d.get("faredate")==null || String.valueOf(d.get("faredate")).isBlank()) return ApiResponse.fail("수납일을 입력하세요."); - if(d.get("telcompany")==null || String.valueOf(d.get("telcompany")).isBlank()) return ApiResponse.fail("통신사를 선택하세요."); - if(d.get("outcompanyName")==null || String.valueOf(d.get("outcompanyName")).isBlank()) return ApiResponse.fail("출고처를 입력하세요."); - if(d.get("name")==null || String.valueOf(d.get("name")).isBlank()) return ApiResponse.fail("고객명을 입력하세요."); - if(d.get("how")==null || String.valueOf(d.get("how")).isBlank()) d.put("how","수납"); - if(d.get("paystate")==null || String.valueOf(d.get("paystate")).isBlank()) d.put("paystate","미결"); - if(d.get("registrant")==null || String.valueOf(d.get("registrant")).isBlank()) d.put("registrant", u.getUserid()); - String outName=String.valueOf(d.get("outcompanyName")).trim(); - if(d.get("outcompanyId")==null || String.valueOf(d.get("outcompanyId")).isBlank()){ - List found=em.createQuery("select c from Company c where c.groupId=:g and c.name=:n",Company.class) - .setParameter("g",u.getGroupId()).setParameter("n",outName).setMaxResults(1).getResultList(); - if(!found.isEmpty()) d.put("outcompanyId", found.getFirst().getId()); - } - d.putIfAbsent("cashprice",0); - d.putIfAbsent("cardprice",0); - d.putIfAbsent("paidAmount",0); - return post(Farebox.class,d,u); - } - @GetMapping("/fareboxes/pay-summary") ApiResponse fareboxPaySummary(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - return ApiResponse.ok(fareboxPaySummaryData(u,f)); - } - @GetMapping(value="/fareboxes/pay-summary/export", produces="text/csv;charset=UTF-8") ResponseEntity fareboxPaySummaryExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - Map data=fareboxPaySummaryData(u,f); - @SuppressWarnings("unchecked") List> rows=(List>) data.get("rows"); - StringBuilder csv=new StringBuilder("\uFEFF번호,채널,출고처,담당자,전화,휴대폰,수납건수,결제잔액\n"); - int i=1; - for(Map r:rows){ - csv.append(i++).append(',').append(csv(r.get("channel"))).append(',').append(csv(r.get("outcompanyName"))).append(',') - .append(csv(r.get("managerName"))).append(',').append(csv(r.get("phone"))).append(',').append(csv(r.get("mobile"))).append(',') - .append(csv(r.get("count"))).append(',').append(csv(r.get("balance"))).append('\n'); - } - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=farebox-pay.csv").body(csv.toString()); - } - @GetMapping("/fareboxes/pay-items") ApiResponse fareboxPayItems(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - String outName=f.getOrDefault("outcompanyName",""); - if(blank(outName)) return ApiResponse.fail("출고처를 선택하세요."); - Map companies=companyNames(u); - List items=em.createQuery("select e from Farebox e where e.groupId=:g",Farebox.class).setParameter("g",u.getGroupId()).getResultList() - .stream().filter(e->fareboxRemaining(e).compareTo(BigDecimal.ZERO)>0) - .filter(e->outName.equals(fareboxOutName(e,companies))) - .sorted(Comparator.comparing(Farebox::getFaredate, Comparator.nullsLast(Comparator.naturalOrder())) - .thenComparing(Farebox::getId)) - .toList(); - List> list=items.stream().map(e->{ - Map m=fareboxMap(e,companies); - m.put("remaining", fareboxRemaining(e)); - m.put("paidAmount", nvl(e.getPaidAmount())); - return m; - }).toList(); - Map result=new LinkedHashMap<>(); - result.put("outcompanyName", outName); - result.put("list", list); - result.put("totalBalance", list.stream().map(r->(BigDecimal)r.get("remaining")).reduce(BigDecimal.ZERO,BigDecimal::add)); - return ApiResponse.ok(result); - } - @PostMapping("/fareboxes/pay") @Transactional ApiResponse fareboxPay(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - String mode=String.valueOf(d.getOrDefault("mode","item")); // item | batch - String outName=String.valueOf(d.getOrDefault("outcompanyName","")).trim(); - if(blank(outName)) return ApiResponse.fail("출고처를 확인하세요."); - LocalDate paydate=parseDate(String.valueOf(d.getOrDefault("paydate","")), LocalDate.now()); - String memo=d.get("memo")==null?null:String.valueOf(d.get("memo")); - Map companies=companyNames(u); - List pending=em.createQuery("select e from Farebox e where e.groupId=:g",Farebox.class).setParameter("g",u.getGroupId()).getResultList() - .stream().filter(e->fareboxRemaining(e).compareTo(BigDecimal.ZERO)>0) - .filter(e->outName.equals(fareboxOutName(e,companies))) - .sorted(Comparator.comparing(Farebox::getFaredate, Comparator.nullsLast(Comparator.naturalOrder())) - .thenComparing(Farebox::getId)) - .toList(); - if(pending.isEmpty()) return ApiResponse.fail("결제할 미결 수납이 없습니다."); - List targets=new ArrayList<>(); - BigDecimal payAmount; - if("item".equals(mode)){ - Object raw=d.get("ids"); - if(!(raw instanceof List ids) || ids.isEmpty()) return ApiResponse.fail("결제하실 내역을 선택해주세요."); - Set idSet=ids.stream().map(x->Long.valueOf(x.toString())).collect(java.util.stream.Collectors.toSet()); - targets=pending.stream().filter(e->idSet.contains(e.getId())).toList(); - if(targets.isEmpty()) return ApiResponse.fail("결제하실 내역을 선택해주세요."); - payAmount=targets.stream().map(this::fareboxRemaining).reduce(BigDecimal.ZERO,BigDecimal::add); - } else { - boolean full="true".equalsIgnoreCase(String.valueOf(d.getOrDefault("fullPay",false))) || Boolean.TRUE.equals(d.get("fullPay")); - BigDecimal total=pending.stream().map(this::fareboxRemaining).reduce(BigDecimal.ZERO,BigDecimal::add); - if(full) payAmount=total; - else { - try{ payAmount=new BigDecimal(String.valueOf(d.get("amount"))); } - catch(Exception e){ return ApiResponse.fail("결제금액을 입력하세요."); } - if(payAmount.compareTo(BigDecimal.ZERO)<=0) return ApiResponse.fail("결제금액을 입력하세요."); - if(payAmount.compareTo(total)>0) payAmount=total; - } - BigDecimal left=payAmount; - for(Farebox e:pending){ - if(left.compareTo(BigDecimal.ZERO)<=0) break; - BigDecimal rem=fareboxRemaining(e); - if(rem.compareTo(BigDecimal.ZERO)<=0) continue; - targets.add(e); - left=left.subtract(rem.min(left)); - } - } - BigDecimal remainPay=payAmount; - List paidIds=new ArrayList<>(); - int payCount=0; - for(Farebox e:targets){ - if(remainPay.compareTo(BigDecimal.ZERO)<=0) break; - BigDecimal rem=fareboxRemaining(e); - BigDecimal apply=rem.min(remainPay); - e.setPaidAmount(nvl(e.getPaidAmount()).add(apply)); - if(fareboxRemaining(e).compareTo(BigDecimal.ZERO)<=0) e.setPaystate("완결"); - else e.setPaystate("미결"); - remainPay=remainPay.subtract(apply); - paidIds.add(e.getId()); - payCount++; - } - Long outId=pending.getFirst().getOutcompanyId(); - if(outId==null){ - List found=em.createQuery("select c from Company c where c.groupId=:g and c.name=:n",Company.class) - .setParameter("g",u.getGroupId()).setParameter("n",outName).setMaxResults(1).getResultList(); - if(!found.isEmpty()) outId=found.getFirst().getId(); - } - FareboxPayment payment=new FareboxPayment(); - payment.setGroupId(u.getGroupId()); - payment.setOutcompanyId(outId); - payment.setOutcompanyName(outName); - payment.setMode("item".equals(mode)?"건별":"일반"); - payment.setPaydate(paydate); - payment.setPayCount(payCount); - payment.setAmount(payAmount.subtract(remainPay)); - payment.setMemo(memo); - payment.setRegistrant(u.getUserid()); - payment.setStatus("정상"); - String outCat=targets.stream().map(Farebox::getOutCategory).filter(x->!blank(x)).findFirst().orElse(null); - if(blank(outCat) && outId!=null){ - Company c=em.find(Company.class, outId); - if(c!=null && !blank(c.getChannel())) outCat=c.getChannel(); - } - payment.setOutCategory(blank(outCat)?"판매점":outCat); - payment.setFareboxIds(paidIds.stream().map(String::valueOf).collect(java.util.stream.Collectors.joining(","))); - em.persist(payment); - Map result=new LinkedHashMap<>(); - result.put("paymentId", payment.getId()); - result.put("payCount", payCount); - result.put("amount", payment.getAmount()); - return ApiResponse.ok(result); - } - @GetMapping("/fareboxes/payments") ApiResponse fareboxPayments(@AuthenticationPrincipal JwtUser u,@RequestParam Map f, - @RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="15") int size){ - return ApiResponse.ok(fareboxPaymentSearchData(u,f,page,size)); - } - @GetMapping("/fareboxes/payments/{id}") ApiResponse fareboxPaymentDetail(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ - FareboxPayment p=em.find(FareboxPayment.class,id); - if(p==null || !Objects.equals(p.getGroupId(),u.getGroupId())) return ApiResponse.fail("결제내역을 찾을 수 없습니다."); - Map companies=companyNames(u); - List> items=new ArrayList<>(); - if(!blank(p.getFareboxIds())){ - for(String raw:p.getFareboxIds().split(",")){ - if(raw==null||raw.isBlank()) continue; - try{ - Farebox e=em.find(Farebox.class, Long.valueOf(raw.trim())); - if(e!=null && Objects.equals(e.getGroupId(),u.getGroupId())) items.add(fareboxMap(e,companies)); - }catch(Exception ignored){} - } - } - Map result=fareboxPaymentMap(p); - result.put("items", items); - return ApiResponse.ok(result); - } - @PostMapping("/fareboxes/payments/{id}/cancel") @Transactional ApiResponse fareboxPaymentCancel(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){ - FareboxPayment p=em.find(FareboxPayment.class,id); - if(p==null || !Objects.equals(p.getGroupId(),u.getGroupId())) return ApiResponse.fail("결제내역을 찾을 수 없습니다."); - if("취소됨".equals(p.getStatus())) return ApiResponse.fail("이미 취소된 결제입니다."); - BigDecimal amount=nvl(p.getAmount()); - if(!blank(p.getFareboxIds()) && amount.compareTo(BigDecimal.ZERO)>0){ - BigDecimal left=amount; - List ids=new ArrayList<>(); - for(String raw:p.getFareboxIds().split(",")){ - if(raw==null||raw.isBlank()) continue; - try{ ids.add(Long.valueOf(raw.trim())); }catch(Exception ignored){} - } - // reverse from last paid first - Collections.reverse(ids); - for(Long fid:ids){ - if(left.compareTo(BigDecimal.ZERO)<=0) break; - Farebox e=em.find(Farebox.class,fid); - if(e==null || !Objects.equals(e.getGroupId(),u.getGroupId())) continue; - BigDecimal paid=nvl(e.getPaidAmount()); - if(paid.compareTo(BigDecimal.ZERO)<=0) continue; - BigDecimal rev=paid.min(left); - e.setPaidAmount(paid.subtract(rev)); - if(fareboxRemaining(e).compareTo(BigDecimal.ZERO)>0) e.setPaystate("미결"); - left=left.subtract(rev); - } - } - p.setStatus("취소됨"); - return ApiResponse.ok(fareboxPaymentMap(p)); - } - private Map fareboxPaymentSearchData(JwtUser u, Map f, int page, int size){ - LocalDate from=parseDate(f.get("dateFrom"), null); - LocalDate to=parseDate(f.get("dateTo"), null); - List all=em.createQuery("select p from FareboxPayment p where p.groupId=:g",FareboxPayment.class) - .setParameter("g",u.getGroupId()).getResultList(); - List filtered=all.stream().filter(p->{ - if(from!=null && (p.getPaydate()==null || p.getPaydate().isBefore(from))) return false; - if(to!=null && (p.getPaydate()==null || p.getPaydate().isAfter(to))) return false; - String status=blank(p.getStatus())?"정상":p.getStatus(); - if(!eq(status, f.get("status"))) return false; - if(!eq(p.getMode(), f.get("mode")) && !eq(p.getMode(), f.get("how"))) return false; - if(!eq(p.getOutCategory(), f.get("outCategory"))) return false; - if(!like(p.getOutcompanyName(), f.get("outcompanyName"))) return false; - return true; - }).sorted(Comparator.comparing(FareboxPayment::getPaydate, Comparator.nullsLast(Comparator.reverseOrder())) - .thenComparing(FareboxPayment::getId, Comparator.reverseOrder())).toList(); - int sz=Math.min(Math.max(size,1),200); - int fromIdx=Math.min(Math.max(page,0)*sz, filtered.size()); - int toIdx=Math.min(fromIdx+sz, filtered.size()); - // number from newest: total - index - List> list=new ArrayList<>(); - for(int i=fromIdx;i row=fareboxPaymentMap(filtered.get(i)); - row.put("no", filtered.size()-i); - list.add(row); - } - Map result=new LinkedHashMap<>(); - result.put("total", filtered.size()); - result.put("list", list); - return result; - } - private Map fareboxPaymentMap(FareboxPayment p){ - Map r=new LinkedHashMap<>(); - r.put("id", p.getId()); - r.put("paydate", p.getPaydate()); - r.put("outcompanyId", p.getOutcompanyId()); - r.put("outcompanyName", p.getOutcompanyName()); - r.put("mode", blank(p.getMode())?"일반":p.getMode()); - r.put("amount", nvl(p.getAmount())); - r.put("payCount", p.getPayCount()==null?0:p.getPayCount()); - r.put("memo", p.getMemo()); - r.put("registrant", p.getRegistrant()); - r.put("status", blank(p.getStatus())?"정상":p.getStatus()); - r.put("outCategory", p.getOutCategory()); - r.put("fareboxIds", p.getFareboxIds()); - return r; - } - private Map fareboxPaySummaryData(JwtUser u, Map f){ - Map companyMap=new LinkedHashMap<>(); - for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) - companyMap.put(c.getId(),c); - Map companies=new LinkedHashMap<>(); - companyMap.forEach((id,c)->companies.put(id,c.getName())); - String q=f.get("outcompanyName"); - Map> byOut=new LinkedHashMap<>(); - BigDecimal totalBalance=BigDecimal.ZERO; - for(Farebox e:em.createQuery("select e from Farebox e where e.groupId=:g",Farebox.class).setParameter("g",u.getGroupId()).getResultList()){ - BigDecimal rem=fareboxRemaining(e); - if(rem.compareTo(BigDecimal.ZERO)<=0) continue; - String outName=fareboxOutName(e,companies); - if(!like(outName,q)) continue; - Map row=byOut.computeIfAbsent(outName,k->{ - Map r=new LinkedHashMap<>(); - Company c=companyMap.values().stream().filter(x->outName.equals(x.getName())).findFirst().orElse(null); - if(c==null && e.getOutcompanyId()!=null) c=companyMap.get(e.getOutcompanyId()); - r.put("outcompanyId", c==null?e.getOutcompanyId():c.getId()); - r.put("outcompanyName", outName); - r.put("channel", c!=null && !blank(c.getChannel())?c.getChannel():(blank(e.getOutCategory())?"판매점":e.getOutCategory())); - r.put("managerName", c==null||blank(c.getManagerName())?"-":c.getManagerName()); - r.put("phone", c==null||blank(c.getPhone())?"-":c.getPhone()); - r.put("mobile", c==null||blank(c.getPhone())?"-":c.getPhone()); - r.put("count", 0); - r.put("balance", BigDecimal.ZERO); - r.put("todayPaid", BigDecimal.ZERO); - return r; - }); - row.put("count", ((Number)row.get("count")).intValue()+1); - row.put("balance", ((BigDecimal)row.get("balance")).add(rem)); - totalBalance=totalBalance.add(rem); - } - LocalDate today=LocalDate.now(); - BigDecimal todayPaid=BigDecimal.ZERO; - for(FareboxPayment p:em.createQuery("select p from FareboxPayment p where p.groupId=:g and p.paydate=:d",FareboxPayment.class) - .setParameter("g",u.getGroupId()).setParameter("d",today).getResultList()){ - if("취소됨".equals(blank(p.getStatus())?"정상":p.getStatus())) continue; - todayPaid=todayPaid.add(nvl(p.getAmount())); - String outName=blank(p.getOutcompanyName())?"-":p.getOutcompanyName(); - Map row=byOut.get(outName); - if(row!=null) row.put("todayPaid", ((BigDecimal)row.get("todayPaid")).add(nvl(p.getAmount()))); - } - List> rows=new ArrayList<>(byOut.values()); - rows.sort(Comparator.comparing(r->String.valueOf(r.get("outcompanyName")))); - int no=1; - for(Map r:rows) r.put("no", no++); - Map result=new LinkedHashMap<>(); - result.put("totalBalance", totalBalance); - result.put("todayPaid", todayPaid); - result.put("rows", rows); - result.put("total", rows.size()); - return result; - } - private String fareboxOutName(Farebox e, Map companies){ - if(!blank(e.getOutcompanyName())) return e.getOutcompanyName().trim(); - return companies.getOrDefault(e.getOutcompanyId(),"-"); - } - private BigDecimal fareboxTotal(Farebox e){ return nvl(e.getCashprice()).add(nvl(e.getCardprice())); } - private BigDecimal fareboxRemaining(Farebox e){ - BigDecimal rem=fareboxTotal(e).subtract(nvl(e.getPaidAmount())); - if(rem.compareTo(BigDecimal.ZERO)<0) return BigDecimal.ZERO; - // 완결인데 paid 미기록이면 잔액 0 - if("완결".equals(normalizePayState(e.getPaystate())) && nvl(e.getPaidAmount()).compareTo(BigDecimal.ZERO)==0 - && fareboxTotal(e).compareTo(BigDecimal.ZERO)>0){ - // treat as unpaid if marked 완결 without paidAmount? Prefer remaining based on paidAmount only for 미결 - return BigDecimal.ZERO; - } - if("완결".equals(normalizePayState(e.getPaystate()))) return BigDecimal.ZERO; - return rem; - } - private Map fareboxSearchData(JwtUser u, Map f, int page, int size){ - Map companies=companyNames(u); - List all=em.createQuery("select e from Farebox e where e.groupId=:g",Farebox.class).setParameter("g",u.getGroupId()).getResultList(); - LocalDate from=parseDate(f.get("dateFrom"), null); - LocalDate to=parseDate(f.get("dateTo"), null); - String tab=f.getOrDefault("tab","ALL"); - List base=all.stream().filter(e->matchesFarebox(e,f,companies,from,to,null)).toList(); - List filtered=base.stream() - .filter(e->matchesFarebox(e,f,companies,from,to,tab)) - .sorted(Comparator.comparing(Farebox::getFaredate, Comparator.nullsLast(Comparator.reverseOrder())) - .thenComparing(Farebox::getId, Comparator.reverseOrder())) - .toList(); - Map counts=new LinkedHashMap<>(); - counts.put("ALL", (long) base.size()); - counts.put("미결", base.stream().filter(e->"미결".equals(normalizePayState(e.getPaystate()))).count()); - counts.put("완결", base.stream().filter(e->"완결".equals(normalizePayState(e.getPaystate()))).count()); - int sz=Math.min(Math.max(size,1),200); - int fromIdx=Math.min(Math.max(page,0)*sz, filtered.size()); - int toIdx=Math.min(fromIdx+sz, filtered.size()); - List> list=filtered.subList(fromIdx,toIdx).stream().map(e->fareboxMap(e,companies)).toList(); - Map result=new LinkedHashMap<>(); - result.put("total", filtered.size()); - result.put("list", list); - result.put("counts", counts); - return result; - } - private boolean matchesFarebox(Farebox e, Map f, Map companies, LocalDate from, LocalDate to, String tab){ - if(!fareboxDateOk(e,from,to)) return false; - if(!eq(e.getHow(), f.get("how"))) return false; - if(!eq(e.getTelcompany(), f.get("telecom")) && !eq(e.getTelcompany(), f.get("telcompany"))) return false; - if(!eq(e.getOutCategory(), f.get("outCategory"))) return false; - String outName=blank(e.getOutcompanyName())?companies.getOrDefault(e.getOutcompanyId(),""):e.getOutcompanyName(); - if(!like(outName, f.get("outcompanyName"))) return false; - String searchType=f.getOrDefault("searchType","고객명"); - String keyword=f.get("keyword"); - if(keyword!=null && !keyword.isBlank()){ - boolean ok=switch(searchType){ - case "연락처"->like(e.getPhone(), keyword); - default->like(e.getName(), keyword); - }; - if(!ok) return false; - } - if(tab!=null && !"ALL".equals(tab)){ - String state=normalizePayState(e.getPaystate()); - if("미결".equals(tab) && !"미결".equals(state)) return false; - if("완결".equals(tab) && !"완결".equals(state)) return false; - } - return true; - } - private boolean fareboxDateOk(Farebox e, LocalDate from, LocalDate to){ - if(from!=null && (e.getFaredate()==null || e.getFaredate().isBefore(from))) return false; - if(to!=null && (e.getFaredate()==null || e.getFaredate().isAfter(to))) return false; - return true; - } - private String normalizePayState(String s){ - if(blank(s)) return "미결"; - if("완료".equals(s) || "DONE".equalsIgnoreCase(s) || "완결".equals(s)) return "완결"; - if("PENDING".equalsIgnoreCase(s) || "미결".equals(s)) return "미결"; - return s; - } - private Map fareboxMap(Farebox e, Map companies){ - Map r=new LinkedHashMap<>(); - r.put("id", e.getId()); - r.put("faredate", e.getFaredate()); - r.put("telcompany", e.getTelcompany()); - r.put("outcompanyId", e.getOutcompanyId()); - String outName=blank(e.getOutcompanyName())?companies.getOrDefault(e.getOutcompanyId(),"-"):e.getOutcompanyName(); - r.put("outcompanyName", outName); - r.put("outCategory", e.getOutCategory()); - r.put("name", e.getName()); - r.put("phone", e.getPhone()); - r.put("how", blank(e.getHow())?"수납":e.getHow()); - r.put("cashprice", e.getCashprice()==null?BigDecimal.ZERO:e.getCashprice()); - r.put("cardprice", e.getCardprice()==null?BigDecimal.ZERO:e.getCardprice()); - r.put("paidAmount", e.getPaidAmount()==null?BigDecimal.ZERO:e.getPaidAmount()); - r.put("paystate", normalizePayState(e.getPaystate())); - BigDecimal pay=nvl(e.getCashprice()).add(nvl(e.getCardprice())); - r.put("payAmount", pay.compareTo(BigDecimal.ZERO)==0?null:pay); - r.put("remaining", fareboxRemaining(e)); - r.put("detail", e.getDetail()); - r.put("registrant", e.getRegistrant()); - r.put("receiptNo", e.getReceiptNo()); - return r; - } - @PutMapping({"/scans/{id}","/indocs/{id}","/returns/{id}","/fareboxes/{id}"}) ApiResponse commonPut(HttpServletRequest r,@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return put(type(r.getRequestURI()),id,d,u);} - @DeleteMapping("/schedules/{id}") ApiResponse scheduleDel(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return del(ScheduleItem.class,id,u);} - @PostMapping("/alims/{id}/confirm") ApiResponse alimConfirm(@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return put(Alim.class,id,Map.of("confirmed",true),u);} - @GetMapping("/settlements") ApiResponse settlements(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){return list(SettlementMonth.class,u,f,0,100);} - @GetMapping("/settlements/docs") ApiResponse settlementDocs(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - String ym=f.getOrDefault("yearMonth",LocalDate.now().toString().substring(0,7)); - List all=em.createQuery("select s from SettlementMonth s where s.groupId=:g and s.yearMonth=:y",SettlementMonth.class).setParameter("g",u.getGroupId()).setParameter("y",ym).getResultList(); - Map companies=companyNames(u); - String outCategory=f.get("outCategory"); - List filtered=all.stream().filter(s->eq(s.getOutCategory(),outCategory)).sorted(Comparator.comparing((SettlementMonth s)->s.getOutcompanyName()!=null?s.getOutcompanyName():companies.getOrDefault(s.getOutcompanyId(),""),Comparator.nullsLast(String::compareTo))).toList(); - List> list=filtered.stream().map(s->settlementDocMap(s,companies)).toList(); - BigDecimal openAmount=BigDecimal.ZERO, afterAmount=BigDecimal.ZERO, homeAmount=BigDecimal.ZERO, realAmount=BigDecimal.ZERO; - int openCount=0, afterCount=0, homeCount=0; - for(Map r:list){ - openCount+=((Number)r.getOrDefault("openCount",0)).intValue(); - afterCount+=((Number)r.getOrDefault("afterSettleCount",0)).intValue(); - homeCount+=((Number)r.getOrDefault("homeProductCount",0)).intValue(); - openAmount=openAmount.add(nvl((BigDecimal)r.get("openAmount"))); - afterAmount=afterAmount.add(nvl((BigDecimal)r.get("afterSettleAmount"))); - homeAmount=homeAmount.add(nvl((BigDecimal)r.get("homeProductAmount"))); - realAmount=realAmount.add(nvl((BigDecimal)r.get("realSettleAmount"))); - } - Map summary=new LinkedHashMap<>(); - summary.put("openCount",openCount); summary.put("openAmount",openAmount); - summary.put("afterSettleCount",afterCount); summary.put("afterSettleAmount",afterAmount); - summary.put("homeProductCount",homeCount); summary.put("homeProductAmount",homeAmount); - summary.put("realSettleAmount",realAmount); - return ApiResponse.ok(Map.of("yearMonth",ym,"total",list.size(),"list",list,"summary",summary)); - } - @PostMapping("/settlements/docs/issue") @Transactional ApiResponse settlementIssue(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Object raw=d.get("ids"); - List ids=new ArrayList<>(); - if(raw instanceof List list) for(Object o:list) if(o!=null) ids.add(Long.valueOf(o.toString())); - if(ids.isEmpty()&&d.get("id")!=null) ids.add(Long.valueOf(d.get("id").toString())); - if(ids.isEmpty()) return ApiResponse.fail("발행할 항목을 선택하세요."); - int issued=0; - for(Long id:ids){ - SettlementMonth s=em.find(SettlementMonth.class,id); - if(s==null||!Objects.equals(s.getGroupId(),u.getGroupId())) continue; - s.setIssued(true); s.setStatus("발행"); issued++; - } - return ApiResponse.ok(Map.of("issued",issued)); - } - @PostMapping("/settlements/docs/delete") @Transactional ApiResponse settlementDocsDelete(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - String ym=d.get("yearMonth")==null?null:String.valueOf(d.get("yearMonth")); - Object raw=d.get("ids"); - int deleted=0; - if(raw instanceof List list && !list.isEmpty()){ - for(Object o:list){ - SettlementMonth s=em.find(SettlementMonth.class,Long.valueOf(o.toString())); - if(s==null||!Objects.equals(s.getGroupId(),u.getGroupId())) continue; - if(s.isIssued()){ s.setIssued(false); s.setStatus("READY"); deleted++; } - } - } else if(ym!=null&&!ym.isBlank()){ - List rows=em.createQuery("select s from SettlementMonth s where s.groupId=:g and s.yearMonth=:y and s.issued=true",SettlementMonth.class).setParameter("g",u.getGroupId()).setParameter("y",ym).getResultList(); - for(SettlementMonth s:rows){ s.setIssued(false); s.setStatus("READY"); deleted++; } - } else return ApiResponse.fail("삭제할 정산서를 선택하세요."); - return ApiResponse.ok(Map.of("deleted",deleted)); - } - @GetMapping(value="/settlements/docs/export", produces="text/csv;charset=UTF-8") ResponseEntity settlementDocsExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - @SuppressWarnings("unchecked") Map data=(Map) settlementDocs(u,f).data(); - @SuppressWarnings("unchecked") List> rows=(List>) data.get("list"); - StringBuilder csv=new StringBuilder("\uFEFF출고처,개통,개통금액,후정산,후정산금액,홈상품,홈상품금액,실정산금액,정산서발행,출고처문의,출고처동의,계산서상태,송금상태,비고\n"); - for(Map r:rows) csv.append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("openCount"))).append(',').append(csv(r.get("openAmount"))).append(',') - .append(csv(r.get("afterSettleCount"))).append(',').append(csv(r.get("afterSettleAmount"))).append(',') - .append(csv(r.get("homeProductCount"))).append(',').append(csv(r.get("homeProductAmount"))).append(',').append(csv(r.get("realSettleAmount"))).append(',') - .append(csv(Boolean.TRUE.equals(r.get("issued"))?"발행":"미발행")).append(',').append(csv(r.get("inquiry"))).append(',').append(csv(r.get("agreement"))).append(',') - .append(csv(r.get("invoiceStatus"))).append(',').append(csv(r.get("remittanceStatus"))).append(',').append(csv(r.get("memo"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=settlement-docs.csv").body(csv.toString()); - } - private Map settlementDocMap(SettlementMonth s,Map companies){ - Map r=new LinkedHashMap<>(); - r.put("id",s.getId()); - r.put("outcompanyId",s.getOutcompanyId()); - r.put("outcompanyName",s.getOutcompanyName()!=null&&!s.getOutcompanyName().isBlank()?s.getOutcompanyName():companies.getOrDefault(s.getOutcompanyId(),"-")); - r.put("yearMonth",s.getYearMonth()); r.put("outCategory",s.getOutCategory()); - r.put("openCount",s.getOpenCount()==null?0:s.getOpenCount()); - r.put("openAmount",s.getOpenAmount()==null?BigDecimal.ZERO:s.getOpenAmount()); - r.put("afterSettleCount",s.getAfterSettleCount()==null?0:s.getAfterSettleCount()); - r.put("afterSettleAmount",s.getAfterSettleAmount()==null?BigDecimal.ZERO:s.getAfterSettleAmount()); - r.put("homeProductCount",s.getHomeProductCount()==null?0:s.getHomeProductCount()); - r.put("homeProductAmount",s.getHomeProductAmount()==null?BigDecimal.ZERO:s.getHomeProductAmount()); - r.put("realSettleAmount",s.getRealSettleAmount()==null?BigDecimal.ZERO:s.getRealSettleAmount()); - r.put("issued",s.isIssued()); - r.put("inquiry",blank(s.getInquiry())?"-":s.getInquiry()); - r.put("agreement",blank(s.getAgreement())?"-":s.getAgreement()); - r.put("invoiceStatus",blank(s.getInvoiceStatus())?"-":s.getInvoiceStatus()); - r.put("remittanceStatus",blank(s.getRemittanceStatus())?"-":s.getRemittanceStatus()); - r.put("memo",s.getMemo()); r.put("status",s.getStatus()); - return r; - } - @PutMapping("/settlements") ApiResponse settlementPut(@RequestParam Long outcompanyId,@RequestParam String yearMonth,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){ - List rows=em.createQuery("select e from SettlementMonth e where e.groupId=:g and e.outcompanyId=:o and e.yearMonth=:y",SettlementMonth.class).setParameter("g",u.getGroupId()).setParameter("o",outcompanyId).setParameter("y",yearMonth).getResultList(); - if(rows.isEmpty()){d.put("outcompanyId",outcompanyId);d.put("yearMonth",yearMonth);return post(SettlementMonth.class,d,u);} return put(SettlementMonth.class,rows.getFirst().getId(),d,u); - } - @GetMapping("/accounts/summary") ApiResponse accountSummary(@AuthenticationPrincipal JwtUser u,@RequestParam(required=false) String yearMonth){PageResponse p=crud.list(AccountEntry.class,u,yearMonth==null?Map.of():Map.of("yearMonth",yearMonth),0,1000);BigDecimal in=BigDecimal.ZERO,out=BigDecimal.ZERO;for(Object o:p.list()){AccountEntry e=(AccountEntry)o;if("IN".equals(e.getType()))in=in.add(nvl(e.getAmount()));else out=out.add(nvl(e.getAmount()));}return ApiResponse.ok(Map.of("in",in,"out",out,"balance",in.subtract(out)));} - @GetMapping({"/reports/stock","/reports/open","/reports/settle","/reports/sales","/reports/day","/reports/month"}) ApiResponse report(HttpServletRequest r,@AuthenticationPrincipal JwtUser u){String path=r.getRequestURI();Class c=path.contains("stock")?Stock.class:path.contains("open")?OpenRecord.class:path.contains("settle")?SettlementMonth.class:AccountEntry.class;return ApiResponse.ok(Map.of("report",path.substring(path.lastIndexOf('/')+1),"total",count(c,u,null)));} - @GetMapping("/settings/company") ApiResponse settingGet(@AuthenticationPrincipal JwtUser u){return list(Setting.class,u,Map.of("settingKey","company"),0,1);} - @PutMapping("/settings/company") ApiResponse settingPut(@AuthenticationPrincipal JwtUser u,@RequestBody Mapd){List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='company'",Setting.class).setParameter("g",u.getGroupId()).getResultList();d.put("settingKey","company");return rows.isEmpty()?post(Setting.class,d,u):put(Setting.class,rows.getFirst().getId(),d,u);} - @GetMapping("/settings/preference") ApiResponse preferenceGet(@AuthenticationPrincipal JwtUser u){ - return ApiResponse.ok(loadPreference(u.getGroupId())); - } - @PutMapping("/settings/preference") @Transactional ApiResponse preferencePut(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Map current=loadPreference(u.getGroupId()); - mergePreference(current,d); - savePreference(u.getGroupId(),current); - return ApiResponse.ok(current); - } - private Map loadPreference(String groupId){ - Map defaults=defaultPreference(); - List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='preference'",Setting.class).setParameter("g",groupId).getResultList(); - if(!rows.isEmpty() && !blank(rows.getFirst().getValue())){ - try{ - @SuppressWarnings("unchecked") Map parsed=new com.fasterxml.jackson.databind.ObjectMapper().readValue(rows.getFirst().getValue(), Map.class); - mergePreference(defaults,parsed); - }catch(Exception ignored){} - } else { - // fallback: legacy settings/company JSON - List companyRows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='company'",Setting.class).setParameter("g",groupId).getResultList(); - if(!companyRows.isEmpty() && !blank(companyRows.getFirst().getValue())){ - try{ - @SuppressWarnings("unchecked") Map legacy=new com.fasterxml.jackson.databind.ObjectMapper().readValue(companyRows.getFirst().getValue(), Map.class); - @SuppressWarnings("unchecked") Map company=(Map) defaults.get("company"); - if(legacy.get("name")!=null) company.put("name", legacy.get("name")); - if(legacy.get("phone")!=null) company.put("mobile", legacy.get("phone")); - if(legacy.get("managerName")!=null) company.put("managerName", legacy.get("managerName")); - if(legacy.get("businessName")!=null) company.put("businessName", legacy.get("businessName")); - if(legacy.get("businessNumber")!=null) company.put("businessNumber", legacy.get("businessNumber")); - if(legacy.get("ceo")!=null) company.put("ceo", legacy.get("ceo")); - if(legacy.get("bizType")!=null) company.put("bizType", legacy.get("bizType")); - if(legacy.get("bizItem")!=null) company.put("bizItem", legacy.get("bizItem")); - if(legacy.get("address")!=null) company.put("address", legacy.get("address")); - }catch(Exception ignored){} - } - } - return defaults; - } - private void savePreference(String groupId, Map data){ - String json=writeSettingJson(data); - List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='preference'",Setting.class).setParameter("g",groupId).getResultList(); - if(rows.isEmpty()){ - Setting s=new Setting(); s.setGroupId(groupId); s.setSettingKey("preference"); s.setValue(json); em.persist(s); - } else { - rows.getFirst().setValue(json); - } - // keep legacy company key in sync for older clients - @SuppressWarnings("unchecked") Map company=(Map) data.getOrDefault("company", Map.of()); - Map legacy=new LinkedHashMap<>(); - legacy.put("name", company.get("name")); - legacy.put("phone", company.get("mobile")); - legacy.put("managerName", company.get("managerName")); - legacy.put("businessName", company.get("businessName")); - legacy.put("businessNumber", company.get("businessNumber")); - legacy.put("ceo", company.get("ceo")); - legacy.put("bizType", company.get("bizType")); - legacy.put("bizItem", company.get("bizItem")); - legacy.put("address", company.get("address")); - String legacyJson=writeSettingJson(legacy); - List companyRows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='company'",Setting.class).setParameter("g",groupId).getResultList(); - if(companyRows.isEmpty()){ - Setting s=new Setting(); s.setGroupId(groupId); s.setSettingKey("company"); s.setValue(legacyJson); em.persist(s); - } else { - companyRows.getFirst().setValue(legacyJson); - } - } - @SuppressWarnings("unchecked") - private void mergePreference(Map target, Map patch){ - if(patch==null) return; - for(Map.Entry e:patch.entrySet()){ - Object val=e.getValue(); - if(val instanceof Map m && target.get(e.getKey()) instanceof Map){ - Map dest=new LinkedHashMap<>((Map) target.get(e.getKey())); - dest.putAll((Map) m); - target.put(e.getKey(), dest); - } else { - target.put(e.getKey(), val); - } - } - } - private Map defaultPreference(){ - Map company=new LinkedHashMap<>(); - company.put("name","에이전트 데모"); - company.put("managerName","김대표"); - company.put("mobile","010-0000-0000"); - company.put("businessName","에이전트 데모"); - company.put("businessNumber","123-45-67890"); - company.put("ceo","김유신"); - company.put("bizType","도소매"); - company.put("bizItem","휴대폰판매"); - company.put("address","대구시 수성구 수성4가 먼길"); - Map accessIps=new LinkedHashMap<>(); - accessIps.put("enabled", false); - accessIps.put("ips", List.of()); - Map masking=new LinkedHashMap<>(); - masking.put("enabled", true); - masking.put("name", true); - masking.put("phone", true); - masking.put("rrn", true); - masking.put("address", false); - Map stock=new LinkedHashMap<>(); - stock.put("duplicateSerialCheck", true); - stock.put("allowNegative", false); - stock.put("autoRecallDays", 0); - Map open=new LinkedHashMap<>(); - open.put("requireUsim", true); - open.put("requirePhone", true); - open.put("defaultOpenHow", ""); - open.put("autoSettle", false); - Map m=new LinkedHashMap<>(); - m.put("company", company); - m.put("accessIps", accessIps); - m.put("masking", masking); - m.put("outCategories", List.of("판매점","도매")); - m.put("stock", stock); - m.put("open", open); - return m; - } - @GetMapping("/staff") ApiResponse staffList(@AuthenticationPrincipal JwtUser u,@RequestParam Map f, - @RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="15") int size){ - return ApiResponse.ok(staffData(u,f,page,size)); - } - @GetMapping("/incompanies") ApiResponse incompanies(@AuthenticationPrincipal JwtUser u,@RequestParam Map f, - @RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="15") int size){ - return ApiResponse.ok(incompanyData(u,f,page,size)); - } - @PostMapping("/incompanies") @Transactional ApiResponse incompanyCreate(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - String name=d.get("name")==null?"":String.valueOf(d.get("name")).trim(); - if(name.isBlank()) return ApiResponse.fail("업체명을 입력하세요."); - Company c=new Company(); - c.setGroupId(u.getGroupId()); - c.setType("IN"); - applyIncompanyFields(c,d); - c.setName(name); - if(d.get("active")==null && d.get("status")==null) c.setActive(true); - em.persist(c); - return ApiResponse.ok(incompanyMap(c)); - } - @PutMapping("/incompanies/{id}") @Transactional ApiResponse incompanyUpdate(@PathVariable Long id,@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Company c=em.find(Company.class,id); - if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId()) || !"IN".equals(c.getType())) return ApiResponse.fail("입고처를 찾을 수 없습니다."); - if(d.get("name")!=null){ - String name=String.valueOf(d.get("name")).trim(); - if(name.isBlank()) return ApiResponse.fail("업체명을 입력하세요."); - c.setName(name); - } - applyIncompanyFields(c,d); - return ApiResponse.ok(incompanyMap(c)); - } - @PostMapping("/incompanies/status") @Transactional ApiResponse incompanyStatus(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Object raw=d.get("ids"); - if(!(raw instanceof List ids) || ids.isEmpty()) return ApiResponse.fail("항목을 선택하세요."); - boolean active=!"미사용".equals(String.valueOf(d.getOrDefault("status",""))) && !"false".equalsIgnoreCase(String.valueOf(d.getOrDefault("active",""))); - int updated=0; - for(Object idObj:ids){ - Company c=em.find(Company.class, Long.valueOf(idObj.toString())); - if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId()) || !"IN".equals(c.getType())) continue; - c.setActive(active); updated++; - } - return ApiResponse.ok(Map.of("updated", updated)); - } - @GetMapping(value="/incompanies/export", produces="text/csv;charset=UTF-8") ResponseEntity incompanyExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - @SuppressWarnings("unchecked") List> rows=(List>) incompanyData(u,f,0,10000).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF상태,업체명,통신사,전화,팩스,담당자,휴대폰,상호,사업자번호,등록일,비고\n"); - for(Map r:rows) csv.append(csv(r.get("status"))).append(',').append(csv(r.get("name"))).append(',') - .append(csv(r.get("telecom"))).append(',').append(csv(r.get("phone"))).append(',').append(csv(r.get("fax"))).append(',') - .append(csv(r.get("managerName"))).append(',').append(csv(r.get("mobile"))).append(',').append(csv(r.get("businessName"))).append(',') - .append(csv(r.get("businessNumber"))).append(',').append(csv(r.get("createdAt"))).append(',').append(csv(r.get("memo"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=incompanies.csv").body(csv.toString()); - } - private void applyIncompanyFields(Company c, Map d){ - if(d.get("telecom")!=null) c.setTelecom(blank(String.valueOf(d.get("telecom")))?null:String.valueOf(d.get("telecom")).trim()); - if(d.get("phone")!=null) c.setPhone(blank(String.valueOf(d.get("phone")))?null:String.valueOf(d.get("phone")).trim()); - if(d.get("fax")!=null) c.setFax(blank(String.valueOf(d.get("fax")))?null:String.valueOf(d.get("fax")).trim()); - if(d.get("managerName")!=null) c.setManagerName(blank(String.valueOf(d.get("managerName")))?null:String.valueOf(d.get("managerName")).trim()); - if(d.get("mobile")!=null) c.setMobile(blank(String.valueOf(d.get("mobile")))?null:String.valueOf(d.get("mobile")).trim()); - if(d.get("businessName")!=null) c.setBusinessName(blank(String.valueOf(d.get("businessName")))?null:String.valueOf(d.get("businessName")).trim()); - if(d.get("businessNumber")!=null) c.setBusinessNumber(blank(String.valueOf(d.get("businessNumber")))?null:String.valueOf(d.get("businessNumber")).trim()); - if(d.get("memo")!=null) c.setMemo(blank(String.valueOf(d.get("memo")))?null:String.valueOf(d.get("memo")).trim()); - if(d.get("status")!=null) c.setActive(!"미사용".equals(String.valueOf(d.get("status"))) && !"중지".equals(String.valueOf(d.get("status")))); - if(d.get("active")!=null) c.setActive(Boolean.parseBoolean(String.valueOf(d.get("active"))) || "true".equalsIgnoreCase(String.valueOf(d.get("active"))) || "사용".equals(String.valueOf(d.get("active")))); - } - private Map incompanyData(JwtUser u, Map f, int page, int size){ - List all=em.createQuery("select c from Company c where c.groupId=:g and c.type='IN'",Company.class) - .setParameter("g",u.getGroupId()).getResultList(); - String searchType=f.getOrDefault("searchType","업체명"); - String keyword=f.getOrDefault("keyword",""); - List filtered=all.stream().filter(c->{ - if(!blank(keyword)){ - String q=keyword.trim(); - return switch(searchType){ - case "통신사" -> like(c.getTelecom(), q); - case "담당자" -> like(c.getManagerName(), q); - case "상호" -> like(c.getBusinessName(), q); - case "사업자번호" -> like(c.getBusinessNumber(), q); - default -> like(c.getName(), q); - }; - } - return true; - }).sorted(incompanyComparator(f.get("sort"))).toList(); - int sz=Math.min(Math.max(size,1),200); - int fromIdx=Math.min(Math.max(page,0)*sz, filtered.size()); - int toIdx=Math.min(fromIdx+sz, filtered.size()); - List> list=filtered.subList(fromIdx,toIdx).stream().map(this::incompanyMap).toList(); - Map result=new LinkedHashMap<>(); - result.put("total", filtered.size()); - result.put("list", list); - return result; - } - private Comparator incompanyComparator(String sort){ - String field=sort==null?"name,asc":sort; boolean asc=field.endsWith(",asc"); - Comparator c=switch(field.split(",")[0]){ - case "telecom"->Comparator.comparing(Company::getTelecom,Comparator.nullsLast(String::compareTo)); - case "createdAt"->Comparator.comparing(Company::getCreatedAt,Comparator.nullsLast(Instant::compareTo)); - default->Comparator.comparing(Company::getName,Comparator.nullsLast(String::compareTo)); - }; - return (asc?c:c.reversed()).thenComparing(Company::getId); - } - private Map incompanyMap(Company c){ - Map r=new LinkedHashMap<>(); - r.put("id", c.getId()); - r.put("status", c.isActive()?"사용":"미사용"); - r.put("active", c.isActive()); - r.put("name", c.getName()); - r.put("telecom", c.getTelecom()); - r.put("phone", c.getPhone()); - r.put("fax", c.getFax()); - r.put("managerName", c.getManagerName()); - r.put("mobile", c.getMobile()); - r.put("businessName", c.getBusinessName()); - r.put("businessNumber", c.getBusinessNumber()); - r.put("createdAt", c.getCreatedAt()==null?null:c.getCreatedAt().atZone(ZoneId.systemDefault()).toLocalDate().toString()); - r.put("memo", c.getMemo()); - return r; - } - @GetMapping("/outcompanies") ApiResponse outcompanies(@AuthenticationPrincipal JwtUser u,@RequestParam Map f, - @RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="15") int size){ - return ApiResponse.ok(outcompanyData(u,f,page,size)); - } - @PostMapping("/outcompanies") @Transactional ApiResponse outcompanyCreate(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - String name=d.get("name")==null?"":String.valueOf(d.get("name")).trim(); - if(name.isBlank()) return ApiResponse.fail("출고처명을 입력하세요."); - Company c=new Company(); - c.setGroupId(u.getGroupId()); - c.setType(blank(String.valueOf(d.getOrDefault("type","")))?"OUT":String.valueOf(d.get("type"))); - if(!"OUT".equals(c.getType()) && !"SHOP".equals(c.getType())) c.setType("OUT"); - applyOutcompanyFields(c,d); - c.setName(name); - if(d.get("active")==null && d.get("status")==null) c.setActive(true); - em.persist(c); - return ApiResponse.ok(outcompanyMap(c)); - } - @PutMapping("/outcompanies/{id}") @Transactional ApiResponse outcompanyUpdate(@PathVariable Long id,@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Company c=em.find(Company.class,id); - if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId()) || !List.of("OUT","SHOP").contains(c.getType())) return ApiResponse.fail("출고처를 찾을 수 없습니다."); - if(d.get("name")!=null){ - String name=String.valueOf(d.get("name")).trim(); - if(name.isBlank()) return ApiResponse.fail("출고처명을 입력하세요."); - c.setName(name); - } - applyOutcompanyFields(c,d); - return ApiResponse.ok(outcompanyMap(c)); - } - @PostMapping("/outcompanies/status") @Transactional ApiResponse outcompanyStatus(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Object raw=d.get("ids"); - if(!(raw instanceof List ids) || ids.isEmpty()) return ApiResponse.fail("항목을 선택하세요."); - boolean active=!"미사용".equals(String.valueOf(d.getOrDefault("status",""))) && !"false".equalsIgnoreCase(String.valueOf(d.getOrDefault("active",""))); - int updated=0; - for(Object idObj:ids){ - Company c=em.find(Company.class, Long.valueOf(idObj.toString())); - if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId()) || !List.of("OUT","SHOP").contains(c.getType())) continue; - c.setActive(active); updated++; - } - return ApiResponse.ok(Map.of("updated", updated)); - } - @GetMapping(value="/outcompanies/export", produces="text/csv;charset=UTF-8") ResponseEntity outcompanyExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - @SuppressWarnings("unchecked") List> rows=(List>) outcompanyData(u,f,0,10000).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF상태,채널,업체명,P코드,영업담당,전화,담당자,휴대폰,상호,사업자번호,계좌,등록일,비고\n"); - for(Map r:rows) csv.append(csv(r.get("status"))).append(',').append(csv(r.get("channel"))).append(',') - .append(csv(r.get("name"))).append(',').append(csv(r.get("pcode"))).append(',').append(csv(r.get("salesperson"))).append(',') - .append(csv(r.get("phone"))).append(',').append(csv(r.get("managerName"))).append(',').append(csv(r.get("mobile"))).append(',') - .append(csv(r.get("businessName"))).append(',').append(csv(r.get("businessNumber"))).append(',').append(csv(r.get("bankAccount"))).append(',') - .append(csv(r.get("createdAt"))).append(',').append(csv(r.get("memo"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=outcompanies.csv").body(csv.toString()); - } - private void applyOutcompanyFields(Company c, Map d){ - if(d.get("channel")!=null) c.setChannel(blank(String.valueOf(d.get("channel")))?null:String.valueOf(d.get("channel")).trim()); - if(d.get("category")!=null) c.setCategory(blank(String.valueOf(d.get("category")))?null:String.valueOf(d.get("category")).trim()); - if(d.get("pcode")!=null) c.setPcode(blank(String.valueOf(d.get("pcode")))?null:String.valueOf(d.get("pcode")).trim()); - if(d.get("salesperson")!=null) c.setSalesperson(blank(String.valueOf(d.get("salesperson")))?null:String.valueOf(d.get("salesperson")).trim()); - if(d.get("phone")!=null) c.setPhone(blank(String.valueOf(d.get("phone")))?null:String.valueOf(d.get("phone")).trim()); - if(d.get("managerName")!=null) c.setManagerName(blank(String.valueOf(d.get("managerName")))?null:String.valueOf(d.get("managerName")).trim()); - if(d.get("mobile")!=null) c.setMobile(blank(String.valueOf(d.get("mobile")))?null:String.valueOf(d.get("mobile")).trim()); - if(d.get("businessName")!=null) c.setBusinessName(blank(String.valueOf(d.get("businessName")))?null:String.valueOf(d.get("businessName")).trim()); - if(d.get("businessNumber")!=null) c.setBusinessNumber(blank(String.valueOf(d.get("businessNumber")))?null:String.valueOf(d.get("businessNumber")).trim()); - if(d.get("bankAccount")!=null) c.setBankAccount(blank(String.valueOf(d.get("bankAccount")))?null:String.valueOf(d.get("bankAccount")).trim()); - if(d.get("memo")!=null) c.setMemo(blank(String.valueOf(d.get("memo")))?null:String.valueOf(d.get("memo")).trim()); - if(d.get("status")!=null) c.setActive(!"미사용".equals(String.valueOf(d.get("status"))) && !"중지".equals(String.valueOf(d.get("status")))); - if(d.get("active")!=null) c.setActive(Boolean.parseBoolean(String.valueOf(d.get("active"))) || "true".equalsIgnoreCase(String.valueOf(d.get("active"))) || "사용".equals(String.valueOf(d.get("active")))); - } - private Map outcompanyData(JwtUser u, Map f, int page, int size){ - List all=em.createQuery("select c from Company c where c.groupId=:g and c.type in ('OUT','SHOP')",Company.class) - .setParameter("g",u.getGroupId()).getResultList(); - String status=f.getOrDefault("status",""); - String channel=f.getOrDefault("channel",""); - String category=f.getOrDefault("category",""); - String searchType=f.getOrDefault("searchType","출고처명"); - String keyword=f.getOrDefault("keyword",""); - List filtered=all.stream().filter(c->{ - if("사용".equals(status) && !c.isActive()) return false; - if("미사용".equals(status) && c.isActive()) return false; - String ch=blank(c.getChannel())?"기타":c.getChannel(); - if(!eq(ch, channel)) return false; - String cat=blank(c.getCategory())?("SHOP".equals(c.getType())?"판매점":"출고처"):c.getCategory(); - if(!eq(cat, category)) return false; - if(!blank(keyword)){ - String q=keyword.trim(); - return switch(searchType){ - case "P코드" -> like(c.getPcode(), q); - case "담당자" -> like(c.getManagerName(), q); - case "영업담당" -> like(c.getSalesperson(), q); - case "상호" -> like(c.getBusinessName(), q); - case "사업자번호" -> like(c.getBusinessNumber(), q); - default -> like(c.getName(), q); - }; - } - return true; - }).sorted(outcompanyComparator(f.get("sort"))).toList(); - int sz=Math.min(Math.max(size,1),200); - int fromIdx=Math.min(Math.max(page,0)*sz, filtered.size()); - int toIdx=Math.min(fromIdx+sz, filtered.size()); - List> list=filtered.subList(fromIdx,toIdx).stream().map(this::outcompanyMap).toList(); - Map result=new LinkedHashMap<>(); - result.put("total", filtered.size()); - result.put("list", list); - return result; - } - private Comparator outcompanyComparator(String sort){ - String field=sort==null?"name,asc":sort; boolean asc=field.endsWith(",asc"); - Comparator c=switch(field.split(",")[0]){ - case "pcode"->Comparator.comparing(Company::getPcode,Comparator.nullsLast(String::compareTo)); - case "channel"->Comparator.comparing(x->blank(x.getChannel())?"기타":x.getChannel(),Comparator.nullsLast(String::compareTo)); - case "createdAt"->Comparator.comparing(Company::getCreatedAt,Comparator.nullsLast(Instant::compareTo)); - default->Comparator.comparing(Company::getName,Comparator.nullsLast(String::compareTo)); - }; - return (asc?c:c.reversed()).thenComparing(Company::getId); - } - private Map outcompanyMap(Company c){ - Map r=new LinkedHashMap<>(); - r.put("id", c.getId()); - r.put("status", c.isActive()?"사용":"미사용"); - r.put("active", c.isActive()); - r.put("channel", blank(c.getChannel())?"기타":c.getChannel()); - r.put("category", blank(c.getCategory())?("SHOP".equals(c.getType())?"판매점":"출고처"):c.getCategory()); - r.put("name", c.getName()); - r.put("pcode", c.getPcode()); - r.put("salesperson", c.getSalesperson()); - r.put("phone", c.getPhone()); - r.put("managerName", c.getManagerName()); - r.put("mobile", c.getMobile()); - r.put("businessName", c.getBusinessName()); - r.put("businessNumber", c.getBusinessNumber()); - r.put("bankAccount", c.getBankAccount()); - r.put("createdAt", c.getCreatedAt()==null?null:c.getCreatedAt().atZone(ZoneId.systemDefault()).toLocalDate().toString()); - r.put("memo", c.getMemo()); - r.put("type", c.getType()); - return r; - } - @GetMapping("/deliveries") ApiResponse deliveries(@AuthenticationPrincipal JwtUser u,@RequestParam Map f, - @RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="15") int size){ - return ApiResponse.ok(deliveryData(u,f,page,size)); - } - @PostMapping("/deliveries") @Transactional ApiResponse deliveryCreate(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - String name=d.get("name")==null?"":String.valueOf(d.get("name")).trim(); - if(name.isBlank()) return ApiResponse.fail("업체명을 입력하세요."); - Company c=new Company(); - c.setGroupId(u.getGroupId()); - c.setType("DELIVERY"); - c.setName(name); - if(d.get("phone")!=null) c.setPhone(blank(String.valueOf(d.get("phone")))?null:String.valueOf(d.get("phone")).trim()); - if(d.get("memo")!=null) c.setMemo(blank(String.valueOf(d.get("memo")))?null:String.valueOf(d.get("memo")).trim()); - c.setActive(!"미사용".equals(String.valueOf(d.getOrDefault("status","")))); - if(d.get("active")==null && d.get("status")==null) c.setActive(true); - em.persist(c); - return ApiResponse.ok(deliveryMap(c)); - } - @PutMapping("/deliveries/{id}") @Transactional ApiResponse deliveryUpdate(@PathVariable Long id,@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Company c=em.find(Company.class,id); - if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId()) || !"DELIVERY".equals(c.getType())) return ApiResponse.fail("배송처를 찾을 수 없습니다."); - if(d.get("name")!=null){ - String name=String.valueOf(d.get("name")).trim(); - if(name.isBlank()) return ApiResponse.fail("업체명을 입력하세요."); - c.setName(name); - } - if(d.get("phone")!=null) c.setPhone(blank(String.valueOf(d.get("phone")))?null:String.valueOf(d.get("phone")).trim()); - if(d.get("memo")!=null) c.setMemo(blank(String.valueOf(d.get("memo")))?null:String.valueOf(d.get("memo")).trim()); - if(d.get("status")!=null) c.setActive(!"미사용".equals(String.valueOf(d.get("status")))); - if(d.get("active")!=null) c.setActive(Boolean.parseBoolean(String.valueOf(d.get("active"))) || "true".equalsIgnoreCase(String.valueOf(d.get("active"))) || "사용".equals(String.valueOf(d.get("active")))); - return ApiResponse.ok(deliveryMap(c)); - } - @PostMapping("/deliveries/status") @Transactional ApiResponse deliveryStatus(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Object raw=d.get("ids"); - if(!(raw instanceof List ids) || ids.isEmpty()) return ApiResponse.fail("항목을 선택하세요."); - boolean active=!"미사용".equals(String.valueOf(d.getOrDefault("status",""))); - int updated=0; - for(Object idObj:ids){ - Company c=em.find(Company.class, Long.valueOf(idObj.toString())); - if(c==null || !Objects.equals(c.getGroupId(),u.getGroupId()) || !"DELIVERY".equals(c.getType())) continue; - c.setActive(active); updated++; - } - return ApiResponse.ok(Map.of("updated", updated)); - } - @GetMapping(value="/deliveries/export", produces="text/csv;charset=UTF-8") ResponseEntity deliveryExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - @SuppressWarnings("unchecked") List> rows=(List>) deliveryData(u,f,0,10000).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF상태,업체명,연락처,비고,등록일\n"); - for(Map r:rows) csv.append(csv(r.get("status"))).append(',').append(csv(r.get("name"))).append(',') - .append(csv(r.get("phone"))).append(',').append(csv(r.get("memo"))).append(',').append(csv(r.get("createdAt"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=deliveries.csv").body(csv.toString()); - } - private Map deliveryData(JwtUser u, Map f, int page, int size){ - List all=em.createQuery("select c from Company c where c.groupId=:g and c.type='DELIVERY'",Company.class) - .setParameter("g",u.getGroupId()).getResultList(); - String searchType=f.getOrDefault("searchType","업체명"); - String keyword=f.getOrDefault("keyword",""); - List filtered=all.stream().filter(c->{ - if(!blank(keyword)){ - String q=keyword.trim(); - return switch(searchType){ - case "연락처" -> like(c.getPhone(), q); - case "비고" -> like(c.getMemo(), q); - default -> like(c.getName(), q); - }; - } - return true; - }).sorted(deliveryComparator(f.get("sort"))).toList(); - int sz=Math.min(Math.max(size,1),200); - int fromIdx=Math.min(Math.max(page,0)*sz, filtered.size()); - int toIdx=Math.min(fromIdx+sz, filtered.size()); - List> list=filtered.subList(fromIdx,toIdx).stream().map(this::deliveryMap).toList(); - Map result=new LinkedHashMap<>(); - result.put("total", filtered.size()); - result.put("list", list); - return result; - } - private Comparator deliveryComparator(String sort){ - String field=sort==null?"name,asc":sort; boolean asc=field.endsWith(",asc"); - Comparator c=switch(field.split(",")[0]){ - case "createdAt"->Comparator.comparing(Company::getCreatedAt,Comparator.nullsLast(Instant::compareTo)); - case "phone"->Comparator.comparing(Company::getPhone,Comparator.nullsLast(String::compareTo)); - default->Comparator.comparing(Company::getName,Comparator.nullsLast(String::compareTo)); - }; - return (asc?c:c.reversed()).thenComparing(Company::getId); - } - private Map deliveryMap(Company c){ - Map r=new LinkedHashMap<>(); - r.put("id", c.getId()); - r.put("status", c.isActive()?"사용":"미사용"); - r.put("active", c.isActive()); - r.put("name", c.getName()); - r.put("phone", c.getPhone()); - r.put("memo", c.getMemo()); - r.put("createdAt", c.getCreatedAt()==null?null:c.getCreatedAt().atZone(ZoneId.systemDefault()).toLocalDate().toString()); - return r; - } - @PostMapping("/staff") @Transactional ApiResponse staffCreate(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - String userid=d.get("userid")==null?"":String.valueOf(d.get("userid")).trim(); - String password=d.get("password")==null?"":String.valueOf(d.get("password")); - String name=d.get("name")==null?"":String.valueOf(d.get("name")).trim(); - if(userid.isBlank()) return ApiResponse.fail("아이디를 입력하세요."); - if(password.isBlank()) return ApiResponse.fail("비밀번호를 입력하세요."); - if(name.isBlank()) return ApiResponse.fail("이름을 입력하세요."); - if(!em.createQuery("select m from Member m where m.userid=:id",Member.class).setParameter("id",userid).getResultList().isEmpty()) - return ApiResponse.fail("이미 사용 중인 아이디입니다."); - Member m=new Member(); - m.setGroupId(u.getGroupId()); - m.setUserid(userid); - m.setPassword(encoder.encode(password)); - m.setPlainPassword(password); - m.setName(name); - m.setPhone(formatStaffPhone(String.valueOf(d.getOrDefault("phone","")))); - m.setRole("AGENT"); - m.setStaffPermission(blank(String.valueOf(d.getOrDefault("staffPermission","")))?"영업":String.valueOf(d.get("staffPermission"))); - m.setActive(!"중지".equals(String.valueOf(d.getOrDefault("status",""))) && !"false".equalsIgnoreCase(String.valueOf(d.getOrDefault("active","")))); - m.setLoginCount(0); - m.setAssignedOutNames(blank(String.valueOf(d.getOrDefault("assignedOutNames","")))?null:String.valueOf(d.get("assignedOutNames")).trim()); - m.setMemo(blank(String.valueOf(d.getOrDefault("memo","")))?null:String.valueOf(d.get("memo")).trim()); - em.persist(m); - return ApiResponse.ok(staffMap(m)); - } - @PutMapping("/staff/{id}") @Transactional ApiResponse staffUpdate(@PathVariable Long id,@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Member m=em.find(Member.class,id); - if(m==null || !Objects.equals(m.getGroupId(),u.getGroupId()) || !"AGENT".equals(m.getRole())) return ApiResponse.fail("직원을 찾을 수 없습니다."); - if(d.get("name")!=null){ - String name=String.valueOf(d.get("name")).trim(); - if(name.isBlank()) return ApiResponse.fail("이름을 입력하세요."); - m.setName(name); - } - if(d.get("phone")!=null) m.setPhone(formatStaffPhone(String.valueOf(d.get("phone")))); - if(d.get("staffPermission")!=null && !String.valueOf(d.get("staffPermission")).isBlank()) m.setStaffPermission(String.valueOf(d.get("staffPermission"))); - if(d.get("assignedOutNames")!=null) m.setAssignedOutNames(blank(String.valueOf(d.get("assignedOutNames")))?null:String.valueOf(d.get("assignedOutNames")).trim()); - if(d.get("memo")!=null) m.setMemo(blank(String.valueOf(d.get("memo")))?null:String.valueOf(d.get("memo")).trim()); - if(d.get("status")!=null) m.setActive(!"중지".equals(String.valueOf(d.get("status")))); - if(d.get("active")!=null) m.setActive(Boolean.parseBoolean(String.valueOf(d.get("active"))) || "true".equalsIgnoreCase(String.valueOf(d.get("active"))) || "사용".equals(String.valueOf(d.get("active")))); - if(d.get("password")!=null && !String.valueOf(d.get("password")).isBlank()){ - String password=String.valueOf(d.get("password")); - m.setPassword(encoder.encode(password)); - m.setPlainPassword(password); - } - return ApiResponse.ok(staffMap(m)); - } - @GetMapping(value="/staff/export", produces="text/csv;charset=UTF-8") ResponseEntity staffExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - @SuppressWarnings("unchecked") List> rows=(List>) staffData(u,f,0,10000).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF상태,권한,아이디,이름,휴대폰,담당출고처,접속수,최종접속,등록일,비고\n"); - for(Map r:rows) csv.append(csv(r.get("status"))).append(',').append(csv(r.get("staffPermission"))).append(',') - .append(csv(r.get("userid"))).append(',').append(csv(r.get("name"))).append(',').append(csv(r.get("phone"))).append(',') - .append(csv(r.get("assignedOutNames"))).append(',').append(csv(r.get("loginCount"))).append(',') - .append(csv(r.get("lastLoginAt"))).append(',').append(csv(r.get("createdAt"))).append(',').append(csv(r.get("memo"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=staff.csv").body(csv.toString()); - } - private Map staffData(JwtUser u, Map f, int page, int size){ - List all=em.createQuery("select m from Member m where m.groupId=:g and m.role='AGENT'",Member.class) - .setParameter("g",u.getGroupId()).getResultList(); - String status=f.getOrDefault("status",""); - String keyword=f.getOrDefault("keyword",""); - List filtered=all.stream().filter(m->{ - if("사용".equals(status) && !m.isActive()) return false; - if("중지".equals(status) && m.isActive()) return false; - if(!blank(keyword)){ - String q=keyword.trim().toLowerCase(); - boolean hit=(m.getName()!=null && m.getName().toLowerCase().contains(q)) - || (m.getUserid()!=null && m.getUserid().toLowerCase().contains(q)); - if(!hit) return false; - } - return true; - }).sorted(staffComparator(f.get("sort"))).toList(); - int sz=Math.min(Math.max(size,1),200); - int fromIdx=Math.min(Math.max(page,0)*sz, filtered.size()); - int toIdx=Math.min(fromIdx+sz, filtered.size()); - List> list=filtered.subList(fromIdx,toIdx).stream().map(this::staffMap).toList(); - Map result=new LinkedHashMap<>(); - result.put("total", filtered.size()); - result.put("list", list); - return result; - } - private Comparator staffComparator(String sort){ - String field=sort==null?"createdAt,asc":sort; boolean asc=field.endsWith(",asc"); - Comparator c=switch(field.split(",")[0]){ - case "staffPermission"->Comparator.comparing(m->blank(m.getStaffPermission())?"영업":m.getStaffPermission(),Comparator.nullsLast(String::compareTo)); - case "userid"->Comparator.comparing(Member::getUserid,Comparator.nullsLast(String::compareTo)); - case "name"->Comparator.comparing(Member::getName,Comparator.nullsLast(String::compareTo)); - case "loginCount"->Comparator.comparing(m->m.getLoginCount()==null?0:m.getLoginCount()); - case "lastLoginAt"->Comparator.comparing(Member::getLastLoginAt,Comparator.nullsLast(LocalDateTime::compareTo)); - default->Comparator.comparing(Member::getCreatedAt,Comparator.nullsLast(Instant::compareTo)); - }; - // 대표 first - Comparator ownerFirst=(a,b)->{ - boolean ao="대표".equals(a.getStaffPermission()); - boolean bo="대표".equals(b.getStaffPermission()); - if(ao==bo) return 0; - return ao?-1:1; - }; - return ownerFirst.thenComparing(asc?c:c.reversed()).thenComparing(Member::getId); - } - private Map staffMap(Member m){ - Map r=new LinkedHashMap<>(); - r.put("id", m.getId()); - r.put("status", m.isActive()?"사용":"중지"); - r.put("active", m.isActive()); - r.put("staffPermission", blank(m.getStaffPermission())?("demo".equals(m.getUserid())?"대표":"영업"):m.getStaffPermission()); - r.put("userid", m.getUserid()); - r.put("name", m.getName()); - r.put("phone", formatStaffPhone(m.getPhone())); - r.put("assignedOutNames", m.getAssignedOutNames()); - r.put("loginCount", m.getLoginCount()==null?0:m.getLoginCount()); - r.put("lastLoginAt", m.getLastLoginAt()==null?null:m.getLastLoginAt().format(java.time.format.DateTimeFormatter.ofPattern("MM-dd HH:mm"))); - r.put("createdAt", m.getCreatedAt()==null?null:m.getCreatedAt().atZone(ZoneId.systemDefault()).toLocalDate().toString()); - r.put("memo", m.getMemo()); - r.put("isOwner", "대표".equals(r.get("staffPermission")) || "demo".equals(m.getUserid())); - return r; - } - private String formatStaffPhone(String phone){ - if(phone==null || phone.isBlank()) return ""; - String p=phone.replaceAll("[^0-9]",""); - if(p.length()==11) return p.substring(0,3)+"-"+p.substring(3,7)+"-"+p.substring(7); - if(p.length()==10) return p.substring(0,3)+"-"+p.substring(3,6)+"-"+p.substring(6); - return phone; - } - @GetMapping("/settings/agentshop") ApiResponse agentShopSettingGet(@AuthenticationPrincipal JwtUser u){ - return ApiResponse.ok(loadAgentShopSetting(u.getGroupId())); - } - @PutMapping("/settings/agentshop") @Transactional ApiResponse agentShopSettingPut(@AuthenticationPrincipal JwtUser u,@RequestBody Map d){ - Map current=loadAgentShopSetting(u.getGroupId()); - current.putAll(d); - String json=writeSettingJson(current); - List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='agentshop'",Setting.class).setParameter("g",u.getGroupId()).getResultList(); - if(rows.isEmpty()){ - Setting s=new Setting(); s.setGroupId(u.getGroupId()); s.setSettingKey("agentshop"); s.setValue(json); em.persist(s); - } else { - rows.getFirst().setValue(json); - } - return ApiResponse.ok(current); - } - private Map loadAgentShopSetting(String groupId){ - Map defaults=defaultAgentShopSetting(); - List rows=em.createQuery("select e from Setting e where e.groupId=:g and e.settingKey='agentshop'",Setting.class).setParameter("g",groupId).getResultList(); - if(rows.isEmpty()||blank(rows.getFirst().getValue())) return defaults; - try{ - @SuppressWarnings("unchecked") Map parsed=new com.fasterxml.jackson.databind.ObjectMapper().readValue(rows.getFirst().getValue(), Map.class); - defaults.putAll(parsed); - }catch(Exception ignored){} - return defaults; - } - private Map defaultAgentShopSetting(){ - Map m=new LinkedHashMap<>(); - m.put("access",true); - m.put("policy",true); m.put("pds",true); m.put("returnPhone",true); m.put("indoc",true); - m.put("scan",true); m.put("scanReceiveSms",false); m.put("scanReceivePhones",""); - m.put("scanSendSms",false); m.put("scanSenderPhone","1600-3903"); - m.put("scanCompleteMsg","스캔신청서가 완료 처리되었습니다."); - m.put("scanHoldMsg","스캔신청서가 보류 처리되었습니다."); - m.put("stock",true); m.put("stockPeriod","always"); - m.put("open",true); m.put("settle",true); m.put("farebox",true); - m.put("shopUrl","https://shop.poncle.co.kr"); - return m; - } - private String writeSettingJson(Map m){ - try{ return new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(m); } - catch(Exception e){ return "{}"; } - } - @DeleteMapping({"/scans/{id}","/indocs/{id}","/returns/{id}","/fareboxes/{id}","/accounts/{id}","/sms/{id}","/cs/{id}"}) ApiResponse commonDel(HttpServletRequest r,@PathVariable Long id,@AuthenticationPrincipal JwtUser u){return del(type(r.getRequestURI()),id,u);} - @PutMapping({"/schedules/{id}","/accounts/{id}","/sms/{id}"}) ApiResponse commonPut2(HttpServletRequest r,@PathVariable Long id,@RequestBody Mapd,@AuthenticationPrincipal JwtUser u){return put(type(r.getRequestURI()),id,d,u);} - @GetMapping("/basic/phones") ApiResponse phones(@AuthenticationPrincipal JwtUser u,@RequestParam(required=false,defaultValue="") String q){return list(PhoneModel.class,u,q.isBlank()?Map.of():Map.of("modelName",q),0,100);} - @GetMapping("/basic/usims") ApiResponse usims(@AuthenticationPrincipal JwtUser u,@RequestParam(required=false,defaultValue="") String q){return list(Stock.class,u,Map.of("gubun","유심"),0,100);} - @GetMapping("/basic/plans") ApiResponse plans(@AuthenticationPrincipal JwtUser u,@RequestParam(required=false,defaultValue="") String telecom){return list(PlanMaster.class,u,telecom==null||telecom.isBlank()?Map.of():Map.of("telecom",telecom),0,100);} - @GetMapping("/basic/petnames") ApiResponse petnames(@AuthenticationPrincipal JwtUser u,@RequestParam(required=false,defaultValue="") String q){return list(Company.class,u,q.isBlank()?Map.of():Map.of("name",q),0,100);} - @GetMapping("/stocks/have-own") ApiResponse haveOwn(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { - return ApiResponse.ok(haveOwnData(u, f)); - } - @GetMapping(value="/stocks/have-own/export", produces="text/csv;charset=UTF-8") ResponseEntity haveOwnExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { - Map data=haveOwnData(u,f); - @SuppressWarnings("unchecked") List models=(List) data.get("models"); - @SuppressWarnings("unchecked") List> rows=(List>) data.get("rows"); - StringBuilder csv=new StringBuilder("\uFEFF채널,영업사원,출고처,전체"); - for(String m:models) csv.append(',').append(csv(m)); - csv.append('\n'); - for(Map r:rows) { - csv.append(csv(r.get("channel"))).append(',').append(csv(r.get("salesperson"))).append(',') - .append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("total"))); - @SuppressWarnings("unchecked") Map counts=(Map) r.get("counts"); - for(String m:models) csv.append(',').append(csv(counts.getOrDefault(m,0)==0?"-":counts.get(m))); - csv.append('\n'); - } - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=have-own.csv").body(csv.toString()); - } - private Map haveOwnData(JwtUser u, Map f) { - Map companies=companyNames(u); - boolean includeColor="true".equalsIgnoreCase(f.getOrDefault("includeColor","false")); - List stocks=em.createQuery("select s from Stock s where s.groupId=:g",Stock.class).setParameter("g",u.getGroupId()).getResultList(); - List held=stocks.stream().filter(s->{ - if(!Set.of("IN","OUT","RECOVERY").contains(s.getState()==null?"":s.getState())) return false; - if(!eq(s.getGubun(),f.get("gubun"))) return false; - if(!eqId(s.getIncompanyId(),f.get("incompanyId"))) return false; - if(!eqId(s.getOutcompanyId(),f.get("outcompanyId"))) return false; - return true; - }).toList(); - LinkedHashSet modelSet=new LinkedHashSet<>(); - Map> rowMap=new LinkedHashMap<>(); - for(Stock s:held) { - String channel=haveOwnChannel(s); - String salesperson=blank(s.getSalesperson())?( "IN".equals(s.getState())?"본점":"-"):s.getSalesperson(); - String outName; - Long outId=s.getOutcompanyId(); - if(outId!=null) outName=companies.getOrDefault(outId,"-"); - else if("IN".equals(s.getState())) outName="본점"; - else outName="-"; - String modelKey=blank(s.getModel())?"-":s.getModel(); - if(includeColor) { - String color=blank(s.getColor())?"-":s.getColor(); - modelKey=modelKey+" / "+color; - } - modelSet.add(modelKey); - String rowKey=channel+"|"+salesperson+"|"+(outId==null?"0":outId)+"|"+outName; - Map row=rowMap.computeIfAbsent(rowKey,k->{ - Map r=new LinkedHashMap<>(); - r.put("channel",channel); r.put("salesperson",salesperson); - r.put("outcompanyId",outId); r.put("outcompanyName",outName); - r.put("total",0); r.put("counts",new LinkedHashMap()); - return r; - }); - @SuppressWarnings("unchecked") Map counts=(Map) row.get("counts"); - counts.put(modelKey, counts.getOrDefault(modelKey,0)+1); - row.put("total", ((Integer)row.get("total"))+1); - } - List models=modelSet.stream().sorted().toList(); - List> rows=new ArrayList<>(rowMap.values()); - rows.sort(Comparator - .comparing((Map r)->String.valueOf(r.get("channel"))) - .thenComparing(r->String.valueOf(r.get("salesperson"))) - .thenComparing(r->String.valueOf(r.get("outcompanyName")))); - // row spans - for(int i=0;i result=new LinkedHashMap<>(); - result.put("asOf", LocalDate.now().toString()); - result.put("title", LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy년 MM월 dd일"))+" 현재보유재고"); - result.put("models", models); - result.put("rows", rows); - result.put("totalQty", held.size()); - return result; - } - private String haveOwnChannel(Stock s) { - if("도매".equals(s.getOutCategory())) return "도매"; - if("IN".equals(s.getState()) && s.getOutcompanyId()==null) return "본점"; - if("본사".equals(s.getSource()) || "판매점".equals(s.getOutCategory())) return "본점"; - return blank(s.getOutCategory())?"본점":s.getOutCategory(); - } - @GetMapping("/stocks/have-model") ApiResponse haveModel(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { - return ApiResponse.ok(haveModelData(u, f)); - } - @GetMapping(value="/stocks/have-model/export", produces="text/csv;charset=UTF-8") ResponseEntity haveModelExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { - Map data=haveModelData(u,f); - boolean totalsOnly="true".equalsIgnoreCase(f.getOrDefault("totalsOnly","false")); - @SuppressWarnings("unchecked") List holders=(List) data.get("holders"); - @SuppressWarnings("unchecked") List> rows=(List>) data.get("rows"); - StringBuilder csv=new StringBuilder("\uFEFF종류,모델명,색상,전체,불량,본점"); - if(!totalsOnly) for(String h:holders) csv.append(',').append(csv(h)); - csv.append('\n'); - for(Map r:rows) { - csv.append(csv(r.get("gubun"))).append(',').append(csv(r.get("model"))).append(',') - .append(csv(r.get("color"))).append(',').append(csv(dashZero(r.get("total")))).append(',') - .append(csv(dashZero(r.get("defect")))).append(',').append(csv(dashZero(r.get("hq")))); - if(!totalsOnly) { - @SuppressWarnings("unchecked") Map counts=(Map) r.get("counts"); - for(String h:holders) csv.append(',').append(csv(dashZero(counts.getOrDefault(h,0)))); - } - csv.append('\n'); - } - String filename=totalsOnly?"have-model-totals.csv":"have-model.csv"; - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename="+filename).body(csv.toString()); - } - private Object dashZero(Object v){ if(v==null) return "-"; if(v instanceof Number n && n.intValue()==0) return "-"; return v; } - private Map haveModelData(JwtUser u, Map f) { - Map companies=companyNames(u); - List stocks=em.createQuery("select s from Stock s where s.groupId=:g",Stock.class).setParameter("g",u.getGroupId()).getResultList(); - List held=stocks.stream().filter(s->{ - if(!Set.of("IN","OUT","RECOVERY").contains(s.getState()==null?"":s.getState())) return false; - if(!eqId(s.getIncompanyId(),f.get("incompanyId"))) return false; - if(!like(s.getModel(),f.get("model"))) return false; - return true; - }).toList(); - LinkedHashSet holderSet=new LinkedHashSet<>(); - Map> rowMap=new LinkedHashMap<>(); - for(Stock s:held) { - String gubun=blank(s.getGubun())?"-":s.getGubun(); - String model=blank(s.getModel())?"-":s.getModel(); - String color=blank(s.getColor())?"-":s.getColor(); - String rowKey=gubun+"|"+model+"|"+color; - Map row=rowMap.computeIfAbsent(rowKey,k->{ - Map r=new LinkedHashMap<>(); - r.put("gubun",gubun); r.put("model",model); r.put("color",color); - r.put("total",0); r.put("defect",0); r.put("hq",0); - r.put("counts",new LinkedHashMap()); - return r; - }); - row.put("total", ((Integer)row.get("total"))+1); - if("불량".equals(s.getCond())) row.put("defect", ((Integer)row.get("defect"))+1); - boolean isHq="IN".equals(s.getState()) && s.getOutcompanyId()==null; - if(isHq) { - row.put("hq", ((Integer)row.get("hq"))+1); - } else { - String holder; - if(s.getOutcompanyId()!=null) holder=companies.getOrDefault(s.getOutcompanyId(),"-"); - else if(!blank(s.getSalesperson())) holder=s.getSalesperson(); - else holder="-"; - holderSet.add(holder); - @SuppressWarnings("unchecked") Map counts=(Map) row.get("counts"); - counts.put(holder, counts.getOrDefault(holder,0)+1); - } - } - List holders=holderSet.stream().sorted().toList(); - List> rows=new ArrayList<>(rowMap.values()); - rows.sort(Comparator - .comparing((Map r)->String.valueOf(r.get("gubun"))) - .thenComparing(r->String.valueOf(r.get("model"))) - .thenComparing(r->String.valueOf(r.get("color")))); - for(int i=0;i result=new LinkedHashMap<>(); - result.put("asOf", LocalDate.now().toString()); - result.put("title", LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy년 MM월 dd일"))+" 현재보유재고"); - result.put("holders", holders); - result.put("rows", rows); - result.put("totalQty", held.size()); - return result; - } - @GetMapping("/stocks/turnover") ApiResponse turnover(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { - return ApiResponse.ok(turnoverData(u, f)); - } - @GetMapping(value="/stocks/turnover/export", produces="text/csv;charset=UTF-8") ResponseEntity turnoverExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { - Map data=turnoverData(u,f); - String mode=String.valueOf(data.getOrDefault("mode","outcompany")); - @SuppressWarnings("unchecked") List> rows=(List>) data.get("rows"); - StringBuilder csv=new StringBuilder("\uFEFF순위,"); - if("model".equals(mode)) csv.append("모델명,개통수,현재 재고수,회전율,회전일,개통예상\n"); - else csv.append("출고처,개통수,현재 재고수,회전율,회전일,개통예상,영업담당자\n"); - for(Map r:rows) { - csv.append(csv(r.get("rank"))).append(','); - if("model".equals(mode)) { - csv.append(csv(r.get("model"))).append(',').append(csv(r.get("openCount"))).append(',') - .append(csv(r.get("stockCount"))).append(',').append(csv(r.get("turnoverRate"))).append(',') - .append(csv(r.get("turnoverDays"))).append(',').append(csv(r.get("expectedOpen"))).append('\n'); - } else { - csv.append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("openCount"))).append(',') - .append(csv(r.get("stockCount"))).append(',').append(csv(r.get("turnoverRate"))).append(',') - .append(csv(r.get("turnoverDays"))).append(',').append(csv(r.get("expectedOpen"))).append(',') - .append(csv(r.get("salesperson"))).append('\n'); - } - } - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=turnover.csv").body(csv.toString()); - } - private Map turnoverData(JwtUser u, Map f) { - String mode=f.getOrDefault("mode","outcompany"); - Map companies=companyNames(u); - List stocks=em.createQuery("select s from Stock s where s.groupId=:g",Stock.class).setParameter("g",u.getGroupId()).getResultList() - .stream().filter(s->!"유심".equals(s.getGubun())).toList(); - List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g",OpenRecord.class).setParameter("g",u.getGroupId()).getResultList(); - Map agg=new LinkedHashMap<>(); - Map salesMap=new LinkedHashMap<>(); - for(OpenRecord o:opens) { - String key="model".equals(mode) - ? turnoverModelKey(o.getPmodel(), o.getPcolor()) - : (o.getOutcompanyId()==null?"본점":companies.getOrDefault(o.getOutcompanyId(),"-")); - int[] v=agg.computeIfAbsent(key,k->new int[]{0,0}); - v[0]++; - } - for(Stock s:stocks) { - if("OPEN".equals(s.getState())) { - String key="model".equals(mode) - ? turnoverModelKey(s.getModel(), s.getColor()) - : (s.getOutcompanyId()==null?"본점":companies.getOrDefault(s.getOutcompanyId(),"-")); - int[] v=agg.computeIfAbsent(key,k->new int[]{0,0}); - v[0]++; - if(!blank(s.getSalesperson())) salesMap.putIfAbsent(key, s.getSalesperson()); - } - if(Set.of("IN","OUT","RECOVERY").contains(s.getState()==null?"":s.getState())) { - String key="model".equals(mode) - ? turnoverModelKey(s.getModel(), s.getColor()) - : (s.getOutcompanyId()==null?"본점":companies.getOrDefault(s.getOutcompanyId(),"-")); - if(!"model".equals(mode) && "IN".equals(s.getState()) && s.getOutcompanyId()!=null) continue; - int[] v=agg.computeIfAbsent(key,k->new int[]{0,0}); - v[1]++; - if(!blank(s.getSalesperson())) salesMap.putIfAbsent(key, s.getSalesperson()); - } - } - List> rows=new ArrayList<>(); - for(var e:agg.entrySet()) { - int openCount=e.getValue()[0], stockCount=e.getValue()[1]; - if(openCount==0 && stockCount==0) continue; - double rate=stockCount>0 ? Math.round(openCount * 10000.0 / stockCount) / 100.0 : 0; - int days=openCount>0 ? (int)Math.round(stockCount * 10.0 / openCount) : 0; - Map row=new LinkedHashMap<>(); - if("model".equals(mode)) row.put("model", e.getKey()); - else row.put("outcompanyName", e.getKey()); - row.put("openCount", openCount); - row.put("stockCount", stockCount); - row.put("turnoverRate", rate); - row.put("turnoverDays", days); - // 10일 기준 개통수를 월(30일) 예상으로 환산 - row.put("expectedOpen", openCount == 0 ? 0 : openCount * 3); - row.put("salesperson", salesMap.getOrDefault(e.getKey(),"-")); - rows.add(row); - } - String sort=f.getOrDefault("sort","turnoverRate,desc"); - boolean asc=sort.endsWith(",asc"); - String field=sort.split(",")[0]; - rows.sort((a,b)->{ - int cmp=switch(field){ - case "outcompanyName"->String.valueOf(a.get("outcompanyName")).compareTo(String.valueOf(b.get("outcompanyName"))); - case "model"->String.valueOf(a.get("model")).compareTo(String.valueOf(b.get("model"))); - case "openCount"->Integer.compare((Integer)a.get("openCount"),(Integer)b.get("openCount")); - case "stockCount"->Integer.compare((Integer)a.get("stockCount"),(Integer)b.get("stockCount")); - case "turnoverDays"->Integer.compare((Integer)a.get("turnoverDays"),(Integer)b.get("turnoverDays")); - case "expectedOpen"->Integer.compare((Integer)a.get("expectedOpen"),(Integer)b.get("expectedOpen")); - case "salesperson"->String.valueOf(a.get("salesperson")).compareTo(String.valueOf(b.get("salesperson"))); - default->Double.compare(((Number)a.get("turnoverRate")).doubleValue(),((Number)b.get("turnoverRate")).doubleValue()); - }; - return asc?cmp:-cmp; - }); - for(int i=0;i result=new LinkedHashMap<>(); - result.put("mode", mode); - result.put("info", info); - result.put("rows", rows); - result.put("total", rows.size()); - return result; - } - private String turnoverModelKey(String model, String color) { - String m=blank(model)?"-":model.trim(); - if(blank(color)) return m; - return m+" "+color.trim(); - } - @GetMapping("/reports/mon-open-stats") ApiResponse monOpenStats(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - return ApiResponse.ok(monOpenStatsData(u,f)); - } - @GetMapping(value="/reports/mon-open-stats/export", produces="text/csv;charset=UTF-8") ResponseEntity monOpenStatsExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - Map data=monOpenStatsData(u,f); - String mode=String.valueOf(data.getOrDefault("mode","outcompany")); - String firstCol=switch(mode){ case "incompany"->"입고처"; case "model"->"모델명"; default->"출고처"; }; - @SuppressWarnings("unchecked") List> rows=(List>) data.get("rows"); - StringBuilder csv=new StringBuilder("\uFEFF").append(firstCol); - for(int m=1;m<=12;m++) csv.append(',').append(m).append("월"); - csv.append(",합계\n"); - for(Map r:rows){ - csv.append(csv(r.get("name"))); - @SuppressWarnings("unchecked") List months=(List) r.get("months"); - for(int m=0;m<12;m++) csv.append(',').append(csv(dashZero(months.get(m)))); - csv.append(',').append(csv(dashZero(r.get("total")))).append('\n'); - } - @SuppressWarnings("unchecked") Map totals=(Map) data.get("totals"); - if(totals!=null){ - csv.append(csv("합계")); - @SuppressWarnings("unchecked") List months=(List) totals.get("months"); - for(int m=0;m<12;m++) csv.append(',').append(csv(dashZero(months.get(m)))); - csv.append(',').append(csv(dashZero(totals.get("total")))).append('\n'); - } - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=mon-open-stats.csv").body(csv.toString()); - } - @GetMapping("/reports/open-charts") ApiResponse openCharts(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - return ApiResponse.ok(openChartsData(u,f)); - } - private Map openChartsData(JwtUser u, Map f){ - String mode=f.getOrDefault("mode","outcompany"); - return switch(mode){ - case "outcompany-annual"->openChartAnnual(u,f,"outcompany"); - case "sales-annual"->openChartSalesAnnual(u,f); - case "sales"->openChartPeriod(u,f,"sales"); - default->openChartPeriod(u,f,"outcompany"); - }; - } - private Map openChartPeriod(JwtUser u, Map f, String dim){ - LocalDate from=parseDate(f.get("from"), LocalDate.now().minusDays(29)); - LocalDate to=parseDate(f.get("to"), LocalDate.now()); - if(to.isBefore(from)){ LocalDate t=from; from=to; to=t; } - Map companies=companyNames(u); - Map counts=new LinkedHashMap<>(); - if("outcompany".equals(dim)){ - List outs=em.createQuery("select c from Company c where c.groupId=:g and c.type in ('OUT','SHOP') order by c.name",Company.class) - .setParameter("g",u.getGroupId()).getResultList(); - for(Company c:outs) counts.putIfAbsent(blank(c.getName())?"-":c.getName(),0); - } - List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g and o.opendate between :a and :b",OpenRecord.class) - .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); - for(OpenRecord o:opens){ - if(!eq(o.getTelecom(),f.get("telecom"))) continue; - String key; - if("sales".equals(dim)){ - if(blank(o.getSalesperson())) continue; - key=o.getSalesperson().trim(); - } else { - key=o.getOutcompanyId()==null?"본점":companies.getOrDefault(o.getOutcompanyId(),"-"); - if("-".equals(key)) continue; - } - counts.put(key, counts.getOrDefault(key,0)+1); - } - if("sales".equals(dim) && counts.isEmpty()){ - // keep empty chart categories from known salespeople on stocks - List names=em.createQuery("select distinct s.salesperson from Stock s where s.groupId=:g and s.salesperson is not null and s.salesperson<>''",String.class) - .setParameter("g",u.getGroupId()).getResultList(); - for(String n:names) counts.putIfAbsent(n,0); - } - String sort=f.getOrDefault("sort","name"); - List> entries=new ArrayList<>(counts.entrySet()); - if("count".equals(sort) || "value".equals(sort)) - entries.sort((a,b)->{ - int c=Integer.compare(b.getValue(),a.getValue()); - return c!=0?c:a.getKey().compareTo(b.getKey()); - }); - else entries.sort(Map.Entry.comparingByKey()); - List> items=new ArrayList<>(); - for(var e:entries){ - Map row=new LinkedHashMap<>(); - row.put("name", e.getKey()); - row.put("value", e.getValue()); - items.add(row); - } - Map result=new LinkedHashMap<>(); - result.put("mode", "sales".equals(dim)?"sales":"outcompany"); - result.put("chartType", "bar"); - result.put("title", "sales".equals(dim)?"영업사원별 개통":"출고처별 개통"); - result.put("subtitle", "기간 : "+from+" ~ "+to); - result.put("from", from.toString()); - result.put("to", to.toString()); - result.put("items", items); - return result; - } - private Map openChartAnnual(JwtUser u, Map f, String dim){ - int year=parseInt(f.get("year"), LocalDate.now().getYear()); - LocalDate from=LocalDate.of(year,1,1); - LocalDate to=LocalDate.of(year,12,31); - Map companies=companyNames(u); - Map seriesMap=new LinkedHashMap<>(); - if("outcompany".equals(dim)){ - List outs=em.createQuery("select c from Company c where c.groupId=:g and c.type in ('OUT','SHOP') order by c.name",Company.class) - .setParameter("g",u.getGroupId()).getResultList(); - for(Company c:outs) seriesMap.putIfAbsent(blank(c.getName())?"-":c.getName(), new int[12]); - } - List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g and o.opendate between :a and :b",OpenRecord.class) - .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); - for(OpenRecord o:opens){ - if(!eq(o.getTelecom(),f.get("telecom"))) continue; - String key=o.getOutcompanyId()==null?"본점":companies.getOrDefault(o.getOutcompanyId(),"-"); - if("-".equals(key)) continue; - int[] arr=seriesMap.computeIfAbsent(key,k->new int[12]); - arr[o.getOpendate().getMonthValue()-1]++; - } - List categories=new ArrayList<>(); - for(int m=1;m<=12;m++) categories.add(m+"월"); - List> series=new ArrayList<>(); - for(var e:seriesMap.entrySet()){ - Map s=new LinkedHashMap<>(); - s.put("name", e.getKey()); - List data=new ArrayList<>(12); - for(int v:e.getValue()) data.add(v); - s.put("data", data); - series.add(s); - } - series.sort(Comparator.comparing(s->String.valueOf(s.get("name")))); - Map result=new LinkedHashMap<>(); - result.put("mode", "outcompany-annual"); - result.put("chartType", "line"); - result.put("title", "출고처별 개통"); - result.put("subtitle", year+"년"); - result.put("year", year); - result.put("categories", categories); - result.put("series", series); - return result; - } - private Map openChartSalesAnnual(JwtUser u, Map f){ - int year=parseInt(f.get("year"), LocalDate.now().getYear()); - int month=parseInt(f.get("month"), LocalDate.now().getMonthValue()); - if(month<1||month>12) month=LocalDate.now().getMonthValue(); - LocalDate end=LocalDate.of(year, month, 1); - LocalDate start=end.minusMonths(11); - LocalDate from=start.withDayOfMonth(1); - LocalDate to=end.withDayOfMonth(end.lengthOfMonth()); - Map seriesMap=new LinkedHashMap<>(); - List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g and o.opendate between :a and :b",OpenRecord.class) - .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); - for(OpenRecord o:opens){ - if(!eq(o.getTelecom(),f.get("telecom"))) continue; - if(blank(o.getSalesperson())) continue; - String key=o.getSalesperson().trim(); - int[] arr=seriesMap.computeIfAbsent(key,k->new int[12]); - int idx=monthIndexInWindow(o.getOpendate(), start); - if(idx>=0) arr[idx]++; - } - if(seriesMap.isEmpty()){ - List names=em.createQuery("select distinct s.salesperson from Stock s where s.groupId=:g and s.salesperson is not null and s.salesperson<>''",String.class) - .setParameter("g",u.getGroupId()).getResultList(); - for(String n:names) seriesMap.putIfAbsent(n, new int[12]); - } - List categories=new ArrayList<>(); - for(int i=0;i<12;i++){ - LocalDate d=start.plusMonths(i); - categories.add(d.getMonthValue()+"월"); - } - int[] sum=new int[12]; - int nSeries=Math.max(seriesMap.size(),1); - List> series=new ArrayList<>(); - List keys=new ArrayList<>(seriesMap.keySet()); - keys.sort(String::compareTo); - for(String key:keys){ - int[] arr=seriesMap.get(key); - Map s=new LinkedHashMap<>(); - s.put("name", key); - s.put("type", "bar"); - List data=new ArrayList<>(12); - for(int i=0;i<12;i++){ data.add(arr[i]); sum[i]+=arr[i]; } - s.put("data", data); - series.add(s); - } - List avg=new ArrayList<>(12); - for(int i=0;i<12;i++) avg.add(Math.round(sum[i]*100.0/nSeries)/100.0); - Map avgSeries=new LinkedHashMap<>(); - avgSeries.put("name", "평균"); - avgSeries.put("type", "line"); - avgSeries.put("data", avg); - series.add(avgSeries); - Map result=new LinkedHashMap<>(); - result.put("mode", "sales-annual"); - result.put("chartType", "combo"); - result.put("title", "영업사원별 개통"); - result.put("subtitle", year+"년 "+String.format("%02d", month)+"월"); - result.put("year", year); - result.put("month", month); - result.put("categories", categories); - result.put("series", series); - return result; - } - private int monthIndexInWindow(LocalDate date, LocalDate windowStart){ - if(date==null) return -1; - int idx=(date.getYear()-windowStart.getYear())*12+(date.getMonthValue()-windowStart.getMonthValue()); - return (idx>=0 && idx<12)?idx:-1; - } - private LocalDate parseDate(String v, LocalDate fallback){ - if(blank(v)) return fallback; - try{ return LocalDate.parse(v); } catch(Exception e){ return fallback; } - } - private int parseInt(String v, int fallback){ - if(blank(v)) return fallback; - try{ return Integer.parseInt(v); } catch(Exception e){ return fallback; } - } - private Map monOpenStatsData(JwtUser u, Map f){ - String mode=f.getOrDefault("mode","outcompany"); - int year; - try{ year=Integer.parseInt(f.getOrDefault("year",String.valueOf(LocalDate.now().getYear()))); } - catch(Exception e){ year=LocalDate.now().getYear(); } - LocalDate from=LocalDate.of(year,1,1); - LocalDate to=LocalDate.of(year,12,31); - Map companies=companyNames(u); - Map agg=new LinkedHashMap<>(); - if("outcompany".equals(mode)){ - List outs=em.createQuery("select c from Company c where c.groupId=:g and c.type in ('OUT','SHOP') order by c.name",Company.class) - .setParameter("g",u.getGroupId()).getResultList(); - for(Company c:outs) agg.putIfAbsent(c.getName()==null?"-":c.getName(), new int[12]); - } else if("incompany".equals(mode)){ - List ins=em.createQuery("select c from Company c where c.groupId=:g and c.type='IN' order by c.name",Company.class) - .setParameter("g",u.getGroupId()).getResultList(); - for(Company c:ins) agg.putIfAbsent(c.getName()==null?"-":c.getName(), new int[12]); - } - List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g and o.opendate between :a and :b",OpenRecord.class) - .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); - for(OpenRecord o:opens){ - if(!eq(o.getTelecom(),f.get("telecom"))) continue; - if(!eq(o.getOutCategory(),f.get("outCategory"))) continue; - if(!eqId(o.getIncompanyId(),f.get("incompanyId"))) continue; - if(!eqId(o.getOutcompanyId(),f.get("outcompanyId"))) continue; - String key; - if("incompany".equals(mode)){ - if(o.getIncompanyId()==null) continue; - key=companies.getOrDefault(o.getIncompanyId(),"-"); - } else if("model".equals(mode)){ - if(blank(o.getPmodel())) continue; - key=o.getPmodel().trim(); - } else { - key=o.getOutcompanyId()==null?"본점":companies.getOrDefault(o.getOutcompanyId(),"-"); - } - if("-".equals(key)) continue; - int[] months=agg.computeIfAbsent(key,k->new int[12]); - int mi=o.getOpendate().getMonthValue()-1; - months[mi]++; - } - List> rows=new ArrayList<>(); - int[] totalMonths=new int[12]; - int grand=0; - List keys=new ArrayList<>(agg.keySet()); - keys.sort(String::compareTo); - long id=1; - for(String key:keys){ - int[] months=agg.get(key); - int rowTotal=0; - List monthList=new ArrayList<>(12); - for(int i=0;i<12;i++){ monthList.add(months[i]); rowTotal+=months[i]; totalMonths[i]+=months[i]; } - if("model".equals(mode) && rowTotal==0) continue; - grand+=rowTotal; - Map row=new LinkedHashMap<>(); - row.put("id", id++); - row.put("name", key); - row.put("months", monthList); - row.put("total", rowTotal); - rows.add(row); - } - List totalMonthList=new ArrayList<>(12); - for(int v:totalMonths) totalMonthList.add(v); - Map totals=new LinkedHashMap<>(); - totals.put("id", -1L); - totals.put("name", "합계"); - totals.put("months", totalMonthList); - totals.put("total", grand); - Map result=new LinkedHashMap<>(); - result.put("mode", mode); - result.put("year", year); - result.put("nameLabel", switch(mode){ case "incompany"->"입고처"; case "model"->"모델명"; default->"출고처"; }); - result.put("rows", rows); - result.put("totals", totals); - return result; - } - @GetMapping("/reports/open-stats") ApiResponse openStats(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - String mode=f.getOrDefault("mode","daily"); - if("monthly".equals(mode)) return ApiResponse.ok(openStatsMonthly(u,f)); - return ApiResponse.ok(openStatsDaily(u,f)); - } - @GetMapping(value="/reports/open-stats/export", produces="text/csv;charset=UTF-8") ResponseEntity openStatsExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - String mode=f.getOrDefault("mode","daily"); - Map data="monthly".equals(mode)?openStatsMonthly(u,f):openStatsDaily(u,f); - @SuppressWarnings("unchecked") List> rows=(List>) data.get("rows"); - String firstCol="monthly".equals(mode)?"월":"일자"; - StringBuilder csv=new StringBuilder("\uFEFF").append(firstCol).append(",신규,MNP,보상,기변,에이징,가개통,선불개통,합계,정산금액,후정산,후정산금액,철회,마진\n"); - for(Map r:rows) appendOpenStatCsv(csv,r,"monthly".equals(mode)?"label":"label"); - @SuppressWarnings("unchecked") Map totals=(Map) data.get("totals"); - if(totals!=null){ totals=new LinkedHashMap<>(totals); totals.put("label","합계"); appendOpenStatCsv(csv,totals,"label"); } - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=open-stats.csv").body(csv.toString()); - } - private void appendOpenStatCsv(StringBuilder csv,Map r,String labelKey){ - csv.append(csv(r.get(labelKey))).append(',').append(csv(r.get("newCount"))).append(',').append(csv(r.get("mnpCount"))).append(',') - .append(csv(r.get("rewardCount"))).append(',').append(csv(r.get("changeCount"))).append(',').append(csv(r.get("agingCount"))).append(',') - .append(csv(r.get("tempCount"))).append(',').append(csv(r.get("prepaidCount"))).append(',').append(csv(r.get("totalCount"))).append(',') - .append(csv(r.get("settleAmount"))).append(',').append(csv(r.get("afterCount"))).append(',').append(csv(r.get("afterAmount"))).append(',') - .append(csv(r.get("withdrawCount"))).append(',').append(csv(r.get("margin"))).append('\n'); - } - @GetMapping("/reports/home-stats") ApiResponse homeStats(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - String mode=f.getOrDefault("mode","daily"); - if("monthly".equals(mode)) return ApiResponse.ok(homeStatsMonthly(u,f)); - return ApiResponse.ok(homeStatsDaily(u,f)); - } - @GetMapping(value="/reports/home-stats/export", produces="text/csv;charset=UTF-8") ResponseEntity homeStatsExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - String mode=f.getOrDefault("mode","daily"); - Map data="monthly".equals(mode)?homeStatsMonthly(u,f):homeStatsDaily(u,f); - @SuppressWarnings("unchecked") List> rows=(List>) data.get("rows"); - String firstCol="monthly".equals(mode)?"월":"일자"; - StringBuilder csv=new StringBuilder("\uFEFF").append(firstCol).append(",인터넷,TV,집전화,인터넷전화,신용카드,홈IoT,동판,기타,후결합,재약정,소계,합계,정산금액\n"); - for(Map r:rows) appendHomeStatCsv(csv,r); - @SuppressWarnings("unchecked") Map totals=(Map) data.get("totals"); - if(totals!=null){ totals=new LinkedHashMap<>(totals); totals.put("label","합계"); appendHomeStatCsv(csv,totals); } - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=home-stats.csv").body(csv.toString()); - } - private void appendHomeStatCsv(StringBuilder csv,Map r){ - csv.append(csv(r.get("label"))).append(',').append(csv(r.get("internet"))).append(',').append(csv(r.get("tv"))).append(',') - .append(csv(r.get("homePhone"))).append(',').append(csv(r.get("voip"))).append(',').append(csv(r.get("creditCard"))).append(',') - .append(csv(r.get("homeIot"))).append(',').append(csv(r.get("dongpan"))).append(',').append(csv(r.get("etc"))).append(',') - .append(csv(r.get("postBind"))).append(',').append(csv(r.get("renew"))).append(',').append(csv(r.get("subtotal"))).append(',') - .append(csv(r.get("saleCount"))).append(',').append(csv(r.get("settleAmount"))).append('\n'); - } - private Map homeStatsDaily(JwtUser u,Map f){ - int year=Integer.parseInt(f.getOrDefault("year",String.valueOf(LocalDate.now().getYear()))); - int month=Integer.parseInt(f.getOrDefault("month",String.valueOf(LocalDate.now().getMonthValue()))); - LocalDate from=LocalDate.of(year,month,1); - LocalDate to=from.withDayOfMonth(from.lengthOfMonth()); - Map> byDay=new LinkedHashMap<>(); - for(int d=1;d<=to.getDayOfMonth();d++) byDay.put(d,emptyHomeStatRow(d+"일("+weekdayKo(from.withDayOfMonth(d))+")",d)); - for(HomeSale h:loadHomeSales(u,f,from,to)){ - Map target=byDay.get(h.getInstallDate().getDayOfMonth()); - if(target!=null) applyHomeSale(target,h); - } - List> rows=new ArrayList<>(byDay.values()); - Map out=new LinkedHashMap<>(); - out.put("mode","daily"); out.put("year",year); out.put("month",month); - out.put("title",year+"년 "+month+"월"); out.put("rows",rows); out.put("totals",sumHomeStatRows(rows)); - out.put("today",LocalDate.now().getYear()==year&&LocalDate.now().getMonthValue()==month?LocalDate.now().getDayOfMonth():null); - return out; - } - private Map homeStatsMonthly(JwtUser u,Map f){ - int year=Integer.parseInt(f.getOrDefault("year",String.valueOf(LocalDate.now().getYear()))); - LocalDate from=LocalDate.of(year,1,1); - LocalDate to=LocalDate.of(year,12,31); - Map> byMonth=new LinkedHashMap<>(); - for(int m=1;m<=12;m++) byMonth.put(m,emptyHomeStatRow(m+"월",m)); - for(HomeSale h:loadHomeSales(u,f,from,to)){ - Map target=byMonth.get(h.getInstallDate().getMonthValue()); - if(target!=null) applyHomeSale(target,h); - } - List> rows=new ArrayList<>(byMonth.values()); - Map out=new LinkedHashMap<>(); - out.put("mode","monthly"); out.put("year",year); - out.put("title",year+"년"); out.put("rows",rows); out.put("totals",sumHomeStatRows(rows)); - out.put("currentMonth",LocalDate.now().getYear()==year?LocalDate.now().getMonthValue():null); - return out; - } - private List loadHomeSales(JwtUser u,Map f,LocalDate from,LocalDate to){ - List all=em.createQuery("select h from HomeSale h where h.groupId=:g and h.installDate between :a and :b",HomeSale.class) - .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); - return all.stream().filter(h->{ - if(!"완료".equals(h.getStatus())&&!"판매완료".equals(h.getStatus())) return false; - if(!eq(h.getTelecom(),f.get("telecom"))) return false; - if(!eqId(h.getIncompanyId(),f.get("incompanyId"))) return false; - if(!eqId(h.getOutcompanyId(),f.get("outcompanyId"))) return false; - if(!eq(h.getOutCategory(),f.get("outCategory"))) return false; - return h.getInstallDate()!=null; - }).toList(); - } - private void applyHomeSale(Map target,HomeSale h){ - addInt(target,"saleCount",1); - addDec(target,"settleAmount",nvl(h.getSettleAmount())); - String products=h.getProducts()==null?"":h.getProducts(); - int productHits=0; - for(String raw:products.split("[,/|]")){ - String token=raw==null?"":raw.trim(); - if(token.isEmpty()) continue; - String bucket=homeProductBucket(token); - if(bucket==null) continue; - addInt(target,bucket,1); - productHits++; - } - if(productHits==0){ addInt(target,"etc",1); productHits=1; } - addInt(target,"subtotal",productHits); - } - private String homeProductBucket(String token){ - String t=token.replace(" ",""); - if(t.equalsIgnoreCase("인터넷")) return "internet"; - if(t.equalsIgnoreCase("TV")||t.equals("티비")) return "tv"; - if(t.equals("집전화")) return "homePhone"; - if(t.equals("인터넷전화")) return "voip"; - if(t.equals("신용카드")) return "creditCard"; - if(t.equalsIgnoreCase("홈IoT")||t.equalsIgnoreCase("홈IOT")||t.equals("홈아이오티")) return "homeIot"; - if(t.equals("동판")) return "dongpan"; - if(t.equals("후결합")) return "postBind"; - if(t.equals("재약정")) return "renew"; - return "etc"; - } - private Map emptyHomeStatRow(String label,int key){ - Map r=new LinkedHashMap<>(); - r.put("key",key); r.put("label",label); - r.put("internet",0); r.put("tv",0); r.put("homePhone",0); r.put("voip",0); r.put("creditCard",0); - r.put("homeIot",0); r.put("dongpan",0); r.put("etc",0); r.put("postBind",0); r.put("renew",0); - r.put("subtotal",0); r.put("saleCount",0); r.put("settleAmount",BigDecimal.ZERO); - return r; - } - private void mergeHomeStat(Map target,Map src){ - for(String k:List.of("internet","tv","homePhone","voip","creditCard","homeIot","dongpan","etc","postBind","renew","subtotal","saleCount")) - addInt(target,k,((Number)src.getOrDefault(k,0)).intValue()); - addDec(target,"settleAmount",nvl((BigDecimal)src.get("settleAmount"))); - } - private Map sumHomeStatRows(List> rows){ - Map t=emptyHomeStatRow("합계",0); - for(Map r:rows) mergeHomeStat(t,r); - return t; - } - @GetMapping("/reports/outcompany-stats") ApiResponse outcompanyStats(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - return ApiResponse.ok(outcompanyStatsData(u,f)); - } - @GetMapping(value="/reports/outcompany-stats/export", produces="text/csv;charset=UTF-8") ResponseEntity outcompanyStatsExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f){ - Map data=outcompanyStatsData(u,f); - @SuppressWarnings("unchecked") List> rows=(List>) data.get("rows"); - StringBuilder csv=new StringBuilder("\uFEFF지역,출고처,개통,개통금액,후정산,후정산금액,홈상품,홈상품금액,실정산금액,마진\n"); - for(Map r:rows){ - csv.append(csv(r.get("region"))).append(',').append(csv(r.get("name"))).append(',') - .append(csv(r.get("openCount"))).append(',').append(csv(r.get("openAmount"))).append(',') - .append(csv(r.get("afterCount"))).append(',').append(csv(r.get("afterAmount"))).append(',') - .append(csv(r.get("homeCount"))).append(',').append(csv(r.get("homeAmount"))).append(',') - .append(csv(r.get("realSettleAmount"))).append(',').append(csv(r.get("margin"))).append('\n'); - } - @SuppressWarnings("unchecked") Map totals=(Map) data.get("totals"); - if(totals!=null){ - csv.append(csv("합계")).append(',').append(csv("")).append(',') - .append(csv(totals.get("openCount"))).append(',').append(csv(totals.get("openAmount"))).append(',') - .append(csv(totals.get("afterCount"))).append(',').append(csv(totals.get("afterAmount"))).append(',') - .append(csv(totals.get("homeCount"))).append(',').append(csv(totals.get("homeAmount"))).append(',') - .append(csv(totals.get("realSettleAmount"))).append(',').append(csv(totals.get("margin"))).append('\n'); - } - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=outcompany-stats.csv").body(csv.toString()); - } - private Map outcompanyStatsData(JwtUser u,Map f){ - int year=Integer.parseInt(f.getOrDefault("year",String.valueOf(LocalDate.now().getYear()))); - int month=Integer.parseInt(f.getOrDefault("month",String.valueOf(LocalDate.now().getMonthValue()))); - LocalDate from=LocalDate.of(year,month,1); - LocalDate to=from.withDayOfMonth(from.lengthOfMonth()); - Map companies=companyNames(u); - Map> byOut=new LinkedHashMap<>(); - List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g and o.opendate between :a and :b",OpenRecord.class) - .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); - for(OpenRecord o:opens){ - if(o.getOutcompanyId()==null) continue; - if(!eq(o.getTelecom(),f.get("telecom"))) continue; - if(!eqId(o.getIncompanyId(),f.get("incompanyId"))) continue; - if(!eq(o.getOutCategory(),f.get("outCategory"))) continue; - Map row=outcompanyStatRow(byOut,o.getOutcompanyId(),companies); - addInt(row,"openCount",1); - addDec(row,"openAmount",nvl(o.getSellprice())); - addDec(row,"margin",nvl(o.getDealermargin1()).add(nvl(o.getDealermargin2()))); - } - List afters=em.createQuery("select b from AfterBalance b where b.groupId=:g and b.basedate between :a and :b",AfterBalance.class) - .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); - for(AfterBalance b:afters){ - if(b.getOutcompanyId()==null) continue; - if(!eq(b.getTelecom(),f.get("telecom"))) continue; - if(!eq(b.getOutCategory(),f.get("outCategory"))) continue; - Map row=outcompanyStatRow(byOut,b.getOutcompanyId(),companies); - addInt(row,"afterCount",1); - addDec(row,"afterAmount",nvl(b.getAmount())); - } - List homes=em.createQuery("select h from HomeSale h where h.groupId=:g and h.installDate between :a and :b",HomeSale.class) - .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); - for(HomeSale h:homes){ - if(h.getOutcompanyId()==null) continue; - if(!"완료".equals(h.getStatus())&&!"판매완료".equals(h.getStatus())) continue; - if(!eq(h.getTelecom(),f.get("telecom"))) continue; - if(!eqId(h.getIncompanyId(),f.get("incompanyId"))) continue; - if(!eq(h.getOutCategory(),f.get("outCategory"))) continue; - Map row=outcompanyStatRow(byOut,h.getOutcompanyId(),companies); - addInt(row,"homeCount",1); - addDec(row,"homeAmount",nvl(h.getSettleAmount())); - } - List> rows=new ArrayList<>(); - for(Map r:byOut.values()){ - BigDecimal real=nvl((BigDecimal)r.get("openAmount")).add(nvl((BigDecimal)r.get("afterAmount"))).add(nvl((BigDecimal)r.get("homeAmount"))); - r.put("realSettleAmount",real); - rows.add(r); - } - rows.sort(Comparator - .comparing((Map r)->String.valueOf(r.get("region")),Comparator.nullsLast(String::compareTo)) - .thenComparing(r->String.valueOf(r.get("name")),Comparator.nullsLast(String::compareTo))); - Map totals=emptyOutcompanyStatRow("","합계",0L); - for(Map r:rows){ - addInt(totals,"openCount",((Number)r.getOrDefault("openCount",0)).intValue()); - addInt(totals,"afterCount",((Number)r.getOrDefault("afterCount",0)).intValue()); - addInt(totals,"homeCount",((Number)r.getOrDefault("homeCount",0)).intValue()); - addDec(totals,"openAmount",nvl((BigDecimal)r.get("openAmount"))); - addDec(totals,"afterAmount",nvl((BigDecimal)r.get("afterAmount"))); - addDec(totals,"homeAmount",nvl((BigDecimal)r.get("homeAmount"))); - addDec(totals,"realSettleAmount",nvl((BigDecimal)r.get("realSettleAmount"))); - addDec(totals,"margin",nvl((BigDecimal)r.get("margin"))); - } - Map out=new LinkedHashMap<>(); - out.put("year",year); out.put("month",month); - out.put("title",year+"년 "+month+"월"); - out.put("total",rows.size()); out.put("rows",rows); out.put("totals",totals); - return out; - } - private Map outcompanyStatRow(Map> byOut,Long outId,Map companies){ - return byOut.computeIfAbsent(outId,id->{ - String full=companies.getOrDefault(id,"-"); - String[] parts=splitRegionName(full); - return emptyOutcompanyStatRow(parts[0],parts[1],id); - }); - } - private Map emptyOutcompanyStatRow(String region,String name,Long id){ - Map r=new LinkedHashMap<>(); - r.put("id",id); r.put("region",region); r.put("name",name); - r.put("openCount",0); r.put("openAmount",BigDecimal.ZERO); - r.put("afterCount",0); r.put("afterAmount",BigDecimal.ZERO); - r.put("homeCount",0); r.put("homeAmount",BigDecimal.ZERO); - r.put("realSettleAmount",BigDecimal.ZERO); r.put("margin",BigDecimal.ZERO); - return r; - } - private String[] splitRegionName(String full){ - if(full==null||full.isBlank()) return new String[]{"-","-"}; - String t=full.trim(); - int sp=t.indexOf(' '); - if(sp>0){ - String left=t.substring(0,sp).trim(); - String right=t.substring(sp+1).trim(); - if(!left.isEmpty()&&!right.isEmpty()&&(left.matches(".*(시|군|구)$")||List.of("대구","경산","서울","부산","인천","광주","대전","울산","세종","수원","성남","용인","창원","청주","전주","포항","제주").contains(left))) - return new String[]{left,right}; - } - return new String[]{"-",t}; - } - private Map openStatsDaily(JwtUser u,Map f){ - int year=Integer.parseInt(f.getOrDefault("year",String.valueOf(LocalDate.now().getYear()))); - int month=Integer.parseInt(f.getOrDefault("month",String.valueOf(LocalDate.now().getMonthValue()))); - LocalDate from=LocalDate.of(year,month,1); - LocalDate to=from.withDayOfMonth(from.lengthOfMonth()); - Map> byDay=new LinkedHashMap<>(); - for(int d=1;d<=to.getDayOfMonth();d++) byDay.put(d,emptyOpenStatRow(d+"일("+weekdayKo(from.withDayOfMonth(d))+")",d)); - accumulateOpenStats(u,f,from,to,(date,row)->{ - Map target=byDay.get(date.getDayOfMonth()); - if(target!=null) mergeOpenStat(target,row); - },(date,afterCount,afterAmount)->{ - Map target=byDay.get(date.getDayOfMonth()); - if(target!=null){ addInt(target,"afterCount",afterCount); addDec(target,"afterAmount",afterAmount); } - },(date,withdrawCount)->{ - Map target=byDay.get(date.getDayOfMonth()); - if(target!=null) addInt(target,"withdrawCount",withdrawCount); - }); - List> rows=new ArrayList<>(byDay.values()); - Map totals=sumOpenStatRows(rows); - Map out=new LinkedHashMap<>(); - out.put("mode","daily"); out.put("year",year); out.put("month",month); - out.put("title",year+"년 "+month+"월"); out.put("rows",rows); out.put("totals",totals); - out.put("today",LocalDate.now().getYear()==year&&LocalDate.now().getMonthValue()==month?LocalDate.now().getDayOfMonth():null); - return out; - } - private Map openStatsMonthly(JwtUser u,Map f){ - int year=Integer.parseInt(f.getOrDefault("year",String.valueOf(LocalDate.now().getYear()))); - LocalDate from=LocalDate.of(year,1,1); - LocalDate to=LocalDate.of(year,12,31); - Map> byMonth=new LinkedHashMap<>(); - for(int m=1;m<=12;m++) byMonth.put(m,emptyOpenStatRow(m+"월",m)); - accumulateOpenStats(u,f,from,to,(date,row)->{ - Map target=byMonth.get(date.getMonthValue()); - if(target!=null) mergeOpenStat(target,row); - },(date,afterCount,afterAmount)->{ - Map target=byMonth.get(date.getMonthValue()); - if(target!=null){ addInt(target,"afterCount",afterCount); addDec(target,"afterAmount",afterAmount); } - },(date,withdrawCount)->{ - Map target=byMonth.get(date.getMonthValue()); - if(target!=null) addInt(target,"withdrawCount",withdrawCount); - }); - List> rows=new ArrayList<>(byMonth.values()); - Map totals=sumOpenStatRows(rows); - Map out=new LinkedHashMap<>(); - out.put("mode","monthly"); out.put("year",year); - out.put("title",year+"년"); out.put("rows",rows); out.put("totals",totals); - out.put("currentMonth",LocalDate.now().getYear()==year?LocalDate.now().getMonthValue():null); - return out; - } - private interface OpenDayConsumer { void accept(LocalDate date,Map row); } - private interface AfterDayConsumer { void accept(LocalDate date,int count,BigDecimal amount); } - private interface WithdrawDayConsumer { void accept(LocalDate date,int count); } - private void accumulateOpenStats(JwtUser u,Map f,LocalDate from,LocalDate to, - OpenDayConsumer openFn,AfterDayConsumer afterFn,WithdrawDayConsumer withdrawFn){ - Map companies=companyNames(u); - List opens=em.createQuery("select o from OpenRecord o where o.groupId=:g and o.opendate between :a and :b",OpenRecord.class) - .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); - for(OpenRecord o:opens){ - if(!matchesOpenStat(o,f,companies)) continue; - Map row=emptyOpenStatRow("",0); - String bucket=openStatBucket(o.getOpenhow()); - if(bucket!=null) addInt(row,bucket,1); - addInt(row,"totalCount",1); - addDec(row,"settleAmount",nvl(o.getSellprice())); - addDec(row,"margin",nvl(o.getDealermargin1()).add(nvl(o.getDealermargin2()))); - openFn.accept(o.getOpendate(),row); - } - List afters=em.createQuery("select b from AfterBalance b where b.groupId=:g and b.basedate between :a and :b",AfterBalance.class) - .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); - for(AfterBalance b:afters){ - if(b.getBasedate()==null) continue; - if(!eqId(b.getOutcompanyId(),f.get("outcompanyId"))) continue; - if(!eq(b.getTelecom(),f.get("telecom"))) continue; - if(!eq(b.getOutCategory(),f.get("outCategory"))) continue; - afterFn.accept(b.getBasedate(),1,nvl(b.getAmount())); - } - List withdraws=em.createQuery("select w from OpenWithdrawal w where w.groupId=:g and w.withdrawDate between :a and :b",OpenWithdrawal.class) - .setParameter("g",u.getGroupId()).setParameter("a",from).setParameter("b",to).getResultList(); - for(OpenWithdrawal w:withdraws){ - if(w.getWithdrawDate()==null) continue; - if(!eqId(w.getOutcompanyId(),f.get("outcompanyId"))) continue; - withdrawFn.accept(w.getWithdrawDate(),1); - } - } - private boolean matchesOpenStat(OpenRecord o,Map f,Map companies){ - if(!eq(o.getTelecom(),f.get("telecom"))) return false; - if(!eqId(o.getIncompanyId(),f.get("incompanyId"))) return false; - if(!eqId(o.getOutcompanyId(),f.get("outcompanyId"))) return false; - if(!eq(o.getOutCategory(),f.get("outCategory"))) return false; - return true; - } - private String openStatBucket(String openhow){ - if(blank(openhow)) return null; - return switch(openhow){ - case "신규"->"newCount"; - case "번호이동","MNP"->"mnpCount"; - case "보상"->"rewardCount"; - case "기변"->"changeCount"; - case "에이징","메이징"->"agingCount"; - case "가개통"->"tempCount"; - case "선불개통"->"prepaidCount"; - default->null; - }; - } - private Map emptyOpenStatRow(String label,int key){ - Map r=new LinkedHashMap<>(); - r.put("key",key); r.put("label",label); - r.put("newCount",0); r.put("mnpCount",0); r.put("rewardCount",0); r.put("changeCount",0); - r.put("agingCount",0); r.put("tempCount",0); r.put("prepaidCount",0); r.put("totalCount",0); - r.put("settleAmount",BigDecimal.ZERO); r.put("afterCount",0); r.put("afterAmount",BigDecimal.ZERO); - r.put("withdrawCount",0); r.put("margin",BigDecimal.ZERO); - return r; - } - private void mergeOpenStat(Map target,Map src){ - for(String k:List.of("newCount","mnpCount","rewardCount","changeCount","agingCount","tempCount","prepaidCount","totalCount","afterCount","withdrawCount")) - addInt(target,k,((Number)src.getOrDefault(k,0)).intValue()); - for(String k:List.of("settleAmount","afterAmount","margin")) - addDec(target,k,nvl((BigDecimal)src.get(k))); - } - private Map sumOpenStatRows(List> rows){ - Map t=emptyOpenStatRow("합계",0); - for(Map r:rows) mergeOpenStat(t,r); - return t; - } - private void addInt(Map m,String k,int v){ m.put(k,((Number)m.getOrDefault(k,0)).intValue()+v); } - private void addDec(Map m,String k,BigDecimal v){ m.put(k,nvl((BigDecimal)m.get(k)).add(nvl(v))); } - private String weekdayKo(LocalDate d){ - return switch(d.getDayOfWeek()){ - case MONDAY->"월"; case TUESDAY->"화"; case WEDNESDAY->"수"; case THURSDAY->"목"; - case FRIDAY->"금"; case SATURDAY->"토"; case SUNDAY->"일"; - }; - } - @GetMapping("/stocks/report-day") ApiResponse reportDay(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { - if(blank(f.get("year")) || blank(f.get("month"))) return ApiResponse.fail("년월을 선택하세요."); - return ApiResponse.ok(reportDayData(u, f)); - } - @GetMapping(value="/stocks/report-day/export", produces="text/csv;charset=UTF-8") ResponseEntity reportDayExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { - if(blank(f.get("year")) || blank(f.get("month"))) return ResponseEntity.badRequest().body("년월을 선택하세요."); - Map data=reportDayData(u,f); - @SuppressWarnings("unchecked") List> rows=(List>) data.get("rows"); - StringBuilder csv=new StringBuilder("\uFEFF일자,입고,출고,회수,반품,분실,개통,판매,합계\n"); - for(Map r:rows) { - csv.append(csv(r.get("day"))).append(',').append(csv(r.get("inCount"))).append(',') - .append(csv(r.get("outCount"))).append(',').append(csv(r.get("recoveryCount"))).append(',') - .append(csv(r.get("returnCount"))).append(',').append(csv(r.get("lossCount"))).append(',') - .append(csv(r.get("openCount"))).append(',').append(csv(r.get("sellCount"))).append(',') - .append(csv(r.get("total"))).append('\n'); - } - @SuppressWarnings("unchecked") Map totals=(Map) data.get("totals"); - if(totals!=null) { - csv.append(csv("합계")).append(',').append(csv(totals.get("inCount"))).append(',') - .append(csv(totals.get("outCount"))).append(',').append(csv(totals.get("recoveryCount"))).append(',') - .append(csv(totals.get("returnCount"))).append(',').append(csv(totals.get("lossCount"))).append(',') - .append(csv(totals.get("openCount"))).append(',').append(csv(totals.get("sellCount"))).append(',') - .append(csv(totals.get("total"))).append('\n'); - } - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=report-day.csv").body(csv.toString()); - } - private Map reportDayData(JwtUser u, Map f) { - int year=Integer.parseInt(f.get("year")); - int month=Integer.parseInt(f.get("month")); - LocalDate from=LocalDate.of(year, month, 1); - LocalDate to=from.withDayOfMonth(from.lengthOfMonth()); - LocalDate minAllowed=LocalDate.now().minusYears(1).withDayOfMonth(1); - if(from.isBefore(minAllowed)) from=minAllowed; - Map companies=companyNames(u); - Long inFilter=blank(f.get("incompanyId"))?null:Long.valueOf(f.get("incompanyId")); - List stocks=em.createQuery("select s from Stock s where s.groupId=:g",Stock.class).setParameter("g",u.getGroupId()).getResultList(); - Map days=new LinkedHashMap<>(); // [in,out,recovery,return,loss,open,sell] - for(int d=1;d<=to.getDayOfMonth();d++) days.put(d, new int[]{0,0,0,0,0,0,0}); - for(Stock s:stocks) { - if(inFilter!=null && !Objects.equals(s.getIncompanyId(), inFilter)) continue; - bumpDay(days, s.getIndate(), from, to, 0); - bumpDay(days, s.getOutdate(), from, to, 1); - if("RECOVERY".equals(s.getState())) bumpDay(days, s.getProcessDate()!=null?s.getProcessDate():s.getIndate(), from, to, 2); - bumpDay(days, s.getReturndate(), from, to, 3); - bumpDay(days, s.getLossdate(), from, to, 4); - bumpDay(days, s.getSelldate(), from, to, 6); - if("RETURN".equals(s.getState()) && s.getReturndate()==null) bumpDay(days, s.getProcessDate(), from, to, 3); - if("LOSS".equals(s.getState()) && s.getLossdate()==null) bumpDay(days, s.getProcessDate(), from, to, 4); - if("SELL".equals(s.getState()) && s.getSelldate()==null) bumpDay(days, s.getProcessDate(), from, to, 6); - if("OUT".equals(s.getState()) && s.getOutdate()==null) bumpDay(days, s.getProcessDate(), from, to, 1); - if("OPEN".equals(s.getState())) bumpDay(days, s.getProcessDate()!=null?s.getProcessDate():s.getIndate(), from, to, 5); - } - // OpenRecord는 stock OPEN과 중복될 수 있어 stock 기준만 사용 - List> rows=new ArrayList<>(); - int[] sum=new int[7]; - for(var e:days.entrySet()) { - int[] c=e.getValue(); - int total=c[0]+c[1]+c[2]+c[3]+c[4]+c[5]+c[6]; - if(total==0) continue; - for(int i=0;i<7;i++) sum[i]+=c[i]; - Map row=new LinkedHashMap<>(); - row.put("day", String.format("%04d-%02d-%02d", year, month, e.getKey())); - row.put("inCount", c[0]); row.put("outCount", c[1]); row.put("recoveryCount", c[2]); - row.put("returnCount", c[3]); row.put("lossCount", c[4]); row.put("openCount", c[5]); - row.put("sellCount", c[6]); row.put("total", total); - rows.add(row); - } - Map totals=new LinkedHashMap<>(); - totals.put("inCount", sum[0]); totals.put("outCount", sum[1]); totals.put("recoveryCount", sum[2]); - totals.put("returnCount", sum[3]); totals.put("lossCount", sum[4]); totals.put("openCount", sum[5]); - totals.put("sellCount", sum[6]); totals.put("total", sum[0]+sum[1]+sum[2]+sum[3]+sum[4]+sum[5]+sum[6]); - Map result=new LinkedHashMap<>(); - result.put("year", year); result.put("month", month); - result.put("title", year+"년 "+month+"월 일별이력통계"); - result.put("rows", rows); result.put("totals", totals); - result.put("incompanyName", inFilter==null?null:companies.get(inFilter)); - return result; - } - private void bumpDay(Map days, LocalDate date, LocalDate from, LocalDate to, int idx) { - if(date==null || date.isBefore(from) || date.isAfter(to)) return; - int[] c=days.get(date.getDayOfMonth()); - if(c!=null) c[idx]++; - } - @GetMapping("/stocks/history") ApiResponse stockHistory(@AuthenticationPrincipal JwtUser u, @RequestParam Map f, - @RequestParam(defaultValue="0") int page, @RequestParam(defaultValue="15") int size) { - if(blank(f.get("dateFrom")) || blank(f.get("dateTo"))) return ApiResponse.fail("날짜를 선택하세요."); - return ApiResponse.ok(stockHistoryData(u, f, page, size)); - } - @PostMapping("/stocks/history/delete") @Transactional ApiResponse stockHistoryDelete(@AuthenticationPrincipal JwtUser u, @RequestBody Map d) { - Object raw=d.get("ids"); - if(!(raw instanceof List ids) || ids.isEmpty()) return ApiResponse.fail("삭제할 이력을 선택하세요."); - int deleted=0; - for(Object idObj:ids) { - StockHistory h=em.find(StockHistory.class, Long.valueOf(idObj.toString())); - if(h==null || !Objects.equals(h.getGroupId(),u.getGroupId())) continue; - em.remove(h); deleted++; - } - return ApiResponse.ok(Map.of("deleted", deleted)); - } - @GetMapping(value="/stocks/history/export", produces="text/csv;charset=UTF-8") ResponseEntity stockHistoryExport(@AuthenticationPrincipal JwtUser u, @RequestParam Map f) { - if(blank(f.get("dateFrom")) || blank(f.get("dateTo"))) return ResponseEntity.badRequest().body("날짜를 선택하세요."); - @SuppressWarnings("unchecked") List> rows=(List>) stockHistoryData(u,f,0,10000).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF처리일,현황,종류,일련번호,모델명,색상,상태,입고처,출고처,처리자,비고\n"); - for(Map r:rows) csv.append(csv(r.get("processDate"))).append(',').append(csv(r.get("statusLabel"))).append(',') - .append(csv(r.get("gubun"))).append(',').append(csv(r.get("serial"))).append(',').append(csv(r.get("model"))).append(',') - .append(csv(r.get("color"))).append(',').append(csv(r.get("cond"))).append(',').append(csv(r.get("incompanyName"))).append(',') - .append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("processor"))).append(',').append(csv(r.get("memo"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=stock-history.csv").body(csv.toString()); - } - private Map stockHistoryData(JwtUser u, Map f, int page, int size) { - Map companies=companyNames(u); - LocalDate from=LocalDate.parse(f.get("dateFrom")); - LocalDate to=LocalDate.parse(f.get("dateTo")); - List all=em.createQuery("select h from StockHistory h where h.groupId=:g",StockHistory.class).setParameter("g",u.getGroupId()).getResultList(); - String searchType=f.getOrDefault("searchType","일련번호"); - String keyword=f.get("keyword"); - List filtered=all.stream().filter(h->{ - if(h.getProcessDate()==null || h.getProcessDate().isBefore(from) || h.getProcessDate().isAfter(to)) return false; - if(keyword!=null && !keyword.isBlank()) { - boolean ok=switch(searchType){ - case "모델명"->like(h.getModel(),keyword); - case "색상"->like(h.getColor(),keyword); - case "펫네임"->like(h.getSalesperson(),keyword); - default->like(h.getSerial(),keyword); - }; - if(!ok) return false; - } - return true; - }).sorted((a,b)->{ - int cmp=Comparator.nullsLast(LocalDate::compareTo).compare(b.getProcessDate(), a.getProcessDate()); - return cmp!=0?cmp:Long.compare(b.getId(), a.getId()); - }).toList(); - int fromIdx=Math.min(Math.max(page,0)*Math.max(size,1), filtered.size()); - int toIdx=Math.min(fromIdx+Math.min(Math.max(size,1),200), filtered.size()); - List> list=filtered.subList(fromIdx,toIdx).stream().map(h->{ - Map r=new LinkedHashMap<>(); - r.put("id", h.getId()); r.put("stockId", h.getStockId()); - r.put("processDate", h.getProcessDate()); r.put("statusLabel", h.getStatusLabel()); - r.put("state", h.getState()); r.put("gubun", h.getGubun()); - r.put("serial", h.getSerial()); r.put("model", h.getModel()); r.put("color", h.getColor()); - r.put("cond", h.getCond()); r.put("memo", h.getMemo()); r.put("processor", h.getProcessor()); - r.put("salesperson", h.getSalesperson()); - r.put("incompanyName", companies.getOrDefault(h.getIncompanyId(),"-")); - r.put("outcompanyName", companies.getOrDefault(h.getOutcompanyId(),"-")); - return r; - }).toList(); - return Map.of("total", filtered.size(), "list", list); - } - private void writeStockHistory(Stock s, JwtUser u) { - StockHistory h=new StockHistory(); - h.setGroupId(u.getGroupId()); - h.setStockId(s.getId()); - h.setIncompanyId(s.getIncompanyId()); - h.setOutcompanyId(s.getOutcompanyId()); - h.setProcessDate(s.getProcessDate()!=null?s.getProcessDate():LocalDate.now()); - h.setGubun(s.getGubun()); h.setState(s.getState()); h.setStatusLabel(statusLabel(s.getState())); - h.setSerial(s.getSerial()); h.setModel(s.getModel()); h.setColor(s.getColor()); - h.setCond(s.getCond()); h.setMemo(s.getMemo()); - h.setProcessor(s.getProcessor()!=null?s.getProcessor():memberName(u)); - h.setSalesperson(s.getSalesperson()); - if("OUT".equals(s.getState()) && s.getOutcompanyId()!=null){ - Company oc=em.find(Company.class, s.getOutcompanyId()); - if(oc!=null){ - String label=oc.getName(); - String cat=s.getOutCategory()!=null&&!s.getOutCategory().isBlank()?s.getOutCategory():oc.getCategory(); - if(cat!=null&&!cat.isBlank()) label=label+" ("+cat+")"; - h.setCompanyLabel(label); - } - } else if("IN".equals(s.getState()) && s.getIncompanyId()!=null){ - Company ic=em.find(Company.class, s.getIncompanyId()); - if(ic!=null) h.setCompanyLabel(ic.getName()); - } - em.persist(h); - } - @GetMapping("/stocks/abooks") ApiResponse abooks(@AuthenticationPrincipal JwtUser u,@RequestParam Map f, - @RequestParam(defaultValue="0") int page,@RequestParam(defaultValue="15") int size) { - return ApiResponse.ok(abookData(u,f,page,size)); - } - @PostMapping("/stocks/abooks") @Transactional ApiResponse abookCreate(@AuthenticationPrincipal JwtUser u,@RequestBody Map d) { - UsimLedger row=new UsimLedger(); - row.setGroupId(u.getGroupId()); - Long outId=null; - if(d.get("outcompanyId")!=null && !d.get("outcompanyId").toString().isBlank()) { - outId=Long.valueOf(d.get("outcompanyId").toString()); - } else { - String name=d.get("outcompanyName")==null?"":d.get("outcompanyName").toString().trim(); - if(name.isBlank()) return ApiResponse.fail("출고처명을 입력해주세요."); - List found=em.createQuery("select c from Company c where c.groupId=:g and c.name=:n",Company.class) - .setParameter("g",u.getGroupId()).setParameter("n",name).getResultList(); - if(!found.isEmpty()) outId=found.getFirst().getId(); - else { - Company c=new Company(); c.setGroupId(u.getGroupId()); c.setType("OUT"); c.setName(name); c.setActive(true); - em.persist(c); em.flush(); outId=c.getId(); - } - } - if(d.get("telecom")==null || d.get("telecom").toString().isBlank()) return ApiResponse.fail("통신사를 선택하세요."); - if(d.get("qty")==null || d.get("qty").toString().isBlank()) return ApiResponse.fail("수량을 입력하세요."); - if(d.get("amount")==null || d.get("amount").toString().isBlank()) return ApiResponse.fail("거래금액을 입력하세요."); - row.setOutcompanyId(outId); - row.setTradeDate(d.get("tradeDate")==null||d.get("tradeDate").toString().isBlank()?LocalDate.now():LocalDate.parse(d.get("tradeDate").toString())); - row.setTelecom(String.valueOf(d.get("telecom"))); - row.setQty(Integer.valueOf(d.get("qty").toString().replace(",",""))); - row.setAmount(new BigDecimal(d.get("amount").toString().replace(",",""))); - row.setDetail(d.get("detail")==null||String.valueOf(d.get("detail")).isBlank()?null:String.valueOf(d.get("detail"))); - row.setRegistrant(d.get("registrant")==null||String.valueOf(d.get("registrant")).isBlank()?u.getUserid():String.valueOf(d.get("registrant"))); - row.setStatus("정상거래"); - em.persist(row); - return ApiResponse.ok(abookMap(row, companyNames(u))); - } - @PostMapping("/stocks/abooks/delete") @Transactional ApiResponse abookDelete(@AuthenticationPrincipal JwtUser u,@RequestBody Map d) { - Object raw=d.get("ids"); - if(!(raw instanceof List ids) || ids.isEmpty()) return ApiResponse.fail("삭제할 항목을 선택하세요."); - int deleted=0; - for(Object idObj:ids) { - UsimLedger row=em.find(UsimLedger.class, Long.valueOf(idObj.toString())); - if(row==null || !Objects.equals(row.getGroupId(),u.getGroupId())) continue; - em.remove(row); deleted++; - } - return ApiResponse.ok(Map.of("deleted",deleted)); - } - @GetMapping(value="/stocks/abooks/export", produces="text/csv;charset=UTF-8") ResponseEntity abookExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f) { - @SuppressWarnings("unchecked") List> rows=(List>) abookData(u,f,0,10000).get("list"); - StringBuilder csv=new StringBuilder("\uFEFF거래일,출고처,통신사,수량,거래금액,거래명세,등록자\n"); - for(Map r:rows) csv.append(csv(r.get("tradeDate"))).append(',').append(csv(r.get("outcompanyName"))).append(',') - .append(csv(r.get("telecom"))).append(',').append(csv(r.get("qty"))).append(',').append(csv(r.get("amount"))).append(',') - .append(csv(r.get("detail"))).append(',').append(csv(r.get("registrant"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=abooks.csv").body(csv.toString()); - } - @GetMapping(value="/stocks/abooks/summary-export", produces="text/csv;charset=UTF-8") ResponseEntity abookSummaryExport(@AuthenticationPrincipal JwtUser u,@RequestParam Map f) { - @SuppressWarnings("unchecked") List> rows=(List>) abookData(u,f,0,10000).get("summary"); - StringBuilder csv=new StringBuilder("\uFEFF출고처명,거래,수량,금액\n"); - for(Map r:rows) csv.append(csv(r.get("outcompanyName"))).append(',').append(csv(r.get("trades"))).append(',') - .append(csv(r.get("qty"))).append(',').append(csv(r.get("amount"))).append('\n'); - return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=abooks-summary.csv").body(csv.toString()); - } - private Map companyNames(JwtUser u){ - Map companies=new HashMap<>(); - for(Company c:em.createQuery("select c from Company c where c.groupId=:g",Company.class).setParameter("g",u.getGroupId()).getResultList()) companies.put(c.getId(),c.getName()); - return companies; - } - private Map abookData(JwtUser u,Map f,int page,int size) { - Map companies=companyNames(u); - List all=em.createQuery("select e from UsimLedger e where e.groupId=:g",UsimLedger.class).setParameter("g",u.getGroupId()).getResultList(); - String status=f.getOrDefault("status","정상거래"); - Long outFilter=blank(f.get("outcompanyId"))?null:Long.valueOf(f.get("outcompanyId")); - LocalDate from=blank(f.get("dateFrom"))?null:LocalDate.parse(f.get("dateFrom")); - LocalDate to=blank(f.get("dateTo"))?null:LocalDate.parse(f.get("dateTo")); - List filtered=all.stream().filter(e->{ - if(!"전체".equals(status) && !status.equals(e.getStatus())) return false; - if(outFilter!=null && !Objects.equals(e.getOutcompanyId(),outFilter)) return false; - if(from!=null && (e.getTradeDate()==null || e.getTradeDate().isBefore(from))) return false; - return to==null || (e.getTradeDate()!=null && !e.getTradeDate().isAfter(to)); - }).sorted(abookComparator(f.get("sort"))).toList(); - Map qtyMap=new LinkedHashMap<>(); - Map amountMap=new LinkedHashMap<>(); - Map tradeMap=new LinkedHashMap<>(); - for(UsimLedger e:filtered) { - Long key=e.getOutcompanyId()==null?-1L:e.getOutcompanyId(); - tradeMap.put(key, tradeMap.getOrDefault(key,0)+1); - qtyMap.put(key, new int[]{qtyMap.getOrDefault(key,new int[]{0})[0]+(e.getQty()==null?0:e.getQty())}); - amountMap.put(key, amountMap.getOrDefault(key,BigDecimal.ZERO).add(e.getAmount()==null?BigDecimal.ZERO:e.getAmount())); - } - boolean summaryAsc=f.getOrDefault("summarySort","amount,desc").endsWith(",asc"); - String summaryField=f.getOrDefault("summarySort","amount,desc").split(",")[0]; - List> summary=tradeMap.keySet().stream().map(key->{ - Map row=new LinkedHashMap<>(); - row.put("outcompanyId", key<0?null:key); - row.put("outcompanyName", key<0?"-":companies.getOrDefault(key,"-")); - row.put("trades", tradeMap.get(key)); - row.put("qty", qtyMap.get(key)[0]); - row.put("amount", amountMap.get(key)); - return row; - }).sorted((a,b)->{ - int cmp=switch(summaryField){ - case "outcompanyName"->String.valueOf(a.get("outcompanyName")).compareTo(String.valueOf(b.get("outcompanyName"))); - case "trades"->Integer.compare((Integer)a.get("trades"),(Integer)b.get("trades")); - case "qty"->Integer.compare((Integer)a.get("qty"),(Integer)b.get("qty")); - default->((BigDecimal)a.get("amount")).compareTo((BigDecimal)b.get("amount")); - }; - return summaryAsc?cmp:-cmp; - }).toList(); - int tradeTotal=filtered.size(); - int qtyTotal=filtered.stream().mapToInt(e->e.getQty()==null?0:e.getQty()).sum(); - BigDecimal amountTotal=filtered.stream().map(e->e.getAmount()==null?BigDecimal.ZERO:e.getAmount()).reduce(BigDecimal.ZERO,BigDecimal::add); - int fromIdx=Math.min(Math.max(page,0)*Math.max(size,1),filtered.size()); - int toIdx=Math.min(fromIdx+Math.min(Math.max(size,1),200),filtered.size()); - List> list=filtered.subList(fromIdx,toIdx).stream().map(e->abookMap(e,companies)).toList(); - Map totals=new LinkedHashMap<>(); - totals.put("trades",tradeTotal); totals.put("qty",qtyTotal); totals.put("amount",amountTotal); - return Map.of("total",tradeTotal,"list",list,"summary",summary,"totals",totals); - } - private Comparator abookComparator(String sort){ - String field=sort==null?"tradeDate,desc":sort; boolean asc=field.endsWith(",asc"); - Comparator c=switch(field.split(",")[0]){ - case "telecom"->Comparator.comparing(UsimLedger::getTelecom,Comparator.nullsLast(String::compareTo)); - case "qty"->Comparator.comparing(UsimLedger::getQty,Comparator.nullsLast(Integer::compareTo)); - case "amount"->Comparator.comparing(UsimLedger::getAmount,Comparator.nullsLast(BigDecimal::compareTo)); - case "registrant"->Comparator.comparing(UsimLedger::getRegistrant,Comparator.nullsLast(String::compareTo)); - default->Comparator.comparing(UsimLedger::getTradeDate,Comparator.nullsLast(LocalDate::compareTo)); - }; - return (asc?c:c.reversed()).thenComparing(UsimLedger::getId); - } - private Map abookMap(UsimLedger e,Map companies){ - Map r=new LinkedHashMap<>(); - r.put("id",e.getId()); r.put("outcompanyId",e.getOutcompanyId()); - r.put("outcompanyName",companies.getOrDefault(e.getOutcompanyId(),"-")); - r.put("tradeDate",e.getTradeDate()); r.put("telecom",e.getTelecom()); - r.put("qty",e.getQty()); r.put("amount",e.getAmount()); r.put("detail",e.getDetail()); - r.put("registrant",e.getRegistrant()); r.put("status",e.getStatus()); - return r; - } - @SuppressWarnings("unchecked") private Class type(String uri){if(uri.contains("scans"))return (Class)ScanDoc.class;if(uri.contains("indocs"))return (Class)IncompleteDoc.class;if(uri.contains("returns"))return (Class)UnreturnedPhone.class;if(uri.contains("fareboxes"))return (Class)Farebox.class;if(uri.contains("accounts"))return (Class)AccountEntry.class;if(uri.contains("schedules"))return (Class)ScheduleItem.class;if(uri.contains("templates"))return (Class)SmsTemplate.class;if(uri.contains("sms"))return (Class)SmsMessage.class;if(uri.contains("alims"))return (Class)Alim.class;return (Class)CsInquiry.class;} - private BigDecimal nvl(BigDecimal v){return v==null?BigDecimal.ZERO:v;} -}