diff --git a/apps/dm/app/main/_sections/IntroCards.tsx b/apps/dm/app/main/_sections/IntroCards.tsx index e8c13c3d6..6c690d08f 100644 --- a/apps/dm/app/main/_sections/IntroCards.tsx +++ b/apps/dm/app/main/_sections/IntroCards.tsx @@ -17,7 +17,7 @@ type IntroCardItem = { const INTRO_CARDS: IntroCardItem[] = [ { - title: "물 재이용 법", + title: "물재이용법", desc: "물의 재이용 촉진 및 지원에 관한 법률을 시행하고, 일부 법률은 개정되었습니다.", image: card01, href: "/intro#law-plan", diff --git a/apps/dm/components/CaseTabsCarousel.tsx b/apps/dm/components/CaseTabsCarousel.tsx index f9ad4f2a8..5ce6e9422 100644 --- a/apps/dm/components/CaseTabsCarousel.tsx +++ b/apps/dm/components/CaseTabsCarousel.tsx @@ -83,14 +83,14 @@ const MOCK: Record = { sewage: [ { id: "sewage-1", - title: "구리시 하수 재이용", + title: "구리시 하수처리수 재이용", desc: "고농도 미세먼지 발생 및 폭염의 급증에 따라 공공하수처리시설 발생 재이용수 일부를 도로자동청소시스템에 활용하여 미세먼지 저감 및 폭염 취약성 완화", imageSrc: "/images/landing/business/sewage/guri_01.png", href: "/example#example-sewage-guri", }, { id: "sewage-2", - title: "부천시 하수재이용", + title: "부천시 하수처리수 재이용", desc: "굴포하수처리시설 하수처리수 재이용수 생산시설 설치 운영을 통해 여월천 등 부천 관내 하천의 건천화 방지, 하천 생태계 보존 및 주민들에게 친수 공간 제공", imageSrc: "/images/landing/business/sewage/bucheon_01.png", href: "/example#example-sewage-bucheon", @@ -104,42 +104,42 @@ const MOCK: Record = { }, { id: "sewage-4", - title: "파주시 하수 재이용 시설", + title: "파주시 하수처리수 재이용", desc: "운정 및 금촌하수처리시설의 하수처리수를 막공법으로 재처리하여 파주LCD산업단지 내 3개 업체에 공업용수를 공급(LG디스플레이, 희성전자, 엔비텍)", imageSrc: "/images/landing/business/sewage/paju_01.png", href: "/example#example-sewage-paju", }, { id: "sewage-5", - title: "포항시 하수 재이용 시설", + title: "포항시 하수처리수 재이용", desc: "포항시 내 공단 개발 확대로 지속적이고 안정적인 공업용수 공급 추진(포스코, 동국산업 등), 포항하수처리장 하수처리수를 재처리하여 공업용수로 공급", imageSrc: "/images/landing/business/sewage/pohang_01.png", href: "/example#example-sewage-pohang", }, { id: "sewage-6", - title: "울산광역시 하수 재이용 시설", + title: "울산광역시 하수처리수 재이용", desc: "18년 갈수기시 낙동강 수질 악화로 울산석유화학단지 용수 공급 부족을 해소하기 위해 미포산단 내 한화케미칼 등 8개사 공업용수 공급", imageSrc: "/images/landing/business/sewage/ulsan_01.png", href: "/example#example-sewage-ulsan", }, { id: "sewage-7", - title: "여수시 하수 재이용 시설", + title: "여수시 하수처리수 재이용", desc: "수요자가 원하는 양질의 공업용수를 저렴하게 공급하여 한정된 수자원의 효율적 이용 및 지역적 물 부족 해소, 하수재이용을 통한 오염총량 저감 및 방류수역 수질개선을 위한 여수국가산단 내 공업용수 공급(GS칼텍스, YNCC 등)", imageSrc: "/images/landing/business/sewage/yeosu_01.png", href: "/example#example-sewage-yeosu", }, { id: "sewage-8", - title: "보령시 하수 재이용 시설", + title: "보령시 하수처리수 재이용", desc: "한국중부발전(주)의 보령 및 신보령화력발전소에 10,000㎥/일의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업 추진, 하수처리수 재이용을 통한 수자원 절약 및 지역 물 부족 해소", imageSrc: "/images/landing/business/sewage/boryeong_01.png", href: "/example#example-sewage-boryeong", }, { id: "sewage-9", - title: "아산시 하수 재이용 시설", + title: "아산시 하수처리수 재이용", desc: "신도시 개발에 따른 인구 급증으로 발생하는 하수를 재처리하여 탕정산업단지(삼성디스플레이)에 공업용수를 안정적으로 공급함으로써 지역기반 및 활성화를 도모", imageSrc: "/images/landing/business/sewage/asan_01.png", href: "/example#example-sewage-asan", diff --git a/apps/dm/public/images/landing/intro/rel_note.png b/apps/dm/public/images/landing/intro/rel_note.png index 6b26f5248..94880c743 100644 Binary files a/apps/dm/public/images/landing/intro/rel_note.png and b/apps/dm/public/images/landing/intro/rel_note.png differ diff --git a/apps/fo/app/components/common/FacilityTermModalHeader.tsx b/apps/fo/app/components/common/FacilityTermModalHeader.tsx index a46b3652d..c8d2bba40 100644 --- a/apps/fo/app/components/common/FacilityTermModalHeader.tsx +++ b/apps/fo/app/components/common/FacilityTermModalHeader.tsx @@ -237,7 +237,7 @@ export default function FacilityTermModalHeader({ onSelectValue={(val) => setFlowPanel(val.toString())} className="mx-3 w-[150px]" label="" - selectClass="w-full p-1 border rounded bg-white" + selectClass="w-full rounded border border-input bg-form p-1" /> @@ -251,7 +251,7 @@ export default function FacilityTermModalHeader({ mode="single" selected={startDate} onSelect={(date) => handleStartDateSelect(date as Date)} - className="w-full p-1 border rounded bg-white" + className="w-full rounded border border-input bg-form p-1 text-foreground" disabled={(date) => !isDateInRanges(date)} defaultMonth={startDate} /> @@ -262,7 +262,7 @@ export default function FacilityTermModalHeader({ mode="single" selected={endDate} onSelect={(date) => handleEndDateSelect(date as Date)} - className="w-full p-1 border rounded bg-white" + className="w-full rounded border border-input bg-form p-1 text-foreground" disabled={(date) => !isDateInRanges(date)} defaultMonth={endDate} /> @@ -275,4 +275,4 @@ export default function FacilityTermModalHeader({ ); -} \ No newline at end of file +} diff --git a/apps/fo/app/components/common/FacilityTermModalHeaderMonth.tsx b/apps/fo/app/components/common/FacilityTermModalHeaderMonth.tsx index a284d04cf..07c4a3f01 100644 --- a/apps/fo/app/components/common/FacilityTermModalHeaderMonth.tsx +++ b/apps/fo/app/components/common/FacilityTermModalHeaderMonth.tsx @@ -219,7 +219,7 @@ export default function FacilityTermModalHeaderMonth -
+
@@ -237,7 +237,7 @@ export default function FacilityTermModalHeaderMonth setFlowPanel(val.toString())} className="mx-3 w-[150px]" label="" - selectClass="w-full p-1 border rounded bg-white" + selectClass="w-full rounded border border-input bg-form p-1" /> @@ -251,7 +251,7 @@ export default function FacilityTermModalHeaderMonth handleStartDateSelect(date as Date)} - className="w-full p-1 border rounded bg-white" + className="w-full rounded border border-input bg-form p-1 text-foreground" defaultMonth={startDate} /> @@ -261,7 +261,7 @@ export default function FacilityTermModalHeaderMonth handleEndDateSelect(date as Date)} - className="w-full p-1 border rounded bg-white" + className="w-full rounded border border-input bg-form p-1 text-foreground" defaultMonth={endDate} /> @@ -273,4 +273,4 @@ export default function FacilityTermModalHeaderMonth ); -} \ No newline at end of file +} diff --git a/apps/fo/app/components/common/HeadMakeColSpan.tsx b/apps/fo/app/components/common/HeadMakeColSpan.tsx index 06617839a..43476204f 100644 --- a/apps/fo/app/components/common/HeadMakeColSpan.tsx +++ b/apps/fo/app/components/common/HeadMakeColSpan.tsx @@ -5,55 +5,55 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp return ( layoutNum === 'A' ? ( <> - {upHeadList.map((head: TableUpperProps) => + {upHeadList.map((head: TableUpperProps) => (head.id === 'trtFrfNm' ? {head.title} : head.id === 'laeNm' ? {head.title} : head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? {head.title} : head.id === 'trstOprEtpNm' ? {head.title} : - head.upName === 'upChangeRe' && head.upSequnce === 1 ? {'행정구역'} : - !head.upName ? {head.title} : null))} + head.upName === 'upChangeRe' && head.upSequnce === 1 ? {'행정구역'} : + !head.upName ? {head.title} : null))} - + {upHeadList.map((head: TableUpperProps) => { if (head.upName === 'upChangeRe') { - return {head.title}; + return {head.title}; } })} ) : layoutNum === 'R' ? ( <> - {upHeadList.map((head: TableUpperProps) => + {upHeadList.map((head: TableUpperProps) => (head.id === 'trtFrfNm' ? {head.title} : head.id === 'laeNm' ? {head.title} : head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? {head.title} : head.id === 'trstOprEtpNm' ? {head.title} : - head.upName === 'upChangeRe' && head.upSequnce === 1 ? {'유역'} : - !head.upName ? {head.title} : null))} + head.upName === 'upChangeRe' && head.upSequnce === 1 ? {'유역'} : + !head.upName ? {head.title} : null))} - + {upHeadList.map((head: TableUpperProps) => { if (head.upName === 'upChangeRe') { - return {head.title}; + return {head.title}; } })} ) : ( <> - {upHeadList.map((head: TableUpperProps) => + {upHeadList.map((head: TableUpperProps) => (head.id === 'trtFrfNm' ? {head.title} : head.id === 'laeNm' ? {head.title} : head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? {head.title} : head.id === 'trstOprEtpNm' ? {head.title} : - head.upName === 'upChangeRe1' && head.upSequnce === 1 ? {'권역'} : - head.upName === 'upChangeRe2' && head.upSequnce === 1 ? {'행정구역'} : - !head.upName ? {head.title} : null))} + head.upName === 'upChangeRe1' && head.upSequnce === 1 ? {'권역'} : + head.upName === 'upChangeRe2' && head.upSequnce === 1 ? {'행정구역'} : + !head.upName ? {head.title} : null))} - + {upHeadList.map((head: TableUpperProps) => { if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2') { - return {head.title}; + return {head.title}; } })} diff --git a/apps/fo/app/components/common/SearchInput.tsx b/apps/fo/app/components/common/SearchInput.tsx index 7e2ef522e..e8ab2a567 100644 --- a/apps/fo/app/components/common/SearchInput.tsx +++ b/apps/fo/app/components/common/SearchInput.tsx @@ -99,22 +99,22 @@ export default function SearchInputText({label=undefined, setValue=()=>{}, sidoS return (
- {label && } + {label && } {isOpen && processFacilityList.length > 0 && ( -
+
{processFacilityList.map((item : facilityListProps, index : number) => (
handleSuggestionClick(item.bscFctCd, item.bscFctNm, Number(item.fctCpc), item.trtFrfNm, item.efflWqltStndrdNm)} role="button" tabIndex={0} diff --git a/apps/fo/app/components/common/UnderComment.tsx b/apps/fo/app/components/common/UnderComment.tsx index 898eb861e..eda31e81a 100644 --- a/apps/fo/app/components/common/UnderComment.tsx +++ b/apps/fo/app/components/common/UnderComment.tsx @@ -17,7 +17,7 @@ function UnderComment({commentStartPoint="☞", commentArray=[], divHeight="14"}
{commentArray.map((item, index) => ( - + {commentStartPoint} {item}
@@ -28,4 +28,4 @@ function UnderComment({commentStartPoint="☞", commentArray=[], divHeight="14"} } } -export default UnderComment; \ No newline at end of file +export default UnderComment; diff --git a/apps/fo/app/pages/(map-layout)/map/page.tsx b/apps/fo/app/pages/(map-layout)/map/page.tsx index 4fa553c97..09e8f969f 100644 --- a/apps/fo/app/pages/(map-layout)/map/page.tsx +++ b/apps/fo/app/pages/(map-layout)/map/page.tsx @@ -127,15 +127,18 @@ const Map = () => { } const el = overlayContainerRef.current; el.className = - "ol-custom-overlay shadow-xl rounded-2xl bg-white/95 backdrop-blur p-3 border border-gray-200 min-w-[220px] max-w-[350px] "; + "ol-custom-overlay shadow-xl rounded-2xl backdrop-blur p-3 min-w-[220px] max-w-[350px]"; + el.style.backgroundColor = "color-mix(in srgb, var(--popover) 95%, transparent)"; + el.style.color = "var(--popover-foreground)"; + el.style.border = "1px solid var(--border)"; el.innerHTML = ` -
${title}
-
+
${title}
+
${Object.entries(props) .map( ([k, v]) => ` - + ` ) diff --git a/apps/fo/app/pages/(with-layout)/dashboard/drainageEquipment/page.tsx b/apps/fo/app/pages/(with-layout)/dashboard/drainageEquipment/page.tsx index fb59cfc13..3b670f651 100644 --- a/apps/fo/app/pages/(with-layout)/dashboard/drainageEquipment/page.tsx +++ b/apps/fo/app/pages/(with-layout)/dashboard/drainageEquipment/page.tsx @@ -54,23 +54,23 @@ export default function DrainageEquipment({searchYear = '', fcltyList = []}: Dra const HeadMakeColSpan = ({ upHeadList }: { upHeadList: TableUpperProps[] }) => { return ( <> - {upHeadList.map((head: TableUpperProps) => + {upHeadList.map((head: TableUpperProps) => (head.id === 'part1' ? 구분 : head.id === 'part2' ? 구분 : head.id == 'nowSum' ? {'현재\n(기준년도)'} : head.id == 'stepOneSum' ? {'변경(금회계획)'} : head.id == 'etc' ? 비고 : null))} - {upHeadList.map((head: TableUpperProps) => + {upHeadList.map((head: TableUpperProps) => (head.id === 'stepOneSum' ? 1단계 : head.id === 'stepTwoSum' ? 2단계 : head.id === 'stepThreeSum' ? 3단계 : head.id === 'stepFourSum' ? 4단계 : null))} - + {upHeadList.map((head: TableUpperProps) => { if (head.upName === 'upChangeRe2' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') { - return {head.title}; + return {head.title}; } })} diff --git a/apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/modal/page.tsx b/apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/modal/page.tsx index 850d52528..d08a5f78b 100644 --- a/apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/modal/page.tsx +++ b/apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/modal/page.tsx @@ -147,7 +147,7 @@ export default function DashBoardChartModal({item='', period='', title='',modalD if (label) { config[label] = { label: label, - color : "hsl(var(--chart-1))" + color : "var(--chart-1)" }; } return config; diff --git a/apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/page.tsx b/apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/page.tsx index e65fe3cc8..ac33ffe28 100644 --- a/apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/page.tsx +++ b/apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/page.tsx @@ -32,12 +32,12 @@ const headMakeColSpanPlanFclty = (upHeadList: TableUpperProps[], planYearList: s return ( <> { upHeadList.map((head: TableUpperProps) => - (!head.upSequnce?{head.title}: - head.upName === 'upChangeRe'&& head.upSequnce=== 1?{'시설계획\n(금회)'}:null))} + (!head.upSequnce?{head.title}: + head.upName === 'upChangeRe'&& head.upSequnce=== 1?{'시설계획\n(금회)'}:null))} {upHeadList.map((head: TableUpperProps, index: number) => - (head.upName === 'upChangeRe'?{index===0?head.title:head.title+'\n('+planYearList[index-1]+')'}:null))} + (head.upName === 'upChangeRe'?{index===0?head.title:head.title+'\n('+planYearList[index-1]+')'}:null))} ) } @@ -52,20 +52,20 @@ const headMakeColSpanPlanFclty = (upHeadList: TableUpperProps[], planYearList: s const headMakeColSpanOperationStatus = (upHeadList: TableUpperProps[], searchYear: string) => { return ( <> - {searchYear+'년도 운영실태'} + {searchYear+'년도 운영실태'} { upHeadList.map((head: TableUpperProps) => - (head.upName === 'upChangeRe1'&&head.upSequnce===1?{head.title}: - !head.upSequnce?{head.title}: - head.upName === 'upChangeRe2'&&head.upSequnce===1?{'연간 운영실태'}: - head.upName === 'upChangeRe3'&&head.upSequnce===1?{'하절기 운영실태\n(6~8월)'}: - head.upName === 'upChangeRe4'&&head.upSequnce===1?{'동절기 운영실태\n(1~2월)'}: - head.upName === 'upChangeRe5'&&head.upSequnce===1?{'지하수 유입량 추정'}:null))} + (head.upName === 'upChangeRe1'&&head.upSequnce===1?{head.title}: + !head.upSequnce?{head.title}: + head.upName === 'upChangeRe2'&&head.upSequnce===1?{'연간 운영실태'}: + head.upName === 'upChangeRe3'&&head.upSequnce===1?{'하절기 운영실태\n(6~8월)'}: + head.upName === 'upChangeRe4'&&head.upSequnce===1?{'동절기 운영실태\n(1~2월)'}: + head.upName === 'upChangeRe5'&&head.upSequnce===1?{'지하수 유입량 추정'}:null))} {upHeadList.map((head: TableUpperProps) => - (head.upName === 'upChangeRe2'||head.upName === 'upChangeRe3'||head.upName === 'upChangeRe4'||head.upName === 'upChangeRe5'?{head.title}:null))} + (head.upName === 'upChangeRe2'||head.upName === 'upChangeRe3'||head.upName === 'upChangeRe4'||head.upName === 'upChangeRe5'?{head.title}:null))} ) } @@ -239,18 +239,18 @@ export default function ProcessFacilitySearch({searchYear = '', fcltyList = []}: :((index === 6 || index === 7 || index === 8) && (head.id === 'now' || head.id === 'stepOne' || head.id === 'stepThree' || head.id === 'stepFour'))? null :((index === 5) && (head.id === 'provisionalApproval' || head.id === 'stepTwo'))? - {el[head.id]} + {el[head.id]} :((index === 5) && (head.id === 'now' || head.id === 'stepOne' || head.id === 'stepThree'|| head.id === 'stepFour'))? null :(index < 5 || index > 5)&& (index!= 4) && (head.id ==='provisionalApproval'||head.id ==='now'||head.id ==='stepOne'||head.id ==='stepTwo'||head.id ==='stepThree'||head.id ==='stepFour') ?{el[head.id]} :(head.id === "provisionalApproval" && index === 4) - ?{roundNumber(String((Number(waterUsage1)+Number(waterUsage2)+Number(waterUsage3))/3), 0)} + ?{roundNumber(String((Number(waterUsage1)+Number(waterUsage2)+Number(waterUsage3))/3), 0)} :((head.id ==='now'||head.id ==='stepOne'||head.id ==='stepTwo'||head.id ==='stepThree'||head.id ==='stepFour') ) ?null :(head.id === 'item' && index != 5)? - {el[head.id]} - :{el[head.id]} + {el[head.id]} + :{el[head.id]} ))} ))} @@ -278,15 +278,15 @@ export default function ProcessFacilitySearch({searchYear = '', fcltyList = []}: (head.id === 'rdiRain'&&index === 0)?{el['rdiRain']}: (head.id === 'rdiRain'&&index === 7)?: (head.id === 'waterQlty')?{el[head.id]}: - ((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=0 && index<=4))?{fncLoadModal('flowRate', 'year', el['waterQlty'] as string);}}>{el[head.id]}: - ((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=5 && index<=10))?{fncLoadModal('waterQlty', 'year', el['waterQlty'] as string);}}>{el[head.id]}: - ((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=11 && index<=12))?{fncLoadModal('chongin', 'year', el['waterQlty'] as string);}}>{el[head.id]}: - ((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=0 && index<=4))?{fncLoadModal('flowRate', 'summer', el['waterQlty'] as string);}}>{el[head.id]}: - ((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=5 && index<=10))?{fncLoadModal('waterQlty', 'summer', el['waterQlty'] as string);}}>{el[head.id]}: - ((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=11 && index<=12))?{fncLoadModal('chongin', 'summer', el['waterQlty'] as string);}}>{el[head.id]}: - ((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=0 && index<=4))?{fncLoadModal('flowRate', 'winter', el['waterQlty'] as string);}}>{el[head.id]}: - ((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=5 && index<=10))?{fncLoadModal('waterQlty', 'winter', el['waterQlty'] as string);}}>{el[head.id]}: - ((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=11 && index<=12))?{fncLoadModal('chongin', 'winter', el['waterQlty'] as string);}}>{el[head.id]}: + ((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=0 && index<=4))?{fncLoadModal('flowRate', 'year', el['waterQlty'] as string);}}>{el[head.id]}: + ((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=5 && index<=10))?{fncLoadModal('waterQlty', 'year', el['waterQlty'] as string);}}>{el[head.id]}: + ((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=11 && index<=12))?{fncLoadModal('chongin', 'year', el['waterQlty'] as string);}}>{el[head.id]}: + ((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=0 && index<=4))?{fncLoadModal('flowRate', 'summer', el['waterQlty'] as string);}}>{el[head.id]}: + ((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=5 && index<=10))?{fncLoadModal('waterQlty', 'summer', el['waterQlty'] as string);}}>{el[head.id]}: + ((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=11 && index<=12))?{fncLoadModal('chongin', 'summer', el['waterQlty'] as string);}}>{el[head.id]}: + ((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=0 && index<=4))?{fncLoadModal('flowRate', 'winter', el['waterQlty'] as string);}}>{el[head.id]}: + ((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=5 && index<=10))?{fncLoadModal('waterQlty', 'winter', el['waterQlty'] as string);}}>{el[head.id]}: + ((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=11 && index<=12))?{fncLoadModal('chongin', 'winter', el['waterQlty'] as string);}}>{el[head.id]}: null ))} diff --git a/apps/fo/app/pages/(with-layout)/dashboard/maingraph/page.tsx b/apps/fo/app/pages/(with-layout)/dashboard/maingraph/page.tsx index 8eddbd098..595fe89e1 100644 --- a/apps/fo/app/pages/(with-layout)/dashboard/maingraph/page.tsx +++ b/apps/fo/app/pages/(with-layout)/dashboard/maingraph/page.tsx @@ -60,7 +60,14 @@ export default function DashBoardChart({searchYear = '', fcltyList = []}: facili /** * 차트 Config */ - const colors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#00b50c", "#D4A5A5"]; + const colors = [ + "var(--chart-5)", + "var(--chart-2)", + "var(--chart-1)", + "var(--chart-3)", + "var(--primary)", + "var(--chart-6)", + ]; //유입하수량-강수량 const chartConfig_inflowRain = { diff --git a/apps/fo/app/pages/(with-layout)/dashboard/rainManage/page.tsx b/apps/fo/app/pages/(with-layout)/dashboard/rainManage/page.tsx index 55e6275c8..33f1d298b 100644 --- a/apps/fo/app/pages/(with-layout)/dashboard/rainManage/page.tsx +++ b/apps/fo/app/pages/(with-layout)/dashboard/rainManage/page.tsx @@ -50,7 +50,7 @@ export default function RainManageTab({ searchYear = '', fcltyList = [] }: facil widthFixed={true} > - ☞ 일수에 강우영향일 포함됨 + ☞ 일수에 강우영향일 포함됨 ) } diff --git a/apps/fo/app/pages/(with-layout)/dashboardMain/page.tsx b/apps/fo/app/pages/(with-layout)/dashboardMain/page.tsx index 67e704570..05d906500 100644 --- a/apps/fo/app/pages/(with-layout)/dashboardMain/page.tsx +++ b/apps/fo/app/pages/(with-layout)/dashboardMain/page.tsx @@ -1,5 +1,5 @@ /** - * 대시보드 - 표로보기 상단구역 페에지 + * 대시보드 - 표로보기 상단구역 페이지 * * - 대시보드 - 표로보기의 3개의 탭 상단에 조회년도, 행정정보, 시설명의 구역을 컴포넌트 */ diff --git a/apps/fo/app/pages/(with-layout)/facilityPrecipitation/modal/page.tsx b/apps/fo/app/pages/(with-layout)/facilityPrecipitation/modal/page.tsx index 811f1c0ff..392deb858 100644 --- a/apps/fo/app/pages/(with-layout)/facilityPrecipitation/modal/page.tsx +++ b/apps/fo/app/pages/(with-layout)/facilityPrecipitation/modal/page.tsx @@ -198,7 +198,7 @@ export default function PrecipitationModal(props: PrecipitationModalProps) { mode="single" selected={startDate} onSelect={(date) => handleStartDateSelect(date as Date)} - className="w-full p-1 border rounded bg-white" + className="w-full rounded border border-input bg-form p-1 text-foreground" disabled={(date) => !isDateInRanges(date)} defaultMonth={startDate} /> @@ -209,7 +209,7 @@ export default function PrecipitationModal(props: PrecipitationModalProps) { mode="single" selected={endDate} onSelect={(date) => handleEndDateSelect(date as Date)} - className="w-full p-1 border rounded bg-white" + className="w-full rounded border border-input bg-form p-1 text-foreground" disabled={(date) => !isDateInRanges(date)} defaultMonth={endDate} /> @@ -239,4 +239,4 @@ export default function PrecipitationModal(props: PrecipitationModalProps) { ); -} \ No newline at end of file +} diff --git a/apps/fo/app/pages/(with-layout)/facilityWaterQlty/HeadMakeColSpan.tsx b/apps/fo/app/pages/(with-layout)/facilityWaterQlty/HeadMakeColSpan.tsx index aa1e36abd..472c13e6a 100644 --- a/apps/fo/app/pages/(with-layout)/facilityWaterQlty/HeadMakeColSpan.tsx +++ b/apps/fo/app/pages/(with-layout)/facilityWaterQlty/HeadMakeColSpan.tsx @@ -17,27 +17,27 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp return ( layoutNum === 0 ? ( <> - + {upHeadList.map((head: TableUpperProps) => { if(!head.upName) { - return {head.title} + return {head.title} }else if(head.upName==="upChangeRe1"&&head.upSequnce===1) { - return 행정구역 + return 행정구역 }else if(head.upName==="upChangeRe3"&&head.upSequnce===1) { - return 유입 + return 유입 }else if(head.upName==="upChangeRe4"&&head.upSequnce===1) { - return 유출 + return 유출 } })} - + {upHeadList.map((head: TableUpperProps, idx: number) => { if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4') { if (idx == 1 || idx == 2 || idx == 3) { - return {head.title}; + return {head.title}; } else { - return {head.title}; + return {head.title}; } } })} @@ -45,27 +45,27 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp ) : layoutNum === 1 ? ( <> - + {upHeadList.map((head: TableUpperProps) => { if(!head.upName) { - return {head.title} + return {head.title} }else if(head.upName==="upChangeRe1"&&head.upSequnce===1) { - return 권역 + return 권역 }else if(head.upName==="upChangeRe3"&&head.upSequnce===1) { - return 유입 + return 유입 }else if(head.upName==="upChangeRe4"&&head.upSequnce===1) { - return 유출 + return 유출 } })} - + {upHeadList.map((head: TableUpperProps, idx: number) => { if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4') { if (idx == 1 || idx == 2 || idx == 3) { - return {head.title}; + return {head.title}; } else { - return {head.title}; + return {head.title}; } } })} @@ -73,29 +73,29 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp ) : layoutNum === 2 ? ( <> - + {upHeadList.map((head: TableUpperProps) => { if(!head.upName) { - return {head.title} + return {head.title} }else if(head.upName==="upChangeRe1"&&head.upSequnce===1) { - return 권역 + return 권역 }else if(head.upName==="upChangeRe2"&&head.upSequnce===1) { - return 행정구역 + return 행정구역 }else if(head.upName==="upChangeRe3"&&head.upSequnce===1) { - return 유입 + return 유입 }else if(head.upName==="upChangeRe4"&&head.upSequnce===1) { - return 유출 + return 유출 } })} - + {upHeadList.map((head: TableUpperProps, idx: number) => { if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4') { if (idx == 1 || idx == 2 || idx == 3) { - return {head.title}; + return {head.title}; } else { - return {head.title}; + return {head.title}; } } })} @@ -103,25 +103,25 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp ) : layoutNum === 3 ? ( <> - + {upHeadList.map((head: TableUpperProps) => { if(!head.upName) { - return {head.title} + return {head.title} }else if(head.upName==="upChangeRe1"&&head.upSequnce===1) { - return 유입 + return 유입 }else if(head.upName==="upChangeRe2"&&head.upSequnce===1) { - return 방류 + return 방류 } })} - + {upHeadList.map((head: TableUpperProps, idx: number) => { if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2') { if (idx == 1 || idx == 2 || idx == 3) { - return {head.title}; + return {head.title}; } else { - return {head.title}; + return {head.title}; } } })} @@ -129,23 +129,23 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp ) : layoutNum === 4 ? ( <> - + {upHeadList.map((head: TableUpperProps) => { if(!head.upName) { - return {head.title} + return {head.title} }else if(head.upName==="upChangeRe"&&head.upSequnce===1) { - return 방류 + return 방류 } })} - + {upHeadList.map((head: TableUpperProps, idx: number) => { if (head.upName === 'upChangeRe') { if (idx == 1 || idx == 2 || idx == 3) { - return {head.title}; + return {head.title}; } else { - return {head.title}; + return {head.title}; } } })} @@ -153,21 +153,21 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp ) : layoutNum === 5 ? ( // 총인설비 수질현황 헤더 <> - + {upHeadList.map((head: TableUpperProps) => { if(!head.upName) { - return {head.title} + return {head.title} }else if(head.upName==="upChangeRe1" && head.upSequnce===1) { - return 유입 + return 유입 }else if(head.upName==="upChangeRe2" && head.upSequnce===1) { - return 방류 + return 방류 } })} - + {upHeadList.map((head: TableUpperProps) => { if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2') { - return {head.title}; + return {head.title}; } })} diff --git a/apps/fo/app/pages/(with-layout)/flowRate/modal/page.tsx b/apps/fo/app/pages/(with-layout)/flowRate/modal/page.tsx index 0cffa775e..ecf09d7da 100644 --- a/apps/fo/app/pages/(with-layout)/flowRate/modal/page.tsx +++ b/apps/fo/app/pages/(with-layout)/flowRate/modal/page.tsx @@ -237,7 +237,7 @@ export default function FlowRateModal(props: { fcltyName: string; facilityCd: st 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" : ""; + return value > 3 ? "fo-rain-impact-row" : ""; }, [activeTab], ); diff --git a/apps/fo/app/pages/(with-layout)/processArea/HeadMakeColSpan.tsx b/apps/fo/app/pages/(with-layout)/processArea/HeadMakeColSpan.tsx index 4045ed7b2..89829445e 100644 --- a/apps/fo/app/pages/(with-layout)/processArea/HeadMakeColSpan.tsx +++ b/apps/fo/app/pages/(with-layout)/processArea/HeadMakeColSpan.tsx @@ -5,77 +5,77 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp return ( layoutNum === 0 ? ( <> - {upHeadList.map((head: TableUpperProps) => + {upHeadList.map((head: TableUpperProps) => (head.id === 'trtFrfNm' ? {head.title} : head.id === 'laeNm' ? {head.title} : head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? {head.title} : head.id === 'trstOprEtpNm' ? {head.title} : - head.upName === 'upChangeRe' && head.upSequnce === 1 ? {'행정구역'} : - head.upName === 'upChangeRe3' && head.upSequnce === 1 ? {'증설계획'} : - head.upName === 'upChangeRe4' && head.upSequnce === 1 ? {'시설계획'} : - !head.upName ? {head.title} : null))} + head.upName === 'upChangeRe' && head.upSequnce === 1 ? {'행정구역'} : + head.upName === 'upChangeRe3' && head.upSequnce === 1 ? {'증설계획'} : + head.upName === 'upChangeRe4' && head.upSequnce === 1 ? {'시설계획'} : + !head.upName ? {head.title} : null))} - + {upHeadList.map((head: TableUpperProps) => ( - head.upName === 'upChangeRe4' && head.upSequnce === 1 ? {'증설계획(m³/일)'} : - head.upName === 'upChangeRe5' && head.upSequnce === 1 ? {'처리인구(인)'} : - head.upName === 'upChangeRe6' && head.upSequnce === 1 ? {'처리구역(k㎡)'} : + head.upName === 'upChangeRe4' && head.upSequnce === 1 ? {'증설계획(m³/일)'} : + head.upName === 'upChangeRe5' && head.upSequnce === 1 ? {'처리인구(인)'} : + head.upName === 'upChangeRe6' && head.upSequnce === 1 ? {'처리구역(k㎡)'} : null ))} - + {upHeadList.map((head: TableUpperProps) => { if (head.upName === 'upChangeRe' || head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') { - return {head.title}; + return {head.title}; } })} ) : layoutNum === 1 ? ( <> - {upHeadList.map((head: TableUpperProps) => + {upHeadList.map((head: TableUpperProps) => (head.id === 'trtFrfNm' ? {head.title} : head.id === 'laeNm' ? {head.title} : head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? {head.title} : head.id === 'trstOprEtpNm' ? {head.title} : - head.upName === 'upChangeRe' && head.upSequnce === 1 ? {'유역'} : - head.upName === 'upChangeRe3' && head.upSequnce === 1 ? {'증설계획'} : - !head.upName ? {head.title} : null))} + head.upName === 'upChangeRe' && head.upSequnce === 1 ? {'유역'} : + head.upName === 'upChangeRe3' && head.upSequnce === 1 ? {'증설계획'} : + !head.upName ? {head.title} : null))} - + {upHeadList.map((head: TableUpperProps) => ( - head.upName === 'upChangeRe4' && head.upSequnce === 1 ? {'증설계획(m³/일)'} : - head.upName === 'upChangeRe5' && head.upSequnce === 1 ? {'처리인구(인)'} : - head.upName === 'upChangeRe6' && head.upSequnce === 1 ? {'처리구역(k㎡)'} : + head.upName === 'upChangeRe4' && head.upSequnce === 1 ? {'증설계획(m³/일)'} : + head.upName === 'upChangeRe5' && head.upSequnce === 1 ? {'처리인구(인)'} : + head.upName === 'upChangeRe6' && head.upSequnce === 1 ? {'처리구역(k㎡)'} : null ))} - + {upHeadList.map((head: TableUpperProps) => { if (head.upName === 'upChangeRe' || head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') { - return {head.title}; + return {head.title}; } })} ) : ( <> - {upHeadList.map((head: TableUpperProps) => + {upHeadList.map((head: TableUpperProps) => (head.id === 'trtFrfNm' ? {head.title} : head.id === 'laeNm' ? {head.title} : head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? {head.title} : head.id === 'trstOprEtpNm' ? {head.title} : - head.upName === 'upChangeRe1' && head.upSequnce === 1 ? {'권역'} : - head.upName === 'upChangeRe2' && head.upSequnce === 1 ? {'환경청'} : - head.upName === 'upChangeRe3' && head.upSequnce === 1 ? {'증설계획(m³/일)'} : - head.upName === 'upChangeRe5' && head.upSequnce === 1 ? {'처리인구(인)'} : - head.upName === 'upChangeRe6' && head.upSequnce === 1 ? {'처리구역(k㎡)'} : - !head.upName ? {head.title} : null))} + head.upName === 'upChangeRe1' && head.upSequnce === 1 ? {'권역'} : + head.upName === 'upChangeRe2' && head.upSequnce === 1 ? {'환경청'} : + head.upName === 'upChangeRe3' && head.upSequnce === 1 ? {'증설계획(m³/일)'} : + head.upName === 'upChangeRe5' && head.upSequnce === 1 ? {'처리인구(인)'} : + head.upName === 'upChangeRe6' && head.upSequnce === 1 ? {'처리구역(k㎡)'} : + !head.upName ? {head.title} : null))} - + {upHeadList.map((head: TableUpperProps) => { if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2' ||head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') { - return {head.title}; + return {head.title}; } })} @@ -84,4 +84,4 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp ); } -export { HeadMakeColSpan }; \ No newline at end of file +export { HeadMakeColSpan }; diff --git a/apps/fo/app/pages/map/searchBar/searchBar.tsx b/apps/fo/app/pages/map/searchBar/searchBar.tsx index 0b1315465..b4981bfbd 100644 --- a/apps/fo/app/pages/map/searchBar/searchBar.tsx +++ b/apps/fo/app/pages/map/searchBar/searchBar.tsx @@ -116,7 +116,7 @@ export function SearchBar({ onFeatureSelect, gisMaps }: SearchBarProps) { onSelectValue(val.toString(), "ctpvCd")} @@ -125,7 +125,7 @@ export function SearchBar({ onFeatureSelect, gisMaps }: SearchBarProps) { onSelectValue(val.toString(), "sggCd")} @@ -134,7 +134,7 @@ export function SearchBar({ onFeatureSelect, gisMaps }: SearchBarProps) { onSelectValue(val.toString(), "fcltCd")} diff --git a/apps/fo/docs/작업계획/테마적용 작업계획 fo_20260625.md b/apps/fo/docs/작업계획/테마적용 작업계획 fo_20260625.md new file mode 100644 index 000000000..f1486a67d --- /dev/null +++ b/apps/fo/docs/작업계획/테마적용 작업계획 fo_20260625.md @@ -0,0 +1,164 @@ +# apps/fo 테마 적용 작업계획 + +작성일: 2026-06-25 +대상 앱: `D:\iipbs\frontend\apps\fo` + +## 1. 목적 + +- `apps/fo` 프로그램 페이지 내부에 직접 박혀 있는 색상 하드코딩을 식별한다. +- 가능한 범위는 `packages/common/assets/src/styles/base.css`의 CSS 변수 영향권으로 옮긴다. +- 즉 `bg-white`, `text-gray-*`, `border-gray-*`, `bg-blue-*`, `bg-green-*`, `bg-red-*` 같은 직접 색상 표현을 `background`, `foreground`, `label`, `border`, `secondary`, `accent`, `primary`, `destructive` 계열 토큰으로 치환한다. + +## 2. 전제 + +- `apps/fo`는 [tailwind.config.ts](D:/iipbs/frontend/apps/fo/tailwind.config.ts:1)에서 공통 preset을 사용한다. +- 공통 토큰은 [packages/common/assets/src/styles/base.css](D:/iipbs/frontend/packages/common/assets/src/styles/base.css:1)에 정의되어 있다. +- `packages/common/components`, `packages/common/assets`는 기준 레이어이며, 앱 작업은 이 기준을 따르는 방향으로 진행한다. +- 단, 이번 요청의 1차 목표는 `apps/fo` 내부 코드에 박혀 있는 색상을 줄이는 것이다. + +## 3. 확인된 하드코딩 유형 + +현재 `apps/fo`에서 확인된 대표 패턴은 아래와 같다. + +- 검색 입력/선택 UI + - `apps/fo/app/components/common/SearchInput.tsx` + - `apps/fo/app/pages/map/searchBar/searchBar.tsx` + - `apps/fo/app/components/common/FacilityTermModalHeader.tsx` + - `apps/fo/app/components/common/FacilityTermModalHeaderMonth.tsx` +- 대시보드/안내문구 + - `apps/fo/app/components/common/UnderComment.tsx` + - `apps/fo/app/pages/(with-layout)/dashboard/rainManage/page.tsx` +- 대시보드 표 셀 상태색 + - `apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/page.tsx` + 이 페이지는 특별한 페이지로, 연간, 하절기, 동절기 색을 구분하여 보여주어야 한다. 따라서 text-foreground 가 보이도록 하되 연간, 하절기, 동절기 색을 apps\fo\src\app\styles 아래 테마별 css에 정의해서 보여주어야 한다. +- 지도 오버레이/팝업 + - `apps/fo/app/pages/(map-layout)/map/page.tsx` +- 테스트/샘플성 버튼 + - `apps/fo/app/pages/(with-layout)/test/page.tsx` +- 모달 입력창 + - `apps/fo/app/pages/(with-layout)/facilityPrecipitation/modal/page.tsx` + +## 4. 기본 원칙 + +### 4.1 바로 토큰 치환 가능한 경우 + +아래는 우선 토큰으로 바로 바꾼다. + +- `bg-white` -> `bg-background` 또는 `bg-card` 또는 `bg-popover` +- `text-gray-900`, `text-gray-800` -> `text-foreground` +- `text-gray-700`, `text-gray-600` -> `text-label` 또는 `text-foreground` +- `text-gray-500` -> `text-muted-foreground` 또는 `text-placeholder` +- `border-gray-200`, `border-gray-300` -> `border-border` +- `focus:ring-blue-500` -> `focus:ring-ring` +- `text-blue-500`, `text-blue-600` -> `text-primary` +- `bg-blue-500`, `bg-blue-600` -> `bg-primary` +- `text-red-500`, `text-red-600` -> `text-destructive` +- `bg-red-50` 등 경고 계열 -> `bg-accent` 또는 별도 위험 상태 규칙 검토 + +### 4.2 바로 토큰 치환하면 안 되는 경우 + +아래는 의미색일 가능성이 높아 바로 토큰화하지 않는다. + +- 범례 색상 +- 데이터 구분용 heatmap/구간 색상 +- 수질/계절/유형별 비교 색상 +- 지도 오버레이에서 정보 카테고리 의미를 나타내는 색 + +이 경우는 다음 중 하나로 처리한다. + +- 유지 +- 별도 의미 클래스 부여 +- 앱 전용 CSS로 상태 클래스를 만든 뒤 theme-safe surface만 토큰화 + +## 5. 우선순위 + +### 5.1 1순위: 일반 UI surface + +가장 먼저 처리한다. + +- 검색 입력창 배경/보더/텍스트 +- 검색 결과 dropdown 배경/hover +- 팝업/모달 wrapper 배경/보더 +- 제목/라벨/안내문구 색 +- 버튼의 직접 blue/red/white 색상 + +이 영역은 theme 적용 체감이 가장 크고, 토큰 치환도 안전하다. + +### 5.2 2순위: 테이블/리스트 + +- `selectedFcltList` +- `facilityInfo` +- modal 내부 표 +- overlay 내 표 구조 + +이 영역은 `bg-secondary`, `bg-accent`, `text-secondary-foreground`, `border-border` 계열로 정리한다. + +### 5.3 3순위: 의미색 데이터 표현 + +- `facilityInfo`의 계절/항목별 `bg-green-*`, `bg-blue-*` +- 지도 범례 색상 +- 지도 feature overlay 강조색 +- 대시보드 표 셀 상태색 + - `apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/page.tsx` + 이 페이지는 특별한 페이지로, 연간, 하절기, 동절기 색을 구분하여 보여주어야 한다. 따라서 text-foreground 가 보이도록 하되 연간, 하절기, 동절기 색을 apps\fo\src\app\styles 아래 테마별 css에 정의해서 보여주어야 한다. + +이 영역은 “없애는 것”보다 “역할을 분리하는 것”이 중요하다. + +## 6. 실제 작업 순서 + +1. `apps/fo` 내부 하드코딩 색상 파일 목록 확정 +2. 공통 UI와 데이터 의미색을 분리 +3. 일반 UI 색상부터 `base.css` 토큰으로 치환 +4. 필요 시 `apps/fo/app/styles/*.css`에 앱 전용 의미 클래스 추가 +5. `dashboard`, `map`, `modal`, `search` 대표 화면 점검 +6. 남은 의미색 영역은 별도 상태색 정책으로 정리 + +## 7. 권장 치환 전략 + +### 7.1 JSX 안에서 바로 치환 + +아래처럼 className을 직접 토큰 기반으로 변경한다. + +- `bg-white` -> `bg-background` +- `text-gray-600` -> `text-label` +- `border-gray-300` -> `border-border` + +이 방식은 빠르지만 반복이 많으면 유지보수성이 떨어진다. + +### 7.2 반복되는 UI는 앱 전용 클래스 도입 + +예: + +- `fo-search-panel` +- `fo-search-input` +- `fo-popup-card` +- `fo-empty-text` + +반복되는 구조는 `apps/fo/app/styles/components*.css`에 올리고, JSX에서는 의미 클래스만 사용한다. + +이 방식이 장기적으로 더 좋다. + +## 8. 예상 주의사항 + +- `h-90` 같은 Tailwind v3 미지원 클래스가 섞여 있을 수 있다. +- `bg-white`를 `bg-background`로 바꾸면 black theme에서 contrast가 크게 달라질 수 있으므로 텍스트 색도 같이 바꿔야 한다. +- `bg-blue-*`, `bg-green-*`는 실제 status color인지 단순 강조색인지 구분하고 바꿔야 한다. +- 지도 오버레이는 HTML string으로 만드는 부분이 있으면 className 치환만으로 끝나지 않을 수 있다. + +## 9. 1차 적용 추천 대상 + +우선 아래 파일부터 적용하는 것이 효율적이다. + +1. `apps/fo/app/components/common/SearchInput.tsx` +2. `apps/fo/app/pages/map/searchBar/searchBar.tsx` +3. `apps/fo/app/components/common/UnderComment.tsx` +4. `apps/fo/app/pages/(with-layout)/dashboard/rainManage/page.tsx` +5. `apps/fo/app/components/common/FacilityTermModalHeader.tsx` +6. `apps/fo/app/components/common/FacilityTermModalHeaderMonth.tsx` +7. `apps/fo/app/pages/(with-layout)/facilityPrecipitation/modal/page.tsx` + +## 10. 결론 + +- `apps/fo`도 `apps/wr`와 동일하게 `base.css` 토큰 중심으로 정리 가능하다. +- 1차는 일반 UI 하드코딩 제거에 집중하고, 데이터 의미색은 유지 또는 별도 정책으로 분리하는 것이 맞다. +- 가장 안전한 방향은 “반복 UI는 앱 전용 의미 클래스”, “일회성은 JSX에서 직접 토큰 치환”의 혼합 방식이다. diff --git a/apps/fo/src/app/index.tsx b/apps/fo/src/app/index.tsx index 692b55ef0..4cf2ad5fa 100644 --- a/apps/fo/src/app/index.tsx +++ b/apps/fo/src/app/index.tsx @@ -1,5 +1,9 @@ import "./styles/base.css"; import "./styles/components.css"; +import "./styles/components-black.css"; +import "./styles/components-blue.css"; +import "./styles/components-green.css"; +import "./styles/components-red.css"; import "@fontsource-variable/noto-sans-kr"; import { AppProvider } from "./providers"; diff --git a/apps/fo/src/app/styles/components-black.css b/apps/fo/src/app/styles/components-black.css new file mode 100644 index 000000000..9d2916269 --- /dev/null +++ b/apps/fo/src/app/styles/components-black.css @@ -0,0 +1,43 @@ +@tailwind components; + +@layer components { + .card { + @apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666]; + } + .card-border { + @apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur; + } + .card-header { + @apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4; + } + .card-footer { + @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; + } + + [data-theme="black"] { + --fo-facility-info-flow-year-bg: color-mix(in srgb, var(--chart-4) 20%, var(--secondary)); + --fo-facility-info-water-year-bg: color-mix(in srgb, var(--chart-3) 20%, var(--secondary)); + --fo-facility-info-phosphorus-year-bg: color-mix(in srgb, var(--chart-1) 20%, var(--secondary)); + --fo-facility-info-flow-summer-bg: color-mix(in srgb, var(--chart-4) 28%, var(--secondary)); + --fo-facility-info-water-summer-bg: color-mix(in srgb, var(--chart-3) 28%, var(--secondary)); + --fo-facility-info-phosphorus-summer-bg: color-mix(in srgb, var(--chart-1) 28%, var(--secondary)); + --fo-facility-info-flow-winter-bg: color-mix(in srgb, var(--chart-4) 36%, var(--secondary)); + --fo-facility-info-water-winter-bg: color-mix(in srgb, var(--chart-3) 36%, var(--secondary)); + --fo-facility-info-phosphorus-winter-bg: color-mix(in srgb, var(--chart-1) 36%, var(--secondary)); + --fo-rain-impact-row-bg: color-mix(in srgb, var(--chart-1) 16%, var(--secondary)); + --fo-facility-info-header-bg: #5e7b9c; + --fo-facility-info-header-fg: #ffffff; + --fo-facility-info-header-border: var(--border); + + .card-border { + @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)]; + } + .card-header { + @apply border-b-[#3f4042]; + } + } + + input[type="file"] { + @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; + } +} diff --git a/apps/fo/src/app/styles/components-blue.css b/apps/fo/src/app/styles/components-blue.css new file mode 100644 index 000000000..30200d31b --- /dev/null +++ b/apps/fo/src/app/styles/components-blue.css @@ -0,0 +1,95 @@ +@tailwind components; + +@layer components { + .card { + @apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666]; + } + .card-border { + @apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur; + } + .card-header { + @apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4; + } + .card-footer { + @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; + } + + [data-theme="blue"] { + --fo-facility-info-flow-year-bg: color-mix(in srgb, var(--chart-4) 16%, var(--background)); + --fo-facility-info-water-year-bg: color-mix(in srgb, var(--chart-3) 16%, var(--background)); + --fo-facility-info-phosphorus-year-bg: color-mix(in srgb, var(--chart-1) 16%, var(--background)); + --fo-facility-info-flow-summer-bg: color-mix(in srgb, var(--chart-4) 24%, var(--background)); + --fo-facility-info-water-summer-bg: color-mix(in srgb, var(--chart-3) 24%, var(--background)); + --fo-facility-info-phosphorus-summer-bg: color-mix(in srgb, var(--chart-1) 24%, var(--background)); + --fo-facility-info-flow-winter-bg: color-mix(in srgb, var(--chart-4) 32%, var(--background)); + --fo-facility-info-water-winter-bg: color-mix(in srgb, var(--chart-3) 32%, var(--background)); + --fo-facility-info-phosphorus-winter-bg: color-mix(in srgb, var(--chart-1) 32%, var(--background)); + --fo-rain-impact-row-bg: color-mix(in srgb, var(--chart-1) 12%, var(--background)); + --fo-facility-info-header-bg: #eaf3fb; + --fo-facility-info-header-fg: #647784; + --fo-facility-info-header-border: #e5e5e5; + + .card-border { + @apply border-[#d8e3eb] bg-white shadow-blur; + } + .card-header { + @apply border-b-[#d8e3eb]; + } + + .card { + @apply text-[#4b5563]; + } + + .card.card-border { + box-shadow: 0 1px 8px rgba(15, 23, 42, 0.06); + } + + .card.card-border .card.card-border { + @apply border-[#dde6ed] bg-white; + box-shadow: none; + } + + .card.card-border input, + .card.card-border button[role="combobox"] { + @apply border-[#d8e3eb] bg-white text-[#737373]; + } + + .card.card-border label { + @apply text-[#666666]; + } + + + + + + nav[aria-label="pagination"] li a { + border-color: transparent !important; + background-color: transparent !important; + color: #0a0a0a !important; + } + + nav[aria-label="pagination"] li a:hover { + background-color: #f6f6f6 !important; + color: #0a0a0a !important; + } + + nav[aria-label="pagination"] li a[aria-current="page"] { + border-color: #2094fa !important; + background-color: #2094fa !important; + color: #fafafa !important; + } + + nav[aria-label="pagination"] li a[aria-label="Go to previous page"], + nav[aria-label="pagination"] li a[aria-label="Go to next page"] { + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + border: 1px solid #e5e7eb !important; + color: #666666 !important; + } + } + + input[type="file"] { + @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; + } +} diff --git a/apps/fo/src/app/styles/components-green.css b/apps/fo/src/app/styles/components-green.css new file mode 100644 index 000000000..470e47f98 --- /dev/null +++ b/apps/fo/src/app/styles/components-green.css @@ -0,0 +1,43 @@ +@tailwind components; + +@layer components { + .card { + @apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666]; + } + .card-border { + @apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur; + } + .card-header { + @apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4; + } + .card-footer { + @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; + } + + [data-theme="green"] { + --fo-facility-info-flow-year-bg: color-mix(in srgb, var(--chart-4) 15%, var(--background)); + --fo-facility-info-water-year-bg: color-mix(in srgb, var(--chart-3) 15%, var(--background)); + --fo-facility-info-phosphorus-year-bg: color-mix(in srgb, var(--chart-1) 15%, var(--background)); + --fo-facility-info-flow-summer-bg: color-mix(in srgb, var(--chart-4) 23%, var(--background)); + --fo-facility-info-water-summer-bg: color-mix(in srgb, var(--chart-3) 23%, var(--background)); + --fo-facility-info-phosphorus-summer-bg: color-mix(in srgb, var(--chart-1) 23%, var(--background)); + --fo-facility-info-flow-winter-bg: color-mix(in srgb, var(--chart-4) 31%, var(--background)); + --fo-facility-info-water-winter-bg: color-mix(in srgb, var(--chart-3) 31%, var(--background)); + --fo-facility-info-phosphorus-winter-bg: color-mix(in srgb, var(--chart-1) 31%, var(--background)); + --fo-rain-impact-row-bg: color-mix(in srgb, var(--chart-3) 12%, var(--background)); + --fo-facility-info-header-bg: #ebf7f7; + --fo-facility-info-header-fg: #64908f; + --fo-facility-info-header-border: var(--border); + + .card-border { + @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)]; + } + .card-header { + @apply border-b-[#3f4042]; + } + } + + input[type="file"] { + @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; + } +} diff --git a/apps/fo/src/app/styles/components-red.css b/apps/fo/src/app/styles/components-red.css new file mode 100644 index 000000000..802f628d0 --- /dev/null +++ b/apps/fo/src/app/styles/components-red.css @@ -0,0 +1,36 @@ +@tailwind components; + +@layer components { + .card { + @apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666]; + } + .card-border { + @apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur; + } + .card-header { + @apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4; + } + .card-footer { + @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; + } + + [data-theme="red"] { + --fo-facility-info-flow-year-bg: color-mix(in srgb, var(--chart-4) 14%, var(--background)); + --fo-facility-info-water-year-bg: color-mix(in srgb, var(--chart-3) 14%, var(--background)); + --fo-facility-info-phosphorus-year-bg: color-mix(in srgb, var(--chart-1) 14%, var(--background)); + --fo-facility-info-flow-summer-bg: color-mix(in srgb, var(--chart-4) 22%, var(--background)); + --fo-facility-info-water-summer-bg: color-mix(in srgb, var(--chart-3) 22%, var(--background)); + --fo-facility-info-phosphorus-summer-bg: color-mix(in srgb, var(--chart-1) 22%, var(--background)); + --fo-facility-info-flow-winter-bg: color-mix(in srgb, var(--chart-4) 30%, var(--background)); + --fo-facility-info-water-winter-bg: color-mix(in srgb, var(--chart-3) 30%, var(--background)); + --fo-facility-info-phosphorus-winter-bg: color-mix(in srgb, var(--chart-1) 30%, var(--background)); + --fo-rain-impact-row-bg: color-mix(in srgb, var(--destructive) 12%, var(--background)); + --fo-facility-info-header-bg: #f6f6f9; + --fo-facility-info-header-fg: #808080; + --fo-facility-info-header-border: var(--border); + } + + input[type="file"] { + @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; + } +} diff --git a/apps/fo/src/app/styles/components.css b/apps/fo/src/app/styles/components.css index 4dc30bafd..521ae07f0 100644 --- a/apps/fo/src/app/styles/components.css +++ b/apps/fo/src/app/styles/components.css @@ -1,6 +1,22 @@ @tailwind components; @layer components { + :root { + --fo-facility-info-flow-year-bg: color-mix(in srgb, var(--chart-4) 18%, var(--background)); + --fo-facility-info-water-year-bg: color-mix(in srgb, var(--chart-3) 18%, var(--background)); + --fo-facility-info-phosphorus-year-bg: color-mix(in srgb, var(--chart-1) 18%, var(--background)); + --fo-facility-info-flow-summer-bg: color-mix(in srgb, var(--chart-4) 28%, var(--background)); + --fo-facility-info-water-summer-bg: color-mix(in srgb, var(--chart-3) 28%, var(--background)); + --fo-facility-info-phosphorus-summer-bg: color-mix(in srgb, var(--chart-1) 28%, var(--background)); + --fo-facility-info-flow-winter-bg: color-mix(in srgb, var(--chart-4) 38%, var(--background)); + --fo-facility-info-water-winter-bg: color-mix(in srgb, var(--chart-3) 38%, var(--background)); + --fo-facility-info-phosphorus-winter-bg: color-mix(in srgb, var(--chart-1) 38%, var(--background)); + --fo-rain-impact-row-bg: color-mix(in srgb, var(--chart-1) 10%, var(--background)); + --fo-facility-info-header-bg: #eaf3fb; + --fo-facility-info-header-fg: #647784; + --fo-facility-info-header-border: var(--border); + } + .card { @apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666]; } @@ -14,13 +30,63 @@ @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; } - [data-theme="black"] { - .card-border { - @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)]; - } - .card-header { - @apply border-b-[#3f4042]; - } + .fo-facility-info-flow-year, + .fo-facility-info-water-year, + .fo-facility-info-phosphorus-year, + .fo-facility-info-flow-summer, + .fo-facility-info-water-summer, + .fo-facility-info-phosphorus-summer, + .fo-facility-info-flow-winter, + .fo-facility-info-water-winter, + .fo-facility-info-phosphorus-winter { + color: var(--foreground); + } + + .fo-facility-info-flow-year { + background-color: var(--fo-facility-info-flow-year-bg); + } + + .fo-facility-info-water-year { + background-color: var(--fo-facility-info-water-year-bg); + } + + .fo-facility-info-phosphorus-year { + background-color: var(--fo-facility-info-phosphorus-year-bg); + } + + .fo-facility-info-flow-summer { + background-color: var(--fo-facility-info-flow-summer-bg); + } + + .fo-facility-info-water-summer { + background-color: var(--fo-facility-info-water-summer-bg); + } + + .fo-facility-info-phosphorus-summer { + background-color: var(--fo-facility-info-phosphorus-summer-bg); + } + + .fo-facility-info-flow-winter { + background-color: var(--fo-facility-info-flow-winter-bg); + } + + .fo-facility-info-water-winter { + background-color: var(--fo-facility-info-water-winter-bg); + } + + .fo-facility-info-phosphorus-winter { + background-color: var(--fo-facility-info-phosphorus-winter-bg); + } + + .fo-rain-impact-row td { + background-color: var(--fo-rain-impact-row-bg); + } + + + .fo-facility-info-header-surface { + border: 1px solid var(--fo-facility-info-header-border); + background-color: var(--fo-facility-info-header-bg); + color: var(--fo-facility-info-header-fg); } input[type="file"] { diff --git a/apps/sn/src/app/index.tsx b/apps/sn/src/app/index.tsx index 29d73b03f..d70dc3d73 100644 --- a/apps/sn/src/app/index.tsx +++ b/apps/sn/src/app/index.tsx @@ -1,3 +1,9 @@ +import "./styles/base.css"; +import "./styles/components.css"; +import "./styles/components-black.css"; +import "./styles/components-blue.css"; +import "./styles/components-green.css"; +import "./styles/components-red.css"; import { AppProvider } from "./providers"; import { cookies } from "next/headers"; diff --git a/apps/sn/src/app/styles/components-black.css b/apps/sn/src/app/styles/components-black.css new file mode 100644 index 000000000..4dc30bafd --- /dev/null +++ b/apps/sn/src/app/styles/components-black.css @@ -0,0 +1,29 @@ +@tailwind components; + +@layer components { + .card { + @apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666]; + } + .card-border { + @apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur; + } + .card-header { + @apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4; + } + .card-footer { + @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; + } + + [data-theme="black"] { + .card-border { + @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)]; + } + .card-header { + @apply border-b-[#3f4042]; + } + } + + input[type="file"] { + @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; + } +} diff --git a/apps/sn/src/app/styles/components-blue.css b/apps/sn/src/app/styles/components-blue.css new file mode 100644 index 000000000..7c1e9db27 --- /dev/null +++ b/apps/sn/src/app/styles/components-blue.css @@ -0,0 +1,29 @@ +@tailwind components; + +@layer components { + .card { + @apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666]; + } + .card-border { + @apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur; + } + .card-header { + @apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4; + } + .card-footer { + @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; + } + + [data-theme="blue"] { + .card-border { + @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)]; + } + .card-header { + @apply border-b-[#3f4042]; + } + } + + input[type="file"] { + @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; + } +} diff --git a/apps/sn/src/app/styles/components-green.css b/apps/sn/src/app/styles/components-green.css new file mode 100644 index 000000000..4b4cc770e --- /dev/null +++ b/apps/sn/src/app/styles/components-green.css @@ -0,0 +1,29 @@ +@tailwind components; + +@layer components { + .card { + @apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666]; + } + .card-border { + @apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur; + } + .card-header { + @apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4; + } + .card-footer { + @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; + } + + [data-theme="green"] { + .card-border { + @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)]; + } + .card-header { + @apply border-b-[#3f4042]; + } + } + + input[type="file"] { + @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; + } +} diff --git a/apps/sn/src/app/styles/components-red.css b/apps/sn/src/app/styles/components-red.css new file mode 100644 index 000000000..cadffac52 --- /dev/null +++ b/apps/sn/src/app/styles/components-red.css @@ -0,0 +1,29 @@ +@tailwind components; + +@layer components { + .card { + @apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666]; + } + .card-border { + @apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur; + } + .card-header { + @apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4; + } + .card-footer { + @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; + } + + [data-theme="red"] { + .card-border { + @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)]; + } + .card-header { + @apply border-b-[#3f4042]; + } + } + + input[type="file"] { + @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; + } +} diff --git a/apps/sn/src/app/styles/components.css b/apps/sn/src/app/styles/components.css index 4dc30bafd..3aa3c770d 100644 --- a/apps/sn/src/app/styles/components.css +++ b/apps/sn/src/app/styles/components.css @@ -14,15 +14,6 @@ @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; } - [data-theme="black"] { - .card-border { - @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)]; - } - .card-header { - @apply border-b-[#3f4042]; - } - } - input[type="file"] { @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; } diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/drgtLegend.tsx b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/drgtLegend.tsx index e20ce9f36..4c38b30b3 100644 --- a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/drgtLegend.tsx +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/drgtLegend.tsx @@ -15,34 +15,34 @@ export function DrgtLegend() {
- {label} + {label}
); } return ( -
+
- +
-
+
가 뭄 정 보
- + {/* 관심 - 1회 미만 */} {/* 관심 - 1회 이상 */} - + {/* 주의 - 1회 미만 */} {/* 주의 - 1회 이상 */} - + {/* 관심+주의 - 1회 미만 */} {/* 관심+주의 - 1회 이상 */}
@@ -50,4 +50,4 @@ export function DrgtLegend() {
); -} \ No newline at end of file +} diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx index 75a938672..6514f5c06 100644 --- a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx @@ -172,12 +172,12 @@ export function SearchBar({ gisMaps, ctpvCode, sggCode }: CtpvSggPrcsFcltSelectB return ( -
+
- + - +
); -} \ No newline at end of file +} diff --git a/apps/wr/app/(popUp-layout)/mapFeaturePopup/page.tsx b/apps/wr/app/(popUp-layout)/mapFeaturePopup/page.tsx index 90b1cbc8a..af09e6eb6 100644 --- a/apps/wr/app/(popUp-layout)/mapFeaturePopup/page.tsx +++ b/apps/wr/app/(popUp-layout)/mapFeaturePopup/page.tsx @@ -1,3 +1,6 @@ +/** + * 지도에서 feature 선택시 팝업 + */ "use client"; import { useWrStore } from "@/app/store"; import CoSttnDetailPopup from "@/components/ui/map/detailPopup/coSttnDetailPopup/coSttnDetailPopup"; @@ -20,7 +23,7 @@ export default function MapFeaturePopup() { coSttnMapDetail } = useWrStore((state) => state); const { pbnMapHeadList } = pbnMap; - const [isDetail, setIsDetail] = useState(undefined); + const [isDetail, setIsDetail] = useState(undefined); // isFeatureCd && layerNm const [layerNm, setLayerNm] = useState(); const searchParams = useSearchParams(); @@ -57,32 +60,32 @@ export default function MapFeaturePopup() { useEffect(() => { console.log(' layerNm ' , layerNm) if (isFeatureCd && layerNm) { - if (layerNm === "tb_wr_bdst_ruse_fclt") { + if (layerNm === "tb_wr_bdst_ruse_fclt") { // 하수재이용시설 setIsDetail(isFeatureCd); - } else if (layerNm === "tb_il_swage_prcs_fclt") { + } else if (layerNm === "tb_il_swage_prcs_fclt") { // 하수처리시설 pbnMapDetaile(isFeatureCd).then((res) => { console.log('res', res) setIsDetail([res]); }); - } else if (layerNm === "tb_wr_rnwtr_utztn_fclt") { + } else if (layerNm === "tb_wr_rnwtr_utztn_fclt") { // 빗물이용시설 rainMapDetile(isFeatureCd).then((res) => { setIsDetail(res); }); - } else if (layerNm === "tb_wr_rswtr_fclt") { + } else if (layerNm === "tb_wr_rswtr_fclt") { // 중수도시설 rswtMapDetile(isFeatureCd).then((res) => { setIsDetail(res); }); - } else if (layerNm === "tb_wr_htdfl_fclt") { + } else if (layerNm === "tb_wr_htdfl_fclt") { // 온배수시설 htdfMapDetile(isFeatureCd).then((res) => { setIsDetail(res); }); - }else if(layerNm ==='tb_wr_ruse_fclt_dmd_co'){ + }else if(layerNm ==='tb_wr_ruse_fclt_dmd_co'){ // 수요처 coSttnMapDetail(isFeatureCd).then((res) => { console.log('tb_wr_ruse_fclt_dmd_co ', res) setIsDetail(res); }); - } else if (layerNm === "tb_wr_tmpr_dmd_co") { + } else if (layerNm === "tb_wr_tmpr_dmd_co") { // 잠재수요처 ldrmntMapDetile(isFeatureCd).then((res) => { setIsDetail(res); }); @@ -94,7 +97,7 @@ export default function MapFeaturePopup() { return ( <> {isDetail ? ( -
+
{layerNm === "tb_wr_bdst_ruse_fclt" ? ( ) : layerNm === "tb_il_swage_prcs_fclt" ? ( diff --git a/apps/wr/app/(popUp-layout)/splyPopup/SearchInputText/SearchInputText.tsx b/apps/wr/app/(popUp-layout)/splyPopup/SearchInputText/SearchInputText.tsx index 05f41d09b..e1e77e1c7 100644 --- a/apps/wr/app/(popUp-layout)/splyPopup/SearchInputText/SearchInputText.tsx +++ b/apps/wr/app/(popUp-layout)/splyPopup/SearchInputText/SearchInputText.tsx @@ -94,7 +94,7 @@ export default function SearchInputText({ value={textValue || ""} onChange={handleInputChange} onKeyDown={handleKeyDown} - className="w-full h-[3.2rem] !text-xl pl-3 text-gray-600 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + className="h-[3.2rem] w-full rounded-md border border-input pl-3 !text-xl text-foreground focus:outline-none focus:ring-2 focus:ring-ring" placeholder={`${label}을(를) 입력하세요`} onClick={() => { setTextValue(""); @@ -103,7 +103,7 @@ export default function SearchInputText({ }} /> {isOpen && ( -
+
{searchList.length > 0 ? ( searchList.map((item, index) => ( @@ -111,8 +111,8 @@ export default function SearchInputText({ key={index} className={`w-full cursor-pointer overflow-auto px-5 py-3 text-left !text-xl ${ highlightIndex === index - ? "bg-blue-100" - : "hover:bg-gray-100" + ? "bg-accent text-accent-foreground" + : "hover:bg-accent hover:text-accent-foreground" }`} onClick={() => handleSuggestionClick(item.bscFctNm)} role="button" @@ -127,7 +127,7 @@ export default function SearchInputText({
)) ) : ( -
+
검색 결과가 없습니다.
)} diff --git a/apps/wr/app/(popUp-layout)/splyPopup/page.tsx b/apps/wr/app/(popUp-layout)/splyPopup/page.tsx index eac81032e..2947b2abf 100644 --- a/apps/wr/app/(popUp-layout)/splyPopup/page.tsx +++ b/apps/wr/app/(popUp-layout)/splyPopup/page.tsx @@ -200,17 +200,17 @@ const Map = () => { // HTML 또는 React 오버레이 컴포넌트 구성 const overlayContent = ` -
+
-
+
${feature.get("swtrmt_fclt_nm") || "-"}
-
${k}${k} ${safe(v)}
+
- - + + diff --git a/apps/wr/app/(popUp-layout)/splyPopup/searchBar/searchBar.tsx b/apps/wr/app/(popUp-layout)/splyPopup/searchBar/searchBar.tsx index 0f5430771..7abb3af50 100644 --- a/apps/wr/app/(popUp-layout)/splyPopup/searchBar/searchBar.tsx +++ b/apps/wr/app/(popUp-layout)/splyPopup/searchBar/searchBar.tsx @@ -60,12 +60,12 @@ export function SearchBar({ return (
-
+
onSelectValue(val.toString(), "ctpvCd")} value={sidoValue} @@ -74,12 +74,12 @@ export function SearchBar({ labelClass="w-[65px]" className="w-[200px]" label="시군구" - selectClass="rounded-lg bg-white" + selectClass="rounded-lg" selectArray={filteredSigunData || []} onSelectValue={(val) => onSelectValue(val.toString(), "sggCd")} value={sigunValue} /> - +
용량(m³/일)
용량(m³/일) ${formatNumber(feature.get("fclt_cpct"))}
- +
+ - diff --git a/apps/wr/app/(with-layout)/matMng/coMat/coMatRvw/page.tsx b/apps/wr/app/(with-layout)/matMng/coMat/coMatRvw/page.tsx index 4996b1cd9..2fba54fb6 100644 --- a/apps/wr/app/(with-layout)/matMng/coMat/coMatRvw/page.tsx +++ b/apps/wr/app/(with-layout)/matMng/coMat/coMatRvw/page.tsx @@ -93,7 +93,7 @@ const EMPTY_DETAIL_FORM: Partial = { }; export default function CoMatRvw() { - const readOnlyInputClass = "bg-slate-50 text-gray-700"; + const readOnlyInputClass = "bg-secondary text-secondary-foreground"; const { auth, code, coRvw, coMatRvwInit, coMatRvwSearch, coMatRvwInqua } = useWrStore((state) => state); const { coMatRvwRes, coMatRvwCount } = coRvw; @@ -305,7 +305,7 @@ export default function CoMatRvw() { />
-
@@ -573,7 +573,7 @@ export default function CoMatRvw() { value={formatNumberWithComma(field.value)} readOnly /> - + 이하
@@ -601,7 +601,7 @@ export default function CoMatRvw() { value={formatNumberWithComma(field.value)} readOnly /> - + 이하 @@ -643,7 +643,7 @@ export default function CoMatRvw() { value={formatNumberWithComma(field.value)} readOnly /> - + 이하 @@ -670,7 +670,7 @@ export default function CoMatRvw() { value={formatNumberWithComma(field.value)} readOnly /> - + 이하 @@ -697,7 +697,7 @@ export default function CoMatRvw() { value={formatNumberWithComma(field.value)} readOnly /> - + 이하 @@ -748,7 +748,7 @@ export default function CoMatRvw() { value={formatNumberWithComma(field.value)} readOnly /> - + 이하 @@ -775,7 +775,7 @@ export default function CoMatRvw() { value={formatNumberWithComma(field.value)} readOnly /> - + 이하 diff --git a/apps/wr/app/(with-layout)/matMng/coMat/ruseCaseMng/page.tsx b/apps/wr/app/(with-layout)/matMng/coMat/ruseCaseMng/page.tsx index 9fc636427..b8ca96ca1 100644 --- a/apps/wr/app/(with-layout)/matMng/coMat/ruseCaseMng/page.tsx +++ b/apps/wr/app/(with-layout)/matMng/coMat/ruseCaseMng/page.tsx @@ -347,7 +347,7 @@ export default function RuseCaseMng() { }} resetPageNo={resetPage} /> - + ☞ 본 사례의 사업비는 재원협의 단계의 정보이므로, 최종확정된 비용이 아닙니다. diff --git a/apps/wr/app/(with-layout)/matMng/coMat/ruseRvwHstry/page.tsx b/apps/wr/app/(with-layout)/matMng/coMat/ruseRvwHstry/page.tsx index b142bff16..83f6fbe69 100644 --- a/apps/wr/app/(with-layout)/matMng/coMat/ruseRvwHstry/page.tsx +++ b/apps/wr/app/(with-layout)/matMng/coMat/ruseRvwHstry/page.tsx @@ -250,7 +250,7 @@ export default function RuseRvwHstry() {
-
시설명 @@ -60,7 +60,7 @@ export default function SelectedFcltList({
+ 선택된 시설이 없습니다.
) -} \ No newline at end of file +} diff --git a/apps/wr/components/ui/map/wrOverLay/wrOverLay.tsx b/apps/wr/components/ui/map/wrOverLay/wrOverLay.tsx index a6d04973d..36c733951 100644 --- a/apps/wr/components/ui/map/wrOverLay/wrOverLay.tsx +++ b/apps/wr/components/ui/map/wrOverLay/wrOverLay.tsx @@ -43,16 +43,16 @@ export default function WrOverLay({ onDrag={(e, data) => setPos({ x: data.x, y: data.y })} >
- + {/* 헤더: 제목 바 + 우측 정렬 버튼 (절대배치 제거, 타이틀 중앙 정렬 유지) */} -
+
{/* 좌측 스페이서: 우측 버튼과 동일한 너비를 차지하도록 동일 버튼을 투명하게 복제 */}
@@ -78,7 +78,7 @@ export default function WrOverLay({
@@ -110,7 +110,7 @@ export default function WrOverLay({ onClick={() => setOpen(true)} aria-label="확장" title={title || "정보 보기"} - className="w-[30px] h-[30px] bg-white font-bold shadow flex items-center justify-center border border-slate-300" + className="flex h-[30px] w-[30px] items-center justify-center border border-border bg-popover font-bold text-popover-foreground shadow" > {bubbleLabel} diff --git a/apps/wr/components/ui/modal/chkgd/chkgdExcel/chkgdExcel.tsx b/apps/wr/components/ui/modal/chkgd/chkgdExcel/chkgdExcel.tsx index 0e4152db6..6cdebc4c4 100644 --- a/apps/wr/components/ui/modal/chkgd/chkgdExcel/chkgdExcel.tsx +++ b/apps/wr/components/ui/modal/chkgd/chkgdExcel/chkgdExcel.tsx @@ -265,7 +265,7 @@ export default function ChkgdExcel({ >
-

+

{"점검정보 입력"}

diff --git a/apps/wr/components/ui/modal/chkgd/chkgdModal/chkgdModal.tsx b/apps/wr/components/ui/modal/chkgd/chkgdModal/chkgdModal.tsx index c5ba35e17..90bb0c91c 100644 --- a/apps/wr/components/ui/modal/chkgd/chkgdModal/chkgdModal.tsx +++ b/apps/wr/components/ui/modal/chkgd/chkgdModal/chkgdModal.tsx @@ -289,7 +289,7 @@ export default function ChkgdModal({ <> )}
-

+

{type === "RNB" ? `시설명 : (${obj.bdstNm})` : `시설명 : (${obj.building.bdstNm})`} diff --git a/apps/wr/components/ui/modal/chkgd/pbnPrfExcelModal/pbnPrfExcelModal.tsx b/apps/wr/components/ui/modal/chkgd/pbnPrfExcelModal/pbnPrfExcelModal.tsx index 0857340e3..d1d4fcd59 100644 --- a/apps/wr/components/ui/modal/chkgd/pbnPrfExcelModal/pbnPrfExcelModal.tsx +++ b/apps/wr/components/ui/modal/chkgd/pbnPrfExcelModal/pbnPrfExcelModal.tsx @@ -102,7 +102,7 @@ export default function PbnPrfExcelModal({ 저장

-

+

{"점검정보 입력"}

diff --git a/apps/wr/components/ui/modal/coMat/constCostFeeTable.tsx b/apps/wr/components/ui/modal/coMat/constCostFeeTable.tsx index d3f02e278..f1d80a9be 100644 --- a/apps/wr/components/ui/modal/coMat/constCostFeeTable.tsx +++ b/apps/wr/components/ui/modal/coMat/constCostFeeTable.tsx @@ -106,7 +106,7 @@ export function ConstCostFeeTable() {
- +
- 단위: 백만원 + 단위: 백만원
{ }} /> -
+
시설용량 구간내 공사비는 직선보간법에 의하여 산출
diff --git a/apps/wr/components/ui/modal/coMat/rateFeeModal.tsx b/apps/wr/components/ui/modal/coMat/rateFeeModal.tsx index 5439d489c..40bac420c 100644 --- a/apps/wr/components/ui/modal/coMat/rateFeeModal.tsx +++ b/apps/wr/components/ui/modal/coMat/rateFeeModal.tsx @@ -104,14 +104,14 @@ export function RateFeeTableModal() { return ( -
+

시설부대경비 요율(%)

-
+
- +
-
+
출처 : 하수도분야 보조금 편성 및 실무요령
시설부대경비 및 감리비 요율적용시 공사비가 요율표의 중간에 있을 때는 직선보간법에 의하여 산출 diff --git a/apps/wr/components/ui/modal/coMat/ruseCaseMng/ruseCaseMngExcelModal.tsx b/apps/wr/components/ui/modal/coMat/ruseCaseMng/ruseCaseMngExcelModal.tsx index 8f0445cd6..e85234c03 100644 --- a/apps/wr/components/ui/modal/coMat/ruseCaseMng/ruseCaseMngExcelModal.tsx +++ b/apps/wr/components/ui/modal/coMat/ruseCaseMng/ruseCaseMngExcelModal.tsx @@ -185,7 +185,7 @@ export default function RuseCaseMngExcel({setIsExcelOpen}:{setIsExcelOpen: (isOp
-

+

{"물재이용 사례 파일"}

-

+

{"잠재수요처 파일"}

diff --git a/apps/wr/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal.tsx b/apps/wr/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal.tsx index 236797711..b3c5dcfcf 100644 --- a/apps/wr/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal.tsx +++ b/apps/wr/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal.tsx @@ -177,7 +177,7 @@ export function BdstReg({ > {" "} -
-
+

건축물 목록 - + (유의 사항: 신규등록하려는 건축물이 목록에 없을 경우 하단 건축물 정보에 추가하여 주시기 바랍니다.) @@ -221,7 +221,7 @@ export function BdstReg({ >

-
+
{moType === "RNB" ? ( diff --git a/apps/wr/components/ui/modal/fcltReg/fcOrgwtsRegModal/fcOrgwtsRegModal.tsx b/apps/wr/components/ui/modal/fcltReg/fcOrgwtsRegModal/fcOrgwtsRegModal.tsx index 3be1806f8..0b5707b6e 100644 --- a/apps/wr/components/ui/modal/fcltReg/fcOrgwtsRegModal/fcOrgwtsRegModal.tsx +++ b/apps/wr/components/ui/modal/fcltReg/fcOrgwtsRegModal/fcOrgwtsRegModal.tsx @@ -105,14 +105,14 @@ export default function FcOrgwtsRegModal({ return (
-
+
<div className="flex items-center gap-2 mt-8"> - <Label className="text-xl text-gray-600">시설명</Label> + <Label className="text-xl text-label">시설명</Label> <Input className="w-48 text-xl" type="text" @@ -144,7 +144,7 @@ export default function FcOrgwtsRegModal({ {/* 왼쪽 테이블 */} <div className="w-5/12 min-w-[400px]"> - <Label className="text-2xl text-gray-600 mb-2 block text-right"> + <Label className="mb-2 block text-right text-2xl text-label"> 공공하수처리시설 목록 </Label> <RuseMngTable @@ -170,7 +170,7 @@ export default function FcOrgwtsRegModal({ } setbdstTable(unchecked ?? []); }} - className="w-8 h-8 text-gray-500 cursor-pointer" + className="h-8 w-8 cursor-pointer text-muted-foreground" /> <ArrowLeft onClick={() => { @@ -187,13 +187,13 @@ export default function FcOrgwtsRegModal({ } setRightData(unchecked ?? []); }} - className="w-8 h-8 text-gray-500 cursor-pointer" + className="h-8 w-8 cursor-pointer text-muted-foreground" /> </div> {/* 오른쪽 테이블 */} <div className="w-5/12 min-w-[400px]"> - <Label className="text-2xl text-gray-600 mb-2 block text-right"> + <Label className="mb-2 block text-right text-2xl text-label"> 취수하수처리장 목록 </Label> <RuseMngTable diff --git a/apps/wr/components/ui/modal/fcltReg/pbnstRegModal/pbnstRegModal.tsx b/apps/wr/components/ui/modal/fcltReg/pbnstRegModal/pbnstRegModal.tsx index fcb6fe392..c1dd95f95 100644 --- a/apps/wr/components/ui/modal/fcltReg/pbnstRegModal/pbnstRegModal.tsx +++ b/apps/wr/components/ui/modal/fcltReg/pbnstRegModal/pbnstRegModal.tsx @@ -72,7 +72,7 @@ export function PbnstRegModal({ 하수재이용 시설 신규등록 </h1> - <div className="border-t border-gray-300 my-4"></div> + <div className="my-4 border-t border-border"></div> <Tabs defaultValue="mng" onValueChange={handleTabChange}> <div className="flex justify-between items-center"> diff --git a/apps/wr/components/ui/searchInput/searchInput.css b/apps/wr/components/ui/searchInput/searchInput.css new file mode 100644 index 000000000..e37d76e5e --- /dev/null +++ b/apps/wr/components/ui/searchInput/searchInput.css @@ -0,0 +1,19 @@ +@tailwind components; + +@layer components { + .wrsearchinput_field { + @apply h-[3.2rem] w-full rounded-md border border-input pl-3 !text-xl text-label focus:outline-none focus:ring-2 focus:ring-ring; + } + + .wrsearchinput_list { + @apply z-10 mt-2 h-[90px] w-full overflow-auto rounded-md border border-border bg-popover shadow-lg; + } + + .wrsearchinput_item { + @apply w-full cursor-pointer overflow-auto px-5 py-3 text-left !text-xl text-foreground hover:bg-accent; + } + + .wrsearchinput_empty { + @apply w-full overflow-auto px-5 py-3 !text-xl text-label; + } +} diff --git a/apps/wr/components/ui/searchInput/searchInput.tsx b/apps/wr/components/ui/searchInput/searchInput.tsx index f512516b8..dfac70681 100644 --- a/apps/wr/components/ui/searchInput/searchInput.tsx +++ b/apps/wr/components/ui/searchInput/searchInput.tsx @@ -1,3 +1,5 @@ +import "./searchInput.css"; + import { Input } from "@common/components/ui"; import React, { useCallback, useEffect, useRef, useState } from "react"; @@ -73,18 +75,18 @@ export default function SearchInputText({ value={textValue} onChange={handleInputChange} /* onFocus={() => setIsOpen(true)} */ - className="w-full h-[3.2rem] !text-xl pl-3 text-gray-600 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + className="wrsearchinput_field" placeholder={`${label}을(를) 입력하세요`} readOnly={readonly} // readonly 상태 적용 /> {isOpen && ( - <div className=" z-1 w-full mt-2 bg-white border border-gray-300 rounded-md shadow-lg h-90 overflow-auto"> + <div className="wrsearchinput_list"> <div className="max-h-72"> {searchList && searchList.length > 0 ? ( searchList.map((item: SearchType, index: number) => ( <div key={index} - className="!text-xl w-full text-left px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto" + className="wrsearchinput_item" onClick={() => handleSuggestionClick(item.searchNm)} role="button" tabIndex={0} @@ -99,7 +101,7 @@ export default function SearchInputText({ </div> )) ) : ( - <div className="!text-xl w-full px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto"> + <div className="wrsearchinput_empty"> 검색 결과가 없습니다. </div> )} diff --git a/apps/wr/components/ui/table/chkgdTable/guideDetailTable/guideDetailTable.tsx b/apps/wr/components/ui/table/chkgdTable/guideDetailTable/guideDetailTable.tsx index 35745077c..52a766262 100644 --- a/apps/wr/components/ui/table/chkgdTable/guideDetailTable/guideDetailTable.tsx +++ b/apps/wr/components/ui/table/chkgdTable/guideDetailTable/guideDetailTable.tsx @@ -43,7 +43,7 @@ export function GuideDetailsTable({ return ( <> - <div className={`card !m-[1.2rem] !p-0r `}> + <div className={`card !m-[1.2rem] !p-0 `}> <Table variant="secondary" className="table-fixed"> <colgroup> {headList diff --git a/apps/wr/components/ui/table/pbnstReg/ruseMngTable/ruseMngTable.tsx b/apps/wr/components/ui/table/pbnstReg/ruseMngTable/ruseMngTable.tsx index 2e056a401..12e0061a3 100644 --- a/apps/wr/components/ui/table/pbnstReg/ruseMngTable/ruseMngTable.tsx +++ b/apps/wr/components/ui/table/pbnstReg/ruseMngTable/ruseMngTable.tsx @@ -60,7 +60,7 @@ const RuseMngTable = React.forwardRef<any, RuseMngTableProps>( {headList.map((head) => ( <TableHead key={"table-head-row " + head.id} - className={`border border-gray-600 px-2 py-1 `} + className={`border border-border px-2 py-1 `} > {head.title} </TableHead> diff --git a/apps/wr/components/ui/table/pbnstReg/ruseMngTableForm/ruseMngTableForm.tsx b/apps/wr/components/ui/table/pbnstReg/ruseMngTableForm/ruseMngTableForm.tsx index 298c2bf5e..380cb9c88 100644 --- a/apps/wr/components/ui/table/pbnstReg/ruseMngTableForm/ruseMngTableForm.tsx +++ b/apps/wr/components/ui/table/pbnstReg/ruseMngTableForm/ruseMngTableForm.tsx @@ -84,7 +84,7 @@ const RuseMngTableForm = React.forwardRef<any, RuseMngTableProps>( {headList.map((head) => ( <TableHead key={"table-head-row " + head.id} - className={`border border-gray-600 px-2 py-1 `} + className={`border border-border px-2 py-1 `} > {head.title} </TableHead> @@ -118,7 +118,7 @@ const RuseMngTableForm = React.forwardRef<any, RuseMngTableProps>( <FormUi label=""> {head.edit === true ? ( <Input - className={`${head.type === "number" ? "text-right" : "text-center"} text-gray-600`} + className={`${head.type === "number" ? "text-right" : "text-center"} text-foreground`} type="text" onChange={(txt) => { if (head.type === "number") { @@ -186,7 +186,7 @@ const RuseMngTableForm = React.forwardRef<any, RuseMngTableProps>( value={ field.value? moment(field.value, "YYYYMMDD").format( "YYYY-MM-DD"): "" } - className={"w-full text-gray-600"} + className={"w-full text-foreground"} readOnly={readonly||head.readonly ? true : false} /> ) : head.type === "checkbox" ? ( @@ -202,7 +202,7 @@ const RuseMngTableForm = React.forwardRef<any, RuseMngTableProps>( }} ></Checkbox> ) : ( - <span className="text-black"> + <span className="text-foreground"> {head.type === "number" ? formatCommaNumber(field.value) : field.value || ""} diff --git a/apps/wr/components/ui/toast/toastFromUi.tsx b/apps/wr/components/ui/toast/toastFromUi.tsx index 5886c2e0b..4590d7b38 100644 --- a/apps/wr/components/ui/toast/toastFromUi.tsx +++ b/apps/wr/components/ui/toast/toastFromUi.tsx @@ -16,7 +16,7 @@ export function ToastFormUi({ body, title, url, buttonName }: toastFormType) { <div className={`${ t.visible ? "animate-enter" : "animate-leave" - } w-4/12 h-40 bg-white shadow-lg rounded-lg pointer-events-auto flex ring-1 ring-black ring-opacity-5`} + } h-40 w-4/12 pointer-events-auto flex rounded-lg border border-border bg-popover text-popover-foreground shadow-lg`} > <div className="w-full flex-1 py-10"> <div className="flex items-start"> diff --git a/apps/wr/docs/작업계획/지도팝업_20260626.md b/apps/wr/docs/작업계획/지도팝업_20260626.md new file mode 100644 index 000000000..3da41c87d --- /dev/null +++ b/apps/wr/docs/작업계획/지도팝업_20260626.md @@ -0,0 +1,300 @@ +# 지도 팝업 레이아웃 점검 작업계획 + +작성일: 2026-06-26 +대상 앱: `D:\iipbs\frontend\apps\wr` +대상 화면: +- `http://localhost:3001/mapFeaturePopup?layerNm=tb_wr_bdst_ruse_fclt&featureCd=43150SW001F` +- `http://localhost:3001/mapFeaturePopup?layerNm=tb_wr_tmpr_dmd_co&featureCd=미래산업` +- 선택 팝업 공통 검토 범위: + - `tb_wr_bdst_ruse_fclt` 하수재이용시설 + - `tb_wr_tmpr_dmd_co` 잠재수요처 + - `tb_wr_ruse_fclt_dmd_co` 수요처 + - `tb_wr_rnwtr_utztn_fclt` 빗물이용시설 + - `tb_wr_rswtr_fclt` 중수도시설 + - `tb_wr_htdfl_fclt` 온배수시설 + - `tb_il_swage_prcs_fclt` 하수처리시설 +- 공통 진입 파일: + - `apps/wr/app/(popUp-layout)/mapFeaturePopup/page.tsx` + - `apps/wr/components/lib/func/mapUtil.ts` + +## 1. 작업 배경 + +- 지도 팝업에서 두 종류의 레이아웃 문제가 확인되었다. +- `하수재이용시설` 팝업은 컨텐츠 높이가 늘어나지 못해 하단 영역이 잘린다. +- `잠재수요처` 팝업은 컨텐츠 자체는 보이지만, 팝업 창 하단이 검은 배경으로 채워지지 않고 흰색 여백으로 남는다. +- 이 문제는 개별 폼 내부만이 아니라 지도 팝업 공통 래퍼, 팝업별 본문 높이 전략, 새 창 크기 전략을 함께 점검해야 재발을 막을 수 있다. + +## 2. 현재 구조 분석 + +### 2.1 지도 팝업 공통 진입 구조 + +- `apps/wr/app/(popUp-layout)/mapFeaturePopup/page.tsx` + - `layerNm` 값에 따라 서로 다른 상세 컴포넌트를 렌더링한다. + - 공통 본문 래퍼는 다음 클래스를 사용한다. + - `className="box-border m-0 bg-background p-4 pb-24 text-foreground md:p-6"` +- 현재 이 공통 래퍼에는 `min-h-screen`, `min-h-dvh`, `h-full` 같은 viewport 채움 조건이 없다. +- 따라서 컨텐츠 높이가 창 높이보다 작을 때는 문서 body의 기본 배경색이 하단에 그대로 노출될 수 있다. + +### 2.2 하수재이용시설 경로 + +- `mapFeaturePopup/page.tsx` + - `layerNm === "tb_wr_bdst_ruse_fclt"` 일 때 `RuseDetailPopup` 렌더링 +- `apps/wr/components/ui/map/detailPopup/ruseDetailPopup.tsx` + - 시설별 탭을 만들고 각 탭에서 `PbnstSttnModal data={item} isMap={true}` 렌더링 +- `apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx:257` + - `const Wrapper: React.ElementType = isMap ? "div" : ScrollArea;` +- `apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx:267` + - 지도 팝업 여부와 무관하게 `<Wrapper className="h-[70vh]">` 적용 +- 지도 팝업에서는 `Wrapper`가 일반 `div`이므로: + - 내부 스크롤이 생기지 않고 + - 높이만 `70vh`로 고정되어 + - 아래쪽 `하수재이용시설 현황`, `하수재이용실적` 테이블이 박스 밖으로 밀린다. + +### 2.3 잠재수요처 경로 + +- `mapFeaturePopup/page.tsx` + - `layerNm === "tb_wr_tmpr_dmd_co"` 일 때 `CoDetailModal isMap={true}` 렌더링 +- `apps/wr/components/ui/modal/sttn/sttnDetailModal/coDatailModal.tsx` + - 내부 본문은 `<ScrollArea className="h-[79vh] mx-8">` 사용 +- 즉 잠재수요처는 내부 폼 스크롤은 이미 있으나, + - 공통 팝업 바깥 래퍼가 viewport 전체 높이를 채우지 못하면 + - 컨텐츠가 끝난 뒤 창 하단의 남는 공간이 `bg-background`가 아닌 흰색으로 보일 수 있다. +- 추가로 `pb-24` 같은 하단 padding이 공통 래퍼에 고정 적용되어 있어, + - 팝업별 실제 본문 높이와 맞지 않으면 시각적으로 불필요한 하단 여백처럼 보일 수 있다. + +### 2.4 다른 선택 팝업 경로 + +- `tb_wr_ruse_fclt_dmd_co` + - `apps/wr/components/ui/map/detailPopup/coSttnDetailPopup/coSttnDetailPopup.tsx` + - 내부 `bodyClassNm={"w-full mx-auto bg-background rounded-lg shadow-md p-6"}` 사용 +- `tb_wr_rnwtr_utztn_fclt`, `tb_wr_rswtr_fclt`, `tb_wr_htdfl_fclt` + - `apps/wr/components/ui/modal/sttn/sttnDetailModal/sttnDetailModal.tsx` + - 별도 `ScrollArea`는 주석 처리되어 있고, 최상위 공통 팝업 래퍼 의존도가 높다. +- `tb_il_swage_prcs_fclt` + - `mapFeaturePopup/page.tsx` 안에서 `UiTable` 2개를 직접 렌더링 + - 역시 공통 래퍼 배경/높이 정책의 영향을 그대로 받는다. + +### 2.5 흰 여백이 없는 팝업과 있는 팝업의 구조 차이 + +- 흰 여백이 없다고 확인된 팝업: + - `tb_wr_ruse_fclt_dmd_co` + - `tb_wr_rnwtr_utztn_fclt` + - `tb_wr_rswtr_fclt` +- 흰 여백이 나타난다고 확인된 팝업: + - `tb_wr_tmpr_dmd_co` + - `tb_wr_bdst_ruse_fclt` + - `tb_il_swage_prcs_fclt` + +차이점 1. 개별 본문 컴포넌트의 배경 책임 + +- 흰 여백이 없는 `tb_wr_ruse_fclt_dmd_co`는 + - `coSttnDetailPopup.tsx:23`에서 `bodyClassNm={"w-full mx-auto bg-background rounded-lg shadow-md p-6"}`를 직접 넘긴다. + - 즉 공통 팝업 래퍼 바깥이 아니라, 실제 본문 카드 자체가 `bg-background`를 명시적으로 가진다. +- 반대로 `tb_wr_tmpr_dmd_co`는 + - `coDatailModal.tsx:52`의 최상위가 단순 `<div>`이고, + - 내부는 `ScrollArea className="h-[79vh] mx-8"` 중심 구조다. + - 본문 전체 배경을 개별 카드로 강하게 감싸기보다 공통 팝업 래퍼 배경에 더 의존한다. +- `tb_il_swage_prcs_fclt`는 + - `mapFeaturePopup/page.tsx:103` 이하에서 `UiTable` 2개를 직접 렌더링하고, + - 별도 카드형 본문 래퍼 없이 공통 래퍼 위에 바로 쌓는다. + +차이점 2. 본문 높이 점유 방식 + +- 흰 여백이 없는 `tb_wr_rnwtr_utztn_fclt`, `tb_wr_rswtr_fclt`는 + - `sttnDetailModal.tsx:81` 이하에서 상세 폼 컴포넌트를 직접 렌더링한다. + - 현재 지도 팝업용 `ScrollArea`는 주석 처리되어 있어, 공통 래퍼 안에서 폼이 자연 높이로 이어진다. + - 실제 데이터량이 비교적 충분하면 본문이 창 하단 가까이까지 내려와 흰 여백이 드러나지 않을 수 있다. +- 반대로 `tb_wr_tmpr_dmd_co`는 + - `coDatailModal.tsx:67`의 `h-[79vh]` 스크롤 영역이 명시되어 있고, + - 바깥 공통 래퍼에는 `pb-24`가 남아 있어 하단 잔여 공간이 시각적으로 더 잘 드러날 가능성이 있다. +- `tb_wr_bdst_ruse_fclt`는 + - `pbnstSttnModal.tsx:267`에서 `h-[70vh]` 고정 높이를 사용한다. + - 이 경우 본문이 자연스럽게 창 끝까지 이어지지 못하고, 잘림 또는 하단 여백 문제가 함께 나타날 수 있다. + +차이점 3. 공통 래퍼 의존도 + +- 흰 여백이 없는 쪽은 + - 개별 컴포넌트가 자체 배경 카드나 충분한 컨텐츠 높이를 가져 공통 래퍼 결함이 눈에 덜 띈다. +- 흰 여백이 있는 쪽은 + - 개별 카드 배경이 약하거나 + - 본문 높이가 고정되거나 짧아 + - `mapFeaturePopup/page.tsx:100`의 공통 래퍼가 viewport 끝까지 채워지지 않는 문제가 바로 노출된다. + +### 2.6 팝업 창 자체 크기 + +- `apps/wr/components/lib/func/mapUtil.ts:146` + - 하수재이용시설: `window.innerHeight - 100` + - 잠재수요처: `window.innerHeight - 300` + - 수요처: `window.innerHeight - 430` +- 팝업 창 크기 자체 차이도 존재하지만, +- 이번 증상은 우선 새 창 크기보다 내부 공통 래퍼와 개별 팝업의 높이 전략 불일치 영향이 더 크다. + +## 3. 문제 유형 정리 + +### 3.1 유형 A: 내부 컨텐츠 잘림 + +- 대상: + - `tb_wr_bdst_ruse_fclt` +- 원인: + - `isMap=true`인데도 내부 래퍼가 `h-[70vh]`로 고정 + - `ScrollArea`가 아닌 일반 `div`라서 컨텐츠가 늘어나지 못함 + +### 3.2 유형 B: 팝업 하단 흰색 잔여 영역 + +- 우선 확인 대상: + - `tb_wr_tmpr_dmd_co` +- 함께 점검할 대상: + - `tb_wr_bdst_ruse_fclt` + - `tb_il_swage_prcs_fclt` +- 원인 후보: + - 공통 래퍼가 viewport 전체 높이를 채우지 않음 + - 공통 래퍼의 하단 padding이 팝업별 컨텐츠 높이와 맞지 않음 + - 팝업별 내부 스크롤 높이와 새 창 높이 계산이 제각각임 + - 개별 팝업이 본문 배경을 자체적으로 채우지 못하고 공통 래퍼에 의존함 + +## 4. 수정 대상 + +### 4.1 공통 우선 수정 파일 + +- `D:\iipbs\frontend\apps\wr\app\(popUp-layout)\mapFeaturePopup\page.tsx` + +### 4.2 하수재이용시설 직접 수정 파일 + +- `D:\iipbs\frontend\apps\wr\components\ui\modal\sttn\pbnstSttnModal\pbnstSttnModal.tsx` +- `D:\iipbs\frontend\apps\wr\components\ui\map\detailPopup\ruseDetailPopup.tsx` +- `D:\iipbs\frontend\apps\wr\components\ui\form\fcltReg\pbnstReg\mngReg\mngRegForm.tsx` + +### 4.3 잠재수요처 및 타 팝업 점검 파일 + +- `D:\iipbs\frontend\apps\wr\components\ui\modal\sttn\sttnDetailModal\coDatailModal.tsx` +- `D:\iipbs\frontend\apps\wr\components\ui\map\detailPopup\coSttnDetailPopup\coSttnDetailPopup.tsx` +- `D:\iipbs\frontend\apps\wr\components\ui\modal\sttn\sttnDetailModal\sttnDetailModal.tsx` +- `D:\iipbs\frontend\apps\wr\components\lib\func\mapUtil.ts` + +## 5. 권장 구현 방향 + +### 5.1 공통 팝업 래퍼 정규화 + +- `mapFeaturePopup/page.tsx`의 최상위 래퍼를 지도 팝업 공통 레이아웃 기준점으로 삼는다. +- 다음 방향으로 정리하는 것이 우선이다. +- `bg-background`가 항상 팝업 창 하단까지 채워지도록 `min-h-screen` 또는 `min-h-dvh` 적용 +- 필요 시 `h-full`과 조합해 새 창 viewport를 안정적으로 덮도록 조정 +- 공통 `pb-24`는 팝업별로 과도한 여백을 만드는지 점검하고, 필요하면 축소 또는 분기 처리 + +### 5.2 하수재이용시설 전용 높이 분기 + +- `PbnstSttnModal`에서 `isMap`일 때 `h-[70vh]`를 제거하거나 `max-h` 기반으로 완화 +- 비지도 화면에서는 기존 `ScrollArea + h-[70vh]` 유지 +- 지도 팝업에서는: + - 본문 전체가 자연스럽게 늘어나게 하거나 + - 큰 테이블만 별도 스크롤 처리 + +### 5.3 잠재수요처 하단 흰 영역 대응 + +- `CoDetailModal` 자체의 `ScrollArea h-[79vh]`는 유지하되, +- 먼저 공통 팝업 래퍼의 배경/최소높이 문제를 해결한다. +- 그 다음에도 하얀 영역이 남으면 아래 순서로 추가 점검: + - `pb-24` 제거 또는 축소 시 시각적 개선 여부 + - `ScrollArea` 높이를 `79vh` 고정 대신 `calc(100vh - 헤더/패딩)` 기반으로 변경할지 검토 + - 팝업 창 자체 높이(`mapUtil.ts`)가 본문 높이 전략과 맞는지 재조정 + +### 5.4 선택된 타 팝업 공통 점검 + +- 잠재수요처 수정과 동일한 공통 래퍼 아래에서 동작하는 팝업들은 모두 한 번씩 확인한다. +- 특히 다음을 확인: + - 컨텐츠가 짧을 때 창 하단이 흰색으로 비는지 + - 컨텐츠가 길 때 내부 스크롤/전체 스크롤이 자연스러운지 + - 배경색이 본문 카드까지만 적용되고 바깥 여백은 흰색으로 남지 않는지 +- 이번 확인 결과상 우선순위는 두 그룹으로 나눈다. +- 흰 여백 발생 그룹: + - `tb_wr_tmpr_dmd_co` + - `tb_wr_bdst_ruse_fclt` + - `tb_il_swage_prcs_fclt` +- 비교 기준 그룹: + - `tb_wr_ruse_fclt_dmd_co` + - `tb_wr_rnwtr_utztn_fclt` + - `tb_wr_rswtr_fclt` + +## 6. 상세 작업 단계 + +### 단계 1. 공통 래퍼 수정 + +- `mapFeaturePopup/page.tsx` + - 최상위 래퍼에 viewport 채움 클래스 추가 + - `pb-24`의 필요성 재검토 + - 팝업 종류별로 공통 패딩 분기 필요 여부 확인 + +### 단계 2. 하수재이용시설 잘림 수정 + +- `pbnstSttnModal.tsx` + - `isMap`일 때 `h-[70vh]` 제거 또는 `max-h`로 전환 + - 비지도 화면 동작은 유지 +- 필요 시: + - `RuseDetailPopup`의 `TabsContent` + - `PbnstMngForm` 내부 `h-[200px] overflow-y-auto` + - `하수재이용실적` 테이블 영역 + - 순서로 추가 보정 + +### 단계 3. 잠재수요처 하단 흰 영역 점검 + +- `coDatailModal.tsx` + - 공통 래퍼 수정 후에도 하얀 하단이 남는지 재확인 + - 남는 경우 `ScrollArea h-[79vh]`를 `calc()` 기반으로 조정 검토 +- 함께 비교: + - `coSttnDetailPopup.tsx`의 `bodyClassNm bg-background` 구조와 어떤 차이로 결과가 갈리는지 확인 +- `mapUtil.ts` + - 잠재수요처 팝업 높이(`window.innerHeight - 300`)가 현재 본문 구조와 맞는지 재검토 + +### 단계 4. 선택된 타 팝업 회귀 점검 + +- 발생 그룹 확인: + - `tb_wr_tmpr_dmd_co` + - `tb_wr_bdst_ruse_fclt` + - `tb_il_swage_prcs_fclt` +- 비교 그룹 확인: + - `tb_wr_ruse_fclt_dmd_co` + - `tb_wr_rnwtr_utztn_fclt` + - `tb_wr_rswtr_fclt` +- 두 그룹 비교 기준: + - 개별 본문이 `bg-background` 카드를 직접 가지는지 + - 본문이 자연 높이인지 고정 `vh` 높이인지 + - 공통 래퍼 `pb-24`와 결합될 때 하단 빈 공간이 드러나는지 +- 이후 추가 증상이 확인되면: + - 공통 래퍼로 해결 가능한지 우선 판단 + - 개별 컴포넌트 고정 높이/패딩이 있으면 2차 수정 대상으로 분리 + +## 7. 구현 시 주의사항 + +- 지도 팝업 컴포넌트들은 일반 화면/등록 화면과 공용인 경우가 많다. +- 따라서 수정은 반드시 `isMap` 또는 팝업 전용 경로 기준으로 제한해야 한다. +- `PbnstSttnModal`의 일반 화면 `ScrollArea + h-[70vh]` 동작은 회귀되지 않게 유지해야 한다. +- 잠재수요처의 경우 내부 폼이 아니라 바깥 공통 래퍼 문제일 가능성이 크므로, 개별 폼 높이부터 섣불리 줄이지 않는다. +- `UiTable`, `ScrollArea`, 카드형 래퍼가 혼합되어 있어, 공통 래퍼와 개별 본문 래퍼의 책임을 분리해서 봐야 한다. + +## 8. 검증 계획 + +### 8.1 하수재이용시설 검증 + +- `tb_wr_bdst_ruse_fclt` 클릭 시 팝업 본문 하단이 잘리지 않는다. +- `하수재이용시설 현황` 아래 `하수재이용실적` 영역이 같은 배경 안에서 정상적으로 이어진다. +- 데이터가 길어도 전체 스크롤 또는 의도한 내부 스크롤이 정상 동작한다. + +### 8.2 잠재수요처 검증 + +- `tb_wr_tmpr_dmd_co` 클릭 시 마지막 입력 필드 아래로 흰색 빈 영역이 남지 않는다. +- 팝업 창 하단까지 `bg-background`가 자연스럽게 이어진다. +- 내부 스크롤은 유지되되, 배경색이 창 하단에서 끊기지 않는다. + +### 8.3 선택 팝업 공통 검증 + +- 발생 그룹인 `tb_wr_tmpr_dmd_co`, `tb_wr_bdst_ruse_fclt`, `tb_il_swage_prcs_fclt`는 흰 여백이 제거되는지 확인한다. +- 비교 그룹인 `tb_wr_ruse_fclt_dmd_co`, `tb_wr_rnwtr_utztn_fclt`, `tb_wr_rswtr_fclt`는 기존처럼 흰 여백 없이 유지되는지 확인한다. +- 컨텐츠가 짧은 팝업에서 하단 흰색 잔여 영역이 남지 않는다. +- 컨텐츠가 긴 팝업에서 잘림 없이 스크롤이 가능하다. + +## 9. 결론 + +- 이번 이슈는 `하수재이용시설`의 고정 높이 문제와 `잠재수요처`·`하수처리시설`의 하단 배경 노출 문제로 나뉘지만, 비교 결과 공통 출발점은 `mapFeaturePopup/page.tsx` 공통 래퍼다. +- 1차 우선순위는 `mapFeaturePopup/page.tsx` 공통 래퍼를 viewport 기준으로 정규화하는 것이다. +- 2차 우선순위는 `PbnstSttnModal`의 지도 팝업 전용 `70vh` 고정 높이 제거다. +- 이후에는 "개별 본문 배경을 직접 가지는 팝업"과 "공통 래퍼 배경에 의존하는 팝업"의 차이를 기준으로, 하단 흰색 잔여 영역과 컨텐츠 잘림을 함께 정리하는 것이 맞다. diff --git a/apps/wr/docs/작업계획/테마적용 작업계획_20260623.md b/apps/wr/docs/작업계획/테마적용 작업계획_20260623.md new file mode 100644 index 000000000..cad73ed99 --- /dev/null +++ b/apps/wr/docs/작업계획/테마적용 작업계획_20260623.md @@ -0,0 +1,191 @@ +# apps/wr 테마 적용 작업계획 + +작성일: 2026-06-23 +대상 앱: `D:\iipbs\frontend\apps\wr` + +수정불가 component : `packages/common/components`와 `packages/common/assets` +수정가능 파일 : `packages/common/business_components/*`, `apps/wr/*` + +## 1. 분석 요약 + +- 현재 테마 구조는 이미 `data-theme` 기반으로 잡혀 있다. +- 실제 테마 키는 `default + black + blue + green + red` 기준으로 운영되고 있다. +- `apps/wr`는 `src/app/index.tsx`에서 `site-theme-key` 쿠키값을 읽어 `<html data-theme=...>`에 주입한다. +- `packages/common/components`와 `packages/common/assets`는 CSS 변수와 컴포넌트별 `.css` 파일을 통해 테마를 적용하는 구조다. +- 이번 작업의 전제는 `business_components`가 1차로 `assets`를 참조하고, 2차로 관련 `components`의 CSS 변경을 따라가야 한다는 점이다. +- 반면 현재 `packages/common/business_components`는 다수의 UI가 TSX 내부 Tailwind 클래스에 직접 의존하고 있어 위 전제를 충분히 만족하지 못한다. +- `apps/wr` 내부에서 `@common/business_components`를 참조하는 파일 수가 많아 영향 범위가 크다. + +## 2. 확인된 현재 구조 + +### 2.1 테마 진입점 + +- `D:\iipbs\frontend\apps\wr\src\app\index.tsx` + - `site-theme-key` 쿠키를 읽어 `data-theme`를 설정한다. +- `D:\iipbs\frontend\packages\common\components\src\ui\header\index.tsx` + - 사이트 정보 조회 후 `site-theme-key` 쿠키와 `document.documentElement[data-theme]`를 갱신한다. + +### 2.2 공통 테마 자산 + +- `D:\iipbs\frontend\packages\common\assets\src\styles\base.css` + - `:root`, `[data-theme="black"]`, `[data-theme="red"]`, `[data-theme="green"]`에서 CSS 변수를 정의한다. +- `D:\iipbs\frontend\packages\common\components\src\ui\*.css` + - `header.css`, `table.css`, `sidebar.css` 등에서 컴포넌트별 테마 오버라이드를 관리한다. +- `D:\iipbs\frontend\apps\wr\src\app\styles\components*.css` + - 앱 레벨의 blue/green/red/black 보정 CSS가 이미 존재한다. + +### 2.3 business_components 현황 + +- `packages/common/business_components/src/ui` 기준 소스 파일 수: 81 +- 같은 경로 기준 CSS 파일 수: 6 +- 이 중 일반 UI용 CSS는 사실상 `date_picker/date-picker.css` 중심이고, 나머지는 지도 legacy CSS 성격이 강하다. +- 하드코딩된 `text-gray-*`, `bg-white`, `border-gray-*`, `hover:bg-gray-*`, `dark:*` 패턴이 확인된 파일 수: 28 + +## 3. 핵심 판단 + +### 3.1 CSS 전환 가능 여부 + +- 가능하다. +- 다만 "모든 Tailwind를 제거"가 아니라 "테마 의존 스타일을 CSS로 추출하고, `assets` 및 `components` 참조 구조에 편입"하는 방향이 맞다. +- 즉 배경색, 글자색, 보더색, hover/active 색상, 선택 상태, 강조색은 CSS 변수 기반 클래스로 이동한다. +- 반대로 `flex`, `grid`, `gap`, `w-*`, `h-*`, `p-*` 같은 구조/간격 유틸리티까지 전부 CSS로 옮기는 것은 효율이 낮다. + +### 3.2 왜 이 방향이 맞는가 + +- 기존 `components`가 이미 같은 패턴을 사용한다. +- `assets/base.css`의 토큰(`--background`, `--foreground`, `--primary`, `--border`, `--label`, `--container`, `--title` 등)을 재사용할 수 있다. +- `business_components`는 `주 component가 바뀌면 따라 바뀌어야 한다`는 전제가 있으므로, 자체 색상 하드코딩보다 상위 스타일 자산 참조가 우선되어야 한다. +- 따라서 `business_components` 스타일은 독립 규격이 아니라 `assets -> components -> business_components` 흐름 안에서 정리되어야 한다. +- `apps/wr`는 `business_components` 사용 범위가 넓어, 테마 속성만 먼저 추출해도 화면 일관성이 크게 개선된다. +- 전부 TSX 내부 유틸리티로 유지하면 5개 테마 차이를 한 번에 통제하기 어렵다. + +### 3.3 참조 원칙 + +- 1차 기준: `@common/assets/styles/*` +- 2차 기준: 관련 `@common/components` 하위 CSS +- 3차 보완: 위 두 곳으로 흡수되지 않는 business 전용 표현만 `business_components` 내부 CSS에 작성 + +즉 `business_components`는 가능한 한 직접 색상을 소유하지 않고, 상위 레이어가 정의한 테마 결과를 따라가는 구조로 맞춘다. + +## 4. 우선 전환 대상 + +### 4.1 1순위: 색상 하드코딩이 직접 보이는 공통 UI + +- `packages/common/business_components/src/ui/file-upload/index.tsx` + - `text-gray-*`, `outline-slate-500`, `dark:text-gray-400` 사용 +- `packages/common/business_components/src/ui/tab/index.tsx` + - `text-blue-600`, `text-gray-500`, `border-gray-200`, `hover:border-gray-300` 사용 +- `packages/common/business_components/src/ui/search_input/index.tsx` +- `packages/common/business_components/src/ui/search_input_select/index.tsx` +- `packages/common/business_components/src/ui/select/index.tsx` +- `packages/common/business_components/src/ui/form_ui/index.tsx` +- `packages/common/business_components/src/ui/search_icon/index.tsx` + +이 영역은 `apps/wr`의 검색조건, 입력폼, 탭, 업로드 UI 전반에 재사용될 가능성이 높아서 우선순위가 높다. + +### 4.2 2순위: 테이블/검색/모달 계열 + +- `packages/common/business_components/src/ui/table/index.tsx` +- `packages/common/business_components/src/ui/table/HeadMakeColSpan.tsx` +- `packages/common/business_components/src/ui/search_address/SearchAddressModal.tsx` +- `packages/common/business_components/src/ui/map/searchBar/SearchInputText.tsx` +- `packages/common/business_components/src/ui/map/searchBar/searchBar.tsx` +- `packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx` + +이 영역은 화면 체감도가 높고, 회색/흰색/파란색 하드코딩 비중이 높다. + +### 4.3 3순위: 차트 계열 + +- `packages/common/business_components/src/ui/chart/*.tsx` +- `packages/common/business_components/src/ui/chart/lib/chart.tsx` +- `packages/common/business_components/src/ui/chart/lib/card.tsx` + +차트는 레이아웃보다 tooltip, legend, card, axis text 색상이 중요하다. 우선 색상과 surface만 토큰화하고, 차트 팔레트는 기존 CSS 변수(`--chart-*`)를 재사용하는 방식이 적절하다. + +### 4.4 별도 트랙: 지도 legacy 영역 + +- `packages/common/business_components/src/ui/map/**` +- `packages/common/business_components/src/ui/map/lib/css/**` + +지도는 legacy JS/CSS, sprite/image, OpenLayers 연동이 섞여 있어 일반 UI와 분리해서 작업하는 것이 안전하다. + +## 5. 권장 작업 방식 + +### 단계 1. 테마 기준 정리 + +- `business_components`의 참조 순서를 먼저 고정한다. + - 1차: `assets` 토큰/공통 스타일 참조 + - 2차: 관련 `components` CSS 참조 + - 3차: business 전용 CSS 최소 추가 +- `components`에서 이미 쓰는 토큰과 클래스 패턴을 기준 문법으로 고정한다. +- `business_components`에서 새로 쓰는 색상 클래스는 직접 색상값을 쓰지 말고, 우선 상위 참조 자산으로 해결 가능한지 확인한 뒤 CSS 변수 기반으로 통일한다. +- 예시 기준 + - 텍스트: `text-foreground`, `text-label`, `text-title` + - 배경: `bg-background`, `bg-card`, `bg-container`, `bg-form` + - 보더: `border-border` + - 포커스/선택: `--primary`, `--primary-transparent` + +### 단계 2. 컴포넌트별 CSS 파일 추가 + +- 먼저 관련 `components`에 이미 존재하는 CSS가 있는지 확인한다. +- 같은 성격의 UI라면 `business_components`가 그 CSS 결과를 따라가도록 구조를 맞춘다. +- 상위 참조만으로 해결되지 않는 경우에만 `components`와 동일하게 각 컴포넌트 폴더에 `.css`를 둔다. +- CSS 파일명은 `packages/common/components`와 동일한 규칙으로 작성한다. +- 즉 CSS 파일명은 컴포넌트 폴더/컴포넌트명 기준의 kebab-case를 유지한다. +- CSS 클래스명은 너무 길게 만들지 않는다. +- 네이밍은 `__` 대신 `_`를 사용한다. +- 컴포넌트명도 필요 이상으로 길게 쓰지 않고 축약형을 허용한다. +- 예시 + - `file-upload/file-upload.css` + - `tab/tab.css` + - `search_input/search-input.css` + - `select/select.css` + - `search_address/search-address.css` +- TSX에서는 색상/상태 관련 className을 의미 있는 클래스명으로 치환한다. +- 이때 business 전용 CSS는 상위 CSS와 충돌하지 않도록 override 범위를 최소화한다. + +예시 방향 + +- 변경 전: `text-gray-500`, `bg-white`, `hover:bg-gray-50`, `data-[state=active]:text-blue-600` +- 변경 후: `fileup_hint`, `tab_trigger`, `searchaddr_suggestion`, `is_active` + +### 단계 3. 테마별 분기 작성 + +- 기본값은 `:root` 또는 기본 클래스에서 처리한다. +- 테마 차이가 필요한 경우만 `[data-theme="black"]`, `[data-theme="blue"]`, `[data-theme="green"]`, `[data-theme="red"]` 블록을 추가한다. +- 동일한 값은 중복 정의하지 않는다. +- 가능하면 테마 값은 `assets` 또는 `components`에서 정의하고, `business_components`는 해당 결과를 소비하는 쪽으로 유지한다. + +### 단계 4. apps/wr 화면 검증 + +- `apps/wr`에서 `business_components` 사용도가 높은 화면부터 확인한다. +- 우선 검증 화면 + - 통계 화면 + - 재이용시설 관리 등록/현황 화면 + - 자재관리 검색/등록 화면 + - 지도 검색바/상세팝업/레이어팝업 + +## 6. 실제 작업 순서 + +1. `business_components` 내 테마 하드코딩 파일 목록 확정 +2. 각 대상 UI별로 `assets` 참조와 `components` 참조 가능 여부를 먼저 판정 +3. 공통 토큰 매핑표 작성 +4. 1순위 UI에 상위 참조 우선 적용 후 부족한 부분만 business 전용 CSS 추가 +5. `apps/wr` 주요 화면에서 5개 테마 수동 점검 +6. 2순위 테이블/검색/모달 영역 확장 +7. 차트 계열 색상 토큰화 +8. 지도 legacy 영역은 별도 보수 계획으로 분리하며 배경지도는 수정하지 않는다. + +## 7. 검증 기준 + +- `assets` , `components`가 수정되면 안된다. +- 같은 화면에서 5개 테마 전환 시 텍스트, 보더, 배경, hover, 선택상태가 모두 읽혀야 한다. +- black 테마에서 회색/흰색 하드코딩이 남아 흐릿하거나 과도하게 튀는 영역이 없어야 한다. +- blue/green/red 테마에서 활성 탭, 버튼, 선택상태가 `primary` 계열로 일관되게 바뀌어야 한다. +- `apps/wr` 기존 기능 동작은 그대로 유지되어야 한다. + +## 8. 결론 + +- `business_components`의 Tailwind 스타일을 `assets` 1차 참조, `components` 2차 참조 구조에 맞춰 재편하는 것은 가능하다. +- 다만 전체 유틸리티를 전부 CSS로 옮기기보다, 테마에 영향을 주는 색상/상태 표현을 우선 상위 자산 참조 구조로 흡수하는 방식이 가장 현실적이다. +- 우선순위는 `공통 입력 UI -> 테이블/검색/모달 -> 차트 -> 지도 legacy` 순으로 잡는 것이 좋다. diff --git a/apps/wr/docs/작업계획/테마적용.md b/apps/wr/docs/작업계획/테마적용.md new file mode 100644 index 000000000..c5d8cbd9b --- /dev/null +++ b/apps/wr/docs/작업계획/테마적용.md @@ -0,0 +1,11 @@ +#frontend 5가지 테마(theme) css 적용 작업 +## 대상 dir: D:\iipbs\fr\frontend +## branch :iipbs/dm/dev +## 대상앱 : apps/wr +## 전제 : + - 주 component는 packages/common/components, packages/common/assets이다. 5가지 테마가 적용되어 있다. + - 부 component인 packages/common/business_components는 주 component가 바뀌면 따라 바뀌어야 한다. +## 지시 : + - business_components에 css로 안되어 있고 tailwind 코드가 삽입된 경우는 components와 같이 css로 넣어 줄 수 있는지 확인 부탁해. + - 어떤 방향으로 작업이 진행되어야 할지 프로젝트를 분석해 주고 작업계획_[오늘날짜].md 파일 만들어줘. + \ No newline at end of file diff --git a/apps/wr/docs/작업계획/호출병합2_20260625.md b/apps/wr/docs/작업계획/호출병합2_20260625.md new file mode 100644 index 000000000..e35d07fad --- /dev/null +++ b/apps/wr/docs/작업계획/호출병합2_20260625.md @@ -0,0 +1,201 @@ +# apps/wr 호출 병합 2차 작업계획 + +작성일: 2026-06-25 +대상 앱: `D:\iipbs\frontend\apps\wr` +대상 화면: +- `apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/rswtSttn/page.tsx` +- `apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/htdflSttn/page.tsx` + +## 1. 작업 배경 + +- `rainSttn`에서 정리한 호출 병합 방향을 `rswtSttn`, `htdflSttn`에도 확장할 필요가 있다. +- `rswtSttn`은 `searchTrigger`, `skipNextSearchRef`, `rswtSttnInit` 분리 구조가 그대로 남아 있어 `rainSttn`과 동일한 페이징 불량 가능성이 높다. +- `htdflSttn`은 `searchTrigger`는 없지만, 조회 버튼/페이징/엑셀 다운로드가 모두 `objSearch`, `fcltNm`을 직접 읽고 있어 “입력 중 값”과 “실제 적용된 값”이 섞일 수 있다. +- 두 화면 모두 엑셀 다운로드는 store의 `excelList` 반영 타이밍에 의존하고 있어, 첫 클릭 시 "다운로드할 데이터가 없습니다."가 발생할 수 있다. + +## 2. 현재 구조 분석 + +### 2.1 rswtSttn 현재 흐름 + +- 초기 진입: + - `getLaeSeCdCheck()` 실행 + - `laeSeCd` 준비 후 `rswtSttnInit(false, 1, rswtSttnSize, ...)` 호출 +- 이후 페이징/조회: + - `pageNation`, `searchTrigger`, `appliedSearch`를 감시하는 effect에서 `rswtSttnSearch(...)` 호출 +- 추가 제어: + - `skipNextSearchRef`로 초기 effect 1회를 건너뜀 + - `searchTrigger === 0`이면 검색 effect를 중단 +- 엑셀: + - `isDownloadTriggered.current = true` 설정 + - `rswtSttnSearch(true, ...)` 호출 + - 이후 `rswtSttn.excelList` 변경 effect에서 다운로드 실행 + +### 2.2 htdflSttn 현재 흐름 + +- 초기 진입: + - `getLaeSeCdCheck()` 실행 + - `initHtdfl(false, 1, htdflSttnSize, String(getFixedConValue()), fcltNm, ...)` 호출 +- 조회 버튼: + - 클릭 시 즉시 `htdflSttnSearch(false, 1, ..., String(getFixedConValue()), fcltNm, ...)` 호출 + - `pageNation`은 1로 돌리지 않고 `resetPageNo`만 켬 +- 페이징: + - `pageNation` 변경 effect에서 `String(getFixedConValue())`, `fcltNm`을 직접 읽어 `htdflSttnSearch(...)` 호출 +- 엑셀: + - `isDownloadTriggered.current = true` 설정 + - `htdflSttnSearch(true, ...)` 호출 + - 이후 `htdflSttn.excelList` 변경 effect에서 다운로드 실행 + +### 2.3 실제 문제 지점 + +- `rswtSttn` + - 사용자가 조회 버튼을 누르기 전에는 `searchTrigger === 0` 때문에 페이지 이동 시 API 호출이 막힌다. + - 초기 조회와 이후 조회가 `init/search`로 나뉘어 동일한 목록 조회인데도 상태 플래그에 의존한다. +- `htdflSttn` + - 페이지 이동 시 `objSearch`, `fcltNm`의 현재 입력값을 그대로 읽으므로, 마지막 조회 결과와 다른 조건으로 페이지가 바뀔 수 있다. + - 조회 버튼이 `pageNation`과 분리되어 있어 "1페이지로 되돌아간 뒤 같은 조건으로 재조회" 흐름이 일관되지 않다. +- 공통 + - 엑셀 다운로드는 클릭 직후 `excelList`를 검사할 수 있어 비동기 응답 전에 빈 배열로 판정될 수 있다. + - 첫 클릭 뒤 뒤늦게 store에 엑셀 데이터가 남으면, 다음 클릭에서 이전 응답 결과로 다운로드될 위험도 있다. + +## 3. 목표 구조 + +### 3.1 조회 호출 단일화 + +- 초기 진입도 `search` 계열 호출로 통일한다. +- 같은 파라미터라면 초기 진입, 조회 버튼, 페이지 이동, 엑셀 다운로드가 모두 동일한 조건 계산 경로를 사용한다. +- 조회 버튼은 “적용 조건 확정 + 페이지 1 초기화”만 담당하고, 실제 목록 조회는 하나의 경로로만 실행한다. + +### 3.2 적용 조건 분리 + +- 입력 중 값은 `objSearch`, `fcltNm`으로 유지한다. +- 실제 조회 기준 값은 `appliedSearch`로 분리한다. +- 페이지 이동, 재조회, 엑셀 다운로드는 반드시 `appliedSearch`만 사용한다. + +### 3.3 엑셀 다운로드 직접 처리 + +- `excelClick`에서 `await ...Search(true, ...)` 완료를 직접 기다린다. +- store의 `excelList` 변경 effect를 기다리지 않고, 반환받은 엑셀 데이터로 즉시 다운로드를 실행한다. +- store는 필요하면 기존 `excelList`도 유지할 수 있으나, 다운로드 성공 여부는 반환값 기준으로 판단한다. + +## 4. 수정 대상 + +### 4.1 화면 + +- `D:\iipbs\frontend\apps\wr\app\(with-layout)\ruseFcltMng\fcltSttn\rswtSttn\page.tsx` +- `D:\iipbs\frontend\apps\wr\app\(with-layout)\ruseFcltMng\fcltSttn\htdflSttn\page.tsx` + +### 4.2 store + +- `D:\iipbs\frontend\apps\wr\app\store\ruseFcltMng\fcltSttn\rswtSttn\index.tsx` +- `D:\iipbs\frontend\apps\wr\app\store\ruseFcltMng\fcltSttn\htdflSttn\index.tsx` + +## 5. 권장 구현 방향 + +### 5.1 rswtSttn 화면 정리 + +- `rswtSttnInit()` 직접 호출 제거 +- `searchTrigger` 제거 +- `skipNextSearchRef` 제거 또는 최소화 +- `appliedSearch`를 `null` 가능 상태로 두고, 초기 진입 시 1회 세팅 +- 목록 조회 effect는 `pageNation`, `appliedSearch`, `isLaeSeCdReady`만 감시 +- 조회 버튼은 `appliedSearch` 갱신 후 `setPageNation(1)`만 수행 +- 엑셀 다운로드는 `await rswtSttnSearch(true, ...)` 후 반환값으로 직접 다운로드 + +### 5.2 rswtSttn store 정리 + +- `rswtSttnInit`는 삭제 `rswtSttnSearch`으로 병합. +- `isExcel === true`일 때 파싱된 엑셀 행 데이터를 반환하도록 시그니처 확장 +- 목록 조회일 때는 기존처럼 `rswtSttnRes`, `rswtSttnResCount`를 갱신 + +### 5.3 htdflSttn 화면 정리 + +- `objSearch`, `fcltNm`와 별도로 `appliedSearch`를 추가 +- 초기 진입 시 `laeSeCd` 기준 초기 `appliedSearch`를 세팅하고, 목록 조회는 `htdflSttnSearch(false, 1, ...)` 경로로 통일 +- 조회 버튼은 `appliedSearch` 갱신 후 `setPageNation(1)`만 수행 +- 페이지 이동 effect는 반드시 `appliedSearch` 기준으로만 호출 +- 현재 `initHtdfl(false, ..., String(getFixedConValue()), fcltNm, ...)`처럼 입력 중 값에 직접 의존하는 구조를 제거 +- 엑셀 다운로드는 `await htdflSttnSearch(true, ...)` 후 반환값으로 직접 다운로드 + +### 5.4 htdflSttn store 정리 + +- `initHtdfl`는 삭제 `htdflSttnSearch` 으로 병합 +- `isExcel === true`일 때 파싱된 엑셀 행 데이터를 반환하도록 시그니처 확장 +- 목록 조회 시 `htdflSttnRes`, `htdflSttnResCount` 갱신 경로는 유지 + +## 6. 상세 작업 단계 + +### 단계 1. rswtSttn 호출 병합 + +- `page.tsx` + - `rswtSttnInit` 제거 + - `searchTrigger` 제거 + - `skipNextSearchRef` 제거 + - 초기 effect에서 `appliedSearch`만 세팅 + - 목록 조회 effect는 `appliedSearch` 준비 후 항상 `rswtSttnSearch(false, pageNation, ...)` 호출 +- `index.tsx` + - 공통 fetch 함수 도입 + - `rswtSttnSearch(true, ...)`가 `string[][]` 반환 가능하도록 수정 + +### 단계 2. rswtSttn 엑셀 다운로드 직접화 + +- `isDownloadTriggered` ref 제거 +- `rswtSttn.excelList` 감시 effect 제거 +- `excelClick`를 `async`로 변경 +- `const excelData = await rswtSttnSearch(true, ...)` 후: + - 데이터가 있으면 즉시 `excelDownloadNumStyle(...)` + - 없으면 `excelDownErrMsg` 노출 + +### 단계 3. htdflSttn 적용 조건 분리 + +- `appliedSearch` 상태 추가 +- 초기 진입 시 `laeSeCd` 기준 기본 조건을 `appliedSearch`에 반영 +- 조회 버튼은 입력값을 `appliedSearch`로 확정한 뒤 페이지를 1로 초기화 +- 페이지 이동 effect는 `appliedSearch`만 기준으로 목록 조회 +- `resetPageNo`는 UI 초기화용으로만 유지 + +### 단계 4. htdflSttn store 병합 및 엑셀 직접화 + +- `initHtdfl`는 삭제 `htdflSttnSearch` 으로 병합 +- `htdflSttnSearch(true, ...)`가 엑셀 데이터 반환 가능하도록 수정 +- 화면에서 `await htdflSttnSearch(true, ...)` 결과로 직접 다운로드 +- `isDownloadTriggered` ref 및 `htdflSttn.excelList` 감시 effect 제거 + +## 7. 구현 시 주의사항 + +- `objSearch`는 입력 UI 상태이고, API 호출 기준은 `appliedSearch`다. +- `setPageNation(1)` 직후 중복 조회가 생기지 않도록, 조회 버튼에서 직접 API를 다시 호출하지 말고 하나의 effect 경로만 사용한다. +- `htdflSttn`은 현재 조회 버튼이 `setPageNation(1)` 없이 직접 API를 호출하고 있으므로 이 부분을 반드시 통일해야 한다. +- 엑셀은 전체 건수 기준 조회를 유지하되, 조건 계산은 일반 목록 조회와 동일해야 한다. +- store에서 엑셀 데이터 반환을 추가하더라도 기존 목록 갱신 로직은 깨지지 않게 분기해야 한다. + +## 8. 검증 계획 + +### 8.1 rswtSttn 검증 + +- 초기 진입 시 1페이지 목록이 조회된다. +- 초기 진입 직후 조회 버튼을 누르지 않고 2페이지로 이동하면 API가 호출된다. +- 조회 조건 변경 후 조회 버튼 클릭 시 1페이지로 이동하며 API가 1회만 호출된다. +- 페이지 2, 3으로 이동해도 같은 `appliedSearch` 조건이 유지된다. +- 첫 엑셀다운로드 클릭에서 즉시 파일이 내려오고, "다운로드할 데이터가 없습니다."가 잘못 뜨지 않는다. + +### 8.2 htdflSttn 검증 + +- 초기 진입 시 1페이지 목록이 조회된다. +- 조회 버튼 클릭 시 항상 1페이지 기준으로 재조회된다. +- 조회 후 입력값만 바꾸고 조회 버튼을 누르지 않은 상태에서 페이지 이동해도, 마지막 조회 조건으로 페이지가 유지된다. +- 첫 엑셀다운로드 클릭에서도 현재 적용 조건 기준으로 정상 다운로드된다. + +### 8.3 회귀 검증 + +- 상세 모달 열기 동작 유지 +- 권한별 `laeSeCd` 초기 조건 유지 +- 시도/시군구 선택 제한 유지 +- 조회항목설정 모달 동작 유지 + +## 9. 결론 + +- `rswtSttn`은 `rainSttn`과 거의 동일한 구조적 문제를 갖고 있으므로 같은 방식으로 정리하는 것이 맞다. +- `htdflSttn`은 페이징 가드 문제는 없지만, 적용 조건 분리와 엑셀 다운로드 처리 방식은 동일하게 손봐야 한다. +- 이번 2차 작업의 핵심은 다음 두 가지다. +- “목록 조회는 항상 `appliedSearch` 기준의 단일 경로로 통일” +- “엑셀 다운로드는 store 반영 effect가 아니라 `await ...Search(true, ...)` 반환값으로 직접 처리” diff --git a/apps/wr/docs/작업계획/호출병합_20260625.md b/apps/wr/docs/작업계획/호출병합_20260625.md new file mode 100644 index 000000000..460519d8f --- /dev/null +++ b/apps/wr/docs/작업계획/호출병합_20260625.md @@ -0,0 +1,171 @@ +# apps/wr 호출 병합 및 페이징 정상화 작업계획 + +작성일: 2026-06-25 +대상 앱: `D:\iipbs\frontend\apps\wr` +대상 화면: +- `apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/rainSttn/page.tsx` +- 연관 검토 대상: `rswtSttn`, `htdflSttn` + +## 1. 작업 배경 + +- `rainSttn` 화면에서 초기 진입 시 목록은 조회되지만, 바로 페이지를 변경하면 API 호출이 발생하지 않아 페이징이 동작하지 않는다. +- 현재 구조는 초기 진입 시 `init` 계열 호출과 사용자 조회 이후 `search` 호출을 분리하고 있다. +- 이 분리 구조를 제어하기 위해 `searchTrigger`와 `skipNextSearchRef`가 함께 사용되고 있는데, 그 결과 초기 조회 이후의 페이지 이동까지 차단되는 상태가 발생했다. +- 요구사항은 초기 진입 시에도 `search` 호출로 통일하고, 페이징이 항상 같은 목록 조회 경로를 타도록 정리하는 것이다. + +## 2. 현재 구조 분석 + +### 2.1 rainSttn 현재 흐름 + +- 초기 진입: + - `getLaeSeCdCheck()` 실행 + - `laeSeCd` 준비 후 `rainSttnInit(false, 1, rainSttnSize, ...)` 호출 +- 이후 페이징/조회: + - `pageNation`, `appliedSearch`, `searchTrigger`를 감시하는 effect에서 `rainSttnSearch(...)` 호출 +- 추가 제어: + - `skipNextSearchRef`로 초기 effect 1회를 건너뜀 + - `searchTrigger === 0`이면 검색 effect를 중단 + +### 2.2 실제 문제 지점 + +- `UiTable`의 `onPageChange={setPageNation}` 자체는 정상이다. +- 그러나 페이지 번호가 바뀐 뒤 실행되는 검색 effect에서 `searchTrigger === 0` 조건으로 return 한다. +- 따라서 사용자가 `조회` 버튼을 한 번도 누르지 않은 초기 상태에서는 페이지 변경만으로는 `rainSttnSearch()`가 호출되지 않는다. +- 결과적으로 첫 진입 후 페이징 클릭 시 API가 호출되지 않는다. + +### 2.3 구조적 문제 + +- 동일한 목록 조회인데 `init`과 `search`를 별도 액션으로 분리해 두었다. +- 화면은 “초기 조회”, “수동 조회”, “페이징 조회”를 서로 다른 상태 플래그로 구분한다. +- 이 구조는 중복 호출 방지에는 일부 도움이 되지만, 목록 조회의 단일 진입점을 없애고 상태 의존성을 높인다. +- 특히 페이징은 “현재 조건으로 특정 페이지를 조회한다”는 단순 동작이어야 하는데, 현재는 “사용자가 조회 버튼을 눌렀는지”까지 알아야 동작한다. + +## 3. 목표 구조 + +### 3.1 조회 호출 단일화 + +- 초기 진입 시에도 `search` 계열 호출을 사용한다. +- 화면에서 목록 조회는 하나의 함수만 사용한다. +- 같은 파라미터라면 초기 진입, 조회 버튼, 페이지 이동, 엑셀 다운로드가 모두 동일한 검색 조건 계산 방식을 사용한다. + +### 3.2 상태 제어 단순화 + +- `searchTrigger` 제거 +- 가능하면 `rainSttnInit` 사용 제거 또는 `rainSttnSearch`로 내부 위임 +- 초기 중복 호출 방지는 `init/search` 분리가 아니라 effect 설계 단순화로 해결 +- 페이지 이동은 항상 API 호출로 이어지도록 보장 + +### 3.3 기대 효과 + +- 초기 진입 직후에도 페이징 정상 동작 +- 조회 버튼을 누르기 전/후에 따라 동작이 달라지는 문제 제거 +- 목록 조회 흐름이 단일화되어 유지보수 용이성 개선 +- `rswtSttn` 같은 유사 화면에도 동일 패턴 적용 가능 + +## 4. 수정 대상 + +### 4.1 1차 수정 대상 + +- `D:\iipbs\frontend\apps\wr\app\(with-layout)\ruseFcltMng\fcltSttn\rainSttn\page.tsx` +- `D:\iipbs\frontend\apps\wr\app\store\ruseFcltMng\fcltSttn\rainSttn\index.tsx` + +### 4.2 연계 검토 대상 + +- `D:\iipbs\frontend\apps\wr\app\(with-layout)\ruseFcltMng\fcltSttn\rswtSttn\page.tsx` +- `D:\iipbs\frontend\apps\wr\app\store\ruseFcltMng\fcltSttn\rswtSttn\index.tsx` +- `D:\iipbs\frontend\apps\wr\app\(with-layout)\ruseFcltMng\fcltSttn\htdflSttn\page.tsx` + +## 5. 권장 구현 방향 + +### 5.1 화면 레벨 정리 + +- `rainSttn/page.tsx`에서 다음을 정리한다. +- 초기 진입용 `rainSttnInit()` 호출을 `rainSttnSearch()` 호출로 대체한다. +- `searchTrigger` 상태 제거 +- `handleSearch()`는 검색 조건을 반영하고 `pageNation`을 1로 되돌린 뒤, 목록 조회를 직접 실행하거나 페이지 effect 하나로 일관되게 처리한다. +- 페이지 변경 시에는 항상 현재의 `appliedSearch` 기준으로 `rainSttnSearch()`가 호출되게 한다. +- 초기 1회 중복 호출만 필요하다면 `skipNextSearchRef` 유지 여부를 최소 범위에서 재판단한다. + +### 5.2 store 레벨 정리 + +- `rainSttn/index.tsx`에서 `rainSttnInit`와 `rainSttnSearch`의 구현이 사실상 동일하므로 병합 후보로 본다. +- 다음 방법으로 수정: + - `rainSttnInit` 제거 + - 화면에서는 `rainSttnSearch`만 사용 + +### 5.3 추천 방식 + +- 화면에서는 `search`만 사용하도록 통일 +- store에서는 당장 외부 참조 영향이 있으면 `init` 시그니처는 남기되 내부는 `search` 재사용으로 정리 +- 이후 호출처 정리까지 완료되면 `init` 제거. + +## 6. 상세 작업 단계 + +### 단계 1. rainSttn 화면 호출 흐름 단일화 + +- `searchTrigger` 제거 +- 초기 진입 effect에서 `rainSttnSearch(false, 1, rainSttnSize, initialConValue, "", code)` 호출 +- 조회 버튼 클릭 시: + - `appliedSearch` 갱신 + - 페이지를 1로 초기화 + - 조건에 따라 직접 `rainSttnSearch()` 호출하거나, page 1 변경 effect에서 1회만 호출되도록 정리 +- 페이지 이동 시: + - 현재 `appliedSearch`로 항상 `rainSttnSearch()` 호출 + +### 단계 2. 중복 호출 방지 로직 최소화 + +- 초기 진입 시점에 실제로 중복 호출이 발생하는지 확인 +- 불필요하면 `skipNextSearchRef`도 제거하고 `useEffect` 수를 축소 + +### 단계 3. store 액션 병합 + +- `rainSttnInit`는 제거 `rainSttnSearch`로 공통 URL 조합 로직 정리 +- 엑셀/일반 조회 분기만 남기고 목록 조회 경로를 단일화 + +### 단계 4. 유사 화면 점검 + +- `rswtSttn`은 `searchTrigger` 구조가 동일하므로 같은 페이징 불량 가능성이 높다. +- `rainSttn` 수정 후 동일 방식으로 `rswtSttn` 동시 정리 여부를 판단한다. +- `htdflSttn`은 이미 `searchTrigger` 없이 동작하므로 참조 구현으로 활용한다. + +## 7. 구현 시 주의사항 + +- 검색 조건 입력값(`objSearch`)과 실제 적용 조건(`appliedSearch`)을 구분하는 구조는 유지할 수 있다. +- 다만 API 호출은 반드시 `appliedSearch` 기준으로만 가야 한다. +- `setPageNation(1)` 직후 effect 호출 시점 때문에 중복 조회가 생기지 않도록, “직접 호출”과 “effect 기반 호출” 중 하나만 선택해야 한다. +- 엑셀 다운로드는 전체 건수 기준 조회를 유지하되, 검색 조건 계산 로직은 일반 목록 조회와 동일해야 한다. +- `resetPageNo` 플래그가 테이블 페이지 번호 UI 초기화용이라면, API 호출 조건과 분리해서 관리해야 한다. + +## 8. 검증 계획 + +### 8.1 rainSttn 검증 + +- 초기 진입 시 1페이지 목록이 조회된다. +- 초기 진입 직후 조회 버튼을 누르지 않고 2페이지로 이동하면 API가 호출되고 목록이 바뀐다. +- 조회 조건 변경 후 조회 버튼 클릭 시 1페이지로 이동하며 API가 1회만 호출된다. +- 조회 후 2페이지, 3페이지 이동 시 동일 조건으로 API가 정상 호출된다. +- 엑셀 다운로드 시 현재 적용된 조건으로 전체 건수 조회가 수행된다. + +### 8.2 회귀 검증 + +- 상세 모달 열기 동작 유지 +- 권한별 `laeSeCd` 초기 조건 유지 +- 시도/시군구 선택 제한 유지 +- 검색어 없는 상태/있는 상태 모두 페이징 정상 동작 + +## 9. 후속 권장사항 + +- `rainSttn`, `rswtSttn`, `htdflSttn`의 목록 조회 패턴을 공통화하는 것이 바람직하다. +- 가능하면 다음 공통 원칙으로 정리한다. +- “초기 진입도 search” +- “페이지 이동도 search” +- “조회 버튼도 search” +- “엑셀만 isExcel=true search” +- 이 원칙으로 맞추면 `init/search` 이원화에서 반복적으로 발생하는 상태 꼬임을 줄일 수 있다. + +## 10. 결론 + +- 현재 문제의 직접 원인은 `searchTrigger` 가드이지만, 근본 원인은 `init`과 `search`를 분리한 화면 구조다. +- 이번 작업은 단순 버그 수정이 아니라 “목록 조회 진입점 단일화” 관점으로 진행하는 것이 맞다. +- 우선 `rainSttn`에서 초기 진입을 `search`로 통일하고 페이징이 항상 API를 타도록 수정한다. +- 이후 동일 패턴의 `rswtSttn`까지 함께 정리하면 재발 가능성을 크게 줄일 수 있다. diff --git a/apps/wr/global.d.ts b/apps/wr/global.d.ts new file mode 100644 index 000000000..cbe652dbe --- /dev/null +++ b/apps/wr/global.d.ts @@ -0,0 +1 @@ +declare module "*.css"; diff --git a/apps/wr/src/app/index.tsx b/apps/wr/src/app/index.tsx index 692b55ef0..4cf2ad5fa 100644 --- a/apps/wr/src/app/index.tsx +++ b/apps/wr/src/app/index.tsx @@ -1,5 +1,9 @@ import "./styles/base.css"; import "./styles/components.css"; +import "./styles/components-black.css"; +import "./styles/components-blue.css"; +import "./styles/components-green.css"; +import "./styles/components-red.css"; import "@fontsource-variable/noto-sans-kr"; import { AppProvider } from "./providers"; diff --git a/apps/wr/src/app/styles/components-black.css b/apps/wr/src/app/styles/components-black.css new file mode 100644 index 000000000..016ebec9f --- /dev/null +++ b/apps/wr/src/app/styles/components-black.css @@ -0,0 +1,137 @@ +@tailwind components; + +@layer components { + .card { + @apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666]; + } + .card-border { + @apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur; + } + .card-header { + @apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4; + } + .card-footer { + @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; + } + + [data-theme="black"] { + .card { + color: #fff; + background-color: #0a0a0a !important; + } + form { + color: rgb(255 255 255); + } + label { + color: rgb(241, 241, 241); + } + input { + height: 3.2rem; + background-color: #1a1a1a; + border: 1px solid #262626; + border-radius: 4px; + } + input.text-gray-600 { + color: #f1f1f1; + } + button { + color: #171717; + } + button[role="combobox"] { + color: #f1f1f1; + background-color: rgb(26, 26, 26); + } + div [role='tablist'] { + border: unset; + gap: 4px; + } + .tabs-trigger[data-state="inactive"] { + color: #a4b7bf; + background: #55595d; + } + .tabs-trigger[data-state="inactive"]:hover { + color: #6bbaff; + background: #4c4f52; + } + .card-border { + box-shadow: 0 0 10px -1px #0000000f; + border: 1px solid #393939; + } + a[aria-current="page"] { + background-color: rgb(32, 148, 250); + color: #000; + } + .card-border { + @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)]; + } + .card-header { + @apply border-b-[#3f4042]; + } + + .form-section-title { + @apply bg-[#5e7b9c] text-[#fff]; + } + + .gis-layer-panel { + background: #111214; + border-right-color: #2f3136; + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.03); + } + + .gis-layer-panel-title { + color: #f3f4f6; + } + + .gis-layer-panel hr { + border-color: #2f3136; + } + + .gis-layer-row { + color: #e5e7eb; + } + + .gis-layer-row:hover { + background: rgba(255, 255, 255, 0.08); + } + + .gis-layer-row span, + .gis-layer-row label { + color: #e5e7eb !important; + } + + .gis-layer-row input[type="checkbox"] { + accent-color: #c026d3; + } + + .gis-layer-action-button { + background: #1a1c20; + border-color: #3b82f6; + color: #cfe3ff; + } + + .gis-layer-action-button:hover { + background: #23262b; + } + + .gis-layer-setting { + background: #111214; + border-top: 1px solid #2f3136; + } + + .gis-layer-toggle { + background: #111214; + border-color: #2f3136; + color: #f3f4f6; + } + + .gis-layer-toggle:hover { + background: #1a1c20; + } + + + } + + input[type="file"] { + @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; + } +} diff --git a/apps/wr/src/app/styles/components-blue.css b/apps/wr/src/app/styles/components-blue.css new file mode 100644 index 000000000..1e49c377c --- /dev/null +++ b/apps/wr/src/app/styles/components-blue.css @@ -0,0 +1,35 @@ +@tailwind components; + +@layer components { + .card { + @apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666]; + } + .card-border { + @apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur; + } + .card-header { + @apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4; + } + .card-footer { + @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; + } + + [data-theme="blue"] { + .card-border { + @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)]; + } + .card-header { + @apply border-b-[#3f4042]; + } + + .form-section-title { + @apply bg-[#eaf3fb] text-[#647784]; + } + + + } + + input[type="file"] { + @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; + } +} diff --git a/apps/wr/src/app/styles/components-green.css b/apps/wr/src/app/styles/components-green.css new file mode 100644 index 000000000..8803e7cb2 --- /dev/null +++ b/apps/wr/src/app/styles/components-green.css @@ -0,0 +1,35 @@ +@tailwind components; + +@layer components { + .card { + @apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666]; + } + .card-border { + @apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur; + } + .card-header { + @apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4; + } + .card-footer { + @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; + } + + [data-theme="green"] { + .card-border { + @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)]; + } + .card-header { + @apply border-b-[#3f4042]; + } + + .form-section-title { + @apply bg-[#ebf7f7] text-[#64908f]; + } + + + } + + input[type="file"] { + @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; + } +} diff --git a/apps/wr/src/app/styles/components-red.css b/apps/wr/src/app/styles/components-red.css new file mode 100644 index 000000000..9bfc1b792 --- /dev/null +++ b/apps/wr/src/app/styles/components-red.css @@ -0,0 +1,34 @@ +@tailwind components; + +@layer components { + .card { + @apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666]; + } + .card-border { + @apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur; + } + .card-header { + @apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4; + } + .card-footer { + @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; + } + + [data-theme="red"] { + .card-border { + @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)]; + } + .card-header { + @apply border-b-[#3f4042]; + } + + .form-section-title { + @apply bg-[#f6f6f9] text-[#808080]; + } + + } + + input[type="file"] { + @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; + } +} diff --git a/apps/wr/src/app/styles/components.css b/apps/wr/src/app/styles/components.css index fd651fe6c..60549b226 100644 --- a/apps/wr/src/app/styles/components.css +++ b/apps/wr/src/app/styles/components.css @@ -14,71 +14,12 @@ @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; } - [data-theme="black"] { - .card-border { - @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)]; - } - .card-header { - @apply border-b-[#3f4042]; - } - - .gis-layer-panel { - background: #111214; - border-right-color: #2f3136; - box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.03); - } - - .gis-layer-panel-title { - color: #f3f4f6; - } - - .gis-layer-panel hr { - border-color: #2f3136; - } - - .gis-layer-row { - color: #e5e7eb; - } - - .gis-layer-row:hover { - background: rgba(255, 255, 255, 0.08); - } - - .gis-layer-row span, - .gis-layer-row label { - color: #e5e7eb !important; - } - - .gis-layer-row input[type="checkbox"] { - accent-color: #c026d3; - } - - .gis-layer-action-button { - background: #1a1c20; - border-color: #3b82f6; - color: #cfe3ff; - } - - .gis-layer-action-button:hover { - background: #23262b; - } - - .gis-layer-setting { - background: #111214; - border-top: 1px solid #2f3136; - } - - .gis-layer-toggle { - background: #111214; - border-color: #2f3136; - color: #f3f4f6; - } - - .gis-layer-toggle:hover { - background: #1a1c20; - } + .form-section-title { + @apply mt-4 border border-border bg-[#eaf3fb] p-2 text-center text-xl font-medium text-[#647784]; } + + input[type="file"] { @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; } diff --git a/packages/common/business_components/src/ui/file-upload/file-upload.css b/packages/common/business_components/src/ui/file-upload/file-upload.css new file mode 100644 index 000000000..9a8737063 --- /dev/null +++ b/packages/common/business_components/src/ui/file-upload/file-upload.css @@ -0,0 +1,67 @@ +@tailwind components; + +@layer components { + .fileup_root { + @apply relative rounded-lg bg-background p-2; + } + + .fileup_dropzone { + @apply border border-border outline-dashed outline-1 outline-border; + } + + .fileup_panel { + @apply flex w-full flex-col items-center justify-center p-1; + } + + .fileup_icon { + @apply h-4 w-5 text-label; + } + + .fileup_hint { + @apply mb-1 text-sm text-label; + } + + .fileup_hint_strong { + @apply font-semibold text-foreground; + } + + .fileup_meta { + @apply text-xs text-label; + } + + .fileup_item { + @apply text-foreground; + } + + .fileup_item_active { + @apply bg-muted; + } + + .fileup_item_name { + @apply flex h-full w-4/5 items-center gap-1.5 overflow-hidden font-medium leading-normal tracking-tight; + } + + .fileup_item_remove { + @apply text-label; + } + + .fileup_item_remove_icon { + @apply h-4 w-4 transition-colors duration-200 ease-in-out hover:stroke-destructive; + } + + .fileup_input { + @apply border; + } + + .fileup_input_idle { + @apply border-border; + } + + .fileup_input_accept { + @apply border-primary; + } + + .fileup_input_reject { + @apply border-destructive; + } +} diff --git a/packages/common/business_components/src/ui/file-upload/index.tsx b/packages/common/business_components/src/ui/file-upload/index.tsx index 6836691e2..ae6e8cc55 100644 --- a/packages/common/business_components/src/ui/file-upload/index.tsx +++ b/packages/common/business_components/src/ui/file-upload/index.tsx @@ -1,4 +1,6 @@ "use client"; +import "./file-upload.css"; + import { CloudUpload, Paperclip } from "lucide-react"; import { FileInput, FileUploader, FileUploaderContent, FileUploaderItem } from "./lib/file_upload"; @@ -36,19 +38,19 @@ export const FileUpload = onValueChange={setFiles} dropzoneOptions={dropZoneConfig} acceptName={fileTypes} - className="relative bg-background rounded-lg p-2" + className="fileup_root" > <FileInput id="fileInput" - className="outline-dashed outline-1 outline-slate-500" + className="fileup_dropzone" > - <div className="flex items-center justify-center flex-col p-1 w-full "> - <CloudUpload className='text-gray-500 w-5 h-4' /> - <p className="mb-1 text-sm text-gray-500 dark:text-gray-400"> - <span className="font-semibold">클릭 후 업로드 </span> + <div className="fileup_panel"> + <CloudUpload className='fileup_icon' /> + <p className="fileup_hint"> + <span className="fileup_hint_strong">클릭 후 업로드 </span>   혹은 파일을 드래그 해주세요 </p> - <p className="text-xs text-gray-500 dark:text-gray-400"> + <p className="fileup_meta"> {!fileTypes?"SVG, PNG, JPG, GIF":fileTypes} </p> </div> diff --git a/packages/common/business_components/src/ui/file-upload/lib/file_upload.tsx b/packages/common/business_components/src/ui/file-upload/lib/file_upload.tsx index 9e47d296e..f0071748e 100644 --- a/packages/common/business_components/src/ui/file-upload/lib/file_upload.tsx +++ b/packages/common/business_components/src/ui/file-upload/lib/file_upload.tsx @@ -344,14 +344,14 @@ export const FileUploaderItem = forwardRef< ref={ref} className={cn( buttonVariants({ variant: "ghost" }), - "h-9 justify-between cursor-pointer relative w-full", + "fileup_item h-9 justify-between cursor-pointer relative w-full", className, - isSelected ? "bg-muted" : "", + isSelected ? "fileup_item_active" : "", )} {...props} > - <div className="font-medium leading-normal tracking-tight flex items-center gap-1.5 h-full !w-4/5 overflow-hidden " + <div className="fileup_item_name" onClick={() =>{onFileClick?.(index) }}> {children} @@ -359,7 +359,7 @@ export const FileUploaderItem = forwardRef< <button type="button" className={cn( - "absolute", + "fileup_item_remove absolute", "!w-1/5 z-50", direction === "rtl" ? "left-1 top-1/2 -translate-y-1/2" @@ -372,7 +372,7 @@ export const FileUploaderItem = forwardRef< removeFileFromSet(index)}} > <span className="sr-only">remove item {index}</span> - <RemoveIcon className="w-4 h-4 hover:stroke-destructive duration-200 ease-in-out" /> + <RemoveIcon className="fileup_item_remove_icon" /> </button> </div> ); @@ -396,14 +396,12 @@ export const FileInput = forwardRef< > <div className={cn( - `w-full rounded-lg duration-300 ease-in-out - ${ - dropzoneState.isDragAccept - ? "border-green-500" - : dropzoneState.isDragReject || isFileTooBig - ? "border-red-500" - : "border-gray-300" - }`, + "fileup_input w-full rounded-lg duration-300 ease-in-out", + dropzoneState.isDragAccept && "fileup_input_accept", + (dropzoneState.isDragReject || isFileTooBig) && "fileup_input_reject", + !dropzoneState.isDragAccept && + !(dropzoneState.isDragReject || isFileTooBig) && + "fileup_input_idle", className, )} {...rootProps} diff --git a/packages/common/business_components/src/ui/form_ui/form-ui.css b/packages/common/business_components/src/ui/form_ui/form-ui.css new file mode 100644 index 000000000..95059a81c --- /dev/null +++ b/packages/common/business_components/src/ui/form_ui/form-ui.css @@ -0,0 +1,19 @@ +@tailwind components; + +@layer components { + .formui_label { + @apply text-xl; + } + + .formui_option_wrap { + @apply float-right; + } + + .formui_option { + @apply flex items-center gap-2; + } + + .formui_option_label { + @apply w-28 text-xl text-label; + } +} diff --git a/packages/common/business_components/src/ui/form_ui/index.tsx b/packages/common/business_components/src/ui/form_ui/index.tsx index 27b74bd97..39aa83b5d 100644 --- a/packages/common/business_components/src/ui/form_ui/index.tsx +++ b/packages/common/business_components/src/ui/form_ui/index.tsx @@ -1,9 +1,11 @@ +import "./form-ui.css"; + import React, { useCallback, useEffect, useState } from "react"; import { FormControl, FormItem, FormLabel, FormMessage } from "../form"; -import { Button, Checkbox } from "@common/components/ui"; +import { Checkbox } from "@common/components/ui"; -export const BZNM_TOT_NO = '종사업자등록번호'; -export const CANDIDATE = '공급처 후보지 선택(필수아님)'; +export const BZNM_TOT_NO = "종사업자등록번호"; +export const CANDIDATE = "공급처 후보지 선택(필수아님)"; interface FormUiProps extends React.HTMLAttributes<HTMLDivElement> { label?: string; @@ -29,17 +31,19 @@ const FormUi = React.forwardRef<HTMLDivElement, FormUiProps>( }, [onCheckboxChange]); return ( <FormItem ref={ref}> - {label && <FormLabel className="text-xl">{label}</FormLabel>} + {label && <FormLabel className="formui_label">{label}</FormLabel>} {label === BZNM_TOT_NO && ( <> - <div className="float-right"> - <div className="flex items-center gap-2"> - <Checkbox - id="check" + <div className="formui_option_wrap"> + <div className="formui_option"> + <Checkbox + id="check" checked={isChecked} onCheckedChange={handleCheckboxChange} /> - <label className="w-28 text-xl text-gray-600" htmlFor="check">해당없음</label> + <label className="formui_option_label" htmlFor="check"> + 해당없음 + </label> </div> </div> {children} @@ -48,14 +52,14 @@ const FormUi = React.forwardRef<HTMLDivElement, FormUiProps>( {label === CANDIDATE && ( <FormControl> {children} - </FormControl> + </FormControl> )} {label !== BZNM_TOT_NO && label !== CANDIDATE && ( <FormControl> {React.isValidElement(children) && isDisabled - ? React.cloneElement(children, { + ? React.cloneElement(children, { disabled: isDisabled, - ...(children.props as FormElementProps) + ...(children.props as FormElementProps), }) : children} </FormControl> @@ -66,5 +70,5 @@ const FormUi = React.forwardRef<HTMLDivElement, FormUiProps>( } ); -FormUi.displayName = 'FormUi'; -export { FormUi }; \ No newline at end of file +FormUi.displayName = "FormUi"; +export { FormUi }; diff --git a/packages/common/business_components/src/ui/sdmInsertForm/form.tsx b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx index c5c9ef809..9da949bee 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/form.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx @@ -4,7 +4,6 @@ import React, { useCallback, useEffect, useState } from "react"; import type { ControllerRenderProps, UseFormReturn } from "react-hook-form"; import { FormField } from "../form"; import { FormUi } from "../form_ui"; -import { SelectBox } from "../select"; import { SdmSearchForm } from "./sdmNameBox"; import { SearchAddressModal } from "../search_address/SearchAddressModal"; @@ -80,6 +79,11 @@ type FieldComponent = ( field: ControllerRenderProps<FormSchema, keyof FormSchema>, ) => React.ReactElement; +const getReadonlyInputClassName = (isReadonly?: boolean) => + isReadonly + ? "!text-xl text-gray-600 bg-gray-100 cursor-default" + : "!text-xl text-gray-600"; + export function SdmRegForm({ form, onCheckboxChange, @@ -92,11 +96,6 @@ export function SdmRegForm({ readOnly, openNetwork, }: SdmRegFormProps) { - const [sidoValue, setSidoValue] = useState<string>(""); - const [sigunValue, setSigunValue] = useState<string>(""); - const [filteredSigunData, setFilteredSigunData] = useState<SelectDataType[]>([ - { text: "전체", val: "0" }, - ]); const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(true); const [orgBznmNo, setOrgBznmNo] = useState<string>(""); const [updateCoNm, setUpdateNm] = useState<string>(""); @@ -134,46 +133,12 @@ export function SdmRegForm({ const initializeSigunData = useCallback( (sidoCd: string) => { if (sidoCd === "0") { - setSigunValue("0"); - setFilteredSigunData([{ text: "전체", val: "0" }]); + form.setValue("sggCd", ""); } else { - const newFilteredData = [ - { text: "전체", val: "0" }, - ...selectSigunData.filter( - (item: SelectDataType) => item.val.substring(0, 2) === sidoCd, - ), - ]; - setSigunValue("0"); - setFilteredSigunData(newFilteredData); + form.setValue("sggCd", ""); } }, - [selectSigunData], - ); - - const onSelectValue = useCallback( - ( - val: string, - type: string, - field: ControllerRenderProps<FormSchema, keyof FormSchema> | undefined, - ) => { - if (!updateList?.dmdInfoId) - switch (type) { - case "ctpvCd": - initializeSigunData(val); - setSidoValue(val); - field?.onChange(val); - form.setValue("dmdInfoId", ""); - form.setValue("bznmTotNo", ""); - break; - case "sggCd": - setSigunValue(val); - field?.onChange(val); - form.setValue("dmdInfoId", ""); - form.setValue("bznmTotNo", ""); - break; - } - }, - [initializeSigunData, form, updateList], + [form], ); const handleCheckboxChange = useCallback( @@ -207,13 +172,11 @@ export function SdmRegForm({ // 수요처 불러온 후 사업자번호를 바꿀경우를 대비함 setOrgBznmNo(rtnCompData?.dmdInfoId); const sidoCd = rtnCompData?.ctpvCd.trim() || ""; - setSidoValue(sidoCd); form.setValue("ctpvCd", sidoCd); initializeSigunData(sidoCd); setTimeout(() => { const sigunCd = rtnCompData?.sggCd.trim() || ""; - setSigunValue(sigunCd); form.setValue("sggCd", sigunCd); }, 100); @@ -258,7 +221,6 @@ export function SdmRegForm({ ); if (!matchedSido) return; - setSidoValue(matchedSido.val); form.setValue("ctpvCd", matchedSido.val, { shouldValidate: true }); initializeSigunData(matchedSido.val); @@ -279,7 +241,6 @@ export function SdmRegForm({ if (!matchedSigun) return; - setSigunValue(matchedSigun.val); form.setValue("sggCd", matchedSigun.val, { shouldValidate: true }); }, [form, initializeSigunData, selectSidoData, selectSigunData], @@ -364,68 +325,6 @@ export function SdmRegForm({ label: string; component: FieldComponent; }> = [ - { - name: "ctpvCd", - label: "시도", - component: (field) => - updateList?.dmdInfoId ? ( - <Input - placeholder="시도" - type="text" - className="!text-xl text-gray-600" - onChange={(e) => { - field.onChange(e.target.value); - }} - value={ - selectSidoData.find((item) => item.val === field.value?.trim()) - ?.text ?? "" - } - readOnly={!!updateList?.dmdInfoId} - /> - ) : ( - <SelectBox - selectClass={undefined} - label="" - selectArray={selectSidoData} - onSelectValue={(val) => - onSelectValue(val.toString(), "ctpvCd", field ?? "") - } - className="" - value={sidoValue} - /> - ), - }, - { - name: "sggCd", - label: "시군구", - component: (field) => - updateList?.dmdInfoId ? ( - <Input - placeholder="시도" - type="text" - className="!text-xl text-gray-600" - onChange={(e) => { - field.onChange(e.target.value); - }} - value={ - selectSigunData.find((item) => item.val === field.value?.trim()) - ?.text ?? "" - } - readOnly={!!updateList?.dmdInfoId} - /> - ) : ( - <SelectBox - selectClass={"!text-xl text-gray-600"} - label="" - selectArray={filteredSigunData} - onSelectValue={(val) => - onSelectValue(val.toString(), "sggCd", field) - } - className="" - value={sigunValue} - /> - ), - }, { name: "dmdCoNm", label: "회사명", @@ -449,7 +348,7 @@ export function SdmRegForm({ <Input placeholder="사업자등록번호를 입력하세요" type="text" - className="!text-xl text-gray-600" + className={getReadonlyInputClassName(!!updateList?.dmdInfoId)} onChange={(e) => bznNoChange(e, "dmdInfoId", field)} value={formatBusinessNumber(field.value ?? "")} readOnly={!!updateList?.dmdInfoId} @@ -468,7 +367,7 @@ export function SdmRegForm({ <Input placeholder="여러사업장이 있을 경우 입력해주세요" type="text" - className="!mt-5 !text-xl text-gray-600" + className={`${getReadonlyInputClassName(!!updateList?.dmdInfoId)} !mt-5`} disabled={isBznmTotNoDisabled} onChange={(e) => bznNoChange(e, "bznmTotNo", field)} // {...field} @@ -488,7 +387,7 @@ export function SdmRegForm({ {/* 우편번호 */} <Input type="text" - className="!text-xl text-gray-600 w-64" + className={`${getReadonlyInputClassName(true)} w-64`} readOnly={true} value={form.watch("zip") ?? ""} placeholder="우편번호" @@ -498,7 +397,7 @@ export function SdmRegForm({ {/* 주소 */} <Input type="text" - className="!text-xl text-gray-600" + className={getReadonlyInputClassName(true)} readOnly={true} value={address ?? ""} placeholder="주소" @@ -507,7 +406,7 @@ export function SdmRegForm({ <Input placeholder="상세 주소를 입력하세요" type="text" - className="!text-xl text-gray-600" + className={getReadonlyInputClassName(readOnly)} readOnly={readOnly} onChange={(e) => { const value = e.target.value; @@ -521,13 +420,51 @@ export function SdmRegForm({ <Input placeholder="주소를 입력하세요" type="text" - className="!text-xl text-gray-600" + className={getReadonlyInputClassName(readOnly)} readOnly={readOnly} {...field} value={field.value ?? ""} /> ), }, + { + name: "ctpvCd", + label: "시도", + component: (field) => ( + <Input + placeholder="시도" + type="text" + className={getReadonlyInputClassName(true)} + onChange={(e) => { + field.onChange(e.target.value); + }} + value={ + selectSidoData.find((item) => item.val === field.value?.trim()) + ?.text ?? "" + } + readOnly={true} + /> + ), + }, + { + name: "sggCd", + label: "시군구", + component: (field) => ( + <Input + placeholder="시군구" + type="text" + className={getReadonlyInputClassName(true)} + onChange={(e) => { + field.onChange(e.target.value); + }} + value={ + selectSigunData.find((item) => item.val === field.value?.trim()) + ?.text ?? "" + } + readOnly={true} + /> + ), + }, { name: "telno", label: "전화번호", @@ -538,7 +475,7 @@ export function SdmRegForm({ <Input placeholder="전화번호를 입력하세요" type="text" - className="!text-xl text-gray-600" + className={getReadonlyInputClassName(readOnly)} value={formattedValue} readOnly={readOnly} onChange={(e) => { @@ -557,7 +494,7 @@ export function SdmRegForm({ <Input placeholder="패스워드를 입력하세요" type="password" - className="!text-xl text-gray-600" + className={getReadonlyInputClassName(readOnly)} readOnly={readOnly} {...field} value={field.value ?? ""} @@ -571,7 +508,7 @@ export function SdmRegForm({ <Input placeholder="패스워드를 입력하세요" type="password" - className="!text-xl text-gray-600" + className={getReadonlyInputClassName(readOnly)} readOnly={readOnly} {...field} value={field.value ?? ""} @@ -595,8 +532,6 @@ export function SdmRegForm({ const inputId = `${name}-input`; const errorId = `${name}-error`; const isRequired = [ - "ctpvCd", - "sggCd", "dmdCoNm", "dmdInfoId", "daddr", diff --git a/packages/common/business_components/src/ui/search_icon/index.tsx b/packages/common/business_components/src/ui/search_icon/index.tsx index 0ef0e17e3..5c608354f 100644 --- a/packages/common/business_components/src/ui/search_icon/index.tsx +++ b/packages/common/business_components/src/ui/search_icon/index.tsx @@ -1,3 +1,5 @@ +import "./search-icon.css"; + export default function SearchIcon() { return ( <svg @@ -6,7 +8,7 @@ export default function SearchIcon() { viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" - className="w-6 h-6 text-gray-700" + className="searchicon_svg" > <path strokeLinecap="round" @@ -15,4 +17,4 @@ export default function SearchIcon() { /> </svg> ); - } \ No newline at end of file + } diff --git a/packages/common/business_components/src/ui/search_icon/search-icon.css b/packages/common/business_components/src/ui/search_icon/search-icon.css new file mode 100644 index 000000000..b0ebb1f0a --- /dev/null +++ b/packages/common/business_components/src/ui/search_icon/search-icon.css @@ -0,0 +1,7 @@ +@tailwind components; + +@layer components { + .searchicon_svg { + @apply h-6 w-6 text-label; + } +} diff --git a/packages/common/business_components/src/ui/search_input/index.tsx b/packages/common/business_components/src/ui/search_input/index.tsx index 503322559..e7be9f75d 100644 --- a/packages/common/business_components/src/ui/search_input/index.tsx +++ b/packages/common/business_components/src/ui/search_input/index.tsx @@ -1,5 +1,8 @@ +import "./search-input.css"; + import { Input, Label } from "@common/components"; import React from "react"; +import { cn } from "../../lib/utils"; const SearchInput = React.forwardRef<HTMLInputElement, {className:string, label:string, textValue:string, setText:(val:string)=>void}>(({className, label, textValue, setText}, ref) => { const changeText=(e:React.ChangeEvent<HTMLInputElement>)=>{ @@ -20,9 +23,9 @@ const SearchInput = React.forwardRef<HTMLInputElement, {className:string, label: } return ( - <div className={className+" flex items-center"} > - {label && <Label className="w-20 text-xl text-gray-600">{label}</Label>} - <Input className="!text-lg flex-1 w-3/4 h-13 border rounded" value={textValue} onChange={changeText} onKeyUp={keyUp}></Input> + <div className={cn("searchinput_root", className)} > + {label && <Label className="searchinput_label">{label}</Label>} + <Input ref={ref} className="searchinput_field" value={textValue} onChange={changeText} onKeyUp={keyUp}></Input> </div> ) }); diff --git a/packages/common/business_components/src/ui/search_input/search-input.css b/packages/common/business_components/src/ui/search_input/search-input.css new file mode 100644 index 000000000..f272abc7e --- /dev/null +++ b/packages/common/business_components/src/ui/search_input/search-input.css @@ -0,0 +1,15 @@ +@tailwind components; + +@layer components { + .searchinput_root { + @apply flex items-center; + } + + .searchinput_label { + @apply w-20 text-xl; + } + + .searchinput_field { + @apply h-14 w-3/4 flex-1 rounded-md border border-input text-lg; + } +} diff --git a/packages/common/business_components/src/ui/search_input_select/index.tsx b/packages/common/business_components/src/ui/search_input_select/index.tsx index bf8787356..8632eac78 100644 --- a/packages/common/business_components/src/ui/search_input_select/index.tsx +++ b/packages/common/business_components/src/ui/search_input_select/index.tsx @@ -1,6 +1,9 @@ +import "./search-input-select.css"; + import { Input, Label } from "@common/components"; import React from "react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../select/lib/selectBox"; +import { cn } from "../../lib/utils"; export type SelectDataType = { text: string; val: string; @@ -23,12 +26,12 @@ const SearchInputSelect = React.forwardRef<HTMLInputElement, {className:string, } return ( - <div className={className+" flex items-center"} > - {label && <Label className="w-20 text-xl text-gray-600">{label}</Label>} - <div className="flex w-full items-center"> - <Input className="w-2/3 text-xl h-14 p-2 border rounded" value={textValue} onChange={changeText} /> + <div className={cn("searchinputselect_root", className)} > + {label && <Label className="searchinputselect_label">{label}</Label>} + <div className="searchinputselect_controls"> + <Input ref={ref} className="searchinputselect_input" value={textValue} onChange={changeText} /> <Select onValueChange={onSelectValue} value={selectValue}> - <SelectTrigger className="ml-2 w-1/3" > + <SelectTrigger className="searchinputselect_select" > <SelectValue placeholder={selectData?.[0]?.text ?? ""} /> </SelectTrigger> {/* <SelectContent ref={refs ? (refs as unknown as RefType)?.selectValue : null}> */} diff --git a/packages/common/business_components/src/ui/search_input_select/search-input-select.css b/packages/common/business_components/src/ui/search_input_select/search-input-select.css new file mode 100644 index 000000000..e37add037 --- /dev/null +++ b/packages/common/business_components/src/ui/search_input_select/search-input-select.css @@ -0,0 +1,23 @@ +@tailwind components; + +@layer components { + .searchinputselect_root { + @apply flex items-center; + } + + .searchinputselect_label { + @apply w-20 text-xl; + } + + .searchinputselect_controls { + @apply flex w-full items-center; + } + + .searchinputselect_input { + @apply h-14 w-2/3 rounded-md border border-input p-2 text-xl; + } + + .searchinputselect_select { + @apply ml-2 w-1/3; + } +} diff --git a/packages/common/business_components/src/ui/select/index.tsx b/packages/common/business_components/src/ui/select/index.tsx index 32510eb18..851d0539d 100644 --- a/packages/common/business_components/src/ui/select/index.tsx +++ b/packages/common/business_components/src/ui/select/index.tsx @@ -1,3 +1,5 @@ +import "./select.css"; + // selectBox import { Label } from "@common/components"; @@ -10,6 +12,7 @@ import { SelectTrigger, SelectValue, } from "./lib/selectBox"; +import { cn } from "../../lib/utils"; /** 차트 기본 스타일 */ interface SelectBox1 { @@ -43,7 +46,7 @@ const SelectBox = React.forwardRef< value, readOnly = false, selectContentClass, - labelClass = "w-20 whitespace-nowrap text-xl text-gray-600", + labelClass = "w-20 whitespace-nowrap text-xl", }, ref, ) => { @@ -72,9 +75,9 @@ const SelectBox = React.forwardRef< : undefined; return ( - <div className={className + " flex items-center"} ref={ref}> + <div className={cn("selectbox_root", className)} ref={ref}> {label && ( - <Label className={labelClass}> + <Label className={cn("selectbox_label", labelClass)}> {label} </Label> )} @@ -92,9 +95,7 @@ const SelectBox = React.forwardRef< value={currentValue} // disabled={readOnly} //물재이용 감리 지적사항으로 일단 주석처리 > - <SelectTrigger - className={selectClass ? selectClass : "flex-1 rounded border p-2"} - > + <SelectTrigger className={cn("selectbox_trigger", selectClass)}> <SelectValue placeholder={placeHolder} /> </SelectTrigger> <SelectContent className={selectContentClass}> diff --git a/packages/common/business_components/src/ui/select/select.css b/packages/common/business_components/src/ui/select/select.css new file mode 100644 index 000000000..8aa16f386 --- /dev/null +++ b/packages/common/business_components/src/ui/select/select.css @@ -0,0 +1,15 @@ +@tailwind components; + +@layer components { + .selectbox_root { + @apply flex items-center; + } + + .selectbox_label { + @apply text-label; + } + + .selectbox_trigger { + @apply flex-1 rounded-md border border-input bg-form px-3 py-2; + } +} diff --git a/packages/common/business_components/src/ui/tab/index.tsx b/packages/common/business_components/src/ui/tab/index.tsx index 273a4b907..949884df9 100644 --- a/packages/common/business_components/src/ui/tab/index.tsx +++ b/packages/common/business_components/src/ui/tab/index.tsx @@ -1,46 +1,61 @@ -import { Tabs, TabsList, TabsTrigger } from '@common/components/ui'; -import React from 'react'; +import "./tab.css"; + +import { Tabs, TabsList, TabsTrigger } from "@common/components/ui"; +import React from "react"; +import { cn } from "../../lib/utils"; interface TabItem { value: string; label: string; } -interface UITabProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'dir'> { +interface UITabProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "dir"> { defaultValue?: string; tabList: TabItem[]; children?: React.ReactNode; onValueChange?: (value: string) => void; + variant?: "default" | "underline" | "outline"; } -const UITab = React.forwardRef< - HTMLDivElement, - UITabProps ->(({defaultValue, tabList, children, onValueChange, ...props}, ref) => { - return ( - <Tabs - defaultValue={defaultValue || tabList[0]?.value} - className={"w-full space-x-1 border-gray-200"} - onValueChange={onValueChange} - ref={ref} - {...props} - > - <TabsList className="flex space-x-1"> - {tabList.map((tab) => ( - <TabsTrigger - key={tab.value} - value={tab.value} - className="py-2 px-4 text-sm font-medium border-b-2 focus:outline-none data-[state=active]:border-blue-500 data-[state=active]:text-blue-600 data-[state=inactive]:border-transparent data-[state=inactive]:text-gray-500 hover:text-gray-700 hover:border-gray-300" - > - {tab.label} - </TabsTrigger> - ))} - </TabsList> - {children} - </Tabs> - ); -}); +const UITab = React.forwardRef<HTMLDivElement, UITabProps>( + ( + { + defaultValue, + tabList, + children, + onValueChange, + className, + variant = "default", + ...props + }, + ref, + ) => { + return ( + <Tabs + variant={variant} + defaultValue={defaultValue || tabList[0]?.value} + className={cn("w-full", variant === "underline" && "btab_root", className)} + onValueChange={onValueChange} + ref={ref} + {...props} + > + <TabsList className={cn(variant === "underline" && "btab_list")}> + {tabList.map((tab) => ( + <TabsTrigger + key={tab.value} + value={tab.value} + className={cn(variant === "underline" && "btab_trigger")} + > + {tab.label} + </TabsTrigger> + ))} + </TabsList> + {children} + </Tabs> + ); + }, +); -UITab.displayName = 'UITab'; +UITab.displayName = "UITab"; export { UITab }; diff --git a/packages/common/business_components/src/ui/tab/tab.css b/packages/common/business_components/src/ui/tab/tab.css new file mode 100644 index 000000000..5e6fbcd29 --- /dev/null +++ b/packages/common/business_components/src/ui/tab/tab.css @@ -0,0 +1,27 @@ +@tailwind components; + +@layer components { + .btab_root { + @apply w-full; + } + + .btab_list { + @apply flex space-x-1 border-b border-border bg-transparent p-0; + } + + .btab_trigger { + @apply rounded-none border-b-2 border-transparent bg-transparent px-4 py-2 text-sm font-medium text-label; + } + + .btab_root .btab_trigger[data-state="active"] { + @apply border-primary text-primary; + } + + .btab_root .btab_trigger[data-state="inactive"] { + @apply text-label; + } + + .btab_root .btab_trigger:hover { + @apply border-border text-foreground; + } +} diff --git a/packages/common/business_components/src/ui/table/HeadMakeColSpan.tsx b/packages/common/business_components/src/ui/table/HeadMakeColSpan.tsx index b3d3fdc40..3ad4c8e23 100644 --- a/packages/common/business_components/src/ui/table/HeadMakeColSpan.tsx +++ b/packages/common/business_components/src/ui/table/HeadMakeColSpan.tsx @@ -29,7 +29,7 @@ const HeadMakeColSpan = ({ upHeadList }: { upHeadList: TableUpperProps[] }) => { colSpan={head.colSpan || 1} rowSpan={head.rowSpan || 1} style={{ whiteSpace: 'pre-line' }} - className="bg-gray-50" + className="" > {head.title} </TableHead> diff --git a/packages/common/components/src/ui/tabs/tabs.css b/packages/common/components/src/ui/tabs/tabs.css index b1227b0ce..bb097be0e 100644 --- a/packages/common/components/src/ui/tabs/tabs.css +++ b/packages/common/components/src/ui/tabs/tabs.css @@ -22,4 +22,6 @@ .tabs[data-variant="outline"] .tabs-trigger { @apply relative rounded-none rounded-t-sm border-border bg-background text-muted-foreground after:absolute after:bottom-0 after:left-0 after:h-[0.1rem] after:w-full after:bg-border after:content-[''] data-[state=active]:border data-[state=active]:border-b-0 data-[state=active]:border-border data-[state=active]:text-foreground data-[state=active]:after:bg-transparent; } + + }