Merge remote-tracking branch 'origin/main' into iipbs/dm/dev

This commit is contained in:
dongsilove
2026-02-14 15:21:57 +09:00
56 changed files with 2446 additions and 719 deletions
+1
View File
@@ -25,3 +25,4 @@ apps/fo/.next/**/*
pnpm-lock.yaml
settings.json
.npmrc
.cursor
-1
View File
@@ -15,4 +15,3 @@ export async function POST() {
cookieStore.delete(CLAIM_NAME);
return NextResponse.json({ message: "Logged out" }, { status: 200 });
}
@@ -1,4 +1,4 @@
import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config";
import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, REFRESH_TOKEN } from "@/shared/config";
import { getCookie, setCookie, removeCookie } from "@/shared/lib";
export class TokenManager {
@@ -76,6 +76,8 @@ export class TokenManager {
this.removeToken(CLAIM_NAME);
this.removeToken(AUTH_USER_ID);
this.removeToken(REFRESH_TOKEN);
this.removeToken(ACCESS_TOKEN);
}
}
+10 -4
View File
@@ -3,7 +3,7 @@
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { getCoreRowModel } from "@tanstack/react-table";
import { PostApi, type MyPost } from "@/entities/post";
import { PostApi, type MyPost, type NoticePost } from "@/entities/post";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import {
@@ -23,15 +23,21 @@ const MyPostList: React.FC = () => {
}),
});
const handleMyPostRowClick = (row: MyPost) => {
router.push(`/cms/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`);
};
const { tableComponent: myPostTableComponent } = useDataTable({
query: myPostQuery,
tableOptions: {
columns: myPostTableColumns,
getCoreRowModel: getCoreRowModel(),
columns: myPostTableColumns,
getCoreRowModel: getCoreRowModel(),
},
dataTableOptions: {
variant: "line",
variant: "line",
onRowClick: handleMyPostRowClick
},
});
return (
@@ -26,7 +26,7 @@ const NoticeList: React.FC = () => {
});
const handleNoticePostRowClick = (row: NoticePost) => {
router.push(`/bbs/bbsnotice/${row.bbsId}/${row.pstNo}`);
router.push(`/cms/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`);
};
const handleMoreClick = () => {
@@ -40,12 +40,12 @@ const NoticeList: React.FC = () => {
const { tableComponent: noticePostTableComponent } = useDataTable({
query: noticePostQuery,
tableOptions: {
columns: noticePostTableColumns,
getCoreRowModel: getCoreRowModel(),
columns: noticePostTableColumns,
getCoreRowModel: getCoreRowModel(),
},
dataTableOptions: {
variant: "line",
onRowClick: handleNoticePostRowClick,
variant: "line",
onRowClick: handleNoticePostRowClick,
},
});
+831
View File
@@ -0,0 +1,831 @@
# 표준단어정의서
AI 이해용으로 정리된 표준단어명, 영문약어명, 영문명 목록입니다.
| 표준단어명 | 표준단어 영문약어명 | 표준단어 영문명 |
|-----------|-------------------|----------------|
| BOD | BOD | Biochemical Oxygen Demand |
| C/N비 | CNR | Carbon-Nitrogen Ratio |
| COD | COD | Chemical Oxygen Demand |
| F/M비 | FMR | Food-Microorganism Ratio |
| GISD | GISDATA | Geographic Information System Data |
| HTML | HTML | Hypertext Markup Language |
| ID | ID | Id |
| IP | IP | Internet Protocol |
| KEY | KEY | Key |
| ND | ND | Not Detected |
| NON | NON | Non |
| NTU | NTU | Nephelometry Turbidity Unit |
| OPT | OPT | Opt |
| PAC | PAC | Polyaluminium Chloride |
| PH | PH | Hydrogen Exponent |
| PKI | PKI | Public Key Infrastructure |
| POOL | POOL | Pool |
| SS | SS | Suspended Solids (Ss) |
| TCE | TCE | Trichloroethane |
| TM | TM | Transverse Mercator |
| T-N | TN | Total Nitrogen |
| TOC | TOC | Total Organic Carbon |
| T-P | TP | Total Phosphorus |
| URL | URL | Uniform Resource Locator |
| 가구 | HSHD | Household |
| 가능 | PSBL | Possible |
| 가동 | OPR | Operation |
| 가산 | ADTN | Addition |
| 가성소다 | NAOH | Caustic Soda(Nach) |
| 가축분뇨 | ANMA | Animal Manure |
| 값 | VAL | Value |
| 강도 | IN | Intensity |
| 강우 | RAIN | Rain |
| 개방 | OPN | Opening |
| 개별 | INDIV | Individual |
| 개보수비 | RPAFEE | Repair Fee |
| 개설 | ESTBL | Establishment |
| 개시 | STRT | Start |
| 개정 | RVSN | Revision |
| 개조 | REMDEL | Remodeling |
| 개체 | INDVD | Individual |
| 개체군 | POPU | Population |
| 개폐 | RORGN | Reorganization |
| 갱신 | UPDT | Update |
| 건물 | BLDG | Building |
| 건설 | CONC | Construction |
| 건수 | NOCS | The Number Of Cases |
| 건조 | DRY | Dry |
| 검사 | INSP | Inspection |
| 검사서 | EXRPT | Examination Report |
| 검정 | RCGN | Recognition |
| 검증 | VRFC | Verification |
| 검출 | DETECT | Detection |
| 검토 | RVW | Review |
| 격자 | LATTC | Lattice |
| 결과 | RSLT | Result |
| 결의 | DCIS | Decision |
| 결정 | DCSN | Decision |
| 경계 | BNDRY | Boundary |
| 경과 | PROC | Process |
| 경도 | LOT | Longitude |
| 경보 | ALARM | Alarm |
| 경사판 | ICNTPT | Inclination plate |
| 계 | SUM | Summation |
| 계량기 | MRNR | Measuring Instrument |
| 계량기분리 | METRSPRT | Meter Separation |
| 계면 | SGLV | Sludge Level |
| 계산식 | FMUL | Formula |
| 계약 | CTRT | Contract |
| 계약기간 | CNTRPD | Contract Period |
| 계절 | SEASON | Season |
| 계획 | PLAN | Plan |
| 고도처리 | AWTR | Advanced Water Treatment |
| 고시 | ANCMNT | Announcement |
| 고액분리 | SLSPRT | Solid-Liquid Separation |
| 고형물체류시간 | SRT | Solid Retention Time |
| 공간데이터 | SPCEDATA | Space Data |
| 공공 | PUBLIC | Public |
| 공급 | SPLY | Supply |
| 공기 | AIR | Air |
| 공기여과 | ARFT | Air Filter |
| 공문 | ODOC | Official Documnet |
| 공법 | MSRC | Method Of Construction |
| 공사 | CSTRN | Construction |
| 공사비 | CONSTEXPN | Construction Expenses |
| 공업지역 | MNFTR_ARA | Manufacturing Area |
| 공용면적 | CMAR | Common Area |
| 공통 | CMMN | Commonness |
| 관 | PP | Pipe |
| 관계 | REL | Relation |
| 관련 | RLTN | Relation |
| 관로 | PPLINE | Pipe Line |
| 관리 | MNG | Management |
| 관리기관 | MGC | Management Agency |
| 관리자 | MNGR | Manager |
| 관찰 | WTCH | Watch |
| 관측 | OBSR | Observation |
| 관측소 | OBSRVT | Observatory |
| 교반기 | AGITOR | Agirator |
| 교정 | CRRC | Correction |
| 구간 | SCTN | Section |
| 구동 | DRIVE | Drive |
| 구분 | SE | Section |
| 구성 | CMPSG | Composing |
| 구역 | ZONE | Zone |
| 구조 | STRCT | Structure |
| 권역 | DSTR | District |
| 권한 | AUTH | Authority |
| 규격 | STNDRD | Standard |
| 규모 | SCAL | Scale |
| 규제 | REGL | Regulation |
| 균등 | EQLTY | Equality |
| 금액 | AMT | Amount |
| 급수 | WSP | Water Supply |
| 기간 | WHL | While |
| 기관 | INST | Institution |
| 기록 | RECD | Recording |
| 기록부 | REGI | Register |
| 기밀 | SCRET | Secret |
| 기법 | TECQU | Techniques |
| 기본 | BSIC | Basics |
| 기본정보 | BASINF | Bassinfo |
| 기본키 | PK | Primary Key |
| 기상 | WETHER | Weather |
| 기안 | DRFTG | Drafting |
| 기재 | WRIT | Write |
| 기점 | CDPNT | Cardinal Point |
| 기존 | LGCY | Legacy |
| 기준 | CRTR | Criteria |
| 기타 | ETC | Etc |
| 기한 | TERM | Term |
| 기후 | CLMT | Climate |
| 긴급 | EMRG | Emergency |
| 날개벽 | WW | Wing Wall |
| 날씨 | WETHR | Weather |
| 날짜 | DATE | DATE |
| 남조류 | BLAL | Blue-Green Algae |
| 납 | PB | Plumbum |
| 내려받기 | DWLD | Download |
| 내부반송 | ITNRCC | Internal Recycl |
| 내용 | CN | Content |
| 내용연수 | SVLF | Service Life |
| 냄새 | SMELL | Smell |
| 년 | YY | Year |
| 년간 | FAYR | For A Year |
| 노즐 | NZL | Nozzle |
| 노출 | EXPSR | Exposure |
| 농도 | DNSTY | Density |
| 농축 | TKN | Thickened |
| 누적 | ACML | Accumulate |
| 누출 | LKG | Leakage |
| 눈 | SNW | Snow |
| 단가 | UNPRC | Unit Price |
| 단기 | SRTPD | Short Period |
| 단순 | SIMPL | Simplicity |
| 단위 | UNIT | Unit |
| 대구역 | LZONE | Large Zone |
| 대분류 | LCLAS | Large Classification |
| 대장 | REGSTR | Register |
| 대장균 | COLI | Coliform, Fecal Coliform |
| 대장균군 | COLIB | Coliform Bacteria |
| 대지면적 | SIAR | Site Area |
| 대체 | RPLC | Replace |
| 덮개 | COVR | Cover |
| 데이타 | DAT | Data |
| 도구 | UNT | Unit |
| 도래 | ADVNT | Advent |
| 도로 | ROAD | Road |
| 도로명 | RN | Road Name |
| 도시 | CTY | City |
| 도엽 | MAPDMC | Map Demarcation |
| 도표 | CHART | Chart |
| 독성 | TOXCTY | Toxicity |
| 동의 | AGRE | Agreement |
| 동절기 | WSESN | The Winter Season |
| 등급 | GRD | Grade |
| 등기 | RG | Registered |
| 등록 | REG | Registration |
| 디바이스 | DVICE | Device |
| 디코딩 | DCD | Decoding |
| 레이어 | LYR | Layer |
| 로그인 | LGN | Login |
| 로우 | ROW | Row |
| 마감 | DDLN | Deadline |
| 마이크로필터 | MCRFT | Micro Filter |
| 막 | MBRN | Membrane |
| 만족도 | STFDG | Satisfaction Degree |
| 맑음 | CLAR | Clear |
| 매각 | SAL | Sale |
| 맨홀 | MH | Manhole |
| 맵 | MAP | Map |
| 메인 | MAIN | Main |
| 메일 | EMAIL | Email |
| 메탄올 | MTN | Methanol(CH3OH) |
| 명 | NM | Name |
| 모니터링 | MTRG | Monitoring |
| 모델 | MODL | Model |
| 모델링 | MODLNG | Modeling |
| 모래 | SAND | Sand |
| 목 | MOK | Mok |
| 무산소 | ANX | Anoxic |
| 무산소조 | ANX_TANK | Anoxic Tank |
| 무인센서 | MNLSENSR | Manless Sensor |
| 문서 | DCMT | Document |
| 물 | WTR | Water |
| 미급수 | NONWTRSPL | Non Water Supply |
| 미생물 | BLGC | Biological |
| 미세먼지 | PM | Particulate Matter |
| 미세목 | MCRB | Micro Bar |
| 민간위탁 | ETSVN | Entrustment To Civilian |
| 민감 | SST | Sensitive |
| 민원 | CVLCPT | Civil Complaint |
| 바이러스 | VIRS | Virus |
| 반기 | HT | Half Term |
| 반류수 | SIDSWT | Side Stream Water |
| 반송 | SNDBK | Send Back |
| 반송수 | RCCWT | Recycled Water |
| 반송슬러지 | RTNSLG | Return Sludge |
| 반영 | RFLT | Reflection |
| 반응조 | RETNK | Reactant Tank |
| 반출 | EXPRT | Export |
| 반토 | ALUM | Alumina |
| 반환 | GVBK | Giving Back |
| 받이 | INL | Inlet |
| 발생 | OCRN | Occurrence |
| 발전 | PWGNRT | Power Generation |
| 발주기관 | ORNT | Order Institution |
| 방류 | DCWTR | Discharge Water Stock Etc |
| 방류구 | DSCHG | Discharge |
| 방류수 | EFLEC | Effluence |
| 방문 | VST | Visit |
| 방문자 | VSTR | Visitor |
| 방법 | MTHD | Method |
| 방사 | RLSE | Release |
| 배관 | PLINE | Pipeline |
| 배분 | DSTRB | Distirbution |
| 배설물 | EXCRTN | Excretion |
| 배수 | DRNG | Drainage |
| 배수구역 | DRANGZN | Drainage Zone |
| 배출 | EXHST | Exhaust |
| 배출가스 | EMGA | Emission Gases |
| 배출구 | VENT | Vent |
| 배출시설 | DHGP | Discharge Equipment |
| 배출시험 | EXHTST | Exhaust Test |
| 배출자 | DCHGR | Discharge Person |
| 밸브 | VALVE | Valve |
| 번호 | NO | Number |
| 법 | LAW | Law |
| 법적 | LGL | Legal |
| 법정 | STTY | Statutory |
| 법정동 | STDG | Statutory Dong(Administrative District) |
| 법정리 | STLI | Statutory Li(Administrative District) |
| 변경 | CHG | Change |
| 변동 | FLCTN | Fluctuation |
| 변속기 | TRMIS | Transmission |
| 보고 | RPT | Report |
| 보고서 | RPOT | Report |
| 보관 | STORG | Storage |
| 보수 | MEND | Mend |
| 보유 | HOLD | Hold |
| 보정 | RVSION | Revision |
| 보조 | ASSISC | Assistance |
| 보조금 | SBSIDY | Subsidy |
| 복개 | COVE | Cover |
| 복토 | CWTH | Covering With Soil |
| 본류 | MI_RV | Main River |
| 본번 | MNO | Main Number |
| 부검 | ATPSY | Autopsy |
| 부과 | LEVY | Levy |
| 부과금 | LVYAMT | Levy Amount |
| 부기 | BUKIP | Bookkeeping |
| 부담금 | ALOTM | Allotment |
| 부번 | SNO | Sub Number |
| 부산물 | BYPRD | By-Product |
| 부서 | DEPT | Department |
| 부숙화 | DECPZ | Decomposed ~Ize |
| 부적합 | IMPPRTY | Impropriety |
| 부착 | ASN | Adhesion |
| 부품 | CMPNT | Components |
| 부하량 | LOADQTY | Load Quantity |
| 분과 | DIVSN | Division |
| 분뇨 | ENSO | Excreta, Night Soil |
| 분류 | CL | Classification |
| 분류군 | TAXON | Taxon |
| 분리막 | SPRTMBR | Separation Membrane |
| 분배 | DTRBT | Distribution |
| 분배조 | DTRBT_TANK | Distribution Tank |
| 분야 | RLM | Realm |
| 분원성 | FECL | Fecal |
| 분원성대장균군 | FCOLI | Fecal Coliform |
| 분원성연쇄상구균 | FCSTRCCI | Fecal Streptococci |
| 분할 | PRTIN | Partition |
| 비밀번호 | PSWD | Password |
| 비상 | EMGNY | Emergency |
| 비정상 | ABNRM | Abnormality |
| 사양 | SPECS | Specification |
| 사업비 | WCT | Working Cost |
| 사업장 | BPLC | Business Place |
| 사여과지 | SAND_ FILTR | Sand Filter |
| 사용 | USE | Use |
| 사용가능 | USABLE | Usable |
| 사용권 | RIOUS | Right Of Using |
| 사용량 | USG | USAGE |
| 사용자 | USER | User |
| 사위 | FDG | Four Directions Girth |
| 사유 | RESN | Reason |
| 사이클 | CYCLE | Cycle |
| 사이트 | SITE | Site |
| 사진 | PHOT | Photograph |
| 사항 | MATTR | Matter |
| 사후 | AFTFAT | After Fact |
| 삭제 | DEL | Deletion |
| 산기관 | DFS | Diffuser |
| 산업 | INDUST | Industrial |
| 산화환원전위 | ORP | Oxidation Reduction Potential |
| 살모넬라 | SLMN | Salmonella |
| 삼원촉매장치 | TWCTLC | Three Way Catalytic Converter |
| 상세 | DTL | Details |
| 상세주소 | DADDR | Detailed Address |
| 상수 | WT | Water |
| 상업지역 | CMCL_ARA | Commercial Area |
| 상태 | STUS | Status |
| 상품 | GDS | Goods |
| 상향류 | UFW | Upflow |
| 상호 | CONM | Company Name |
| 색도 | CRMTY | Chromaticity |
| 생략 | OMS | Omission |
| 생물 | LVB | Living Being |
| 생물반응조 | BIRAT | Bioreactor |
| 생물서식처 | ORGNHBT | Organism Habitat |
| 생물종 | BISP | Biological Species |
| 생성 | CRT | Creation |
| 생슬러지 | RSLG | Raw Sludge |
| 생태 | ECLGY | Ecology |
| 생태독성 | ECTOX | ecotoxicity |
| 서류 | PAPE | Papers |
| 서명 | SIGN | Signing |
| 서버 | SRVR | Server |
| 서비스 | SRVC | Service |
| 설계 | DESGN | Design |
| 설계용량 | DESGN_CPCT | Design Capacity |
| 설립 | FNDN | Foundation |
| 설정 | STNG | Setting |
| 설치 | INSTL | Installation |
| 성격 | CHTR | Character |
| 성공 | SCS | Success |
| 성능 | PRFMNC | Performance |
| 성능검사 | EFTST | Efficiency Test |
| 성적서 | SCRRP | Score Report |
| 세균 | BCLS | Bacillus |
| 세목 | FB | Fine bar |
| 세부 | DETAIL | Detail |
| 세정 | WS | Washing |
| 세정제 | CNML | Cleaning Material |
| 소 | SML | Small |
| 소독 | DSNF | Disinfection |
| 소독제 | DSINF | Disinfectant |
| 소분류 | SCLAS | Small Classification |
| 소수점 | DCPT | Decimal Point |
| 소요 | REQ | Required |
| 소유 | OWN | Own |
| 소유역 | SBASIN | Sub Basin |
| 소재지 | PLAC | Place Of Location |
| 소포 | ATFM | Antifoaming |
| 소포수 | FBW | Foam Breaking Water |
| 소포제 | ANAG | Antifoaming Agent |
| 송부 | SENDNG | Sending |
| 송신 | SNDM | Send Message |
| 송풍기 | BLW | Blower |
| 수 | CNT | Count |
| 수거 | TKAWY | Take Away |
| 수계 | WWBH | Watershed, Water Basin, Hydrosphere |
| 수동 | HOPR | Hand Operation |
| 수리학적 체류시간 | HRT | Hydraulic Retention Time |
| 수문 | Gate | Gate |
| 수산화나트륨 | SOHY | Sodium Hydroxide |
| 수소이온 | HION | Hydrogen Ion |
| 수신 | RCPTN | Reception |
| 수용가 | RCPTR | Receptor |
| 수위 | WAL | Water Level |
| 수정 | MDFCN | Modification |
| 수정자 | MDFR | Modifier |
| 수제 | SPRDIKE | Superdike |
| 수조 | Tank | Tank |
| 수질 | QTWT | Quality Of Water |
| 수질개선 | WTRQLIMP | Water Quality Improvement |
| 수질기준 | WQST | Water Quality Standards |
| 수질배출정보 | WTDI | Water Discharge Information |
| 수집 | CLCT | Collection |
| 순환 | CCL | Cycling |
| 쉬겔라 | SHIGLA | Shigella |
| 스컴 | SCM | Scum |
| 스크린 | SCR | Screen |
| 슬러지 | SLG | Sludge |
| 슬러지 밀도지수 | SDI | sludge density index |
| 슬러지 용량 지표 | SVI | Sludge Volume Index |
| 습지 | WTLN | Wetland |
| 승인 | APRV | Approval |
| 승인권 | APPRVRGT | Right Of Approval |
| 시각 | TIME | Time |
| 시간 | HR | Hour |
| 시군구 | SGG | Si/Gun/Gu |
| 시나리오 | SCEN | Scenario |
| 시도 | CTPV | Cities And Provinces |
| 시료 | SMPL | Sample |
| 시설 | FCLT | Facility |
| 시설면적 | FCAR | Facility Area |
| 시설물 | FCLTS | Facilities |
| 시스템 | SYS | System |
| 시안 | CYN | Cyaan |
| 시운전 | TSTRN | Test Run |
| 시작 | BGNG | Beginning |
| 시점 | PTM | Point Time |
| 시행 | ENFC | Enforcement |
| 시험 | TEST | Test |
| 신고 | DCLR | Declaration |
| 신고자 | DCL | Declarer |
| 신규 | NEW | New |
| 신기술 | NTCH | New Technology |
| 신뢰 | CNFDNC | Confidence |
| 신뢰성 | CRDIBLTY | Credibility |
| 실시 | OPRT | Operation |
| 실적 | ACRS | Actual Result |
| 실행 | EXCN | Execution |
| 심사 | SRNG | Screening |
| 심의 | DLBR | Deliberation |
| 아황산환원혐기성포자형성균 | SFSRA | Spore Forming Sulfite Reducing Anaerobes |
| 안정화 | STBLZT | Stabilization |
| 암롤박스(컨테이너) | ARTCT | Arm roll type container |
| 암모니아성질소 | NHN | Ammonia Nitrogen |
| 압력 | PRESSR | Pressure |
| 압송 | PUMPNG | Pumping |
| 압축 | CMPRS | Compress |
| 액비 | LQFERTLZ | Liquid Fertilizer |
| 액상 | LIQD | Liquid |
| 약 | GENTLE | Gentle |
| 약품 | CMC | Chemical |
| 약품비 | MEDFEE | Medicine Fee |
| 약품사용 | USMD | Use Medicines |
| 업로드 | UPLD | Upload |
| 업무 | DTY | Duty |
| 없음 | NOT | Nothing |
| 에너지 | ENR | Energy |
| 에틸벤젠 | ETBNZN | Ethylbenzene |
| 여과 | FILTR | Filteration |
| 여과수 | FTWT | Filtered Water |
| 여과재 | FTMTRL | Filter Material |
| 여과형 | FILTRFOM | Filter Form |
| 여부 | YN | Yes or No |
| 여시니아균 | YERSNA | Yersinia |
| 역률 | PWRER_FCT | Power Factor |
| 역세척 | BWS | Back Wash |
| 연결 | CONN | Connection |
| 연계 | LINK | Link |
| 연도 | YR | Year |
| 연락 | CNTC | Contact |
| 연령 | AGE | Age |
| 연료화 | FUELZ | Fuelize |
| 연월 | YM | Year and Month |
| 염소 | CLRN | Chlorine |
| 염소소독 | CHDI | Chlorine Disinfection |
| 염소이온 | CLION | Chlorine Ion |
| 염화제이철 | IRCLD | Iron(III) Chloride |
| 예보 | FRCST | Forecast |
| 예비 | PRPT | Preparation |
| 예산 | BGT | Budget |
| 예상 | EXPCT | Expectation |
| 예약 | RSVT | Reservation |
| 예외 | EXCPT | Exception |
| 예정 | PRNG | Prearrangement |
| 예찰 | PRDICT | Prediction |
| 오류 | ERR | Error |
| 오수받이 | HUS_INL | House Inlet |
| 오염물질 | CNTMN | Contaminant |
| 오염원 | POLUSRC | Pollution Source |
| 오존 | OZN | Ozone |
| 오프로드 | OFFRD | Off Road |
| 옥살산 | OXACID | Oxalic Acid |
| 와류 | VORTEX | Vortex |
| 와류형 | VORTXFOM | Vortex From |
| 완결 | COMPLT | Completion |
| 완료 | CMPTN | Completion |
| 완전 | PRFECT | Perfection |
| 요건 | REQSIT | Requisite |
| 요막액 | ALLNTFLD | Allantoic Fluid |
| 요청 | DMND | Demand |
| 용량 | CPCT | Capacity |
| 용역 | SERVC | Service |
| 용존산소 | DO | Dissolved Oxygen |
| 용해 | DSLV | Dissolving |
| 우량 | EXLT | Excellent |
| 우수 | ST | Storm |
| 우수받이 | STRT_INL | Street Inlet |
| 우수토구 | STM_OVFL_MUT | Storm Overflow Mouth |
| 우회 | BP | BY PASS |
| 운반 | CNVY | Conveyance |
| 운영 | OPER | Operation |
| 운영비 | OPEXP | Operation Expense |
| 운행 | OPRAT | Operation |
| 원격 | RMRT | Rmrt |
| 원격감시체계 | TMS | Tele Monitoring System |
| 원료 | MTRAL | Material |
| 원수 | ORGWT | Origin'S Water |
| 원인자 | ORGPR | The Origin Person |
| 월 | MM | Month |
| 위도 | LAT | Latitude |
| 위반 | CNTR | Contravene |
| 위촉 | ENTRST | Entrusting |
| 위치 | LOC | Location |
| 위탁 | CNSG | Consign |
| 위탁자 | CONSGNR | Consignor |
| 유기인 | ORGP | Organic Phosphorus |
| 유량 | FLUX | Flux |
| 유량조정조 | EQLZT__TANK | Equalization Tank |
| 유입 | INFlOW | Inflow |
| 유입수 | INFWTR | Influent Water |
| 유주 | SWARMG | Swarming |
| 유지 | MNTNC | Maintenance |
| 유지용수공급 | MWRS | Maintenance Water Requirement Supply |
| 유체 | FLUID | Fluid |
| 유출 | EFFLUX | Efflux |
| 유충 | LAVL | Larval |
| 유형 | TY | Type |
| 유효 | VLD | Validity |
| 음용 | DKPS | For Drinking Purpose |
| 읍면동 | EMD | Eup/Myun/Dong |
| 응답 | RSPNS | Response |
| 응집제 | COAG | Cohesive Agents |
| 이관 | TRNSF | Transfer |
| 이동 | MVMN | Movement |
| 이력 | HST | History |
| 이메일 | EML | Email |
| 이상 | ABOVE | Above |
| 이송 | TRSF | Transfer |
| 이의 | OBJC | Objection |
| 이차 | SCDR | Secondary |
| 이차침전지 | SCDR_SDMT_TANK | Secondary Sedimaentation |
| 이행 | MIG | Migratation |
| 인건비 | PEXP | Personnel Expenses{Expenditure] |
| 인계 | HNDVR | Handover |
| 인계서 | HDOVDC | Handover Document |
| 인구 | POPL | Population |
| 인산 | PHACID | Phosphoric Acid |
| 인산염인 | POP | phosphate |
| 인양기 | LFT | Lifter |
| 인원수 | NOPE | The Number of Persons |
| 인증 | CERT | Certification |
| 인처리 | PPRCS | Phosphorus Process |
| 인허가 | APVL | Approval |
| 일 | DY | Day |
| 일련번호 | SN | Serial Number |
| 일반 | GNRL | General |
| 일별 | DALY | Daily |
| 일시 | DT | Date and Time |
| 일자 | YMD | YEAR Month Day |
| 일지 | DIARY | Diary |
| 일차 | PRM | Primary |
| 일차침전지 | PST_SDMT_TANK | Primary Sedimentation Tank |
| 일평균 | DYRG | Day Average |
| 임시 | TMPR | Temporary |
| 임의 | RNDM | Random |
| 입고 | WRHS | Warehousing |
| 입력 | INPT | Input |
| 잉여 | SPS | Surplus |
| 잉여슬러지 | EC_SLG | Excess Sludge |
| 자 | MAN | Man |
| 자갈 | GRAVEL | Gravel |
| 자료 | DATA | Data |
| 자리 | DIGIT | Digit |
| 자립화 | SLFRLI | Self Reliance |
| 자연형 | NATRLFOM | Natural Form |
| 자외선 | URLI | Ultraviolet Rays, Light |
| 자재 | MTRIL | Material |
| 자체 | ITSL | Itself |
| 작성 | WRT | Writing |
| 작업 | JOB | Job |
| 잠금 | LOCK | Lock |
| 장기 | LNGTR | Long Term |
| 장소 | PLCE | Place |
| 장착 | MNTNG | Mounting |
| 재 | RE | Re |
| 재이용 | REUSE | Reuse |
| 재이용수 | | Reuse Water |
| 재활용 | RUSE | Reuse |
| 저감 | REDUC | Reduction |
| 저류조 | DTT_TANK | Detention Tank |
| 저온 | LOWTMP | Low Temperature |
| 저장 | STRG | Storage |
| 저장소 | STORGE | Storage |
| 적발 | DSCL | Disclose |
| 적산 | ADD | Addition |
| 적용 | APLCN | Application |
| 적합 | STBLT | Suitability |
| 전기 | ELCTY | Electricity |
| 전기료 | ELFEE | Electricity Fee |
| 전력 | PWRER | Power |
| 전문 | SPCY | Specially |
| 전산 | CMPUTT | Computation |
| 전송 | TRSM | Transmission |
| 전압 | VLTGE | Voltage |
| 전용 | EXUSE | Exclusive Use |
| 전원 | SELCT | Source Of Electricity |
| 전처리 | PRETR | Pretreatment |
| 점검 | CHCK | Check |
| 접속 | CNTN | Connection |
| 정밀 | PRCIS | Precision |
| 정보 | INFO | Information |
| 정산 | CLCLN | Calculation |
| 정상 | NRMLT | Normality |
| 정수장 | WPPW | Water Purification Plant, Waterworks |
| 정제 | REFINE | Refining |
| 정지 | STP | Stop |
| 제거 | RMV | Removal |
| 제수변 | SL_VALVE | Sluice Valve |
| 제작 | MNFCT | Manufacture |
| 제조사 | MKR | Maker |
| 제출 | SBMSN | Submission |
| 제한 | LIMT | Limitation |
| 조류 | ALG | Algae |
| 조목 | CSB | Coarse Bar |
| 조사 | EXMN | Examination |
| 조성 | MAKE | Make |
| 조정 | EQLT | Equalization |
| 조치 | ACTN | Action |
| 조회 | INQ | Inquiry |
| 존재 | EXST | Existence |
| 종결 | TRMN | Termination |
| 종균제 | MICBA | Microbial Agent |
| 종료 | END | End |
| 종류 | KND | Kind |
| 종목 | IEM | Item |
| 종별 | ASORT | Assortment |
| 종사자 | ENFSN | Enfilade Person |
| 종점 | TMNL | Terminal |
| 좌표 | COOR | Coordinate |
| 주간 | WIK | Week |
| 주변 | CFR | Circumference |
| 주소 | ADDR | Address |
| 주입 | IJTN | Injection |
| 주파수 | FQNC | Frequency |
| 준공 | CMCN | Completion Of Construction |
| 중권역 | MDSTRC | Medium District |
| 중복 | DPCN | Duplication |
| 중분류 | MLSFC | Middle Classification |
| 중온 | MESPH | Mesophilic |
| 중첩 | RETRTN | Reiteration |
| 중화설비 | NUTEQP | Neutralization Equipment |
| 중화제 | CNTAG | Counteragent |
| 증가 | INCRS | Increase |
| 지도점검표사본 | CITBMR | Copy of Instruction Checklist |
| 지방자치단체 | LGOV | Local Governments |
| 지번 | LOTNO | Lot Number |
| 지상 | GRND | Ground |
| 지역 | ARA | Area |
| 지질 | NTRFS | Nature Of Soil |
| 지질도 | GEOMAP | Geological Map |
| 지하 | UDGD | Underground |
| 지하수 | GROU | Groundwater |
| 지형 | TPGRPH | Topography |
| 진공 | VCM | Vacuum |
| 진단 | DGNSS | Diagnosis |
| 진동 | VIBRAT | Vibration |
| 진행 | PRGRE | Progress |
| 질산 | NIACID | Nitric Acid. |
| 질산성질소 | NO3N | Nitrate Nitrogen |
| 집계 | TOT | Total |
| 집계구 | SUMDST | Summation District |
| 집행 | IMPL | Implementation |
| 찌꺼기 | LEES | Lees |
| 차아염소산 | NAOCL | Hypochlorous Acid |
| 차집 | ITCT | Intercepting |
| 착공 | BGNCST | Begin Construction |
| 채수 | SAMWT | Sampling Of Water |
| 채수기 | WTRSMPLR | Water Sampler |
| 채수병 | WTRBOTL | Water Bottle |
| 처리 | PRCS | Process |
| 처리비 | PSSFEE | Process Fee |
| 처리수 | TRT_WT | Treated Water |
| 처리수조 | TRT_WT_TANK | Treated Water Tank |
| 처리시설 | TRTMTFCLT | Treatment Facility |
| 처리자 | PRCR | Processor |
| 처분 | DSPS | Disposition |
| 철염 | FESLT | Ferric Salt |
| 첨부 | ATCH | Attachment |
| 첨부파일 | ATFI | Attachment File |
| 초과 | EXCS | Excess |
| 초기화 | INITL | Initialization |
| 촉매 | CTLY | Catalyst |
| 총계 | TSM | Summation |
| 총대장균군 | TCOLFGRP | Total Coliform Group |
| 총량 | TQT | Total Quantity |
| 총면적 | GFA | Gross Floor Area |
| 총사업비 | TPCO | Total Project Cost |
| 총인구 | TPOPLTN | Total Population |
| 총인처리시설 | TPFCT | Total Phosphorus facility |
| 최근 | RCNT | Recent |
| 최대값 | MAXV | Maximum Value |
| 최소값 | MINV | Minimum Value |
| 최적 | BMTB | Being The Most Suitable |
| 최종 | LAST | Last |
| 최초 | FRST | First |
| 추출 | EXTR | Extraction |
| 추출정보 | EXTRINFO | Extraction Information |
| 축 | AXS | Axis |
| 출력 | OTPT | Output |
| 출발 | LVE | Leave |
| 충전기 | CHRGR | Charger |
| 취득 | ACQS | Acquisition |
| 취소 | RTRCN | Retraction |
| 측정 | MSUR | Measurement |
| 측정기 | MSRR | Measurement Instrument |
| 측정소 | MSRSTN | Measurement Station |
| 침사지 | GTCB | Grit Chamber |
| 침전 | SDMT | Sediment |
| 침전지 | SDMT_TANK | sedimentation tank |
| 컨베이어 | CVY | Conveyor |
| 컬럼 | COL | Column |
| 코드 | CD | Code |
| 크기 | SZ | Size |
| 크실렌 | XYLEN | Xylene |
| 탄화 | CARB | Carbonization |
| 탈거 | CRYOFF | Carry Off |
| 탈리액 | SPNT | Supernatant |
| 탈수 | DWTR | Dewatering |
| 탈수기 | DEHYM | Dehydration Machine |
| 탈질 | DNTFCT | Denitrificatio |
| 탈취 | DODRZ | Deodorization |
| 탈취기 | DEODRZ | Deodorizer |
| 탈취제 | DEODR | Deodorant |
| 탈황제 | DSLFA | Desulfurizing Agent |
| 터널형 | YUNLTP | Tunnel Type |
| 테이블 | TBL | Table |
| 토구 | OTFL | Outfall |
| 토지 | LAD | Land |
| 토지면적 | LDAR | Land Area |
| 통계 | STATS | Statistics |
| 통보 | NTFCTN | Notification |
| 통신 | COMM | Communication |
| 통지 | AVTSMT | Avertissement |
| 통합 | UNTY | Unity |
| 특성 | CHRT | Character |
| 특수 | SPECL | Special |
| 특정유해물질 | SHCH | Specified Hazardous Chemicals |
| 파일 | FILE | File |
| 판정 | JGMT | Judgement |
| 펌프 | PUMP | Pump |
| 펌프장 | PUMP_STT | Pump Station |
| 페놀 | PHNL | Phenol |
| 평가 | EVL | Evaluation |
| 폐기 | DSCD | Discard |
| 폐기물 | WSTE | Waste |
| 폐쇄 | CLSG | Closing |
| 폐수 | WAWA | Waste Water |
| 폐수처리 | WSWT | Wastewater Treatment |
| 폐열 | WASTHET | Waste Heat |
| 포기 | ARTN | Aeration |
| 포트 | PORT | Port |
| 폭기조 | ARTN_TANK | Aeration Tank |
| 폴리머 | PLYMR | Polymer |
| 폴리철 | FRRCSU | Ferric Sulfate |
| 표고 | AL | Altitude |
| 표기 | MARK | Mark |
| 표시 | INDCT | Indication |
| 표준 | STD | Standard |
| 표출 | EPRSS | Expression |
| 플래그 | FLAG | Flag |
| 필드 | FILD | Field |
| 필수 | ESNTL | Essential |
| 필증 | CERFIN | Certificate Of Finishing |
| 필터 | FLTER | Filter |
| 하수 | SWGE | Sewage |
| 하수도 | DRNGSYS | Drainage System |
| 하수미처리 | SEWGUNTTM | Un Sewage Treatment |
| 하수처리 | SEWGTRTM | Sewage Treatment |
| 하수처리수 | STWTR | Sewage Treatment Water |
| 하위 | LOWR | Lower |
| 하향류 | DWFW | Downflow |
| 할인 | DSCNT | Discount |
| 함수율 | MISTR_CTNT | Moisture content |
| 합계 | SMM | Summation |
| 해당 | RLVT | Relevant |
| 해석 | INTRPRTT | Interpretation |
| 해양투기 | OCDU | Ocean Dumping |
| 행망 | ADMNW | Administration Network |
| 행정 | ADMN | Administration |
| 행정구역 | ADMI | Administrative District |
| 행정동 | DONG | Dong (Administrative District) |
| 허가 | PRMSN | Permission |
| 허용 | PERM | Permission |
| 현상 | PHNMN | Phenomenon |
| 혐기 | AARB | Anaerobic |
| 혐기조 | AARB_TANK | Anaerobic tank |
| 협잡물 | CCMT | Concomitant |
| 형태 | SHAP | Shape |
| 호기 | ARB | Aerobic |
| 호기조 | ARB_TANK | Aerobic Tank |
| 호소수 | LMSHWT | Lake And Marsh Water |
| 호수 | LKE | Lake |
| 호안 | RVTMNT | Revetment |
| 호이스트 | HIST | Hoist |
| 혼합 | CMPN | Compound |
| 혼합액현탁고형물 | MLSS | Mixed liquor suspended solids |
| 혼화지 | MXG_TNAK | Mixing tank |
| 홈페이지 | HMPG | Homepage |
| 홍수 | FLOOD | Flood |
| 확인 | IDNTY | Identify |
| 확인서 | CNFRMN | Confirmation |
| 확인자 | IDFR | Identifier |
| 확장자 | EXTN | Extension |
| 확정 | CFMTN | Confirmation |
| 환경 | ENVR | Environment |
| 환경기초시설 | ENINF | Environmental Infrastructure |
| 환원 | RSTORE | Restoration |
| 활성탄 | AC | Activated Carbon |
| 활성화 | PRMT | Promote/Facilitate |
| 활용 | PRUS | Practical Use |
| 황산 | ECCBT | Encyclopaedia Britannica (H2So4) |
| 회사 | CO | Company |
| 회수 | RTRVL | Retrieval |
| 회신 | RPLY | Reply |
| 효율 | EFC | Efficiency |
| 효율성 | EFFCTV | Effectiveness |
| 휴대 | MBL | Mobile |
| 흡수정 | EXTTW | Extraction Well |
| 흡착 | ADST | Adsorption |
@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import {
keepPreviousData,
useQuery,
@@ -75,7 +75,7 @@ function AddAdminDialog() {
});
const query = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS, pagination, "normal-user"],
queryKey: [ENDPOINT.USER_SERVICE.USERS, "findPageUser", pagination],
queryFn: () =>
UserApi.findPageUser({
...userSearchQuery,
@@ -181,6 +181,21 @@ function AddAdminDialog() {
query.refetch();
};
useEffect(() => {
if (!open) {
setRowSelection({});
setSelectedGroupId("");
setUserSearchQuery({
keywordType: "userName",
keyword: "",
});
setPagination({ pageIndex: 0, pageSize: 10 });
queryClient.removeQueries({
queryKey: [ENDPOINT.USER_SERVICE.USERS, "findPageUser"],
});
}
}, [open]);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
@@ -1,53 +1,22 @@
// import { client } from "@/shared/api";
// import { ENDPOINT } from "@/shared/config";
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import {
type BoardStatisticsParams,
type BoardStatistics,
type BoardStatisticsService,
} from "../model/board-statistics-interface";
// 임시 모의 데이터
const mockBoardStatisticsData: BoardStatistics = {
totalPosts: 2456,
totalComments: 156,
averageRating: 4.2,
totalViews: 45678,
dayList: [
{ date: "1월", posts: 10, comments: 5, views: 800 },
{ date: "2월", posts: 15, comments: 8, views: 1200 },
{ date: "3월", posts: 12, comments: 10, views: 1000 },
{ date: "4월", posts: 18, comments: 15, views: 1500 },
{ date: "5월", posts: 22, comments: 20, views: 1800 },
{ date: "6월", posts: 20, comments: 18, views: 1600 },
{ date: "7월", posts: 25, comments: 22, views: 2000 },
],
boardList: [
{ boardType: "notice", boardName: "공지사항", count: 45 },
{ boardType: "faq", boardName: "FAQ", count: 120 },
{ boardType: "qna", boardName: "Q&A", count: 180 },
{ boardType: "free", boardName: "자유게시판", count: 95 },
],
statusList: [
{ status: "completed", statusName: "답변완료", count: 75 },
{ status: "waiting", statusName: "답변대기", count: 25 },
],
};
class BoardStatisticsApiService implements BoardStatisticsService {
async getBoardStatistics(
params: BoardStatisticsParams,
): Promise<BoardStatistics> {
console.log("params: ", params);
try {
// 실제 API 연동 시 아래 코드 사용
// const response = await client.get<BoardStatistics>(
// `${ENDPOINT.CMS_SERVICE.BOARD_STATISTICS}`,
// { params },
// );
// return response.data;
// 임시 모의 데이터 반환
return Promise.resolve(mockBoardStatisticsData);
const response = await client.get<BoardStatistics>(
`${ENDPOINT.BOARD_SERVICE.STATISTICS}`,
{ params },
);
return response.data;
} catch (error) {
console.error("게시판 통계 데이터 조회 실패:", error);
throw error;
@@ -12,32 +12,36 @@ export interface BoardStatisticsParams extends PageableParams {
}
export interface BoardStatistics {
totalPosts: number;
totalComments: number;
averageRating: number;
totalViews: number;
dayList: DayStatistics[];
boardList: BoardTypeStatistics[];
statusList: StatusStatistics[];
bbsTotalDto: BbsTotalDto; // 게시판 전체 통계
pstAnsList: PstAnswerStatistics[]; // 게시물 답변 상태별 통계 목록
bbsDailyList: BbsDailyStatistics[]; // 게시판 일별 통계 목록
bbsPstCntList: BbsPostCountStatistics[]; // 게시판별 게시물 건수 통계 목록
}
interface DayStatistics {
date: string;
posts: number;
comments: number;
views: number;
interface BbsTotalDto {
pstInqCnt: number; // 게시물 조회 건수
pstEvlAvg: number; // 게시물 평가 평균
pstTotCnt: number; // 게시물 총 건수
pstAnsWaitCnt: number; // 게시물 답변 대기 건수
}
interface BoardTypeStatistics {
boardType: string;
boardName: string;
count: number;
interface PstAnswerStatistics {
answerNm: string; // 답변 명
answerYn: string; // 답변 여부 (Y/N)
ansCnt: number; // 답변 건수
}
interface StatusStatistics {
status: string;
statusName: string;
count: number;
interface BbsDailyStatistics {
dayYmd: string; // 일자 (YYYY-MM-DD)
pstInqCnt: number; // 게시물 조회 건수
pstCmntCnt: number; // 게시물 댓글 건수
pstNewCnt: number; // 게시물 신규 건수
}
interface BbsPostCountStatistics {
bbsId: number; // 게시판 ID
bbsNm: string; // 게시판 명
pstCnt: number; // 게시물 건수
}
export interface BoardStatisticsService {
@@ -11,20 +11,20 @@ export const chartData = {
} => {
return {
data: {
labels: data.dayList.map((item) => item.date),
labels: data.bbsDailyList.map((item) => item.dayYmd),
datasets: [
{
type: "line",
label: "신규게시물",
data: data.dayList.map((item) => item.posts),
data: data.bbsDailyList.map((item) => item.pstNewCnt),
borderColor: getCssVariableValue("--chart-1"),
backgroundColor: "rgba(59, 130, 246, 0.1)",
tension: 0.4,
},
{
type: "line",
label: "답변대기",
data: data.dayList.map((item) => item.comments),
label: "댓글수",
data: data.bbsDailyList.map((item) => item.pstCmntCnt),
borderColor: getCssVariableValue("--chart-2"),
backgroundColor: "rgba(236, 72, 153, 0.1)",
tension: 0.4,
@@ -32,11 +32,10 @@ export const chartData = {
{
type: "line",
label: "조회수",
data: data.dayList.map((item) => item.views),
data: data.bbsDailyList.map((item) => item.pstInqCnt),
borderColor: getCssVariableValue("--chart-3"),
backgroundColor: "rgba(168, 85, 247, 0.1)",
tension: 0.4,
hidden: true,
},
],
},
@@ -67,13 +66,13 @@ export const chartData = {
} => {
return {
data: {
labels: data.boardList.map((item) => item.boardName),
labels: data.bbsPstCntList.map((item) => item.bbsNm),
datasets: [
{
type: "bar",
label: "게시판별 분포",
data: data.boardList.map((item) => item.count),
backgroundColor: data.boardList.map((_, index) =>
data: data.bbsPstCntList.map((item) => item.pstCnt),
backgroundColor: data.bbsPstCntList.map((_, index) =>
getCssVariableValue(`--chart-${(index % 5) + 1}`),
),
},
@@ -106,12 +105,12 @@ export const chartData = {
} => {
return {
data: {
labels: data.statusList.map((item) => item.statusName),
labels: data.pstAnsList.map((item) => item.answerNm),
datasets: [
{
type: "doughnut",
data: data.statusList.map((item) => item.count),
backgroundColor: data.statusList.map((_, index) =>
data: data.pstAnsList.map((item) => item.ansCnt),
backgroundColor: data.pstAnsList.map((_, index) =>
getCssVariableValue(`--chart-${(index % 5) + 1}`),
),
},
@@ -26,7 +26,7 @@ import {
SelectValue,
} from "@common/components";
import { SiteSelect } from "@/entities/site";
// import { ENDPOINT } from "@/shared/config";
import { ENDPOINT } from "@/shared/config";
import { NoData } from "@/shared/ui/no-data";
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
import { BoardStatisticsApi } from "../api/board-statistics-service";
@@ -66,7 +66,7 @@ function BoardStatisticsPage() {
});
const { data: boardStatisticsData } = useQuery({
queryKey: ["board-statistics", searchParams],
queryKey: [ENDPOINT.BOARD_SERVICE.STATISTICS, searchParams],
queryFn: () =>
BoardStatisticsApi.getBoardStatistics({
...searchParams,
@@ -176,7 +176,8 @@ function BoardStatisticsPage() {
<h3 className="text-lg font-medium text-gray-500">총 게시물</h3>
<div className="flex items-center justify-between">
<div className="mt-2 text-4xl font-bold">
{boardStatisticsData?.totalPosts.toLocaleString() || 0}
{boardStatisticsData?.bbsTotalDto?.pstTotCnt?.toLocaleString() ||
0}
</div>
<div className="h-16 w-24">
<Line
@@ -210,7 +211,8 @@ function BoardStatisticsPage() {
<h3 className="text-lg font-medium text-gray-500">답변대기</h3>
<div className="flex items-center justify-between">
<div className="mt-2 text-4xl font-bold">
{boardStatisticsData?.totalComments.toLocaleString() || 0}
{boardStatisticsData?.bbsTotalDto?.pstAnsWaitCnt?.toLocaleString() ||
0}
</div>
<div className="h-16 w-24">
<Line
@@ -244,7 +246,7 @@ function BoardStatisticsPage() {
<h3 className="text-lg font-medium text-gray-500">평균평점</h3>
<div className="flex items-center justify-between">
<div className="mt-2 text-4xl font-bold">
{boardStatisticsData?.averageRating.toFixed(1) || 0}
{boardStatisticsData?.bbsTotalDto?.pstEvlAvg?.toFixed(1) || 0}
</div>
<div className="h-16 w-24">
<Line
@@ -278,7 +280,8 @@ function BoardStatisticsPage() {
<h3 className="text-lg font-medium text-gray-500">총 조회수</h3>
<div className="flex items-center justify-between">
<div className="mt-2 text-4xl font-bold">
{boardStatisticsData?.totalViews.toLocaleString() || 0}
{boardStatisticsData?.bbsTotalDto?.pstInqCnt?.toLocaleString() ||
0}
</div>
<div className="h-16 w-24">
<Line
@@ -315,7 +318,7 @@ function BoardStatisticsPage() {
<h3 className="text-2xl font-medium">일별 게시물 현황</h3>
</div>
<div className="h-96 p-4">
{boardStatisticsData?.dayList?.length ? (
{boardStatisticsData?.bbsDailyList?.length ? (
<Line {...chartData.day(boardStatisticsData)} />
) : (
<NoData />
@@ -329,7 +332,7 @@ function BoardStatisticsPage() {
<h3 className="text-2xl font-medium">답변상태</h3>
</div>
<div className="h-96 p-4">
{boardStatisticsData?.statusList?.length ? (
{boardStatisticsData?.pstAnsList?.length ? (
<Doughnut {...chartData.status(boardStatisticsData)} />
) : (
<NoData />
@@ -342,7 +345,7 @@ function BoardStatisticsPage() {
<h3 className="text-2xl font-medium">게시판별 분포</h3>
</div>
<div className="h-96 p-4">
{boardStatisticsData?.boardList?.length ? (
{boardStatisticsData?.bbsPstCntList?.length ? (
<Bar {...chartData.board(boardStatisticsData)} />
) : (
<NoData />
@@ -1,4 +1,4 @@
import { memo } from "react";
import { memo, useEffect } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery, useQueryClient } from "@tanstack/react-query";
@@ -140,6 +140,10 @@ const LwpoCodeManagementDialog = memo(
}
};
useEffect(() => {
if (!open) form.reset();
}, [open]);
return (
<Dialog open={open} onOpenChange={onOpenChange} {...props}>
<DialogTrigger asChild>{children}</DialogTrigger>
+5 -1
View File
@@ -1,4 +1,4 @@
import { memo } from "react";
import { memo, useEffect } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
@@ -96,6 +96,10 @@ const UpCodeManagementDialog = memo(
if (await confirm(confirmMessage)) callback(data);
};
useEffect(() => {
if (!open) form.reset();
}, [open]);
return (
<Dialog open={open} onOpenChange={onOpenChange} {...props}>
<DialogTrigger asChild>{children}</DialogTrigger>
@@ -1,6 +1,13 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { Dashboard, DashboardService } from "../model/dashboard-interface";
import type {
Dashboard,
DashboardService,
NoticeUser,
Schedule,
ScheduleQueryParams,
} from "../model/dashboard-interface";
import type { PageableList } from "@/shared/model";
export class DashboardApiService implements DashboardService {
public async getDashboard(): Promise<Dashboard> {
@@ -9,6 +16,25 @@ export class DashboardApiService implements DashboardService {
);
return response.data;
}
public async getNoticeUserList(
recvUnqId: string,
): Promise<PageableList<NoticeUser>> {
const response = await client.get<PageableList<NoticeUser>>(
`${ENDPOINT.CMS_SERVICE.NOTICE_USER}/${recvUnqId}/list`,
);
return response.data;
}
public async getScheduleList(
params?: ScheduleQueryParams,
): Promise<Schedule[]> {
const response = await client.get<Schedule[]>(
ENDPOINT.CMS_SERVICE.SCHEDULES,
{ params },
);
return response.data;
}
}
export const DashboardApi = new DashboardApiService();
@@ -1,3 +1,5 @@
import type { PageableList } from "@/shared/model";
export interface SiteStatisticsContent {
siteId: string;
siteNm: string;
@@ -27,6 +29,42 @@ export interface Dashboard {
menuAccessList: MenuStatisticsContent[];
}
export interface NoticeUser {
notiId: number;
recvUnqId: string;
senderUnqId: string;
notiType: string;
notiTtl: string;
notReadYn: string;
regDt: string;
}
export interface Schedule {
schdlBgngDt: string;
schdlEndDt: string;
actualDate: string;
schdlSn: number;
siteId: string;
schdlTtl: string;
schdlSeCd: string;
schdlCn: string;
reptYn: string;
commSchdlYn: string;
atflCd: string | null;
}
export interface ScheduleQueryParams {
siteId?: string;
schdlTtl?: string;
schdlSeCd?: string;
schdlBgngYmd?: string;
schdlEndYmd?: string;
reptYn?: string;
commSchdlYn?: string;
}
export interface DashboardService {
getDashboard(): Promise<Dashboard>;
getNoticeUserList(recvUnqId: string): Promise<PageableList<NoticeUser>>;
getScheduleList(params?: ScheduleQueryParams): Promise<Schedule[]>;
}
@@ -1,5 +1,6 @@
"use client";
import dayjs from "dayjs";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { getCoreRowModel } from "@tanstack/react-table";
@@ -17,8 +18,8 @@ import {
} from "chart.js";
import { Bar, Line } from "react-chartjs-2";
import { PostApi, type RecentPost } from "@/entities/post";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import { AUTH_USER_ID, ENDPOINT, SITE_ID } from "@/shared/config";
import { getCookie, useDataTable } from "@/shared/lib";
import { DashboardApi } from "../api/dashboard-service";
import { chartData } from "../model/chart-data";
import {
@@ -39,12 +40,44 @@ ChartJS.register(
);
function DashboardPage() {
const userId = getCookie(AUTH_USER_ID) ?? "";
const router = useRouter();
const { data } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.DASHBOARD],
queryFn: () => DashboardApi.getDashboard(),
});
const { data: noticeUserList } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.NOTICE_USER],
queryFn: () => DashboardApi.getNoticeUserList(userId),
enabled: !!userId,
select: (data) => {
// 오늘 ~ 한 달 전 사이의 일정
const today = dayjs().endOf("day");
const monthAgo = dayjs().subtract(1, "month").startOf("day");
return data.content?.filter((item) => {
const regDt = dayjs(item.regDt);
return (
(regDt.isSame(monthAgo) || regDt.isAfter(monthAgo)) &&
(regDt.isSame(today) || regDt.isBefore(today))
);
});
},
});
const { data: scheduleList } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.SCHEDULES],
queryFn: () =>
DashboardApi.getScheduleList({
siteId: SITE_ID,
schdlBgngYmd: dayjs().format("YYYY-MM-DD"),
schdlEndYmd: dayjs().add(1, "month").format("YYYY-MM-DD"),
commSchdlYn: "Y",
}),
});
const noticePostQuery = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS],
queryFn: () =>
@@ -323,22 +356,15 @@ function DashboardPage() {
</div>
<div className="p-4">
<div className="space-y-4 text-foreground">
<div className="flex items-center justify-between border-b border-border pb-4">
<span className="font-medium">신규 사용자 가입</span>
<span>{(1).toLocaleString()}분 전</span>
</div>
<div className="flex items-center justify-between border-b border-border pb-4">
<span className="font-medium">메뉴 수정 요청</span>
<span>{(5).toLocaleString()}분 전</span>
</div>
<div className="flex items-center justify-between border-b border-border pb-4">
<span className="font-medium">게시물 신고</span>
<span>{(10).toLocaleString()}분 전</span>
</div>
<div className="flex items-center justify-between">
<span className="font-medium">컨텐츠 승인 요청</span>
<span>{(15).toLocaleString()}분 전</span>
</div>
{noticeUserList?.map((item) => (
<div
key={item.notiId}
className="flex items-center justify-between border-b border-border pb-4"
>
<span className="font-medium">{item.notiTtl}</span>
<span>{dayjs(item.regDt).fromNow()}</span>
</div>
))}
</div>
</div>
</div>
@@ -349,24 +375,44 @@ function DashboardPage() {
</div>
<div className="p-4">
<div className="space-y-4 text-foreground">
<div className="flex items-center justify-between border-b border-border pb-4">
<span className="font-medium">
{(3).toLocaleString()}월 정기 시스템 점검
</span>
<span>내일</span>
</div>
<div className="flex items-center justify-between border-b border-border pb-4">
<span className="font-medium">서버 업데이트 예정</span>
<span>{(3).toLocaleString()}일 후</span>
</div>
<div className="flex items-center justify-between border-b border-border pb-4">
<span className="font-medium">백업 스케줄</span>
<span>{(5).toLocaleString()}일 후</span>
</div>
<div className="flex items-center justify-between">
<span className="font-medium">정기 보안 점검</span>
<span>{(7).toLocaleString()}일 후</span>
</div>
{scheduleList && scheduleList.length > 0 ? (
scheduleList.map((schedule, index) => {
const today = dayjs();
const scheduleDate = dayjs(schedule.actualDate);
const daysUntil = scheduleDate.diff(today, "day");
let timeText = "";
if (daysUntil === 0) {
timeText = "오늘";
} else if (daysUntil === 1) {
timeText = "내일";
} else if (daysUntil > 1) {
timeText = `${daysUntil}일 후`;
} else {
timeText = scheduleDate.format("YYYY-MM-DD");
}
return (
<div
key={schedule.schdlSn}
className={`flex items-center justify-between ${
index < scheduleList.length - 1
? "border-b border-border pb-4"
: ""
}`}
>
<span className="font-medium">
{schedule.schdlTtl}
</span>
<span>{timeText}</span>
</div>
);
})
) : (
<div className="text-center text-muted-foreground">
예정된 일정이 없습니다.
</div>
)}
</div>
</div>
</div>
@@ -1,4 +1,4 @@
import { useState, useRef } from "react";
import { useState, useRef, useEffect } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
@@ -48,25 +48,14 @@ function MenuAuthorityDialog({ groupId }: MenuAuthorityDialogProps) {
keywordType: "",
siteId: "",
});
const handleReset = () => {
if (inputRef.current) inputRef.current.value = "";
setKeywordType("");
setSiteId("");
setSearchParams({
keyword: "",
keywordType: "",
siteId: "",
});
};
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENU_AUTHRTS, groupId, searchParams],
queryFn: () => MenuAuthorityApi.getGroupMenuAuthorityList({
groupId,
...searchParams
}),
queryFn: () =>
MenuAuthorityApi.getGroupMenuAuthorityList({
groupId,
...searchParams,
}),
enabled: open && !!groupId,
});
@@ -117,6 +106,21 @@ function MenuAuthorityDialog({ groupId }: MenuAuthorityDialogProps) {
});
};
const handleReset = () => {
if (inputRef.current) inputRef.current.value = "";
setSiteId("");
setKeywordType("");
setSearchParams({
keyword: "",
keywordType: "",
siteId: "",
});
};
useEffect(() => {
if (!open) handleReset();
}, [open]);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
@@ -1,4 +1,4 @@
import { useState, memo } from "react";
import { useState, memo, useEffect } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
@@ -90,6 +90,10 @@ const ManagementAuthorityDialog = memo(
}
};
useEffect(() => {
if (!open) form.reset();
}, [open]);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
@@ -1,4 +1,4 @@
import { useState, useRef } from "react";
import { useState, useRef, useEffect } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { getCoreRowModel, type ColumnDef } from "@tanstack/react-table";
import {
@@ -113,6 +113,10 @@ function MappingRoleDialog({
},
});
useEffect(() => {
if (!open) handleReset();
}, [open]);
return (
<Dialog open={open} onOpenChange={onOpenChange} {...props}>
<DialogTrigger asChild>
+2
View File
@@ -56,6 +56,7 @@ export const ENDPOINT = {
APILOG: "/cms-service/api/v1/api-logs",
SITE_AUTHORITY: "/cms-service/api/v1/site-authrt",
CONNECTION_LOG: "/cms-service/api/v1/cntnlog",
NOTICE_USER: "/cms-service/api/v1/noti/user",
},
BOARD_SERVICE: {
BBS: "/board-service/api/v1/bbs",
@@ -63,6 +64,7 @@ export const ENDPOINT = {
POSTS: "/board-service/api/v1/posts",
POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts",
POST_EVALUATION: "/board-service/api/v1/pstEvl",
STATISTICS: "/board-service/api/v1/stat/bbs",
},
};
@@ -30,18 +30,18 @@ export default function CalcRainEffect(data: any, weather = 'ALL') {
rtnDataSunny.push(item);
} else if (rainEffectStd >= 0) { // 강우일 + 강우영향일
item.rainEffectDay = 1;
if (idx > 1) {
// 전날 비가 왔고 오늘 강우량 3미만인 경우 전날 강우량이 3이상이면 오늘 toBeInfoRnDay을 전날 강우량으로 설정
if (data[idx - 1]?.infoRnDay && Number(data[idx]?.infoRnDay) < 3 && Number(data[idx - 1]?.infoRnDay.toString().trim()) >= 3) {
data[idx]!.toBeInfoRnDay = data[idx - 1]!.infoRnDay;
}
}
if (idx > 2) {
// 전전날 비가 왔고 오늘 강우량 3미만인 경우 전전날 강우량이 3이상이면 오늘 toBeInfoRnDay을 전전날 강우량으로 설정
if (data[idx - 2]?.infoRnDay && Number(data[idx]?.infoRnDay) < 3 && Number(data[idx - 2]?.infoRnDay.toString().trim()) >= 3) {
data[idx]!.toBeInfoRnDay = data[idx - 2]!.infoRnDay;
}
}
if (idx > 1) {
// 전날 비가 왔고 오늘 강우량 3미만인 경우 전날 강우량이 3이상이면 오늘 toBeInfoRnDay을 전날 강우량으로 설정
if (data[idx - 1]?.infoRnDay && Number(data[idx]?.infoRnDay) < 3 && Number(data[idx - 1]?.infoRnDay.toString().trim()) >= 3) {
data[idx]!.toBeInfoRnDay = data[idx - 1]!.infoRnDay;
}
}
rtnDataRain.push(item);
}
@@ -271,7 +271,7 @@ export default function ProcessFacilitySearch({searchYear = '', fcltyList = []}:
(head.id === 'flowRate' && index === 0)?<TableCell rowSpan={5} className="text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
(head.id === 'flowRate' && index === 5)?<TableCell rowSpan={6} className="text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
(head.id === 'flowRate' && index === 11)?<TableCell rowSpan={2} className="text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
(head.id === 'groundWaterAvg'||head.id === 'groundWaterSummer'||head.id === 'groundWaterWinter')&&(index === 0)?<TableCell rowSpan={7} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
(head.id === 'groundWaterAvg'||head.id === 'groundWaterSummer'||head.id === 'groundWaterWinter')&&(index === 0)?<TableCell rowSpan={7} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{Number.isNaN(el[head.id]) ? "" : el[head.id]}</TableCell>:
(head.id === 'groundWaterAvg'||head.id === 'groundWaterSummer'||head.id === 'groundWaterWinter')&&(index === 7)?<TableCell rowSpan={6} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
(head.id === 'rdi'&&index === 0)?<TableCell rowSpan={7} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
(head.id === 'rdi'&&index === 7)?<TableCell rowSpan={6} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
@@ -7,7 +7,7 @@
"use client"
import { useFoStore } from "@/app/store";
import {
ContentsDiv, downloadExcel, downloadMultipleExcel, filePickedBlobArray, MainDiv
ContentsDiv, downloadExcelFormat, downloadMultipleExcel, filePickedBlobArrayFormat, MainDiv
, SelectBox, UITab, type SelectDataType
} from "@common/business_components";
import { Button, TabsContent } from "@common/components/ui";
@@ -60,6 +60,10 @@ export default function DashBoard() {
const [sidoValue, setSidoValue] = useState<string>('0');
const [sigunValue, setSigunValue] = useState<string>('0');
const { isInit, selectSearchYear, btnClass, selectSidoData, selectSigunData, } = Common;
const defaultSearchYear =
selectSearchYear.find((item) => item.selected)?.val ||
selectSearchYear[0]?.val ||
"0";
const { planFcltyList } = PlanFclty;
const { operationalStatusList } = OperationStatus;
const { excelList } = RainManage;
@@ -75,7 +79,7 @@ export default function DashBoard() {
/******************선택된 데이터******************/
const [searchYear, setSearchYear] = useState<string>(selectSearchYear[0]?.val || '0'); // 연도
const [searchYear, setSearchYear] = useState<string>(defaultSearchYear); // 연도
// 시설명
const [fcltyList, setFcltyList] = useState<FacilityType>({
bscFctCd: '11000SW001R',
@@ -138,13 +142,11 @@ export default function DashBoard() {
}
}
const excelDownload = () => {
const excelDownload = async () => {
if (!fcltyList.bscFctCd) return;
switch (activeTab) {
case 'facilityInfo': // 시설정보 및 운영현황
searchOperationStatus(false, searchYear, fcltyList.bscFctCd).then(() => { setTimeout(() => { makeExcelData(); }, 200); });
const makeExcelData = async () => {
fetch(`/mainDashBoard.xlsx`, {}).then(async (response) => {
if (!response.ok) {
@@ -194,20 +196,27 @@ export default function DashBoard() {
headerNum: 3,
headerRange: 'A1:G2',
sheetName: '운영실태',
startCol: 'A'
startCol: 'A',
decimalRange0: 'B3:G2,B9:B11',
decimalRange1: 'B5:G5',
},
{
data: excelData2,
headerNum: 4,
headerRange: 'A1:Q3',
sheetName: '운영실태',
startCol: 'C'
startCol: 'C',
decimalRange0: 'C4:O8',
decimalRange1: 'C9:L10,M11:O11',
decimalRange3: 'C11:L16',
}
];
// 버퍼에 있는 엑셀데이터 전달 후 json으로 변환
await filePickedBlobArray(blobRet, '시설정보및운영현황', sheetParameter);
await filePickedBlobArrayFormat(blobRet, '시설정보및운영현황', sheetParameter);
});
};
await searchOperationStatus(false, searchYear, fcltyList.bscFctCd);
await makeExcelData();
break;
case 'mainGraph': // 주요그래프
@@ -258,7 +267,15 @@ export default function DashBoard() {
case 'rainManage': // 강우일 운영현황
searchRainManage(searchYear, fcltyList.bscFctCd, true);
if (excelList && excelList.length > 0) {
downloadExcel({ data: excelList as unknown as JSON[], fileName: '강우일 운영현황', sheetNameArray: ['강우일 운영현황'], headerRange: 'A1:T1' });
downloadExcelFormat({
data: excelList as unknown as JSON[],
fileName: '강우일 운영현황',
sheetNameArray: ['강우일 운영현황'],
headerRange: 'A1:T1',
decimalArray0: [2,3],
decimalArray1: [1,4,5,6,7,10,11,12,15,16,17],
decimalArray3: [8,9,13,14,18,19],
});
}
break;
// Add other cases for different tabs as needed
@@ -283,7 +300,7 @@ export default function DashBoard() {
trtFrfNm: '표준활성슬러지법, MLE, A2O',
efflWqltStndrdNm: '500톤 이상(Ⅲ지역)'
});
setSearchYear(selectSearchYear[0]?.val || '0');
setSearchYear(defaultSearchYear);
searchFcltyList.bscFctNm = '난지';
setSidoValue('0');
@@ -5,11 +5,11 @@
*/
"use client"
import { useFoStore } from "@/app/store";
import { StoreContext, useFoStore } from "@/app/store";
import type { TableUpperProps } from "@/app/store/common";
import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components";
import { ContentsDiv, downloadExcelFormat, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components";
import ListDiv from "@common/business_components/ui/list_div";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import PrecipitationModal from "./modal/page";
import LoadingOverlay from "@common/business_components/ui/loadingBar";
import UnderComment from "@/app/components/common/UnderComment";
@@ -22,6 +22,11 @@ export default function FacilityPrecipitation() {
,initializeCommonData
,searchFacilityPrecipitationList
} = useFoStore((state) => state) || { processAreaList: { processAreaList: [] }};
const store = useContext(StoreContext);
if (!store) {
throw new Error("StoreContext is not available. Ensure StoreProvider is mounted.");
}
const {
isInit,
@@ -37,6 +42,11 @@ export default function FacilityPrecipitation() {
btnClass
} = Common;
const defaultSearchYear =
selectSearchYear.find((item) => item.selected)?.val ||
selectSearchYear[0]?.val ||
"0";
const {
labelArray,
} = FacilityPrecipitationSearch;
@@ -45,7 +55,6 @@ export default function FacilityPrecipitation() {
upHeadList1,
upHeadList2,
upHeadList3,
excelList,
facilityPrecipitation,
facilityPrecipitationCount,
isLoading,
@@ -90,8 +99,9 @@ export default function FacilityPrecipitation() {
// 시설상태
const [operationValue, setOperationValue] = useState<string>("0");
// 조회기간
const [searchYear, setSearchYear] = useState<string>(selectSearchYear[0]?.val || "0");
const [searchYear, setSearchYear] = useState<string>(defaultSearchYear);
// 엑셀다운로드 트리거
const [isExcelDownClick, setIsExcelDownClick] = useState<boolean>(false);
/**
* 페이지 함수
*/
@@ -102,7 +112,7 @@ export default function FacilityPrecipitation() {
// 상태값 설정
setFcltCpct(selectUpdownData[0]?.val || "0");
setOperationValue(selectOperationData[0]?.val || "0");
setSearchYear(selectSearchYear[0]?.val || "0");
setSearchYear(defaultSearchYear);
// SearchInputSelect 초기화
setFcltyName("");
if (searchInputRef.current) {
@@ -111,7 +121,7 @@ export default function FacilityPrecipitation() {
input.value = "";
}
}
searchFacilityPrecipitationList(1, 10, selectSearchYear[0]?.val || "0");
searchFacilityPrecipitationList(1, 10, defaultSearchYear);
}
// 모달 닫기
@@ -134,20 +144,9 @@ export default function FacilityPrecipitation() {
useEffect(() => {
if (!isInit) {
initializeCommonData();
searchFacilityPrecipitationList(1, 10, selectSearchYear[0]?.val || "0");
searchFacilityPrecipitationList(1, 10, defaultSearchYear);
}
}, [isInit, initializeCommonData, searchFacilityPrecipitationList, selectSearchYear]);
useEffect(() => {
if (excelList && excelList.length > 0) {
downloadExcel({
data: excelList as unknown as JSON[],
fileName: '시설별 강수현황',
sheetNameArray: ['시설별 강수현황'],
headerRange: 'A1:N1'
});
}
}, [excelList]);
}, [isInit, initializeCommonData, searchFacilityPrecipitationList, defaultSearchYear]);
useEffect(() => {
if (facilityPrecipitation.length > 0) {
@@ -159,7 +158,7 @@ export default function FacilityPrecipitation() {
* 콜백함수
*/
// 조회
const searchFacilityPrecipitation = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => {
const searchFacilityPrecipitation = useCallback(async ({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => {
let nameCd, partStr
switch (partValue) {
case 'R':
@@ -177,9 +176,9 @@ export default function FacilityPrecipitation() {
break;
}
if (partStr.length > 1 || operationValue!== '0' || fcltCpct !== '0' ||nameCd ||(fcltyName.trim() !== "" && fcltyName.trim() !== null)) {
searchFacilityPrecipitationList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined);
await searchFacilityPrecipitationList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined);
} else {
searchFacilityPrecipitationList(pageNo, pageCnt, searchYear);
await searchFacilityPrecipitationList(pageNo, pageCnt, searchYear);
}
}, [operationValue, fcltyName, searchFacilityPrecipitationList, searchYear, fcltCpct]);
@@ -190,9 +189,43 @@ export default function FacilityPrecipitation() {
}, [upHeadList1, upHeadList2, upHeadList3]);
// 엑셀다운로드
const excelDownload = useCallback((searchReturn: ReturnType) => {
searchFacilityPrecipitation(searchReturn, 1, 10000);
}, [searchFacilityPrecipitation]);
const excelDownload = useCallback(async (searchReturn: ReturnType) => {
if (isExcelDownClick) return;
// if (ref.current.partValue === 'A') { // 행정별
// if (!ref.current.sidoValue || ref.current.sidoValue === '0') {
// alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.');
// return;
// }
// } else if (ref.current.partValue === 'R') { // 유역별
// if (!ref.current.regionValue || ref.current.regionValue === '0') {
// alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.');
// return;
// }
// } else if (ref.current.partValue === 'G') { // 환경청별
// if (!ref.current.envPartValue || ref.current.envPartValue === '0') {
// alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.');
// return;
// }
// }
setIsExcelDownClick(true);
try {
await searchFacilityPrecipitation(searchReturn, 1, 10000);
const latestExcelList = store.getState().FacilityPrecipitation.excelList || [];
if (latestExcelList.length > 0) {
downloadExcelFormat({
data: latestExcelList as unknown as JSON[],
fileName: '시설별 강수현황',
sheetNameArray: ['시설별 강수현황'],
headerRange: 'A1:N1',
decimalArray0: [9, 11, 12, 13],
});
} else {
alert("조회된 결과가 없습니다.");
}
} finally {
setIsExcelDownClick(false);
}
}, [isExcelDownClick, searchFacilityPrecipitation, store]);
// 페이지 변경
const handlePageChange = useCallback((page: number) => {
@@ -227,6 +260,7 @@ export default function FacilityPrecipitation() {
btnClass={btnClass}
queryClick={chkVal}
excelClick={excelDownload}
excelDisabled={isExcelDownClick}
initClick={resetVal}
ref={ref} isAdmin={false}/>
<ListDiv>
@@ -254,4 +288,4 @@ export default function FacilityPrecipitation() {
)}
</MainDiv>
);
}
}
@@ -394,7 +394,7 @@ export default function WaterQualityModal({ fcltyName = '', facilityCd = '', sea
"유입BOD(mg/L)": CommaToNumber(item.tpInflWqltBod),
"유입TP(mg/L)": CommaToNumber(item.tpInflWqltTp),
"유출BOD(mg/L)": CommaToNumber(item.efflWqltBod),
"유출TP(mg/L)": CommaToNumber(item.efflWqltTn),
"유출TP(mg/L)": CommaToNumber(item.efflWqltTp),
});
});
}
@@ -447,7 +447,9 @@ export default function WaterQualityModal({ fcltyName = '', facilityCd = '', sea
</div>
<div className="card-header border-t mt-1">
<h3 className="text-2xl font-medium text-label">수질 TMS</h3>
<h3 className="text-2xl font-medium text-label text-right">({msrmtDate ? msrmtDate.substring(0, 4) + '-' + msrmtDate.substring(4, 6) + '-' + msrmtDate.substring(6, 8) : yesterDayStr})</h3>
<h3 className="text-2xl font-medium text-label text-right">
({msrmtDate ? msrmtDate.substring(0, 4) + '-' + msrmtDate.substring(4, 6) + '-' + msrmtDate.substring(6, 8) : yesterDayStr})
</h3>
</div>
<div className="overflow-y-auto h-[250px]">
<UiTable
@@ -6,13 +6,13 @@
"use client"
import { useFoStore } from "@/app/store";
import { StoreContext, useFoStore } from "@/app/store";
import type { TableUpperProps } from "@/app/store/common";
import type { FacilityWaterQltyProps } from "@/app/store/facilityWaterQlty";
import { ContentsDiv, downloadExcelFormat, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components";
import ListDiv from "@common/business_components/ui/list_div";
import LoadingOverlay from "@common/business_components/ui/loadingBar";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import WaterQualityModal from "./modal/page";
export default function ProcessFacilitySearch() {
@@ -21,6 +21,11 @@ export default function ProcessFacilitySearch() {
, initializeCommonData
, searchFacilityWaterQltyList
} = useFoStore((state) => state) || { FacilityWaterQltyList: { facilityWaterQltyList: [], facilityWaterQltyCount: 0 } };
const store = useContext(StoreContext);
if (!store) {
throw new Error("StoreContext is not available. Ensure StoreProvider is mounted.");
}
const {
isInit,
@@ -36,6 +41,11 @@ export default function ProcessFacilitySearch() {
btnClass
} = Common;
const defaultSearchYear =
selectSearchYear.find((item) => item.selected)?.val ||
selectSearchYear[0]?.val ||
"0";
const {
mainLabelArray,
upHeadList1,
@@ -43,13 +53,13 @@ export default function ProcessFacilitySearch() {
upHeadList3,
facilityWaterQltyList,
facilityWaterQltyCount,
excelList,
isLoading,
} = FacilityWaterQltyMain;
const mainList: FacilityWaterQltyProps[] = facilityWaterQltyList || [];
const [initTrigger, setInitTrigger] = useState<boolean>(false);
const [excelTrigger, setExcelTrigger] = useState<boolean>(false);
// 엑셀다운로드 트리거
const [isExcelDownClick, setIsExcelDownClick] = useState<boolean>(false);
// 그리드 관련
const [resetPage, setResetPage] = useState<boolean>(false);
const searchInputRef = useRef<HTMLDivElement & HTMLInputElement>(null);
@@ -57,7 +67,7 @@ export default function ProcessFacilitySearch() {
const [upheadList, setUpheadList] = useState<TableUpperProps[]>(upHeadList1);
const [fcltyName, setFcltyName] = useState<string>("");
const [operationValue, setOperationValue] = useState<string>("0");
const [searchYearValue, setSearchYearValue] = useState<string>(selectSearchYear[0]?.val || "0");
const [searchYearValue, setSearchYearValue] = useState<string>(defaultSearchYear);
const [fcltCpct, setFcltCpct] = useState<string>("0");
// 모달 상태 추가
@@ -66,7 +76,7 @@ export default function ProcessFacilitySearch() {
const [fcltyCd, setFcltyCd] = useState<string>("");
const [modalYear, setModalYear] = useState<string>("");
const searchFcltyQltyList = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt: number = 20) => {
const searchFcltyQltyList = useCallback(async ({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt: number = 20) => {
let nameCd, partStr;
switch (partValue) {
case 'R':
@@ -84,9 +94,9 @@ export default function ProcessFacilitySearch() {
break;
}
if (partStr === 'G') {
searchFacilityWaterQltyList(pageNo, pageCnt, searchYearValue);
await searchFacilityWaterQltyList(pageNo, pageCnt, searchYearValue);
} else {
searchFacilityWaterQltyList(pageNo, pageCnt, searchYearValue, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined);
await searchFacilityWaterQltyList(pageNo, pageCnt, searchYearValue, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined);
}
}, [operationValue, fcltyName, searchFacilityWaterQltyList, searchYearValue, fcltCpct]);
@@ -96,7 +106,7 @@ export default function ProcessFacilitySearch() {
// 상태값 설정
setFcltCpct(selectUpdownData[0]?.val || "0");
setOperationValue(selectOperationData[0]?.val || "0");
setSearchYearValue(selectSearchYear[0]?.val || "0");
setSearchYearValue(defaultSearchYear);
// SearchInputSelect 초기화
setFcltyName("");
if (searchInputRef.current) {
@@ -146,33 +156,51 @@ export default function ProcessFacilitySearch() {
}
}, [isInit, initializeCommonData, searchFacilityWaterQltyList, searchFcltyQltyList, initTrigger]);
useEffect(() => {
if (!excelTrigger) return;
if (excelList && excelList.length > 0) {
downloadExcelFormat(
{
data: excelList as unknown as JSON[],
fileName: '시설별 수질현황',
sheetNameArray: ['시설별 수질현황'],
headerRange: 'A1:T1',
decimalArray1: [10, 11, 12, 15, 16, 17],
decimalArray3: [13, 14, 18, 19]
}
);
}
}, [excelList, excelTrigger]);
const chkVal = useCallback((returntype: ReturnType) => {
// setWidthSize(returntype.partValue=='A' ?"w-[120%]":"w-[130%]")
setUpheadList(returntype.partValue == 'R' ? upHeadList2 : returntype.partValue == 'G' ? upHeadList3 : upHeadList1);
setResetPage(true);
}, [upHeadList1, upHeadList2, upHeadList3]);
const excelDownload = useCallback(() => {
setExcelTrigger(true);
searchFcltyQltyList(ref.current, 1, 10000);
}, [searchFcltyQltyList]);
const excelDownload = useCallback(async () => {
if (isExcelDownClick) return;
if (ref.current.partValue === 'A') { // 행정별
if (!ref.current.sidoValue || ref.current.sidoValue === '0') {
alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.');
return;
}
} else if (ref.current.partValue === 'R') { // 유역별
if (!ref.current.regionValue || ref.current.regionValue === '0') {
alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.');
return;
}
} else if (ref.current.partValue === 'G') { // 환경청별
if (!ref.current.envPartValue || ref.current.envPartValue === '0') {
alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.');
return;
}
}
setIsExcelDownClick(true);
try {
await searchFcltyQltyList(ref.current, 1, 10000);
const latestExcelList = store.getState().FacilityWaterQltyMain.excelList || [];
if (latestExcelList.length > 0) {
downloadExcelFormat(
{
data: latestExcelList as unknown as JSON[],
fileName: '시설별 수질현황',
sheetNameArray: ['시설별 수질현황'],
headerRange: 'A1:T1',
decimalArray0: [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
}
);
} else {
alert("조회된 결과가 없습니다.");
}
} finally {
setIsExcelDownClick(false);
}
}, [isExcelDownClick, searchFcltyQltyList, store]);
const handlePageChange = useCallback((page: number) => {
setResetPage(false);
@@ -201,6 +229,7 @@ export default function ProcessFacilitySearch() {
btnClass={btnClass}
queryClick={chkVal}
excelClick={excelDownload}
excelDisabled={isExcelDownClick}
initClick={resetVal}
ref={ref} isAdmin={false} initSidoValue={"11"} />
<ListDiv>
@@ -224,4 +253,4 @@ export default function ProcessFacilitySearch() {
)}
</MainDiv>
);
}
}
@@ -9,7 +9,7 @@ import FacilityTermModalHeader from "@/app/components/common/FacilityTermModalHe
import { useFoStore } from "@/app/store";
import type { FlowRateModalInterface, modalExcelType } from "@/app/store/flowRate";
import { dataFormSetting } from "@/app/store/flowRate";
import { downloadExcel, UITab, UiTable } from "@common/business_components/ui";
import { downloadExcelFormat, UITab, UiTable } from "@common/business_components/ui";
import { Button, TabsContent } from "@common/components/ui";
import { useCallback, useEffect, useRef, useState } from "react";
import type { ReturnType } from "../../../../components/common/FacilityTermModalHeader";
@@ -34,6 +34,7 @@ export default function FlowRateModal(props: { fcltyName: string; facilityCd: st
const { btnClass } = Common;
const [copyYearList, setCopyYearList] = useState<FlowRateModalInterface[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [activeTab, setActiveTab] = useState<string>("tab_term_year");
const prevExcelListRef = useRef<modalExcelType[] | null>(null);
const { modalFlowRateListYear, upHeadListFlowRateModalYear, } = FlowRateModal;
@@ -183,10 +184,6 @@ export default function FlowRateModal(props: { fcltyName: string; facilityCd: st
fetchData();
}, [props.facilityCd, props.searchYear, searchFlowRateModalList]);
if (isLoading) {
return <div>로딩 중...</div>;
}
const excelDownload = () => {
const excelDownList: modalExcelType[] = [];
if (copyYearList.length > 0) {
@@ -204,16 +201,54 @@ export default function FlowRateModal(props: { fcltyName: string; facilityCd: st
});
}
if (excelDownList && excelDownList.length > 0) {
downloadExcel({ data: JSON.parse(JSON.stringify(excelDownList)), fileName: props.searchYear + '년 ' + props.fcltyName + ' 유량현황', sheetNameArray: [props.fcltyName + ' 유량현황'], headerRange: 'A1:H1' });
downloadExcelFormat({
data: JSON.parse(JSON.stringify(excelDownList)),
fileName: props.searchYear + '년 ' + props.fcltyName + ' 유량현황',
sheetNameArray: [props.fcltyName + ' 유량현황'],
headerRange: 'A1:H1',
decimalArray0: [1, 2, 3, 4, 5, 6],
decimalArray1: [0],
});
} else if (prevExcelListRef.current && prevExcelListRef.current.length > 0) {
alert('다운받을 데이터가 없습니다.');
alert('조회된 결과가 없습니다.');
}
prevExcelListRef.current = excelDownList;
}
// const rainCellClassName = useCallback(
// (row: FlowRateModalInterface, headId: string, rowIndex: number) => {
// if (activeTab !== "tab_term_rain") return "";
// if (headId !== "infoRnDay") return "";
// if (rowIndex < 3) return "";
// const raw = row.infoRnDay;
// const value = typeof raw === "string" ? Number(raw.replace(/,/g, "").trim()) : Number(raw);
// if (!Number.isFinite(value)) return "";
// return value > 3 ? "bg-red-50 font-semibold" : "";
// },
// [activeTab],
// );
const rainRowClassName = useCallback(
(row: FlowRateModalInterface, rowIndex: number) => {
if (activeTab !== "tab_term_rain") return "";
if (rowIndex < 3) return "";
const raw = row.infoRnDay;
const value = typeof raw === "string" ? Number(raw.replace(/,/g, "").trim()) : Number(raw);
if (!Number.isFinite(value)) return "";
return value > 3 ? "bg-slate-50" : "";
},
[activeTab],
);
if (isLoading) {
return <div>로딩 중...</div>;
}
return (
<>
<UITab tabList={tabList} defaultValue="tab_term_year">
<UITab tabList={tabList} defaultValue="tab_term_year" onValueChange={setActiveTab}>
<Button className={btnClass} style={{ float: 'right' }} color="green" onClick={excelDownload}>
{"엑셀다운로드"}
</Button>
@@ -337,7 +372,8 @@ export default function FlowRateModal(props: { fcltyName: string; facilityCd: st
/>
<div className="h-[370px] overflow-y-auto overflow-x-hidden">
<UiTable headName="유량현황" tableData={copyYearList} headlist={upHeadListFlowRateModalYear} pageSize={0} total={0}
cellClick={undefined} onPageChange={() => { }} className="h-[370px]" boldArray={[0, 1, 2]} />
cellClick={undefined} onPageChange={() => { }} className="h-[370px]" boldArray={[0, 1, 2]}
rowClassName={rainRowClassName} />
</div>
<div className="mt-20">
<UnderComment commentArray={["미처리하수 추정량 산식 =[(강우시 일별유입유량*강우시 일별유입BOD)-(청천시 연평균유입유량*청천시 연평균유입BOD)]/강우시 일별유입BOD)", "강우량 3mm 이상만 날과 강우영향일(강우일+2일)의 날"]} />
@@ -346,4 +382,4 @@ export default function FlowRateModal(props: { fcltyName: string; facilityCd: st
</UITab>
</>
);
}
}
@@ -5,13 +5,13 @@
*/
"use client"
import { useFoStore } from "@/app/store";
import { StoreContext, useFoStore } from "@/app/store";
import type { TableUpperProps } from "@/app/store/common";
import type { ExcelType } from "@/app/store/flowRate";
// import type { ExcelType } from "@/app/store/flowRate";
import { ContentsDiv, downloadExcelFormat, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components";
import ListDiv from "@common/business_components/ui/list_div";
import LoadingOverlay from "@common/business_components/ui/loadingBar";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import FlowRateModal from "./modal/page";
export default function ProcessFacilitySearch() {
@@ -21,6 +21,11 @@ export default function ProcessFacilitySearch() {
,initializeCommonData
,searchFlowRateList
} = useFoStore((state) => state);
const store = useContext(StoreContext);
if (!store) {
throw new Error("StoreContext is not available. Ensure StoreProvider is mounted.");
}
const searchInputRef = useRef<HTMLDivElement & HTMLInputElement>(null);
@@ -38,6 +43,11 @@ export default function ProcessFacilitySearch() {
btnClass
} = Common;
const defaultSearchYear =
selectSearchYear.find((item) => item.selected)?.val ||
selectSearchYear[0]?.val ||
"0";
const {
flowRateSearchLabelArray
} = FlowRateSearch;
@@ -49,7 +59,6 @@ export default function ProcessFacilitySearch() {
upHeadList3,
flowRateList,
flowRateCount,
excelList,
isLoading,
} = FlowRateList;
@@ -57,20 +66,20 @@ export default function ProcessFacilitySearch() {
const [fcltCpct, setFcltCpct] = useState<string>("0");
const [fcltyName, setFcltyName] = useState<string>("");
const [operationValue, setOperationValue] = useState<string>("0");
const [searchYearValue, setSearchYearValue] = useState<string>(selectSearchYear[0]?.val || "0");
const [searchYearValue, setSearchYearValue] = useState<string>(defaultSearchYear);
// const [widthSize, setWidthSize] = useState<string>("w-[120%]");
const [upheadList, setUpheadList] = useState<TableUpperProps[]>(upHeadList1);
const [resetPage, setResetPage] = useState<boolean>(false);
const prevExcelListRef = useRef<ExcelType[] | null>(null);
// const prevExcelListRef = useRef<ExcelType[] | null>(null);
// 모달 상태 추가
const [selectedRow, setSelectedRow] = useState<number | null>(null);
const [fcltyNm, setFcltyNm] = useState<string>("");
const [fcltyCd, setFcltyCd] = useState<string>("");
const [excelTrigger, setExcelTrigger] = useState<boolean>(false);
const [isExcelDownClick, setIsExcelDownClick] = useState<boolean>(false);
const [initTrigger, setInitTrigger] = useState<boolean>(false);
const searchFlowRate = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => {
const searchFlowRate = useCallback(async ({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => {
let nameCd, partStr;
switch (partValue) {
@@ -89,9 +98,9 @@ export default function ProcessFacilitySearch() {
break;
}
if(partStr==='G'){
searchFlowRateList(pageNo, pageCnt, searchYearValue);
await searchFlowRateList(pageNo, pageCnt, searchYearValue);
}else{
searchFlowRateList(pageNo, pageCnt, searchYearValue, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined);
await searchFlowRateList(pageNo, pageCnt, searchYearValue, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined);
}
}, [searchFlowRateList, searchYearValue, fcltCpct, operationValue, fcltyName]);
@@ -99,7 +108,7 @@ export default function ProcessFacilitySearch() {
// setWidthSize("w-[120%]");
setUpheadList(upHeadList1)
// 상태값 설정
setSearchYearValue(selectSearchYear[0]?.val || "0");
setSearchYearValue(defaultSearchYear);
setFcltCpct(selectUpdownData[0]?.val || "0");
setOperationValue(selectOperationData[0]?.val || "0");
// SearchInputSelect 초기화
@@ -146,17 +155,6 @@ export default function ProcessFacilitySearch() {
}
}, [isInit, initializeCommonData, searchFlowRateList, selectSearchYear, searchFlowRate, initTrigger]);
useEffect(() => {
if(!excelTrigger) return;
if (excelList && excelList.length > 0) {
downloadExcelFormat({ data: JSON.parse(JSON.stringify(excelList)), fileName: '시설별 유량현황', sheetNameArray: ['시설별 유량현황'], headerRange: 'A1:O1', decimalArray0: [9,10,11,12,13,14] });
} else if (prevExcelListRef.current && prevExcelListRef.current.length > 0) {
alert('다운받을 데이터가 없습니다.');
}
prevExcelListRef.current = excelList;
}, [excelList, excelTrigger]);
const ref = useRef<ReturnType>({
partValue: '',
sidoValue: '11',
@@ -175,10 +173,37 @@ export default function ProcessFacilitySearch() {
setResetPage(true);
}, [upHeadList1, upHeadList2, upHeadList3]);
const excelDownload = useCallback(() => {
searchFlowRate(ref.current, 1, 10000);
setExcelTrigger(true);
}, [searchFlowRate]);
const excelDownload = useCallback(async () => {
if (isExcelDownClick) return;
if (ref.current.partValue === 'A') { // 행정별
if (!ref.current.sidoValue || ref.current.sidoValue === '0') {
alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.');
return;
}
} else if (ref.current.partValue === 'R') { // 유역별
if (!ref.current.regionValue || ref.current.regionValue === '0') {
alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.');
return;
}
} else if (ref.current.partValue === 'G') { // 환경청별
if (!ref.current.envPartValue || ref.current.envPartValue === '0') {
alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.');
return;
}
}
setIsExcelDownClick(true);
try {
await searchFlowRate(ref.current, 1, 10000);
const latestExcelList = store.getState().FlowRateList.excelList || [];
if (latestExcelList.length > 0) {
downloadExcelFormat({ data: JSON.parse(JSON.stringify(latestExcelList)), fileName: '시설별 유량현황', sheetNameArray: ['시설별 유량현황'], headerRange: 'A1:O1', decimalArray0: [9,10,11,12,13,14] });
} else {
alert('조회된 결과가 없습니다.');
}
} finally {
setIsExcelDownClick(false);
}
}, [isExcelDownClick, searchFlowRate, store]);
const total = flowRateCount;
return (
@@ -203,6 +228,7 @@ export default function ProcessFacilitySearch() {
queryClick={chkVal}
excelClick={excelDownload}
initClick={resetVal}
excelDisabled={isExcelDownClick}
ref={ref} isAdmin={false} initSidoValue={'11'}/>
<ListDiv>
<LoadingOverlay isLoading={isLoading} />
@@ -5,11 +5,11 @@
*/
"use client"
import { useFoStore } from "@/app/store";
import { StoreContext, useFoStore } from "@/app/store";
import type { TableUpperProps } from "@/app/store/common";
import { downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiTable, type ReturnType } from "@common/business_components";
import { downloadExcelFormat, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiTable, type ReturnType } from "@common/business_components";
import ListDiv from "@common/business_components/ui/list_div";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import LoadingOverlay from "@common/business_components/ui/loadingBar";
import UnderComment from "@/app/components/common/UnderComment";
@@ -26,6 +26,11 @@ export default function PlanSewage() {
, initializeCommonData
, searchPlanSewageList
} = useFoStore((state) => state) || { processAreaList: { processAreaList: [] } };
const store = useContext(StoreContext);
if (!store) {
throw new Error("StoreContext is not available. Ensure StoreProvider is mounted.");
}
const {
isInit,
@@ -41,6 +46,11 @@ export default function PlanSewage() {
btnClass
} = Common;
const defaultSearchYear =
selectSearchYear.find((item) => item.selected)?.val ||
selectSearchYear[0]?.val ||
"0";
const {
labelArray,
} = PlanSewageSearch;
@@ -51,7 +61,6 @@ export default function PlanSewage() {
upHeadList3,
planSewageList,
planSewageCount,
excelList,
isLoading,
} = PlanSewage;
@@ -85,11 +94,12 @@ export default function PlanSewage() {
// 시설상태
const [operationValue, setOperationValue] = useState<string>("0");
// 조회기간
const [searchYear, setSearchYear] = useState<string>(selectSearchYear[0]?.val || "0");
const [searchYear, setSearchYear] = useState<string>(defaultSearchYear);
const [initTrigger, setInitTrigger] = useState<boolean>(false);
// 엑셀다운로드 트리거
const [isExcelDownClick, setIsExcelDownClick] = useState<boolean>(false);
const [excelTrigger, setExcelTrigger] = useState<boolean>(false);
/**
* 페이지 함수
*/
@@ -100,7 +110,7 @@ export default function PlanSewage() {
// 상태값 설정
setFcltCpct(selectUpdownData[0]?.val || "0");
setOperationValue(selectOperationData[0]?.val || "0");
setSearchYear(selectSearchYear[0]?.val || "0");
setSearchYear(defaultSearchYear);
// SearchInputSelect 초기화
setFcltyName("");
if (searchInputRef.current) {
@@ -113,24 +123,11 @@ export default function PlanSewage() {
searchPlanSewage(ref.current, 1, 10);
}
useEffect(() => {
if (!excelTrigger) return;
if (excelList && excelList.length > 0) {
downloadExcel({
data: excelList as unknown as JSON[],
fileName: '계획하수량 산정지표 현황',
sheetNameArray: ['계획하수량 산정지표 현황'],
headerRange: 'A1:M1'
});
}
}, [excelList, excelTrigger]);
/**
* 콜백함수
*/
// 조회
const searchPlanSewage = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt: number = 20) => {
const searchPlanSewage = useCallback(async ({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt: number = 20) => {
let nameCd, partStr
switch (partValue) {
case 'R':
@@ -150,10 +147,10 @@ export default function PlanSewage() {
if (partStr === 'G') {
partValue = partValue === 'G' ? 'A' : partValue;
searchPlanSewageList(pageNo, pageCnt, searchYear, partValue);
await searchPlanSewageList(pageNo, pageCnt, searchYear, partValue);
}
else {
searchPlanSewageList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined);
await searchPlanSewageList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined);
}
}, [operationValue, fcltyName, searchPlanSewageList, searchYear, fcltCpct]);
@@ -168,7 +165,7 @@ export default function PlanSewage() {
setInitTrigger(true);
}
}
}, [isInit, initializeCommonData, searchPlanSewageList, selectSearchYear, initTrigger, searchPlanSewage]);
}, [isInit, initializeCommonData, initTrigger, searchPlanSewage]);
const chkVal = useCallback((returntype: ReturnType) => {
// setWidthSize(returntype.partValue=='A' ?"w-[120%]":"w-[130%]")
@@ -177,10 +174,66 @@ export default function PlanSewage() {
}, [upHeadList1, upHeadList2, upHeadList3]);
// 엑셀다운로드
const excelDownload = useCallback((searchReturn: ReturnType) => {
searchPlanSewage(searchReturn, 1, 10000);
setExcelTrigger(true);
}, [searchPlanSewage]);
const excelDownload = useCallback(async (searchReturn: ReturnType) => {
if (isExcelDownClick) return;
if (ref.current.partValue === 'A') { // 행정별
if (!ref.current.sidoValue || ref.current.sidoValue === '0') {
alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.');
return;
}
} else if (ref.current.partValue === 'R') { // 유역별
if (!ref.current.regionValue || ref.current.regionValue === '0') {
alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.');
return;
}
} else if (ref.current.partValue === 'G') { // 환경청별
if (!ref.current.envPartValue || ref.current.envPartValue === '0') {
alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.');
return;
}
}
setIsExcelDownClick(true);
try {
await searchPlanSewage(searchReturn, 1, 10000);
const latestExcelList = store.getState().PlanSewage.excelList || [];
if (latestExcelList.length > 0) {
const toNumber = (value: unknown) => {
if (value === null || value === undefined || value === '') return '';
const parsed = typeof value === "number" ? value : Number(String(value).replace(/,/g, ""));
return Number.isFinite(parsed) ? parsed : '';
};
const numericKeys = [
"시설용량(m³/일)",
"여유용량(m³/일)",
"변동부하율(%)",
"분류식화율(%)",
];
const excelData = latestExcelList.map((row) => {
const nextRow = { ...row } as Record<string, unknown>;
numericKeys.forEach((key) => {
if (key in nextRow) {
nextRow[key] = toNumber(nextRow[key]);
}
});
return nextRow;
});
downloadExcelFormat({
data: excelData as unknown as JSON[],
fileName: '계획하수량 산정지표 현황',
sheetNameArray: ['계획하수량 산정지표 현황'],
headerRange: 'A1:M1',
decimalArray0: [9, 10, 11, 12]
});
} else {
alert("조회된 결과가 없습니다.");
}
} finally {
setIsExcelDownClick(false);
}
}, [isExcelDownClick, searchPlanSewage, store]);
// 페이지 변경
const handlePageChange = useCallback((page: number) => {
@@ -211,6 +264,7 @@ export default function PlanSewage() {
btnClass={btnClass}
queryClick={chkVal}
excelClick={excelDownload}
excelDisabled={isExcelDownClick}
initClick={resetVal}
ref={ref} isAdmin={false} initSidoValue={"11"} />
<ListDiv>
@@ -224,4 +278,4 @@ export default function PlanSewage() {
</ListDiv>
</MainDiv>
);
}
}
@@ -5,11 +5,11 @@
*/
"use client"
import { useFoStore } from "@/app/store";
import { StoreContext, useFoStore } from "@/app/store";
import { useShallow } from 'zustand/react/shallow';
import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components";
import { ContentsDiv, downloadExcelFormat, MainDiv, SearchBaseForm, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components";
import ListDiv from "@common/business_components/ui/list_div";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import ProcessFacilityInfoModal from "./modal/page";
import LoadingOverlay from "@common/business_components/ui/loadingBar";
@@ -29,6 +29,11 @@ export default function ProcessArea() {
searchProcessAreaList: state.searchProcessAreaList,
}))
);
const store = useContext(StoreContext);
if (!store) {
throw new Error("StoreContext is not available. Ensure StoreProvider is mounted.");
}
const {
isInit,
@@ -42,6 +47,11 @@ export default function ProcessArea() {
btnClass,
} = Common;
const defaultSearchYear =
selectSearchYear.find((item) => item.selected)?.val ||
selectSearchYear[0]?.val ||
"0";
const {
processFacilityLabelArray,
} = ProcessAreaSearch;
@@ -50,7 +60,6 @@ export default function ProcessArea() {
upHeadList1,
processAreaList,
processAreaCount,
excelList,
isLoading,
} = ProcessArea;
@@ -64,7 +73,9 @@ export default function ProcessArea() {
const [fcltyCd, setFcltyCd] = useState<string>("");
const [resetPage, setResetPage] = useState<boolean>(false);
const [searchYearValue, setSearchYearValue] = useState<string>(selectSearchYear[0]?.val || "0");
const [searchYearValue, setSearchYearValue] = useState<string>(defaultSearchYear);
const [isExcelDownClick, setIsExcelDownClick] = useState<boolean>(false);
const ref = useRef<ReturnType>({
partValue: '',
@@ -78,8 +89,8 @@ export default function ProcessArea() {
sigunText: '',
});
const searchProcessArea = useCallback(({ sidoText, sigunText }: ReturnType, pageNo: number, pageCnt: number = 20) => {
searchProcessAreaList(pageNo, pageCnt, sidoText, sigunText, searchYearValue);
const searchProcessArea = useCallback(async ({ sidoText, sigunText }: ReturnType, pageNo: number, pageCnt: number = 20) => {
await searchProcessAreaList(pageNo, pageCnt, sidoText, sigunText, searchYearValue);
}, [searchProcessAreaList, searchYearValue]);
// 조회
@@ -95,8 +106,8 @@ export default function ProcessArea() {
input.value = "";
}
}
setSearchYearValue(selectSearchYear[0]?.val || "0");
searchProcessAreaList(1, 10, '', '', selectSearchYear[0]?.val || "0");
setSearchYearValue(defaultSearchYear);
searchProcessAreaList(1, 10, '', '', defaultSearchYear);
}
// 셀 클릭 시 모달 표시
@@ -116,24 +127,49 @@ export default function ProcessArea() {
useEffect(() => {
if (!isInit) {
initializeCommonData();
searchProcessAreaList(1, 10, '', '', selectSearchYear[0]?.val || "0");
searchProcessAreaList(1, 10, '', '', defaultSearchYear);
}
}, [isInit, initializeCommonData, searchProcessAreaList]);
}, [isInit, initializeCommonData, searchProcessAreaList, defaultSearchYear]);
useEffect(() => {
if (excelList && excelList.length > 0) {
downloadExcel({
data: excelList as unknown as JSON[],
fileName: '처리구역정보',
sheetNameArray: ['처리구역정보'],
headerRange: 'A1:W1'
});
const excelDownload = useCallback(async () => {
if (isExcelDownClick) return;
// if (ref.current.partValue === 'A') { // 행정별
// if (!ref.current.sidoValue || ref.current.sidoValue === '0') {
// alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.');
// return;
// }
// } else if (ref.current.partValue === 'R') { // 유역별
// if (!ref.current.regionValue || ref.current.regionValue === '0') {
// alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.');
// return;
// }
// } else if (ref.current.partValue === 'G') { // 환경청별
// if (!ref.current.envPartValue || ref.current.envPartValue === '0') {
// alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.');
// return;
// }
// }
setIsExcelDownClick(true);
try {
await searchProcessArea(ref.current, 1, 10000);
const latestExcelList = store.getState().ProcessArea.excelList || [];
if (latestExcelList.length > 0) {
downloadExcelFormat({
data: latestExcelList as unknown as JSON[],
fileName: '처리구역정보',
sheetNameArray: ['처리구역정보'],
headerRange: 'A1:W1',
decimalArray0: [3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15],
decimalArray2: [16, 17, 18, 19, 20, 21],
});
} else {
alert('조회된 결과가 없습니다.');
}
} finally {
setIsExcelDownClick(false);
}
}, [excelList]);
const excelDownload = useCallback(() => {
searchProcessArea(ref.current, 1, 10000);
}, [searchProcessArea]);
}, [isExcelDownClick, searchProcessArea, store]);
const handlePageChange = useCallback((pageNo: number) => {
setResetPage(false);
@@ -159,6 +195,7 @@ export default function ProcessArea() {
btnClass={btnClass}
queryClick={chkVal}
excelClick={excelDownload}
excelDisabled={isExcelDownClick}
initClick={resetVal}
ref={ref}
isAdmin={true}
@@ -183,4 +220,4 @@ export default function ProcessArea() {
)}
</MainDiv>
);
}
}
@@ -5,6 +5,7 @@
*/
"use client"
import { useCallback, useRef } from "react";
import { useFoStore } from "@/app/store";
import type { TableUpperProps } from "@/app/store/dashboard";
import { downloadExcelFormat, filePickedBlobArray, UITab, UiTable } from "@common/business_components/ui";
@@ -121,6 +122,21 @@ export default function ProcessFacilityInfoModal(props: ModalType) {
const upHeadListPlanInfoChange = headMakeList(upHeadListPlanInfo, 2);
const upHeadListFcltyHistInfoChange = headMakeList(upHeadListFcltyHistInfo, 3);
const upHeadListFcltyWaterQltyInfoChange = headMakeList(upHeadListFcltyWaterQltyInfo, 4);
const historyTopScrollRef = useRef<HTMLDivElement>(null);
const historyBottomScrollRef = useRef<HTMLDivElement>(null);
const syncLockRef = useRef(false);
const syncScroll = useCallback((source: HTMLDivElement | null, target: HTMLDivElement | null) => {
if (!source || !target || syncLockRef.current) return;
syncLockRef.current = true;
const nextTop = source.scrollTop;
if (target.scrollTop !== nextTop) {
target.scrollTop = nextTop;
}
requestAnimationFrame(() => {
syncLockRef.current = false;
});
}, []);
const excelDownloadPlan = () => {
const excelDownList: any[] = [];
@@ -137,11 +153,19 @@ export default function ProcessFacilityInfoModal(props: ModalType) {
"시운전완료일": item.trlrnCmplDt,
"가동개시일": item.rnDt,
"시설용량(m³/일)": item.fctCpc&&item.fctCpc.toString().trim()!=''?Number(item.fctCpc.toString().replaceAll(',','')):'',
"공법구분": item.trtSrsHanNm,
"공법계열": item.trtMocHanNm,
"방법": item.trtFrfHanNm,
});
});
}
if (excelDownList && excelDownList.length > 0) {
downloadExcelFormat({ data: JSON.parse(JSON.stringify(excelDownList)), fileName: props.fcltyName + ' 시설이력관리', sheetNameArray: [props.fcltyName + ' 시설이력관리'], headerRange: 'A1:I1' });
downloadExcelFormat({
data: JSON.parse(JSON.stringify(excelDownList)),
fileName: props.fcltyName + ' 시설이력관리',
sheetNameArray: [props.fcltyName + ' 시설이력관리'],
decimalArray0: [8],
headerRange: 'A1:L1' });
} else if (gridListFcltyHistInfo.length == 0) {
alert('다운받을 데이터가 없습니다.');
}
@@ -182,7 +206,7 @@ export default function ProcessFacilityInfoModal(props: ModalType) {
const sheetParameter = [
{
data: excelData,
headerNum: 2,
headerNum: 3,
headerRange: 'A1:S2',
sheetName: '시설 수질정보',
startCol: 'A'
@@ -218,7 +242,7 @@ export default function ProcessFacilityInfoModal(props: ModalType) {
<h3 className="text-2xl font-medium text-label">시설 기본정보</h3>
</div>
<div className="card !m-[1.2rem] max-h-[40rem] overflow-auto !p-0r">
<Table>
<Table variant="secondary">
<thead>
<TableRow className="">
<TableHead className="w-1/6">{'시설명'}</TableHead>
@@ -262,7 +286,11 @@ export default function ProcessFacilityInfoModal(props: ModalType) {
<TabsContent value="history_info">
<ListDiv>
<div className="h-[250px] w-full overflow-y-auto overflow-x-hidden">
<div
className="h-[250px] w-full overflow-y-auto overflow-x-hidden"
ref={historyTopScrollRef}
onScroll={() => syncScroll(historyTopScrollRef.current, historyBottomScrollRef.current)}
>
<Button className={btnClass} style={{float: 'right'}} color="green" onClick={excelDownloadPlan}>
{"엑셀다운로드"}
</Button>
@@ -274,7 +302,11 @@ export default function ProcessFacilityInfoModal(props: ModalType) {
</div>
</ListDiv>
<ListDiv>
<div className="h-[250px] w-full overflow-y-auto overflow-x-hidden">
<div
className="h-[250px] w-full overflow-y-auto overflow-x-hidden"
ref={historyBottomScrollRef}
onScroll={() => syncScroll(historyBottomScrollRef.current, historyTopScrollRef.current)}
>
<Button className={btnClass} style={{float: 'right'}} color="green" onClick={excelDownloadQlty}>
{"엑셀다운로드"}
</Button>
@@ -292,4 +324,4 @@ export default function ProcessFacilityInfoModal(props: ModalType) {
</div>
</>
);
}
}
@@ -6,11 +6,11 @@
"use client"
import UnderComment from "@/app/components/common/UnderComment";
import { useFoStore } from "@/app/store";
import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components";
import { StoreContext, useFoStore } from "@/app/store";
import { ContentsDiv, downloadExcelFormat, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components";
import ListDiv from "@common/business_components/ui/list_div";
import LoadingOverlay from "@common/business_components/ui/loadingBar";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import ProcessFacilityInfoModal from "./modal/page";
export default function ProcessFacilitySearch() {
@@ -25,6 +25,11 @@ export default function ProcessFacilitySearch() {
//시설데이터 수질정보 조회
,searchProcessFacilityWaterQlty
} = useFoStore((state) => state) || { ProcessFacilityList: { processFacilityList: [], processFacilityCount: 0 }};
const store = useContext(StoreContext);
if (!store) {
throw new Error("StoreContext is not available. Ensure StoreProvider is mounted.");
}
const {
isInit,
@@ -69,7 +74,6 @@ export default function ProcessFacilitySearch() {
upHeadList1,
upHeadList2,
upHeadList3,
excelList,
isLoading,
} = ProcessFacilityList;
@@ -89,6 +93,7 @@ export default function ProcessFacilitySearch() {
const [partValue, setPartValue] = useState<string>("A");
// const [widthSize, setWidthSize] = useState<string>("w-[120%]");
const [upHeadList, setUpheadList] = useState(upHeadList1);
const [isExcelDownClick, setIsExcelDownClick] = useState<boolean>(false);
/**
* 모달 상태 변수
@@ -149,13 +154,7 @@ export default function ProcessFacilitySearch() {
}
}, [partValue, upHeadList1, upHeadList2, upHeadList3]);
useEffect(() => {
if (excelList && excelList.length > 0) {
downloadExcel({ data: excelList as unknown as JSON[], fileName: '처리시설 시설정보', sheetNameArray: ['처리시설 시설정보'], headerRange: 'A1:P1' });
}
}, [excelList]);
const searchFcltyList = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=10) => {
const searchFcltyList = useCallback(async ({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=10) => {
let nameCd, partStr
switch (partValue) {
case 'R':
@@ -172,7 +171,7 @@ export default function ProcessFacilitySearch() {
partStr = envPartValue !== '0' ? envPartValue:partValue;
break;
}
searchProcessFacility(pageNo, pageCnt, partStr, nameCd || '0', fcltCpct, operationValue, facilityPartValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined);
await searchProcessFacility(pageNo, pageCnt, partStr, nameCd || '0', fcltCpct, operationValue, facilityPartValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined);
if(isSearch) {
setIsSearch(false);
}
@@ -235,9 +234,44 @@ export default function ProcessFacilitySearch() {
}
}
const excelDownload = useCallback(() => {
searchFcltyList(ref.current, 1, 10000);
}, [searchFcltyList]);
const excelDownload = useCallback(async () => {
if (isExcelDownClick) return;
// if (ref.current.partValue === 'A') { // 행정별
// if (!ref.current.sidoValue || ref.current.sidoValue === '0') {
// alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.');
// return;
// }
// } else if (ref.current.partValue === 'R') { // 유역별
// if (!ref.current.regionValue || ref.current.regionValue === '0') {
// alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.');
// return;
// }
// } else if (ref.current.partValue === 'G') { // 환경청별
// if (!ref.current.envPartValue || ref.current.envPartValue === '0') {
// alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.');
// return;
// }
// }
setIsExcelDownClick(true);
try {
await searchFcltyList(ref.current, 1, 10000);
const latestExcelList = store.getState().ProcessFacilityList.excelList || [];
if (latestExcelList.length > 0) {
downloadExcelFormat({
data: latestExcelList as unknown as JSON[],
fileName: '처리시설 시설정보',
sheetNameArray: ['처리시설 시설정보'],
headerRange: 'A1:P1',
decimalArray0: [9, 10, 11],
});
} else {
alert('조회된 결과가 없습니다.');
}
} finally {
setIsExcelDownClick(false);
}
}, [isExcelDownClick, searchFcltyList, store]);
// 모달 닫기
const closeModal = () => {
@@ -266,6 +300,7 @@ export default function ProcessFacilitySearch() {
btnClass={btnClass}
queryClick={chkVal}
excelClick={excelDownload}
excelDisabled={isExcelDownClick}
initClick={resetVal}
ref={ref} isAdmin={false} />
<ListDiv>
@@ -292,11 +327,11 @@ export default function ProcessFacilitySearch() {
{selectedRow !== null && (
<UiModal
body={<ProcessFacilityInfoModal fcltyName={modalFcltyNm} facilityCd={modalFcltyCd} sido={modalSido} gugun={modalGugun} daddr={modalAddr} fctCpc={modalFctCpc} trtFrfNm={modalTrtFrfNm} rnDt={modalRnDt} cmplDt={modalCmplDt} trstOprEtpNm={modalTrstOprEtpNm} bscFctClsNm={modalBscFctClsNm} mnstr={modalMnstr} efflWtrrgn={modalEfflWtrrgn} lgbsn={modalLgbsn} efflWqltStndrdNm={modalEfflWqltStndrdNm} rgnNm={modalRgnNm} rvbsn={modalRvbsn}></ProcessFacilityInfoModal>}
bodyClass="sm:max-w-[70vw] h-[100dvh] max-h-[800px]"
bodyClass="sm:max-w-[100vw] w-[1300px] h-[100dvh] max-h-[800px]"
open={selectedRow !== null}
onOpenChange={(open) => !open && closeModal()}
/>
)}
</MainDiv>
);
}
}
+87 -28
View File
@@ -5,11 +5,11 @@
*/
"use client"
import { useFoStore } from "@/app/store";
import { StoreContext, useFoStore } from "@/app/store";
import type { TableUpperProps } from "@/app/store/common";
import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiTable, type ReturnType } from "@common/business_components";
import { ContentsDiv, downloadExcelFormat, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiTable, type ReturnType } from "@common/business_components";
import ListDiv from "@common/business_components/ui/list_div";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import LoadingOverlay from "@common/business_components/ui/loadingBar";
import UnderComment from "@/app/components/common/UnderComment";
@@ -26,6 +26,11 @@ export default function Rdi() {
,RdiSearch
,searchRdiList
} = useFoStore((state) => state) || { processAreaList: { processAreaList: [] }};
const store = useContext(StoreContext);
if (!store) {
throw new Error("StoreContext is not available. Ensure StoreProvider is mounted.");
}
const {
isInit,
@@ -41,6 +46,11 @@ export default function Rdi() {
btnClass
} = Common;
const defaultSearchYear =
selectSearchYear.find((item) => item.selected)?.val ||
selectSearchYear[0]?.val ||
"0";
const {
labelArray,
} = RdiSearch;
@@ -51,7 +61,6 @@ export default function Rdi() {
upHeadList3,
rdiList,
rdiCount,
excelList,
isLoading,
} = Rdi;
@@ -83,9 +92,10 @@ export default function Rdi() {
// 시설상태
const [operationValue, setOperationValue] = useState<string>("0");
// 조회기간
const [searchYear, setSearchYear] = useState<string>(selectSearchYear[0]?.val || "0");
const [searchYear, setSearchYear] = useState<string>(defaultSearchYear);
// 엑셀 다운로드 트리거
const [isInitExcelDownload, setIsInitExcelDownload] = useState<boolean>(false);
const [isExcelDownClick, setIsExcelDownClick] = useState<boolean>(false);
const [initTrigger, setInitTrigger] = useState<boolean>(false);
/**
@@ -99,7 +109,7 @@ export default function Rdi() {
// 상태값 설정
setFcltCpct(selectUpdownData[0]?.val || "0");
setOperationValue(selectOperationData[0]?.val || "0");
setSearchYear(selectSearchYear[0]?.val || "0");
setSearchYear(defaultSearchYear);
// SearchInputSelect 초기화
setFcltyName("");
if (searchInputRef.current) {
@@ -112,23 +122,11 @@ export default function Rdi() {
searchRdi(ref.current, 1, 10);
}
useEffect(() => {
if(!isInitExcelDownload) return ;
if (excelList && excelList.length > 0) {
downloadExcel({
data: excelList as unknown as JSON[],
fileName: 'RDII추정',
sheetNameArray: ['RDII추정'],
headerRange: 'A1:R1'
});
}
}, [excelList, isInitExcelDownload]);
/**
* 콜백함수
*/
// 조회
const searchRdi = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => {
const searchRdi = useCallback(async ({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => {
let nameCd, partStr
switch (partValue) {
case 'R':
@@ -147,10 +145,9 @@ export default function Rdi() {
}
if(partStr === 'G'){
searchRdiList(pageNo, pageCnt, searchYear);
await searchRdiList(pageNo, pageCnt, searchYear);
}else{
searchRdiList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined);
await searchRdiList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined);
}
}, [operationValue, fcltyName, searchRdiList, searchYear, fcltCpct]);
@@ -174,10 +171,71 @@ export default function Rdi() {
}, [upHeadList1, upHeadList2, upHeadList3]);
// 엑셀다운로드
const excelDownload = useCallback(() => {
searchRdi(ref.current, 1, 5000);
setIsInitExcelDownload(true);
}, [searchRdi]);
const excelDownload = useCallback(async () => {
if (isExcelDownClick) return;
// if (ref.current.partValue === 'A') { // 행정별
// if (!ref.current.sidoValue || ref.current.sidoValue === '0') {
// alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.');
// return;
// }
// } else if (ref.current.partValue === 'R') { // 유역별
// if (!ref.current.regionValue || ref.current.regionValue === '0') {
// alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.');
// return;
// }
// } else if (ref.current.partValue === 'G') { // 환경청별
// if (!ref.current.envPartValue || ref.current.envPartValue === '0') {
// alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.');
// return;
// }
// }
setIsExcelDownClick(true);
try {
await searchRdi(ref.current, 1, 5000);
const latestExcelList = store.getState().Rdi.excelList || [];
if (latestExcelList.length > 0) {
const toNumber = (value: unknown) => {
if (value === null || value === undefined || value === '') return '';
const parsed = typeof value === "number" ? value : Number(String(value).replace(/,/g, ""));
return Number.isFinite(parsed) ? parsed : '';
};
const numericKeys = [
"시설용량(m³/일)",
"청천시유입하수량(m³/일)",
"하절기강우일최저유입BOD(mg/L)",
"하절기최저유입BOD유입유량(m³/일)",
"동절기청천시유입BOD(mg/L)",
"RDII추정",
"RDII추정시일강수량(mm)",
"강우시총추정유입량(m³/일)",
];
const excelData = latestExcelList.map((row) => {
const nextRow = { ...row } as Record<string, unknown>;
numericKeys.forEach((key) => {
if (key in nextRow) {
nextRow[key] = toNumber(nextRow[key]);
}
});
return nextRow;
});
downloadExcelFormat({
data: excelData as unknown as JSON[],
fileName: 'RDII추정',
sheetNameArray: ['RDII추정'],
headerRange: 'A1:R1',
decimalArray0: [9, 10, 11, 12, 13, 14, 15, 16],
});
} else {
alert("조회된 결과가 없습니다.");
}
} finally {
setIsExcelDownClick(false);
}
}, [isExcelDownClick, searchRdi, store]);
// 페이지 변경
const handlePageChange = useCallback((page: number) => {
@@ -209,6 +267,7 @@ export default function Rdi() {
btnClass={btnClass}
queryClick={chkVal}
excelClick={excelDownload}
excelDisabled={isExcelDownClick}
initClick={resetVal}
ref={ref} isAdmin={false} initSidoValue={"11"}/>
<ListDiv>
@@ -224,4 +283,4 @@ export default function Rdi() {
</ContentsDiv>
</MainDiv>
);
}
}
@@ -6,13 +6,13 @@
*/
"use client"
import { useFoStore } from "@/app/store";
import { StoreContext, useFoStore } from "@/app/store";
import { type TableUpperProps } from "@/app/store/common";
import type { SewageOccurProps } from "@/app/store/sewageOccur";
import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components";
import { ContentsDiv, downloadExcelFormat, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components";
import ListDiv from "@common/business_components/ui/list_div";
import LoadingOverlay from "@common/business_components/ui/loadingBar";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import SewageOccurModal from "./modal/page";
export default function PlanSewage() {
@@ -29,6 +29,11 @@ export default function PlanSewage() {
searchSewageOccurList: () => {},
calculateModalDataForAllFacilities: async () => []
};
const store = useContext(StoreContext);
if (!store) {
throw new Error("StoreContext is not available. Ensure StoreProvider is mounted.");
}
const {
isInit,
@@ -44,6 +49,11 @@ export default function PlanSewage() {
btnClass
} = Common;
const defaultSearchYear =
selectSearchYear.find((item) => item.selected)?.val ||
selectSearchYear[0]?.val ||
"0";
const {
labelArray,
} = SewageOccurSearch;
@@ -54,7 +64,6 @@ export default function PlanSewage() {
upHeadList3,
sewageOccurList,
sewageOccurCount,
excelList,
isLoading,
} = SewageOccur;
@@ -93,10 +102,9 @@ export default function PlanSewage() {
// 시설상태
const [operationValue, setOperationValue] = useState<string>("0");
// 조회기간
const [searchYear, setSearchYear] = useState<string>(selectSearchYear[0]?.val || "0");
// 엑셀다운로드 트리거
const [isInitExcelDownload, setIsInitExcelDownload] = useState<boolean>(false);
const [searchYear, setSearchYear] = useState<string>(defaultSearchYear);
// 엑셀 조회 중 상태
const [isExcelDownClick, setIsExcelDownClick] = useState<boolean>(false);
/**
* 페이지 함수
*/
@@ -107,7 +115,7 @@ export default function PlanSewage() {
// 상태값 설정
setFcltCpct(selectUpdownData[0]?.val || "0");
setOperationValue(selectOperationData[0]?.val || "0");
setSearchYear(selectSearchYear[0]?.val || "0");
setSearchYear(defaultSearchYear);
// SearchInputSelect 초기화
setFcltyName("");
if (searchInputRef.current) {
@@ -116,7 +124,7 @@ export default function PlanSewage() {
input.value = "";
}
}
searchSewageOccurList(1, 10, selectSearchYear[0]?.val || "0");
searchSewageOccurList(1, 10, defaultSearchYear);
}
// 모달 닫기
@@ -139,32 +147,15 @@ export default function PlanSewage() {
useEffect(() => {
if (!isInit) {
initializeCommonData();
searchSewageOccurList(1, 10, selectSearchYear[0]?.val || "0");
searchSewageOccurList(1, 10, defaultSearchYear);
}
}, [isInit, initializeCommonData, searchSewageOccurList, selectSearchYear]);
useEffect(() => {
if(!isInitExcelDownload) return ;
const timer = setTimeout(() => {
if (excelList && excelList.length > 0) {
downloadExcel({
data: excelList as unknown as JSON[],
fileName: '오수발생량 추정',
sheetNameArray: ['오수발생량 추정'],
headerRange: 'A1:O1'
});
}
}, 500); // 약간의 지연을 두고 다운로드 실행
return () => clearTimeout(timer);
}, [excelList, isInit, isInitExcelDownload]);
}, [isInit, initializeCommonData, searchSewageOccurList, defaultSearchYear]);
/**
* 콜백함수
*/
// 조회
const searchFacilityPrecipitation = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => {
const searchFacilityPrecipitation = useCallback(async ({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => {
let nameCd, partStr
switch (partValue) {
case 'R':
@@ -182,9 +173,10 @@ export default function PlanSewage() {
break;
}
if (partStr.length > 1 || fcltCpct !== '0' || operationValue!== '0' ||nameCd ||(fcltyName.trim() !== "" && fcltyName.trim() !== null)) {
searchSewageOccurList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined);
await searchSewageOccurList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue
, fcltyName.trim() !== ""? fcltyName : undefined);
} else {
searchSewageOccurList(pageNo, pageCnt, searchYear);
await searchSewageOccurList(pageNo, pageCnt, searchYear);
}
}, [fcltCpct, operationValue, fcltyName, searchSewageOccurList, searchYear]);
@@ -195,7 +187,8 @@ export default function PlanSewage() {
}, [upHeadList1, upHeadList2, upHeadList3]);
// 엑셀다운로드
const excelDownload = useCallback(() => {
const excelDownload = useCallback(async () => {
if (isExcelDownClick) return;
if (ref.current.partValue === 'A') { //
if (!ref.current.sidoValue || ref.current.sidoValue === '0') {
alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.');
@@ -212,9 +205,50 @@ export default function PlanSewage() {
return;
}
}
searchFacilityPrecipitation(ref.current, 1, 5000);
setIsInitExcelDownload(true);
}, [searchFacilityPrecipitation]);
setIsExcelDownClick(true);
try {
await searchFacilityPrecipitation(ref.current, 1, 5000);
const latestExcelList = store.getState().SewageOccur.excelList || [];
if (latestExcelList.length > 0) {
const toNumber = (value: unknown) => {
if (value === null || value === undefined || value === '') return '';
const parsed = typeof value === "number" ? value : Number(String(value).replace(/,/g, ""));
return Number.isFinite(parsed) ? parsed : '';
};
const numericKeys = [
"시설용량(m³/일)",
"물사용량(m³/일)",
"오수전환율(%)",
"오수발생량추정(m³/일)",
"연간하수유입량(m³/일)",
"청천시하수유입량(m³/일)",
];
const excelData = latestExcelList.map((row) => {
const nextRow = { ...row } as Record<string, unknown>;
numericKeys.forEach((key) => {
if (key in nextRow) {
nextRow[key] = toNumber(nextRow[key]);
}
});
return nextRow;
});
downloadExcelFormat({
data: excelData as unknown as JSON[],
fileName: '오수발생량 추정',
sheetNameArray: ['오수발생량 추정'],
headerRange: 'A1:O1',
decimalArray0: [9, 10, 11, 12, 13, 14]
});
} else {
alert("조회된 결과가 없습니다.");
}
} finally {
setIsExcelDownClick(false);
}
}, [isExcelDownClick, searchFacilityPrecipitation]);
// 페이지 변경
const handlePageChange = useCallback((page: number) => {
@@ -246,6 +280,7 @@ export default function PlanSewage() {
btnClass={btnClass}
queryClick={chkVal}
excelClick={excelDownload}
excelDisabled={isExcelDownClick}
initClick={resetVal}
ref={ref}
isAdmin={false}
@@ -7,11 +7,11 @@
*/
"use client"
import { useFoStore } from "@/app/store";
import { StoreContext, useFoStore } from "@/app/store";
import type { TableUpperProps } from "@/app/store/common";
import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components";
import { ContentsDiv, downloadExcelFormat, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components";
import ListDiv from "@common/business_components/ui/list_div";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import UnderWaterInputModal from "./modal/page";
import type { UnderWaterProps } from "@/app/store/underWaterInput/types";
import LoadingOverlay from "@common/business_components/ui/loadingBar";
@@ -35,6 +35,11 @@ export default function UnderWaterInput() {
initializeCommonData: () => {},
searchUnderWaterList: () => {}
};
const store = useContext(StoreContext);
if (!store) {
throw new Error("StoreContext is not available. Ensure StoreProvider is mounted.");
}
const {
isInit,
@@ -50,6 +55,11 @@ export default function UnderWaterInput() {
btnClass
} = Common;
const defaultSearchYear =
selectSearchYear.find((item) => item.selected)?.val ||
selectSearchYear[0]?.val ||
"0";
const {
labelArray,
} = UnderWaterSearch;
@@ -60,7 +70,6 @@ export default function UnderWaterInput() {
upHeadList3,
underWaterList,
underWaterCount,
excelList,
isLoading,
} = UnderWater;
@@ -98,9 +107,9 @@ export default function UnderWaterInput() {
// 시설상태
const [operationValue, setOperationValue] = useState<string>("0");
// 조회기간
const [searchYear, setSearchYear] = useState<string>(selectSearchYear[0]?.val || "0");
const [searchYear, setSearchYear] = useState<string>(defaultSearchYear);
// 엑셀다운로드 트리거
const [isMount, setIsMount] = useState<boolean>(false);
const [isExcelDownClick, setIsExcelDownClick] = useState<boolean>(false);
/**
* 페이지 함수
@@ -112,7 +121,7 @@ export default function UnderWaterInput() {
// 상태값 설정
setFcltCpct(selectUpdownData[0]?.val || "0");
setOperationValue(selectOperationData[0]?.val || "0");
setSearchYear(selectSearchYear[0]?.val || "0");
setSearchYear(defaultSearchYear);
// SearchInputSelect 초기화
setFcltyName("");
if (searchInputRef.current) {
@@ -121,7 +130,7 @@ export default function UnderWaterInput() {
input.value = "";
}
}
searchUnderWaterList(1, 10, selectSearchYear[0]?.val || "0");
searchUnderWaterList(1, 10, defaultSearchYear);
}
// 모달 닫기
@@ -144,31 +153,15 @@ export default function UnderWaterInput() {
useEffect(() => {
if (!isInit) {
initializeCommonData();
searchUnderWaterList(1, 10, selectSearchYear[0]?.val || "0");
searchUnderWaterList(1, 10, defaultSearchYear);
}
}, [isInit, initializeCommonData, searchUnderWaterList, selectSearchYear]);
useEffect(() => {
if(!isMount) return ;
const timer = setTimeout(() => {
if (excelList && excelList.length > 0) {
downloadExcel({
data: excelList as unknown as JSON[],
fileName: '지하수유입량 추정',
sheetNameArray: ['지하수유입량 추정'],
headerRange: 'A1:O1'
});
}
}, 1000); // 약간의 지연을 두고 다운로드 실행
return () => clearTimeout(timer);
}, [excelList, isMount]);
}, [isInit, initializeCommonData, searchUnderWaterList, defaultSearchYear]);
/**
* 콜백함수
*/
// 조회
const searchFacilityPrecipitation = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => {
const searchFacilityPrecipitation = useCallback(async ({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => {
let nameCd, partStr
switch (partValue) {
case 'R':
@@ -186,9 +179,10 @@ export default function UnderWaterInput() {
break;
}
if (partStr.length > 1 || fcltCpct !== '0' || operationValue!== '0' ||nameCd ||(fcltyName.trim() !== "" && fcltyName.trim() !== null)) {
searchUnderWaterList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined);
await searchUnderWaterList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue
, fcltyName.trim() !== ""? fcltyName : undefined);
} else {
searchUnderWaterList(pageNo, pageCnt, searchYear);
await searchUnderWaterList(pageNo, pageCnt, searchYear);
}
}, [fcltCpct, operationValue, fcltyName, searchUnderWaterList, searchYear]);
@@ -199,7 +193,8 @@ export default function UnderWaterInput() {
}, [upHeadList1, upHeadList2, upHeadList3]);
// 엑셀다운로드
const excelDownload = useCallback(() => {
const excelDownload = useCallback(async () => {
if (isExcelDownClick) return;
if (ref.current.partValue === 'A') { //
if (!ref.current.sidoValue || ref.current.sidoValue === '0') {
alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.');
@@ -216,9 +211,49 @@ export default function UnderWaterInput() {
return;
}
}
searchFacilityPrecipitation(ref.current, 1, 5000);
setIsMount(true);
}, [searchFacilityPrecipitation]);
setIsExcelDownClick(true);
try {
await searchFacilityPrecipitation(ref.current, 1, 5000);
const latestExcelList = store.getState().UnderWater.excelList || [];
if (latestExcelList.length > 0) {
const toNumber = (value: unknown) => {
if (value === null || value === undefined || value === '') return '';
const parsed = typeof value === "number" ? value : Number(String(value).replace(/,/g, ""));
return Number.isFinite(parsed) ? parsed : '';
};
const numericKeys = [
"시설용량(m³/일)",
"평균유입하수량(m³/일)",
"연간지하수유입량(m³/일)",
"하절기지하수유입량(m³/일)",
"동절기지하수유입량(m³/일)",
];
const excelData = latestExcelList.map((row) => {
const nextRow = { ...row } as Record<string, unknown>;
numericKeys.forEach((key) => {
if (key in nextRow) {
nextRow[key] = toNumber(nextRow[key]);
}
});
return nextRow;
});
downloadExcelFormat({
data: excelData as unknown as JSON[],
fileName: '지하수유입량 추정',
sheetNameArray: ['지하수유입량 추정'],
headerRange: 'A1:O1',
decimalArray0: [9, 10, 11, 12, 13]
});
} else {
alert("조회된 결과가 없습니다.");
}
} finally {
setIsExcelDownClick(false);
}
}, [isExcelDownClick, searchFacilityPrecipitation, store]);
// 페이지 변경
const handlePageChange = useCallback((page: number) => {
@@ -250,6 +285,7 @@ export default function UnderWaterInput() {
btnClass={btnClass}
queryClick={chkVal}
excelClick={excelDownload}
excelDisabled={isExcelDownClick}
initClick={resetVal}
ref={ref}
isAdmin={false}
@@ -276,4 +312,4 @@ export default function UnderWaterInput() {
)}
</MainDiv>
);
}
}
+15 -15
View File
@@ -12,6 +12,8 @@ import { getFetch, getFetchAreaList } from "@/app/components/api";
export type SelectDataType = {
text: string;
val: string;
selected?: boolean;
hidden?: boolean;
}
// grid header
export type TableUpperProps = {
@@ -161,22 +163,20 @@ const getYearData = () => {
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth() + 1;
// 상반기(6월까지)는 2년 전, 하반기는 1년 전부터 시작
const startYear = month <= 6 ? (year - 2) : (year - 1);
const defaultIdx = month <= 6 ? 2 : 1;
const startYear = year;
const year1 = startYear;
const year2 = startYear - 1;
const year3 = startYear - 2;
const year4 = startYear - 3;
const year5 = startYear - 4;
return [
{ text: year1 + "년", val: year1.toString() },
{ text: year2 + "년", val: year2.toString() },
{ text: year3 + "년", val: year3.toString() },
{ text: year4 + "년", val: year4.toString() },
{ text: year5 + "년", val: year5.toString() },
]
// 최근 10개년도 제공 (기본값: 첫 번째 항목 선택)
return Array.from({ length: 10 }, (_, index) => {
const targetYear = startYear - index;
return {
text: `${targetYear}년`,
val: targetYear.toString(),
selected: index === defaultIdx,
};
});
}
const selectSearchYear: SelectDataType[] = getYearData();
@@ -486,4 +486,4 @@ export const CommonReducer = (
}
}
export type CommonStore = CommonType & CommonAction;
export type CommonStore = CommonType & CommonAction;
+5 -4
View File
@@ -1712,7 +1712,7 @@ export const dashBoardReducer = (
// RDII 추정 = 하절기(6~8월) 중 최저 유입 BOD일때 유입유량 x ((동절기 청천시 유입BOD - 하절기(6~8월) 중 최저 유입BOD) / (동절기 청천시 유입BOD))
// const resp = await fetchMainGraph(searchYear, fcltyCd);
const resp = mainGraphData;
if (resp && resp.data && resp.data.wqltObjs) {
if (resp && resp.data && resp.data.wqltObjs && resp.data.wqltObjs.length > 0) {
const wqltData = resp.data.wqltObjs;
const wqltDataTot = CalcRainEffect(clone(wqltData));
const wqltDataSunny = CalcRainEffect(clone(wqltData), 'SUNNY');
@@ -1800,10 +1800,11 @@ export const dashBoardReducer = (
const rtnData = await calcWaterQltyData(operationalStatusList, searchYear, fcltyCd, mainGraphData); // 수질 데이터 청천시, 강우시 재계산
let tpFctYn = 'N';
if (mainGraphData.data.wqltObjs && mainGraphData.data.wqltObjs.length > 0)
tpFctYn = mainGraphData.data.wqltObjs?.[0].tpFctYn;
// 총인설비가 없을 경우 제외
console.log('총인시설 여부 : ', mainGraphData.data.wqltObjs?.[0].tpFctYn);
const tpFctYn = mainGraphData.data.wqltObjs?.[0].tpFctYn;
console.log('총인시설 여부 : ', tpFctYn);
// console.log('rtnData : ', rtnData);
if (tpFctYn !== 'Y') {
+31 -23
View File
@@ -28,9 +28,11 @@ export interface FacilityRdiiDetailProps {
export interface FacilityWaterQltyProps {
bscFctCd: string,
bscFctNm: string,
msrDt: string
sidoNm: string,
sggNm: string,
msrDt: string,
sido: string,
gugun: string,
// sidoNm: string,
// sggNm: string,
addr: string,
fctCpc: number,
inflAssctBfrWqltBod: string | '',
@@ -147,8 +149,8 @@ const mainLabelArray: string[] = ['구분', '시도', '시군구', '용량별',
// 시설수질리스트 헤더(행정별)
const upHeadList1: TableUpperProps[] = [
{ id: "C", title: "행정명", colSpan: 2, row: 0, widthPercent: 10 },
{ id: "sidoNm", title: "시도", row: 1, widthPercent: 5 },
{ id: "sggNm", title: "시군구", row: 1, widthPercent: 5 },
{ id: "sido", title: "시도", row: 1, widthPercent: 5 },
{ id: "gugun", title: "시군구", row: 1, widthPercent: 5 },
{ id: "bscFctNm", title: "시설명", rowSpan: 2, row: 0, widthPercent: 10 },
{ id: "addr", title: "주소", rowSpan: 2, row: 0 },
{ id: "fctCpc", title: "시설용량(m³/일)", align: "right", rowSpan: 2, row: 0, widthPercent: 10 },
@@ -197,8 +199,8 @@ const upHeadList3: TableUpperProps[] = [
{ id: "mdmbsn", title: "중권역", row: 1, widthPercent: 5 },
{ id: "rgnNm", title: "환경청", row: 0, rowSpan: 2, widthPercent: 7 },
{ id: "D", title: "행정명", colSpan: 2, row: 0, widthPercent: 10 },
{ id: "sidoNm", title: "시도", upName: "upChangeRe2", row: 1, widthPercent: 5 },
{ id: "sggNm", title: "시군구", upName: "upChangeRe2", row: 1, widthPercent: 5 },
{ id: "sido", title: "시도", upName: "upChangeRe2", row: 1, widthPercent: 5 },
{ id: "gugun", title: "시군구", upName: "upChangeRe2", row: 1, widthPercent: 5 },
{ id: "bscFctNm", title: "시설명", row: 0, rowSpan: 2, widthPercent: 8 },
{ id: "addr", title: "주소", row: 0, rowSpan: 2 },
{ id: "fctCpc", title: "시설용량(m³/L)", align: "right", row: 0, rowSpan: 2, widthPercent: 5 },
@@ -466,16 +468,22 @@ export const facilityWaterQltyReducer = (set: SetFunc): FacilityWaterQltyActions
//---------
const waterQltyList: any[] = [];
let recordCount = 0;
for (let idx = 0; idx < pageSize; idx += 100) {
const tmpPageNo = Math.floor(idx / 100) + 1;
const tmpPageSize = Math.min(100, pageSize - idx);
const result = await fetchFacilityWaterQltyList(tmpPageNo, tmpPageSize, searchYear, part, nameCd, fcltCpct, operationValue, fcltyNm);
if (result?.status === 200 && Array.isArray(result?.data)) {
waterQltyList.push(...result.data);
}
recordCount = result?.count || 0;
if (result?.count < tmpPageNo * tmpPageSize) {
break; // 더 이상 데이터가 없으면 중지
if (pageSize < 5000) {
const res = await fetchFacilityWaterQltyList(pageNo, pageSize, searchYear, part, nameCd, fcltCpct, operationValue, fcltyNm);
waterQltyList.push(...res.data);
recordCount = res.count;
} else {
for (let idx = 0; idx < pageSize; idx += 100) {
const tmpPageNo = Math.floor(idx / 100) + 1;
const tmpPageSize = Math.min(100, pageSize - idx);
const result = await fetchFacilityWaterQltyList(tmpPageNo, tmpPageSize, searchYear, part, nameCd, fcltCpct, operationValue, fcltyNm);
if (result?.status === 200 && Array.isArray(result?.data)) {
waterQltyList.push(...result.data);
}
recordCount = result?.count || 0;
if (result?.count < tmpPageNo * tmpPageSize) {
break; // 더 이상 데이터가 없으면 중지
}
}
}
//-------
@@ -527,8 +535,8 @@ export const facilityWaterQltyReducer = (set: SetFunc): FacilityWaterQltyActions
waterQltyList.forEach(item => {
dataFormSetting(item)
item.fctCpc = parseFloat(item.fctCpc.toString().replace(/,/g, ''));
item.sidoNm = item.lclgvrnmntNm.split(' ')[0]?.toString() || '';
item.sggNm = item.lclgvrnmntNm.split(' ')[1]?.toString() || '';
// item.sidoNm = item.lclgvrnmntNm.split(' ')[0]?.toString() || '';
// item.sggNm = item.lclgvrnmntNm.split(' ')[1]?.toString() || '';
item.inflAssctBfrWqltToc = Number(searchYear) < COD_YEAR ? "(" + item.inflAssctBfrWqltToc + ")" : item.inflAssctBfrWqltToc;
item.efflWqltToc = Number(searchYear) < COD_YEAR ? "(" + item.efflWqltToc + ")" : item.efflWqltToc;
excelDownList.push({
@@ -537,8 +545,8 @@ export const facilityWaterQltyReducer = (set: SetFunc): FacilityWaterQltyActions
단위유역: item.lgbsn,
중권역: item.mdmbsn,
환경청: item.rgnNm,
시도: item.sidoNm,
시군구: item.sggNm,
시도: item.sido,
시군구: item.gugun,
시설이름: item.bscFctNm,
주소: item.addr,
"시설용량(m³/일)": CommaToNumber(item.fctCpc),
@@ -559,8 +567,8 @@ export const facilityWaterQltyReducer = (set: SetFunc): FacilityWaterQltyActions
waterQltyList.forEach((item: FacilityWaterQltyProps) => {
dataFormSetting(item)
item.fctCpc = parseFloat(item.fctCpc.toString().replace(/,/g, ''));
item.sidoNm = item.lclgvrnmntNm.split(' ')[0]?.toString() || '';
item.sggNm = item.lclgvrnmntNm.split(' ')[1]?.toString() || '';
// item.sidoNm = item.;
// item.sggNm = item.lclgvrnmntNm.split(' ')[1]?.toString() || '';
item.inflAssctBfrWqltToc = Number(searchYear) < COD_YEAR ? "(" + item.inflAssctBfrWqltToc + ")" : item.inflAssctBfrWqltToc;
item.efflWqltToc = Number(searchYear) < COD_YEAR ? "(" + item.efflWqltToc + ")" : item.efflWqltToc;
});
+24 -17
View File
@@ -285,23 +285,30 @@ export const flowRateSearchReducer: (set: (fn: (state: FlowRateSearchType) => Fl
//---------
const flowRateList: any[] = [];
const results: any[] = [];
for (let idx = 0; idx < pageSize; idx += 100) {
const tmpPageNo = Math.floor(idx / 100) + 1;
const tmpPageSize = Math.min(100, pageSize - idx);
const result = await fetchFlowRateList( tmpPageNo, tmpPageSize, searchYear, partCd, nameCd, fcltCpct, operationValue, fcltyNm);
results.push(result);
if (result?.count < tmpPageNo * tmpPageSize) {
break; // 더 이상 데이터가 없으면 중지
}
}
console.log(results);
// 결과를 합치기
for (const res of results) {
if (res?.status === 200 && Array.isArray(res?.data)) {
flowRateList.push(...res.data);
if (pageSize < 5000) {
const res = await fetchFlowRateList(pageNo, pageSize, searchYear, partCd, nameCd, fcltCpct, operationValue, fcltyNm);
flowRateList.push(...res.data);
results.push(res);
} else {
for (let idx = 0; idx < pageSize; idx += 100) {
const tmpPageNo = Math.floor(idx / 100) + 1;
const tmpPageSize = Math.min(100, pageSize - idx);
const result = await fetchFlowRateList( tmpPageNo, tmpPageSize, searchYear, partCd, nameCd, fcltCpct, operationValue, fcltyNm);
results.push(result);
if (result?.count < tmpPageNo * tmpPageSize) {
break; // 더 이상 데이터가 없으면 중지
}
}
// console.log(results);
// 결과를 합치기
for (const res of results) {
if (res?.status === 200 && Array.isArray(res?.data)) {
flowRateList.push(...res.data);
}
}
}
//---------
const recordCount = results[0]?.count || 0;
//-------
@@ -381,7 +388,7 @@ export const flowRateSearchReducer: (set: (fn: (state: FlowRateSearchType) => Fl
const yearSunnyAmnt = sunnyDay > 0 ? sunnyInflAmnt / sunnyDay : 0; // 청천시 연평균유입유량
const yearSunnyBod = sunnyDay > 0 ? sunnyBod / sunnyDay : 0; // 청천시 연평균유입BOD
console.log(inflData);
//console.log(inflData);
const rtnData = inflData.map((item: FlowRateModalProps) => {
const targetWqItem = wqltData?.find(wqitem => wqitem.msrDt === item.msrDt.replaceAll('-', ''));
const dayBod = targetWqItem ? Number(targetWqItem.inflAssctBfrWqltBod) : 0;
@@ -397,7 +404,7 @@ export const flowRateSearchReducer: (set: (fn: (state: FlowRateSearchType) => Fl
: item.msrDt,
};
});
console.log(rtnData);
//console.log(rtnData);
set((state: FlowRateSearchType) => ({
...state,
FlowRateModal: {
+15 -4
View File
@@ -113,7 +113,10 @@ export type fcltyHistInfoProps = {
sitArea: number,
trtDstrArea: number,
swrTrtPopl: number,
fctCpc: number | ''
fctCpc: number | '',
trtSrsHanNm?: string, // 공법구분
trtMocHanNm?: string, // 공법계열
trtFrfHanNm?: string, // 방법
}
// 시설 수질정보
@@ -328,6 +331,9 @@ const upHeadListFcltyHistInfo: TableUpperProps[] = [
{ id: "trlrnCmplDt", title: "시운전완료일" },
{ id: "rnDt", title: "가동개시일" },
{ id: "fctCpc", title: "시설용량(m³/일)", align: 'right' },
{ id: "trtSrsHanNm", title: "공법구분" },
{ id: "trtMocHanNm", title: "공법계열" },
{ id: "trtFrfHanNm", title: "방법" },
];
// 시설 이력관리 (리스트)
@@ -344,7 +350,10 @@ const gridListFcltyHistInfo: fcltyHistInfoProps[] = [
sitArea: 0,
trtDstrArea: 0,
swrTrtPopl: 0,
fctCpc: 860000
fctCpc: 860000,
trtFrfHanNm: "",
trtMocHanNm: "",
trtSrsHanNm: "",
}
];
@@ -418,7 +427,9 @@ const gridListFcltyWaterQltyInfo: fcltyWaterQltyInfoProps[] = [
*/
export const fetchFacilityListInit = async (pageNo: number, pageSize: number, partValue?: string, nameCd?: string, fcltCpct?: string, oprtngSttsCd?: string, facilityPartValue?: string, facilityName?: string): Promise<{ data: facilityListProps[], count: number }> => {
try {
const tailStr = partValue ? '/' + (partValue === 'G' ? 'A' : partValue) + '/' + nameCd + '/' + fcltCpct + '/' + oprtngSttsCd + '/' + facilityPartValue + (facilityName && facilityName !== '' ? '/' + facilityName : '') : '';
const tailStr = partValue ? '/' + (partValue === 'G' ? 'A' : partValue) + '/' + nameCd + '/' + fcltCpct + '/' + oprtngSttsCd
+ (facilityPartValue && facilityPartValue !== ''? '/' + facilityPartValue : '')
+ (facilityName && facilityName !== ''? '/' + facilityName : '') : '';
const url = '/prcsFclt/info/' + pageNo + '/' + pageSize + tailStr;
const res = await getFetch('fo', url);
res.data.forEach((item: Record<string, unknown>) => {
@@ -900,4 +911,4 @@ export const processFacilityReducer: (set: SetStateFunction) => ProcessFacilityA
}
}
}
}
}
+9 -1
View File
@@ -317,6 +317,14 @@ export type UnderWaterStore = UnderWaterType & UnderWaterActions;
export const underWaterReducer: (set: (fn: (state: UnderWaterType) => UnderWaterType) => void) => UnderWaterActions = (set: (fn: (state: UnderWaterType) => UnderWaterType) => void) => {
return {
searchUnderWaterList: async (pageNo: number, pageSize: number, searchYear: string, part?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => {
set((state: UnderWaterType) => ({
...state,
UnderWater: {
...state.UnderWater,
isLoading: true,
},
}));
const facilityInitData = await fetchFacilityListInit(pageNo, pageSize, part, nameCd, fcltCpct, operationValue, fcltyNm);
if (pageSize < 5000) {
@@ -724,4 +732,4 @@ export const underWaterReducer: (set: (fn: (state: UnderWaterType) => UnderWater
}))
}
}
}
}
@@ -1,3 +1,4 @@
"use client";
import { createContext, useState } from "react";
interface PromptContext {
@@ -27,7 +27,8 @@ export const MsSwagePplCol = [
// { id: 'ftre_smbol_cd', title: '지형지물부호코드' },
// { id: 'lyrTypeNm', title: '레이어타입명' },
]
export const MsSwagePplCol2 = [
// 하수관로 searchModal 기본정보tab
export const MsSwagePplCol2 = [
{ id: 'rowNumber', title: '일련\n번호' },
{ id: 'fcltMngNo', title: '시설\n관리번호' },
{ id: 'instMngNo', title: '기관\n관리번호' },
@@ -44,13 +45,13 @@ export const MsSwagePplCol2 = [
{ id: 'tmptMhlMngNo', title: '시점맨홀\n관리번호' },
{ id: 'scendDpth', title: '종점깊이\n(m)' },
{ id: 'scendMhlMngNo', title: '종점맨홀\n관리번호' },
// { id: 'vrtcLen', title: '세로길이\n(m)' },
// { id: 'wdthLen', title: '가로길이\n(m)' },
// { id: 'fcltyShpCd', title: '시설물형태코드' },
{ id: 'vrtcLen', title: '세로길이\n(m)' },
{ id: 'wdthLen', title: '가로길이\n(m)' },
{ id: 'fcltyShpCd', title: '시설물형태코드' },
// { id: 'fid', title: 'FID' },
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
// { id: 'mngInstCd', title: '관리기관코드' },
// { id: 'mqltCd', title: '재질코드' },
{ id: 'mqltCd', title: '재질코드' },
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
// { id: 'usgCd', title: '용도코드' },
// { id: 'lyrTypeNm', title: '레이어타입명' },
@@ -69,6 +70,10 @@ export const MsSwagePplExcelCol = {
'tmptMhlMngNo': '시점맨홀 관리번호',
'scendDpth': '종점 깊이(m)',
'scendMhlMngNo': '종점맨홀 관리번호',
'vrtcLen': '세로길이(m)',
'wdthLen': '가로길이(m)',
'fcltyShpCd': '시설물형태코드',
'mqltCd': '재질코드',
};
// 하수관로 용도 코드표
export const MS_SWAGE_PPL_USE_CODES = [
@@ -275,7 +280,80 @@ export const MS_SWAGE_PPL_CODES = [
{ val: "MOP977", text: "전주용입상관" },
{ val: "MOP978", text: "MD관" },
];
// 하수관로 'fcltyShpCd' '시설물형태코드'
export const MS_SWAGE_PPL_SHAPE = [
{ val: "FOR000 ", text: "미분류" },
{ val: "FOR001 ", text: "원형" },
{ val: "FOR002 ", text: "원추형" },
{ val: "FOR003 ", text: "원통형" },
{ val: "FOR004 ", text: "구형" },
{ val: "FOR005 ", text: "타원형" },
{ val: "FOR006 ", text: "난원" },
{ val: "FOR007 ", text: "반원형" },
{ val: "FOR010 ", text: "피라미드형" },
{ val: "FOR011 ", text: "사면체형" },
{ val: "FOR012 ", text: "기둥,축" },
{ val: "FOR013 ", text: "돛대,활대처럼 둥글고 긴 원재" },
{ val: "FOR014 ", text: "아치형" },
{ val: "FOR015 ", text: "다중아치형" },
{ val: "FOR016 ", text: "돔형" },
{ val: "FOR017 ", text: "고형체형" },
{ val: "FOR020 ", text: "사각형" },
{ val: "FOR021 ", text: "평방형" },
{ val: "FOR022 ", text: "다이아몬드형" },
{ val: "FOR023 ", text: "사다리꼴" },
{ val: "FOR024 ", text: "각형" },
{ val: "FOR025 ", text: "마제형관" },
{ val: "FOR026 ", text: "태형" },
{ val: "FOR030 ", text: "A형" },
{ val: "FOR031 ", text: "H형" },
{ val: "FOR032 ", text: "I형" },
{ val: "FOR033 ", text: "Y형" },
{ val: "FOR034 ", text: "T형" },
{ val: "FOR050 ", text: "불규칙한 형태" },
{ val: "FOR900 ", text: "계란형" },
{ val: "FOR901 ", text: "구(사각)형" },
{ val: "FOR902 ", text: "높이조절철개형" },
{ val: "FOR903 ", text: "박스형(RCB)" },
{ val: "FOR904 ", text: "반월개거" },
{ val: "FOR905 ", text: "반월개거(측구)" },
{ val: "FOR906 ", text: "벽돌조" },
{ val: "FOR907 ", text: "사각철판" },
{ val: "FOR908 ", text: "석축개거" },
{ val: "FOR909 ", text: "석축개거(측구)" },
{ val: "FOR910 ", text: "승수로" },
{ val: "FOR911 ", text: "옹벽개거" },
{ val: "FOR912 ", text: "옹벽개거(측구)" },
{ val: "FOR913 ", text: "이단" },
{ val: "FOR914 ", text: "이중침전형" },
{ val: "FOR915 ", text: "자연개거" },
{ val: "FOR916 ", text: "자연개거(측구)" },
{ val: "FOR917 ", text: "조립식" },
{ val: "FOR918 ", text: "지상관" },
{ val: "FOR919 ", text: "지상단구" },
{ val: "FOR920 ", text: "지상쌍구" },
{ val: "FOR921 ", text: "지하단구" },
{ val: "FOR922 ", text: "지하쌍구" },
{ val: "FOR923 ", text: "직사각형" },
{ val: "FOR924 ", text: "타설식" },
{ val: "FOR925 ", text: "토류형" },
{ val: "FOR926 ", text: "흄관원형" },
{ val: "FOR927 ", text: "L형측구" },
{ val: "FOR928 ", text: "LO형측구" },
{ val: "FOR929 ", text: "LU형측구" },
{ val: "FOR930 ", text: "PVC이중벽관" },
{ val: "FOR931 ", text: "RCB 맨홀" },
{ val: "FOR932 ", text: "U형" },
{ val: "FOR933 ", text: "U형측구(미복개)" },
{ val: "FOR934 ", text: "U형측구(복개)" },
{ val: "FOR999 ", text: "기타" }
];
// 하수처리시설
export const SwtrmtFcltCol = [
{ id: 'swtrmt_fclt_nm', title: '하수처리시설명' },
@@ -125,32 +125,30 @@ const SearchModal: React.FC = () => {
{/* 📋 결과 리스트 */}
{/* 기본정보 탭 */}
{activeTab === 'basic' && (
<div className="h-[79dvh] w-full rounded-lg border mt-4">
{/* <div className="flex-1 px-2 pb-2"> */}
<div className="h-[79dvh] w-full rounded-lg border mt-4 overflow-x-scroll">
<LoadingOverlay isLoading={isLoading} />
<UiTable
headName={(() => {
const label = BASIC_TYPES.find((type) => type.val === basicType)?.text
return (<>{label} 조회 결과 ({tableCount.toLocaleString('ko-KR')})건</>);
})()
}
tableData={tableData}
headlist={dynamicHeadlist}
pageSize={10}
total={tableCount}
cellClick={handleBasicCellClick}
onPageChange={setCurPage}
resetPageNo={isResetPage}
resetSelection={isResetSelection}
curPage={curPage}
headButton={
<div className='flex'>
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium primary" size="sm" onClick={viewMultipleFeature}>지도에서 보기</Button>
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium green" size="sm" onClick={basicExcelDownload}>파일 다운로드</Button>
</div>
}
/>
{/* </div> */}
<UiTable className='w-[120%]'
headName={(() => {
const label = BASIC_TYPES.find((type) => type.val === basicType)?.text
return (<>{label} 조회 결과 ({tableCount.toLocaleString('ko-KR')})건</>);
})()
}
tableData={tableData}
headlist={dynamicHeadlist}
pageSize={10}
total={tableCount}
cellClick={handleBasicCellClick}
onPageChange={setCurPage}
resetPageNo={isResetPage}
resetSelection={isResetSelection}
curPage={curPage}
headButton={
<div className='flex'>
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium primary" size="sm" onClick={viewMultipleFeature}>지도에서 보기</Button>
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium green" size="sm" onClick={basicExcelDownload}>파일 다운로드</Button>
</div>
}
/>
</div>
)}
{/* 유지관리 정보 탭 */}
@@ -14,7 +14,7 @@
*/
import { downloadExcelWithKoreanHeaders } from '@common/business_components/ui';
import {
MS_SWAGE_PPL_USE_CODES, MS_SWAGE_MHL_CODES, MS_SWAGE_MHL_USE_CODES,
MS_SWAGE_PPL_USE_CODES, MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_SHAPE, MS_SWAGE_MHL_CODES, MS_SWAGE_MHL_USE_CODES,
MsSwagePplExcelCol, SwtrmtFcltExcelCol, SwtrmtPrcsZoneExcelCol, MsSwageMhlExcelCol, ExclnSewagePumpExcelCol, stofcbrExcelCol
} from '../components/common/table';
import { getData } from "@/components/lib/api";
@@ -46,6 +46,7 @@ export default function useBasicTab() {
} = useSearchModalStore();
const { ctpv, sgg } = useStore();
const fetchBasicFilteredData = async () => {
let cqlFilters: string[] = [];
const queryParams = new URLSearchParams({
@@ -79,8 +80,15 @@ export default function useBasicTab() {
if (!sggCd && code) sggCd = code.slice(0, 5);
}
const matchedMhlCode = MS_SWAGE_MHL_CODES.find(code => code.val === item?.mqltCd?.trim());
const matchedMhlUsgCode = MS_SWAGE_MHL_USE_CODES.find(code => code.val === item?.usgCd);
const matchedMqltCode = basicType === 'tb_ms_swage_ppl'
? MS_SWAGE_PPL_CODES.find(code => code.val === item?.mqltCd?.trim()) // 하수관로 재질
: basicType === 'tb_ms_swage_mhl'
? MS_SWAGE_MHL_CODES.find(code => code.val === item?.mqltCd?.trim()) // 맨홀 재질
: undefined;
const matchedMhlUsgCode = MS_SWAGE_MHL_USE_CODES.find(code => code.val === item?.usgCd); // 맨홀 용도
const matchedPplShpCode = MS_SWAGE_PPL_SHAPE.find(code => code.val === item?.fcltyShpCd); // 하수관로 시설물형태코드
const ctpvNm = ctpv.find(code => code.val.trim() === ctpvCd)?.text || item?.ctpvNm;
const sggNm = sgg.find(code => code.val.trim() === sggCd)?.text || item?.sggNm;
const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item?.usgCd?.trim());
@@ -90,15 +98,17 @@ export default function useBasicTab() {
...formattedItem,
rowNumber: (curPage - 1) * 10 + index + 1, // 1부터 시작하는 일련번호 추가
scsYn: item.scsYn === null || item.scsYn === undefined ? 'N' : item?.scsYn, // scsYn이 null이면 'N'으로 변경
mqltCd: matchedMhlCode ? matchedMhlCode.text : item?.mqltCd,
mqltCd: matchedMqltCode ? matchedMqltCode.text : item?.mqltCd,
usgCd: matchedMhlUsgCode ? matchedMhlUsgCode.text : (usgCdObj ? usgCdObj.text : item?.usgCd),
ctpvNm: ctpvNm ?? '-',
sggNm: sggNm ?? '-',
swtrmt_zone_nm: item.swtrmtZoneNm || item.swtrmt_zone_nm || '-',
fcltyShpCd: matchedPplShpCode? matchedPplShpCode.text : item?.fcltyShpCd,
};
});
setTableData(dataWithRowNumber);
// console.log(tableData);
setTableCount(res.count || []);
};
@@ -150,7 +160,7 @@ export default function useBasicTab() {
// 하수관로
cqlFilters = swagePplCqlFilters()
headerMapping = MsSwagePplExcelCol;
headerRange = "A1:M1";
headerRange = "A1:Q1";
break;
}
case 'tb_il_swage_prcs_fclt': {
@@ -196,6 +206,8 @@ export default function useBasicTab() {
const mhlUsgCdMap = new Map(MS_SWAGE_MHL_USE_CODES.map(code => [code.val?.trim(), code.text]));
const pplUsgCdMap = new Map(MS_SWAGE_PPL_USE_CODES.map(code => [code.val?.trim(), code.text]));
const pplMqltCdMap = new Map(MS_SWAGE_PPL_CODES.map(code => [code.val?.trim(), code.text]));
const pplShpCdMap = new Map(MS_SWAGE_PPL_SHAPE.map(code => [code.val?.trim(), code.text])); // 관로 시설물형태코드
const ctpvMap = new Map(ctpv.map(code => [code.val?.trim(), code.text]));
const sggMap = new Map(sgg.map(code => [code.val?.trim(), code.text]));
@@ -232,6 +244,10 @@ export default function useBasicTab() {
tmptMhlMngNo: formattedItem.tmptMhlMngNo ?? item.tmptMhlMngNo ?? '-',
scendDpth: formattedItem.scendDpth ?? item.scendDpth ?? '-',
scendMhlMngNo: formattedItem.scendMhlMngNo ?? item.scendMhlMngNo ?? '-',
vrtcLen: formattedItem.vrtcLen ?? item.vrtcLen ?? '-',
wdthLen: formattedItem.wdthLen ?? item.wdthLen ?? '-',
fcltyShpCd: pplShpCdMap.get(item?.fcltyShpCd?.trim()) || item.fcltyShpCd || formattedItem.fcltyShpCd || '-',
mqltCd: pplMqltCdMap.get(item?.mqltCd?.trim()) || item.mqltCd || formattedItem.mqltCd || '-',
};
}
case 'tb_il_swage_prcs_fclt': {
@@ -329,4 +345,4 @@ export default function useBasicTab() {
return { fetchBasicFilteredData, handleBasicCellClick, basicExcelDownload };
};
};
@@ -94,7 +94,7 @@ export default function Page() {
const cellClick = () => {
if (searchYear)
if (selFcltDiv === "RNB") {
if (selFcltDiv === "RNB") { // 빗물
if (fcltNm) {
rainChkNmSearch(
rainPageNation,
@@ -107,7 +107,7 @@ export default function Page() {
} else {
rainChkInit(rainPageNation, rainPageSize, searchYear, laeSeCd);
}
} else {
} else { // 중수도
if (fcltNm) {
rswtChkNmSearch(
rainPageNation,
+8 -12
View File
@@ -190,12 +190,10 @@ export const chRowspanTable = (
value={formatCommaNumber(value) || ""}
disabled={!selectedIds.includes(row.chkgdnSn)}
maxLength={numSize}
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
if (
head.type === "number" &&
e.target.value.length > numSize
) {
e.target.value = e.target.value.slice(0, numSize);
onInput={(e: React.FormEvent<HTMLInputElement>) => {
const el = e.currentTarget;
if (head.type === "number" && el.value.length > numSize) {
el.value = el.value.slice(0, numSize);
}
}}
/>
@@ -225,12 +223,10 @@ export const chRowspanTable = (
value={value || ""}
disabled={!selectedIds.includes(row.chkgdnSn)}
maxLength={numSize}
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
if (
head.type === "number" &&
e.target.value.length > numSize
) {
e.target.value = e.target.value.slice(0, numSize);
onInput={(e: React.FormEvent<HTMLInputElement>) => {
const el = e.currentTarget;
if (head.type === "number" && el.value.length > numSize) {
el.value = el.value.slice(0, numSize);
}
}}
/>
@@ -13,6 +13,7 @@ import {
chRowspanTable,
rswtChkTableData,
} from "@/components/lib/func/TableData";
import { codeToText } from "@/components/lib/func/commom";
import UiPageNations from "@common/business_components/ui/table/uiPageNations";
import { Table, TableBody, TableHeader } from "@common/components/ui";
import { useCallback, useEffect, useState } from "react";
@@ -99,9 +100,16 @@ export default function RswtChkgdTable({
const tableTotalData = rswtChkTableData(rswtChkData);
const makeRswtChkData = tableTotalData.map((item: any) => {
const bud = tableData([item.building], { code });
// 운영관리형태 코드 → 한글 텍스트로 치환 (빗물 화면과 동일한 표시)
const operMngCdText =
codeToText(item.operMngCd?.trim(), "rainRswtMng") ??
codeToText(item.reuseBuilding?.operMngCd?.trim(), "rainRswtMng") ??
item.operMngCd;
return {
...item,
...bud[0],
operMngCd: operMngCdText,
rowspan: item.rswtrDetails?.length,
};
});
@@ -11,6 +11,10 @@ type ExcelDownloadProps = {
decimalArray1? : number[];
decimalArray2? : number[];
decimalArray3? : number[];
decimalRange0?: string;
decimalRange1?: string;
decimalRange2?: string;
decimalRange3?: string;
}
export const downloadExcel = ({ data, fileName = new Date().getTime().toString() + '.xlsx', sheetNameArray = ["Sheet1"], headerRange='A1:V3'}: ExcelDownloadProps) => {
@@ -29,7 +33,20 @@ export const downloadExcelHeaders = ({ data, fileName = new Date().getTime().toS
excelStyle(XLSX.write(workbook, { bookType: "xlsx", type: "buffer" }), headerRange, fileName);
};
export const downloadExcelFormat = ({ data, fileName = new Date().getTime().toString() + '.xlsx', sheetNameArray = ["Sheet1"], headerRange='A1:V3', decimalArray0=[], decimalArray1=[], decimalArray2=[], decimalArray3=[]}: ExcelDownloadProps) => {
export const downloadExcelFormat = ({
data,
fileName = new Date().getTime().toString() + '.xlsx',
sheetNameArray = ["Sheet1"],
headerRange = 'A1:V3',
decimalArray0 = [],
decimalArray1 = [],
decimalArray2 = [],
decimalArray3 = [],
decimalRange0,
decimalRange1,
decimalRange2,
decimalRange3,
}: ExcelDownloadProps) => {
const worksheet = XLSX.utils.json_to_sheet(Array.isArray(data) ? data : [data]);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, sheetNameArray[0]);
@@ -61,10 +78,35 @@ export const downloadExcelFormat = ({ data, fileName = new Date().getTime().toSt
}
}
};
const applyRangeFormat = (rangeStr: string | undefined, decimal: number) => {
if (!rangeStr) return;
const rangeInfo = XLSX.utils.decode_range(rangeStr);
for (let row = rangeInfo.s.r; row <= rangeInfo.e.r; row++) {
for (let col = rangeInfo.s.c; col <= rangeInfo.e.c; col++) {
const cellAddr = XLSX.utils.encode_cell({ r: row, c: col });
const cell = worksheet[cellAddr];
if (cell && typeof cell.v === "number") {
if (decimal === 1) {
cell.z = "#,##0.0";
} else if (decimal === 2) {
cell.z = "#,##0.00";
} else if (decimal === 3) {
cell.z = "#,##0.000";
} else {
cell.z = "#,##0";
}
}
}
}
};
chaneValue(decimalArray0, 0);
chaneValue(decimalArray1, 1);
chaneValue(decimalArray2, 2);
chaneValue(decimalArray3, 3);
applyRangeFormat(decimalRange0, 0);
applyRangeFormat(decimalRange1, 1);
applyRangeFormat(decimalRange2, 2);
applyRangeFormat(decimalRange3, 3);
excelStyle(XLSX.write(workbook, { bookType: "xlsx", type: "buffer" }), headerRange, fileName);
};
@@ -74,6 +116,10 @@ export type SheetParameter = {
headerRange: string;
sheetName: string;
startCol?: string;
decimalRange0?: string;
decimalRange1?: string;
decimalRange2?: string;
decimalRange3?: string;
};
export const downloadMultipleExcel = (fileName = new Date().getTime().toString() + '.xlsx', sheetArray: SheetParameter[]): Promise<JSON | undefined> => {
@@ -201,9 +247,29 @@ export const filePickedBlob=(oFile: ArrayBuffer, data:[][],fileNm:string, header
});
}
export const filePickedBlobArray = (oFile: ArrayBuffer, fileNm: string, sheetArray: SheetParameter[]): Promise<JSON | undefined> => {
return new Promise<JSON | undefined>(async () => {
const wb = XLSX.read(oFile, { type: 'binary' });
// export const filePickedBlobArray = (oFile: ArrayBuffer, fileNm: string, sheetArray: SheetParameter[]): Promise<JSON | undefined> => {
// return new Promise<JSON | undefined>(async () => {
// const wb = XLSX.read(oFile, { type: 'binary' });
// wb.SheetNames.forEach(function (sheetName, index) {
// if (sheetArray[index]) {
// const sheet = wb.Sheets[sheetName];
// if (sheet) {
// const startCol = sheetArray[index].startCol ? sheetArray[index].startCol : 'A';
// XLSX.utils.sheet_add_aoa(sheet, sheetArray[index].data, { origin: startCol + sheetArray[index].headerNum });
// } else {
// console.error(`Sheet "${sheetName}" is undefined.`);
// }
// }
// });
// await excelArrayStyle(XLSX.write(wb, { bookType: "xlsx", type: "buffer" }), sheetArray, fileNm)
// });
// }
export const filePickedBlobArray = async (
oFile: ArrayBuffer,
fileNm: string,
sheetArray: SheetParameter[]
): Promise<JSON | undefined> => {
const wb = XLSX.read(oFile, { type: 'binary' });
wb.SheetNames.forEach(function (sheetName, index) {
if (sheetArray[index]) {
const sheet = wb.Sheets[sheetName];
@@ -215,9 +281,68 @@ export const filePickedBlobArray = (oFile: ArrayBuffer, fileNm: string, sheetArr
}
}
});
await excelArrayStyle(XLSX.write(wb, { bookType: "xlsx", type: "buffer" }), sheetArray, fileNm)
await excelArrayStyle(XLSX.write(wb, { bookType: "xlsx", type: "buffer" }), sheetArray, fileNm);
return undefined;
};
export const filePickedBlobArrayFormat = async (
oFile: ArrayBuffer,
fileNm: string,
sheetArray: SheetParameter[]
): Promise<JSON | undefined> => {
const wb = XLSX.read(oFile, { type: "binary" });
wb.SheetNames.forEach((sheetName, index) => {
if (sheetArray[index]) {
const sheet = wb.Sheets[sheetName];
if (sheet) {
const startCol = sheetArray[index].startCol ?? "A";
XLSX.utils.sheet_add_aoa(sheet, sheetArray[index].data, {
origin: startCol + sheetArray[index].headerNum,
});
const applyRangeFormat = (rangeStr: string | undefined, decimal: number) => {
if (!rangeStr) return;
const parts = rangeStr.split(",").map((part) => part.trim()).filter(Boolean);
for (const part of parts) {
const rangeInfo = XLSX.utils.decode_range(part);
for (let row = rangeInfo.s.r; row <= rangeInfo.e.r; row++) {
for (let col = rangeInfo.s.c; col <= rangeInfo.e.c; col++) {
const cellAddr = XLSX.utils.encode_cell({ r: row, c: col });
const cell = sheet[cellAddr];
if (cell && typeof cell.v === "number") {
if (decimal === 1) {
cell.z = "#,##0.0";
} else if (decimal === 2) {
cell.z = "#,##0.00";
} else if (decimal === 3) {
cell.z = "#,##0.000";
} else {
cell.z = "#,##0";
}
}
}
}
}
};
applyRangeFormat(sheetArray[index].decimalRange0, 0);
applyRangeFormat(sheetArray[index].decimalRange1, 1);
applyRangeFormat(sheetArray[index].decimalRange2, 2);
applyRangeFormat(sheetArray[index].decimalRange3, 3);
} else {
console.error(`Sheet "${sheetName}" is undefined.`);
}
}
});
}
await excelArrayStyle(
XLSX.write(wb, { bookType: "xlsx", type: "buffer" }),
sheetArray,
fileNm
);
return undefined;
};
type ExcelDownloadWithHeaderProps = {
data: JSON | JSON[];
@@ -251,4 +376,4 @@ export const downloadExcelWithKoreanHeaders = async ({
// 3. 헤더만 한글로 교체하는 커스텀 스타일 함수 호출
await excelStyleWithKoreanHeaders(buffer, headerRange, fileName, headerMapping, dataArray[0]);
};
};
@@ -172,7 +172,7 @@ GisApp.core.prototype = {
wmtsSource.setTileUrlFunction((tileCoord) => {
const [z, x, y] = tileCoord;
const matrix = matrixIds[z];
console.log(GisApp.NGII_WMTS_URL.replace("{z}", matrix).replace("{x}", x).replace("{y}", y));
// console.log(GisApp.NGII_WMTS_URL.replace("{z}", matrix).replace("{x}", x).replace("{y}", y));
return GisApp.NGII_WMTS_URL
.replace("{z}", matrix)
.replace("{x}", x)
@@ -26,6 +26,7 @@ type SearchBaseType = {
leftSelectClassName?: string;
buttonOrder?: ("query" | "init" | "excel")[];
initSidoValue?: string | '';
excelDisabled?: boolean;
}
export type ReturnType = {
partValue: string;
@@ -60,6 +61,7 @@ function SearchBaseForm ({
leftSelectClassName,
buttonOrder,
initSidoValue='',
excelDisabled=false,
}: SearchBaseType) {
const [partValue, setPartValue] = useState<string>("A");
const [sidoValue, setSidoValue] = useState<string>(initSidoValue?initSidoValue:"0");
@@ -87,7 +89,7 @@ function SearchBaseForm ({
);
case "excel":
return (
<Button className={btnClass} color="green" onClick={excelDownload} key="excel">
<Button className={btnClass} color="green" onClick={excelDownload} key="excel" disabled={excelDisabled}>
{excelName ? excelName : "엑셀다운로드"}
</Button>
);
@@ -191,4 +193,4 @@ function SearchBaseForm ({
}
export { SearchBaseForm };
@@ -1,7 +1,8 @@
// selectBox
import { Label } from "@common/components";
import React, { useEffect, useState } from "react";
// import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import {
Select,
SelectContent,
@@ -15,6 +16,7 @@ interface SelectBox1 {
text: string;
val: string | number;
hidden?: boolean;
selected?: boolean;
}
const SelectBox = React.forwardRef<
@@ -51,18 +53,22 @@ const SelectBox = React.forwardRef<
const [isOpen, setIsOpen] = useState(false);
// hidden이 true인 항목만 드롭다운에서 제외
const visibleItems = selectArray.filter((item) => !item.hidden);
const selectedItem = visibleItems.find((item) => item.selected);
const resolvedValue =
value === undefined && selectedItem ? selectedItem.val : value;
// 현재 값이 hidden 항목이거나 빈 값인 경우 undefined로 설정하여 placeholder가 표시되도록 함
const currentValue =
value !== undefined &&
resolvedValue !== undefined &&
hiddenItem &&
(String(value) === String(hiddenItem.val) ||
value === "" ||
value === "0" ||
value === 0)
(String(resolvedValue) === String(hiddenItem.val) ||
resolvedValue === "" ||
resolvedValue === "0" ||
resolvedValue === 0)
? undefined
: value !== undefined
? String(value)
: resolvedValue !== undefined
? String(resolvedValue)
: undefined;
return (
@@ -16,14 +16,16 @@ export type ChkClass = {
chk: boolean;
readonly: boolean;
};
type UiTableProps = {
type UiTableProps<T extends object> = {
headName: React.ReactNode | undefined;
tableData: { [key: string]: any }[];
tableData: T[];
headlist: UiHeadProps[];
pageSize: number;
total: number;
cellClick: ((index: number | undefined, headId: string) => void) | undefined;
onPageChange: (page: number) => void;
cellClassName?: (row: T, headId: string, rowIndex: number) => string;
rowClassName?: (row: T, rowIndex: number) => string;
className?: string;
resetPageNo?: boolean;
children?: React.ReactNode | undefined;
@@ -46,7 +48,7 @@ type UiHeadProps = {
colSpan?: number;
};
export function UiTable({
export function UiTable<T extends object = Record<string, unknown>>({
tableData,
headlist,
pageSize,
@@ -55,6 +57,8 @@ export function UiTable({
children,
cellClick,
onPageChange,
cellClassName,
rowClassName,
className = "",
resetPageNo = false,
boldArray = [],
@@ -63,7 +67,7 @@ export function UiTable({
curPage = 1,
headButton = undefined,
widthFixed = false
}: UiTableProps) {
}: UiTableProps<T>) {
const [cur, setCur] = useState<number>(curPage);
const [idx, setIdx] = useState<number>(index);
const [headerHeight, setHeaderHeight] = useState<number>(0);
@@ -148,8 +152,8 @@ export function UiTable({
</TableHeader>
<TableBody className="text-center">
{tableData.length > 0 ? (
tableData?.map((el: Record<string, unknown>, index) => (
<TableRow key={`row-${index}`}>
tableData?.map((el: T, index) => (
<TableRow key={`row-${index}`} className={rowClassName ? rowClassName(el, index) : ""}>
{headlist
.filter((item) => item.visible != false)
.map(
@@ -163,29 +167,32 @@ export function UiTable({
: cellClick
? "cursor-pointer"
: ""
} sticky top-20 z-40 bg-secondary !font-bold whitespace-pre-line`}
} ${cellClassName ? cellClassName(el, head.id, index) : ""} sticky top-20 z-40 bg-secondary !font-bold whitespace-pre-line`}
style={{ top: `${headerHeight + index * 32 - 4}px` }}
onClick={() => handleCellClick(index, head.id)}
key={`cell-${head.id}-${index}`}
>
{typeof el[head.id] !== "number"
? String(el[head.id] ?? "")
: String(el[head.id]).includes(".")
? String(el[head.id])
.split(".")
.map((part, index) =>
index === 0
? part.replace(
/\B(?=(\d{3})+(?!\d))/g,
",",
)
: part,
)
.join(".")
: String(el[head.id] ?? "").replace(
/\B(?=(\d{3})+(?!\d))/g,
",",
)}
{(() => {
const cellValue = (el as Record<string, unknown>)[head.id];
return typeof cellValue !== "number"
? String(cellValue ?? "")
: String(cellValue).includes(".")
? String(cellValue)
.split(".")
.map((part, index) =>
index === 0
? part.replace(
/\B(?=(\d{3})+(?!\d))/g,
",",
)
: part,
)
.join(".")
: String(cellValue ?? "").replace(
/\B(?=(\d{3})+(?!\d))/g,
",",
);
})()}
</TableCell>
) : (
<TableCell
@@ -194,47 +201,49 @@ export function UiTable({
: cellClick
? "cursor-pointer"
: ""
} mt-[${boldArray.length * 40}px] whitespace-pre-line`}
} ${cellClassName ? cellClassName(el, head.id, index) : ""} mt-[${boldArray.length * 40}px] whitespace-pre-line`}
onClick={() => handleCellClick(index, head.id)}
key={`cell-${head.id}-${index}`}
>
{
//체크박스 유형애서 readOnly가 필요한 경우를 위해서 ChkClass 를 사용해서 readOnly 처리함.
typeof el[head.id] === "object" &&
el[head.id] !== null &&
"chk" in (el[head.id] as object) &&
"readonly" in (el[head.id] as object) ? (
(() => {
const cellValue = (el as Record<string, unknown>)[head.id];
return typeof cellValue === "object" &&
cellValue !== null &&
"chk" in (cellValue as object) &&
"readonly" in (cellValue as object) ? (
<Checkbox
checked={
(el[head.id] as ChkClass)?.chk === true
(cellValue as ChkClass)?.chk === true
}
onCheckedChange={(checked) => {
{
console.log(el[head.id]);
console.log(cellValue);
}
if (
(el[head.id] as ChkClass).readonly == true
(cellValue as ChkClass).readonly == true
)
return;
(el[head.id] as ChkClass).chk =
(cellValue as ChkClass).chk =
Boolean(checked);
}}
>
<Check className="h-6 w-6 text-primary" />
</Checkbox>
) : typeof el[head.id] === "boolean" ? (
) : typeof cellValue === "boolean" ? (
<Checkbox
checked={el[head.id] === true}
checked={cellValue === true}
onCheckedChange={(checked) => {
el[head.id] = checked;
(el as Record<string, unknown>)[head.id] = checked;
}}
>
<Check className="h-6 w-6 text-primary" />
</Checkbox>
) : typeof el[head.id] !== "number" ? (
String(el[head.id] ?? "")
) : String(el[head.id]).includes(".") ? (
String(el[head.id])
) : typeof cellValue !== "number" ? (
String(cellValue ?? "")
) : String(cellValue).includes(".") ? (
String(cellValue)
.split(".")
.map((part, index) =>
index === 0
@@ -246,11 +255,12 @@ export function UiTable({
)
.join(".")
) : (
String(el[head.id] ?? "").replace(
String(cellValue ?? "").replace(
/\B(?=(\d{3})+(?!\d))/g,
",",
)
)
);
})()
}
</TableCell>
)),
@@ -1,27 +1,42 @@
@tailwind components;
@layer components {
/* 상단 메뉴 호버 시 하단 라인 - 기본 테마 */
.header-nav-link {
position: relative;
}
.header-nav-link::after {
content: '';
/* .header-nav-link::before {
content: "";
position: absolute;
bottom: -4px;
left: 0;
width: 0;
height: 2px;
background-color: #063A74;
background-color: #063a74;
transition: width 0.3s ease;
}
.header-nav-link:hover::after {
.header-nav-link:hover::before {
width: 100%;
} */
/* 네비게이션 아이템 사이 구분선 */
.header-nav-link::after {
content: "";
position: absolute;
left: -0.75rem;
top: 50%;
transform: translateY(-50%);
width: 1px !important;
height: 1.5rem !important;
background-color: var(--border);
}
.header-nav-link:first-child::after {
display: none;
}
[data-theme="blue"] {
header {
background: #00afe5;
@@ -35,7 +50,7 @@
color: #fff;
}
header button[role="button"].primary {
background-color: #008E8D !important;
background-color: #008e8d !important;
color: #fff !important;
}
nav a,
@@ -45,7 +60,7 @@
.header-nav-link:hover {
color: #fff !important;
}
.header-nav-link::after {
.header-nav-link::before {
background-color: #fff;
}
}
@@ -63,7 +78,7 @@
color: #fff;
}
header button[role="button"].primary {
background-color: #008E8D !important;
background-color: #008e8d !important;
color: #fff !important;
}
nav a,
@@ -73,9 +88,8 @@
.header-nav-link:hover {
color: #fff !important;
}
.header-nav-link::after {
.header-nav-link::before {
background-color: #fff;
}
}
}
@@ -21,6 +21,7 @@ import { SidebarTrigger } from "../sidebar";
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
import { SearchForm } from "./search-form";
import { NoticePopover } from "./notice-popover";
import { ScrollArea, ScrollBar } from "../scroll-area";
const appConfig = getConfig();
const userId = getCookie(appConfig.AUTH_USER_ID ?? "");
@@ -64,66 +65,99 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
queryFn: () => getSiteList(),
enabled: isLoggedIn && !viewOnly,
});
const logout = async () => {
try {
// 1. 백엔드 API 로그아웃 호출
const response = await fetch("/api/auth/logout", {
method: "POST",
credentials: "include", // ⭐ 쿠키 포함 중요!
});
if (!response.ok) throw new Error("Failed to logout");
const actualHost = window.location.hostname;
// 서브도메인 쿠키 삭제를 위한 루트 도메인 추출
const getRootDomain = (hostname: string): string => {
// localhost나 IP 주소인 경우
if (hostname === "localhost" || /^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
return hostname;
}
// hasuon.or.kr 도메인인 경우
if (hostname.includes("hasuon.or.kr")) {
return ".hasuon.or.kr";
}
// 일반적인 도메인의 경우 루트 도메인 추출 (예: sub.example.com → .example.com)
const parts = hostname.split(".");
if (parts.length >= 2) {
// 마지막 두 부분을 사용 (예: example.com)
return `.${parts.slice(-2).join(".")}`;
}
return hostname;
};
const domain = getRootDomain(actualHost);
const cookieOptions = domain.startsWith(".") || domain === "localhost"
? { domain }
: {};
// 쿠키 삭제 (서브도메인 포함)
if (appConfig.AUTH_USER_ID) {
removeCookie(appConfig.AUTH_USER_ID, cookieOptions);
if (!response.ok) {
console.error("Logout API failed");
// API 실패해도 계속 진행
}
if (appConfig.REFRESH_TOKEN) {
removeCookie(appConfig.REFRESH_TOKEN, cookieOptions);
}
if (appConfig.ACCESS_TOKEN) {
removeCookie(appConfig.ACCESS_TOKEN, cookieOptions);
}
if (appConfig.CLAIM_NAME) {
removeCookie(appConfig.CLAIM_NAME, cookieOptions);
}
removeCookie("site-id-key", cookieOptions);
removeCookie("site-theme-key", cookieOptions);
window.location.href = "/auth/login";
} catch (error) {
console.error(error);
console.error("Logout error:", error);
// 에러가 나도 계속 진행
} finally {
// 2. 클라이언트 쿠키 삭제 (백업)
clearClientCookies();
// 3. 로컬/세션 스토리지 정리
if (typeof window !== "undefined") {
localStorage.clear();
sessionStorage.clear();
}
// 4. 로그인 페이지로 리다이렉트
window.location.href = "/auth/login";
}
};
// 쿠키 삭제 헬퍼 함수
const clearClientCookies = () => {
const actualHost = window.location.hostname;
// 루트 도메인 추출
const getRootDomain = (hostname: string): string => {
if (hostname === "localhost" || /^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
return hostname;
}
if (hostname.includes("hasuon.or.kr")) {
return ".hasuon.or.kr";
}
const parts = hostname.split(".");
if (parts.length >= 2) {
return `.${parts.slice(-2).join(".")}`;
}
return hostname;
};
const domain = getRootDomain(actualHost);
// 삭제할 쿠키 목록
const cookiesToDelete = [
appConfig.AUTH_USER_ID,
appConfig.REFRESH_TOKEN,
appConfig.ACCESS_TOKEN,
appConfig.CLAIM_NAME,
"site-id-key",
"site-theme-key",
].filter(Boolean); // undefined 제거
// 여러 방법으로 쿠키 삭제 시도
cookiesToDelete.forEach((cookieName) => {
if (!cookieName) return;
// 방법 1: removeCookie 함수 사용
if (domain === "localhost") {
removeCookie(cookieName, {});
} else {
removeCookie(cookieName, { domain });
}
// 방법 2: 직접 document.cookie 설정 (도메인 포함)
if (domain !== "localhost") {
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain}`;
}
// 방법 3: 도메인 없이 삭제 (현재 도메인만)
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
// 방법 4: Secure 속성 포함 삭제
if (window.location.protocol === "https:") {
if (domain !== "localhost") {
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain}; secure`;
}
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; secure`;
}
});
};
const openPopup = useCallback(async (popup: Popup) => {
try {
const cookieName = `cs-popup-hide-for-day-${popup.popupSn}`;
@@ -176,8 +210,8 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
}, []);
return (
<header className="sticky top-0 z-30 box-border flex flex-col sm:flex-row h-[6rem] w-full items-center justify-between border-b border-solid border-border bg-background">
<div className="flex items-center px-2 sm:px-4 md:px-6 w-full sm:w-auto justify-between sm:justify-start">
<header className="sticky top-0 z-30 box-border flex h-[6rem] w-full flex-col items-center justify-between border-b border-solid border-border bg-background sm:flex-row">
<div className="flex w-full items-center justify-between px-2 sm:w-auto sm:justify-start sm:px-4 md:px-6">
<div className="flex items-center gap-2 sm:gap-4">
<SidebarTrigger className="size-12 !bg-transparent p-2 hover:!bg-accent [&>svg]:!size-full [&>svg]:text-foreground" />
<h1 className="flex flex-col items-center gap-2">
@@ -199,7 +233,10 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
<span
className="text-[1.2rem] font-semibold text-foreground"
style={{
letterSpacing: logoWidth > 0 ? `${(logoWidth - 120) / (siteInfo?.siteNm?.length || 1)}px` : '0.05em'
letterSpacing:
logoWidth > 0
? `${(logoWidth - 120) / (siteInfo?.siteNm?.length || 1)}px`
: "0.05em",
}}
>
{siteInfo?.siteNm}
@@ -214,7 +251,9 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="m-0 flex h-[3rem] items-center !bg-background px-3 py-0 text-label"
>
<i className={`diveicon ${isMobileMenuOpen ? 'di-menu-close-fill' : 'di-menu-open-fill'} static text-4xl`} />
<i
className={`diveicon ${isMobileMenuOpen ? "di-menu-close-fill" : "di-menu-open-fill"} static text-4xl`}
/>
</Button>
)}
{mounted && isLoggedIn && (
@@ -222,7 +261,9 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
<PopoverTrigger asChild>
<Button className="group m-0 flex h-[3rem] items-center !bg-background px-2 py-0 text-label">
<i className="diveicon di-account-circle static translate-y-0.5 pr-2 text-3xl" />
<span className="text-xs hidden xs:inline">{userInfo?.userName}</span>
<span className="xs:inline hidden text-xs">
{userInfo?.userName}
</span>
<i className="diveicon di-chevron-down static p-0 transition-transform group-data-[state=open]:rotate-180" />
</Button>
</PopoverTrigger>
@@ -245,57 +286,60 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
</div>
{/* 모바일에서 네비게이션 (토글 시 표시) */}
{isMobileMenuOpen && (
<div className="w-full sm:hidden border-t border-border bg-background py-2">
{mounted && userSiteList?.content && userSiteList.content.length > 0 && (
<nav className="flex flex-col items-center gap-2 px-4">
<div className="w-full border-t border-border bg-background py-2 sm:hidden">
{mounted &&
userSiteList?.content &&
userSiteList.content.length > 0 && (
<nav className="flex flex-col items-center gap-2 px-4">
{userSiteList.content.map((site) => (
<a
key={site.siteId}
href={site.dmnAddr}
className="w-full py-2 text-center text-xl font-medium text-foreground transition-colors hover:text-primary"
target="_blank"
rel="noopener noreferrer"
onClick={() => setIsMobileMenuOpen(false)}
>
{site.siteNm}
</a>
))}
</nav>
)}
</div>
)}
{/* 데스크톱에서 중간 네비게이션 */}
<ScrollArea className="hidden max-w-[calc(100vw-60rem)] flex-1 items-center justify-center px-2 py-4 sm:flex">
{mounted &&
userSiteList?.content &&
userSiteList.content.length > 0 && (
<nav className="flex w-full flex-nowrap items-center justify-start gap-3 overflow-x-auto whitespace-nowrap md:gap-6">
{userSiteList.content.map((site) => (
<a
key={site.siteId}
href={site.dmnAddr}
className="text-xl font-medium text-foreground hover:text-primary transition-colors w-full text-center py-2"
className="header-nav-link whitespace-nowrap text-sm font-medium text-foreground transition-colors hover:text-primary md:text-lg lg:text-2xl"
target="_blank"
rel="noopener noreferrer"
onClick={() => setIsMobileMenuOpen(false)}
>
{site.siteNm}
</a>
))}
</nav>
)}
</div>
)}
{/* 데스크톱에서 중간 네비게이션 */}
<div className="hidden sm:flex items-center flex-1 justify-center px-2">
{mounted && userSiteList?.content && userSiteList.content.length > 0 && (
<nav className="flex items-center gap-3 md:gap-6 flex-wrap justify-center">
{userSiteList.content.map((site, index) => (
<div key={site.siteId} className="flex items-center">
<a
href={site.dmnAddr}
className="text-sm md:text-lg lg:text-2xl font-medium text-foreground hover:text-primary transition-colors header-nav-link"
target="_blank"
rel="noopener noreferrer"
>
{site.siteNm}
</a>
{index < userSiteList.content.length - 1 && (
<div className="w-px h-4 md:h-6 bg-border ml-3 md:ml-6"></div>
)}
</div>
))}
</nav>
)}
</div>
<ScrollBar orientation="horizontal" />
</ScrollArea>
{/* 데스크톱에서 오른쪽 요소들 */}
<div className="hidden sm:flex items-center gap-2 px-2 md:px-4">
<div className="hidden items-center gap-2 px-2 sm:flex md:px-4">
<SearchForm />
{mounted ? (
isLoggedIn && (
<Popover>
<PopoverTrigger asChild>
<Button className="group m-0 flex h-[3rem] items-center !bg-background px-2 md:px-4 py-0 text-label">
<i className="diveicon di-account-circle static translate-y-0.5 pr-1 md:pr-2 text-2xl md:text-3xl" />
<span className="text-sm md:text-base hidden md:inline">{userInfo?.userName}</span>
<Button className="group m-0 flex h-[3rem] items-center !bg-background px-2 py-0 text-label md:px-4">
<i className="diveicon di-account-circle static translate-y-0.5 pr-1 text-2xl md:pr-2 md:text-3xl" />
<span className="hidden text-sm md:inline md:text-base">
{userInfo?.userName}
</span>
<i className="diveicon di-chevron-down static p-0 transition-transform group-data-[state=open]:rotate-180" />
</Button>
</PopoverTrigger>
@@ -34,6 +34,7 @@ export function NoticePopover() {
queryKey: [ENDPOINT.CMS_SERVICE.NOTICE_USER],
queryFn: () => getNoticeUserList(userId!),
enabled: !!userId,
select: (data) => data.content.filter((item) => item.notReadYn === "N"),
});
const { data: unreadNoticeCount } = useQuery({
@@ -53,11 +54,11 @@ export function NoticePopover() {
};
const handleSelectAll = () => {
if (noticeUserList?.content) {
if (selectedNoticeIds.length === noticeUserList.content.length) {
if (noticeUserList) {
if (selectedNoticeIds.length === noticeUserList.length) {
setSelectedNoticeIds([]);
} else {
const allIds = noticeUserList.content.map((item) => item.notiId);
const allIds = noticeUserList.map((item) => item.notiId);
setSelectedNoticeIds(allIds);
}
}
@@ -79,9 +80,9 @@ export function NoticePopover() {
}
};
const isAllSelected = noticeUserList?.content
? selectedNoticeIds.length === noticeUserList.content.length &&
noticeUserList.content.length > 0
const isAllSelected = noticeUserList
? selectedNoticeIds.length === noticeUserList.length &&
noticeUserList.length > 0
: false;
useEffect(() => {
@@ -107,7 +108,7 @@ export function NoticePopover() {
</Label>
</div>
<ul>
{noticeUserList?.content?.map((item) => (
{noticeUserList?.map((item) => (
<li
key={item.notiId}
className={cn(