diff --git a/apps/dm/.gitignore b/apps/dm/.gitignore new file mode 100644 index 000000000..5ef6a5207 --- /dev/null +++ b/apps/dm/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/dm/app/common/transValueFormat.tsx b/apps/dm/app/common/transValueFormat.tsx new file mode 100644 index 000000000..4d76d84e0 --- /dev/null +++ b/apps/dm/app/common/transValueFormat.tsx @@ -0,0 +1,26 @@ +import React from 'react' + +type CommonProps = { + value: string | number | ''; + format: string; +} + +export default function TransValueFormat({value='', format=''}: CommonProps) { + return ( + format === 'yyyyMMdd' ? + <>{value.toString().substring(0,4)+'-'+value.toString().substring(4,6)+'-'+value.toString().substring(6,8)} + : format === 'number' ? + <>{String(value ?? '').includes('.') + ? String(value).split('.').map((part, index) => + index === 0 ? part.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : part + ).join('.') + : String(value ?? '').replace(/\B(?=(\d{3})+(?!\d))/g, ",")} + : format === 'float1' ? + <>{String(value ?? '').includes('.') + ? String(value).split('.').map((part, index) => + index === 0 ? part.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : part + ).join('.') + : String(value ?? '').replace(/\B(?=(\d{3})+(?!\d))/g, ",")} + : <>{value} + ) +} \ No newline at end of file diff --git a/apps/dm/app/favicon.ico b/apps/dm/app/favicon.ico new file mode 100644 index 000000000..718d6fea4 Binary files /dev/null and b/apps/dm/app/favicon.ico differ diff --git a/apps/dm/app/globals.css b/apps/dm/app/globals.css new file mode 100644 index 000000000..f30aa3031 --- /dev/null +++ b/apps/dm/app/globals.css @@ -0,0 +1,70 @@ +@import "../../../packages/common/components/src/base.css"; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem + } + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55% + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/apps/dm/app/layout.tsx b/apps/dm/app/layout.tsx new file mode 100644 index 000000000..0aac0da38 --- /dev/null +++ b/apps/dm/app/layout.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { BaseLayout } from "@common/components"; +import "./globals.css"; +import { StoreProvider } from "./store"; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + + {children} + + + + + ); +} diff --git a/apps/dm/app/page.tsx b/apps/dm/app/page.tsx new file mode 100644 index 000000000..fdb323086 --- /dev/null +++ b/apps/dm/app/page.tsx @@ -0,0 +1,36 @@ +"use client" + +import Link from 'next/link'; +import { Button } from '@common/components/ui'; + +export default function Home() { + return ( + <> +
+ + + + + + +
+
+ +
+ + ); +} diff --git a/apps/dm/app/pages/sdmMng/sdmApplyList/page.tsx b/apps/dm/app/pages/sdmMng/sdmApplyList/page.tsx new file mode 100644 index 000000000..2dda06008 --- /dev/null +++ b/apps/dm/app/pages/sdmMng/sdmApplyList/page.tsx @@ -0,0 +1,196 @@ +"use client" +import { Table, TableBody, TableCell, TableRow } from '@common/components/ui/table'; + +import { useDmStore } from '@/app/store'; +import { ContentsDiv, Title, UiModal, UiTable } from '@common/business_components/ui'; +import ListDiv from '@common/business_components/ui/list_div'; +import { useEffect, useState } from 'react'; +import { Toaster } from 'react-hot-toast'; +import SdmLogin from '../sdmLogin/page'; +import type { DemandCompType } from '@/app/store/sdmMng'; +import TransValueFormat from '@/app/common/transValueFormat'; + +const TABLE_STYLES = { + header: "text-center border border-[var(--border)] bg-[rgb(234,243,251)] text-[rgb(100,119,132)] whitespace-pre-line", + cell: "text-right whitespace-pre-line", + fullWidth: "text-center border border-[var(--border)] bg-[rgb(234,243,251)] text-[rgb(100,119,132)] whitespace-pre-line", + table: "fixed w-full" +} as const; + +type SdmApplyProps = { + sdmNo : string; +} + +export default function SdmApplyList({}: SdmApplyProps) { + const {getSdmInfoStatusList, Sdm, Common} = useDmStore((state) => state); + const {successLogin, sdmInfoStatusList, upHeadSdmInfoStatus} = Sdm; + const {selectSidoData, selectSigunData} = Common; + const [isLogin, setIsLogin] = useState(false); + const [isLoginStorageInfo, setIsLoginStorageInfo] = useState(false); + const [sdmApplyDetail, setSdmApplyDetail] = useState({} as DemandCompType); + const [transformedList, setTransformedList] = useState([]); + + useEffect(() => { + console.log('sessionStorage : ',sessionStorage.getItem('user')); + const user = sessionStorage.getItem('user'); + if (user) { + setIsLoginStorageInfo(true); + } + }, []); + + useEffect(() => { + if (successLogin || isLoginStorageInfo) { + setIsLogin(true); + const user = sessionStorage.getItem('user'); + if (user) { + try { + const userData = JSON.parse(user); + getSdmInfoStatusList(userData.dmdInfoId); + } catch (error) { + console.error('Error parsing user data:', error); + } + } + } + }, [successLogin, getSdmInfoStatusList, isLoginStorageInfo]); + + // 데이터 변환을 위한 별도의 useEffect + useEffect(() => { + if (sdmInfoStatusList.length > 0) { + const newTransformedList = sdmInfoStatusList.map((item: DemandCompType) => ({ + ...item, + ctpvCd: selectSidoData.find(sido => sido.val === item.ctpvCd?.trim())?.text || '', + sggCd: selectSigunData.find(sigun => sigun.val === item.sggCd?.trim())?.text || '' + })); + setTransformedList(newTransformedList); + } + }, [sdmInfoStatusList, selectSidoData, selectSigunData]); + + const onClickSdmStatus = (index: number | undefined) => { + if (index !== undefined) { + const selectedStatus = transformedList[index]; + if (selectedStatus) { + setSdmApplyDetail(selectedStatus); + } + } + } + + return ( + <> + + <ContentsDiv> + <ListDiv> + <div className="overflow-y-scroll h-[250px]"> + <div className="h-[110%] w-[99%]"> + <UiTable + headName='수요정보 신청현황' + tableData={transformedList} + headlist={upHeadSdmInfoStatus} + pageSize={0} + total={0} + cellClick={(index) => onClickSdmStatus(index)} + onPageChange={() => {}} + /> + </div> + </div> + </ListDiv> + <ListDiv> + <div className="card-header"> + <h3 className="text-2xl font-medium text-label">수요신청 상세현황</h3> + </div> + <div className={`card !m-[1.2rem] !p-0r`}> + <Table variant="secondary" style={{ tableLayout: 'fixed', width: '100%' }}> + <TableBody> + <TableRow> + <TableCell colSpan={1} className={TABLE_STYLES.header}>요구시기</TableCell> + <TableCell colSpan={2} className={TABLE_STYLES.cell}>{sdmApplyDetail?.dmndTmng?.toString()}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>요구수량(m³/일)</TableCell> + <TableCell colSpan={2} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.dmndQty} format="number"/>}</TableCell> + </TableRow> + <TableRow> + <TableCell colSpan={1} className={TABLE_STYLES.header}>사용용도</TableCell> + <TableCell colSpan={2} className={TABLE_STYLES.cell}>{sdmApplyDetail?.useUsgCd}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>희망단가(원/m³)</TableCell> + <TableCell colSpan={2} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.wntUntprc} format="number"/>}</TableCell> + </TableRow> + <TableRow> + <TableCell colSpan={6} className={TABLE_STYLES.fullWidth}>요구수량</TableCell> + </TableRow> + <TableRow> + <TableCell colSpan={1} className={TABLE_STYLES.header}>총대장균군수(개/100ml)</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.grssClfg} format="number"/>}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>결합잔류염소(mg/L)​</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.combResdch} format="number"/>}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>탁도(NTU)</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.trbt} format="number"/>}</TableCell> + </TableRow> + <TableRow> + <TableCell colSpan={1} className={TABLE_STYLES.header}>생물학적 산소요구량(BOD)(mg/L)</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.blgcOxygDmndQnt} format="number"/>}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>냄새</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.smll} format="number"/>}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>색도(도)</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.crmty} format="number"/>}</TableCell> + </TableRow> + <TableRow> + <TableCell colSpan={1} className={TABLE_STYLES.header}>총질소(T-N)(mg/L)</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.tn} format="number"/>}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>총인(T-P)(mg/L)​</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.tph} format="number"/>}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>수소이온농도(pH)​</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.ph} format="number"/>}</TableCell> + </TableRow> + <TableRow> + <TableCell colSpan={1} className={TABLE_STYLES.header}>염화물(mgCL/L)​</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.clIonQnt} format="number"/>}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>전기전도도(㎲/cm)​</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.cdtnv} format="number"/>}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>비고</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{sdmApplyDetail?.rmrk}</TableCell> + </TableRow> + <TableRow> + <TableCell colSpan={6} className={TABLE_STYLES.fullWidth}>공급처 후보지​</TableCell> + </TableRow> + <TableRow> + <TableCell colSpan={1} className={TABLE_STYLES.header}>후보지1</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}></TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>후보지2</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}></TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>후보지3</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}></TableCell> + </TableRow> + </TableBody> + </Table> + </div> + </ListDiv> + <ListDiv> + <div className="card-header"> + <h3 className="text-2xl font-medium text-label">수요신청 진행상태</h3> + </div> + <div className={`card !m-[1.2rem] !p-0r`}> + <Table variant="secondary" style={{ tableLayout: 'fixed', width: '100%' }}> + <TableBody> + <TableRow> + <TableCell colSpan={1} className={TABLE_STYLES.header}>진행상태</TableCell> + <TableCell colSpan={2} className={TABLE_STYLES.cell}>{sdmApplyDetail?.status?.toString()}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>공급처매칭결과</TableCell> + <TableCell colSpan={2} className={TABLE_STYLES.cell}></TableCell> + </TableRow> + </TableBody> + </Table> + </div> + </ListDiv> + </ContentsDiv> + {!isLogin? + <UiModal + body={<SdmLogin/>} + bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]" + open={!isLogin} + onOpenChange={(open) => !open && setIsLogin(false)} + noHeaderYn="Y" + noFooterYn="Y" + /> + :null} + <Toaster position="top-center" /> + </> + ); +} diff --git a/apps/dm/app/pages/sdmMng/sdmInfoInsert/modalSimilar/page.tsx b/apps/dm/app/pages/sdmMng/sdmInfoInsert/modalSimilar/page.tsx new file mode 100644 index 000000000..a57e345b3 --- /dev/null +++ b/apps/dm/app/pages/sdmMng/sdmInfoInsert/modalSimilar/page.tsx @@ -0,0 +1,166 @@ +"use client" + +import { useDmStore } from '@/app/store'; +import type { MoneyDetailType, SimilarCaseType } from '@/app/store/sdmMng'; +import { SelectBox, Title, UiTable } from '@common/business_components/ui'; +import { HeadMakeColSpan } from '@common/business_components/ui/table/HeadMakeColSpan'; +import { Table, TableBody, TableCell, TableHeader, TableRow } from '@common/components/ui/table'; +import { useEffect, useMemo, useState } from 'react'; + +type modalType = { + sply: number | 0; + usageCd: string | ''; + money: number | 0; +}; + +const searchUseTypeList = [ + { text: '공급량', val: 'S' }, + { text: '사용용도', val: 'U' }, +]; + +export default function ModalSimilarPage({ sply = 0, usageCd = '', money = 0 }: modalType) { + const { Sdm, getSimilarCaseList } = useDmStore((state) => state); + const [searchUseType, setSearchUseType] = useState<{text: string, value: string}>({text: '공급량', value: 'S'}); + const [moneyDetailList, setMoneyDetailList] = useState<MoneyDetailType[]>([]); + const { + upHeadSimilarCaseList, + similarCaseList, + upHeadMoneyDetailList, + } = Sdm; + + const rowSpans = useMemo(() => { + const spans: { [key: number]: number } = {}; + let currentValue = ''; + let currentSpan = 0; + let startIndex = 0; + + similarCaseList.forEach((item: SimilarCaseType, index: number) => { + if (item.ruseBizNm === currentValue + || item.bizBgngDate === currentValue + || item.bizEndDate === currentValue + || item.reqBizReuseFcltCst === currentValue) { + currentSpan++; + spans[startIndex] = currentSpan; + } else { + currentValue = item.ruseBizNm; + currentSpan = 1; + startIndex = index; + spans[index] = 1; + } + }); + + return spans; + }, [similarCaseList]); + + const handleCellClick = (index: number, headId: string) => { + setMoneyDetailList([{ + reqBizGrssCst: similarCaseList[index]?.reqBizGrssCst ?? '', + reqBizReuseFcltCst: similarCaseList[index]?.reqBizReuseFcltCst ?? '', + reqBizPplCst: similarCaseList[index]?.reqBizPplCst ?? '', + reqBizArchCst: similarCaseList[index]?.reqBizArchCst ?? '', + reqBizMchnCst: similarCaseList[index]?.reqBizMchnCst ?? '', + reqBizMtrngCst: similarCaseList[index]?.reqBizMtrngCst ?? '', + reqBizGisCst: similarCaseList[index]?.reqBizGisCst ?? '', + reqBizCctvCst: similarCaseList[index]?.reqBizCctvCst ?? '', + reqBizCmcnCst: similarCaseList[index]?.reqBizCmcnCst ?? '', + reqBizWstCst: similarCaseList[index]?.reqBizWstCst ?? '', + reqBizDesignCst: similarCaseList[index]?.reqBizDesignCst ?? '', + reqBizSpvsCst: similarCaseList[index]?.reqBizSpvsCst ?? '', + reqBizSbsdCst: similarCaseList[index]?.reqBizSbsdCst ?? '', + reqBizEtc: similarCaseList[index]?.reqBizEtc ?? '', + prcmPlanNlep: similarCaseList[index]?.prcmPlanNlep ?? '', + prcmPlanLcep: similarCaseList[index]?.prcmPlanLcep ?? '', + prcmPlanBrdnCst: similarCaseList[index]?.prcmPlanBrdnCst ?? '', + prcmPlanEcqrRt: similarCaseList[index]?.prcmPlanEcqrRt ?? '', + prcmPlanGrssCst: similarCaseList[index]?.prcmPlanGrssCst ?? '', + clclnNlep: similarCaseList[index]?.clclnNlep ?? '', + clclnLcep: similarCaseList[index]?.clclnLcep ?? '', + clclnBrdnCst: similarCaseList[index]?.clclnBrdnCst ?? '', + clclnEcqrRt: similarCaseList[index]?.clclnEcqrRt ?? '', + clclnGrssCst: similarCaseList[index]?.clclnGrssCst ?? '' + }]); + } + + useEffect(() => { + const type = searchUseType.value; + getSimilarCaseList(type, type === 'S' ? sply : type === 'U' ? usageCd : money); + }, [getSimilarCaseList, money, searchUseType, sply, usageCd]); + + return ( + <div className="align-top w-[1360px] overflow-x-auto"> + <Title title="유사사례 조회" /> + <div className="overflow-x-auto h-[450px] w-full mt-8" tabIndex={0} aria-label="유사사례 테이블 영역"> + <div className="w-[150%]"> + <div className="card-header justify-self-start"> + <h3 className="text-2xl font-medium text-label">유사사례</h3> + <SelectBox + tabIndex={0} + aria-label="검색 기준 선택" + selectArray={searchUseTypeList} + value={searchUseType.value} + onSelectValue={(val) => { + setSearchUseType({text: searchUseTypeList.find(item => item.val === val)?.text ?? '', value: val}); + }} + className="w-[140px] ml-20" + label="" + selectClass="w-[140px]" + /> + </div> + <div className={`card !m-[1.2rem] !p-0r`}> + <Table variant="secondary"> + <TableHeader className="font-bold"> + <HeadMakeColSpan upHeadList={upHeadSimilarCaseList} /> + </TableHeader> + <TableBody className='text-center'> + {similarCaseList.length > 0 ? similarCaseList?.map((el: SimilarCaseType, index) => ( + <TableRow key={`similar-case-row-${index}-${el.ruseBizNm}-${el.bizBgngDate}`}> + {upHeadSimilarCaseList.map((head) => ( + head.id && head.id.length > 1 && ( + <TableCell + tabIndex={head.id === 'someClickableId' ? 0 : undefined} + role="button" + aria-label={`유사사례 ${head.id} 클릭 시 상세 정보 표시`} + className={`${head.align === 'right' ? 'text-right' : head.align === 'center' ? 'text-center' : typeof el[head.id as keyof SimilarCaseType] === 'number' ? 'text-right' : 'text-center'} cursor-pointer`} + key={`similar-case-cell-${head.id}-${index}-${el.ruseBizNm}`} + rowSpan={(head.id === 'businessName' || head.id ==='businessDuration' || head.id === 'money') ? rowSpans[index] : undefined} + style={(head.id === 'businessName' || head.id === 'businessDuration' || head.id === 'money') && index > 0 && similarCaseList[index - 1]?.businessName === el.businessName ? { display: 'none' } : undefined} + onClick={() => handleCellClick(index, head.id)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + handleCellClick(index, head.id); + } + }} + > + {typeof el[head.id as keyof SimilarCaseType] !== 'number' + ? String(el[head.id as keyof SimilarCaseType] ?? '') + : String(el[head.id as keyof SimilarCaseType]).includes('.') + ? String(el[head.id as keyof SimilarCaseType]).split('.').map((part, index) => + index === 0 ? part.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : part + ).join('.') + : String(el[head.id as keyof SimilarCaseType] ?? '').replace(/\B(?=(\d{3})+(?!\d))/g, ",")} + </TableCell> + ) + ))} + </TableRow> + )) : <></>} + </TableBody> + </Table> + </div> + </div> + </div> + <div className="overflow-x-auto h-[270px] w-full mt-8"> + <div className="w-[130%]"> + <UiTable + headName={"사업비 상세정보"} + tableData={moneyDetailList} + headlist={upHeadMoneyDetailList} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={() => {}} + /> + </div> + </div> + </div> + ) +} \ No newline at end of file diff --git a/apps/dm/app/pages/sdmMng/sdmInfoInsert/page.tsx b/apps/dm/app/pages/sdmMng/sdmInfoInsert/page.tsx new file mode 100644 index 000000000..e36d10781 --- /dev/null +++ b/apps/dm/app/pages/sdmMng/sdmInfoInsert/page.tsx @@ -0,0 +1,249 @@ +"use client"; + +import { useDmStore } from "@/app/store"; +import type { DemandCompType } from "@/app/store/sdmMng"; +import { SdmInfoRegForm } from "@/components/ui/sdmInfoInsertForm/component"; +import { UiModal } from "@common/business_components/ui/dialog_ui"; +import { Button } from "@common/components/ui"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useSearchParams } from "next/navigation"; +import { Suspense, useEffect, useState } from "react"; +import { FormProvider, useForm, type SubmitHandler } from "react-hook-form"; +import { Toaster, toast } from "react-hot-toast"; +import * as z from "zod"; +import SdmLogin from "../sdmLogin/page"; + +const createFormSchema = () => z.object({ + sdmNm: z.string() + .min(1, "수요처를 선택해주세요"), + demandYmd: z.date().optional(), + demandQnty: z.string() + .min(1, "요구수량을 입력해주세요") + .regex(/^[0-9]+$/, "요구수량은 숫자만 입력 가능합니다"), + usePurpose: z.string().optional(), + money: z.string() + .regex(/^[0-9]+$/, "희망단가는 숫자만 입력 가능합니다"), + /**요구수질 */ + reqWaterQlty : z.string().optional(), + grssClfg: z.string() + .min(1, "총대장균군수를 입력해주세요") + .regex(/^[0-9]+$/, "총대장균군수는 숫자만 입력 가능합니다"), + tn : z.string() + .min(1, "총질소를 입력해주세요") + .regex(/^[0-9]+$/, "총질소는 숫자만 입력 가능합니다"), + combResdch : z.string() + .min(1, "결합잔류염소를 입력해주세요") + .regex(/^[0-9]+$/, "결합잔류염소는 숫자만 입력 가능합니다"), + tph : z.string() + .min(1, "총인산물을 입력해주세요") + .regex(/^[0-9]+$/, "총인산물은 숫자만 입력 가능합니다"), + trbt : z.string() + .min(1, "탁도를 입력해주세요") + .regex(/^[0-9]+$/, "탁도는 숫자만 입력 가능합니다"), + ph : z.string() + .min(1, "수소이온농도를 입력해주세요") + .regex(/^[0-9]+$/, "수소이온농도는 숫자만 입력 가능합니다"), + bod : z.string() + .min(1, "생물학적 산소요구량을 입력해주세요") + .regex(/^[0-9]+$/, "생물학적 산소요구량은 숫자만 입력 가능합니다"), + /**컬럼확인필요 */ + clIonQnt : z.string() + .min(1, "염화물을 입력해주세요") + .regex(/^[0-9]+$/, "염화물은 숫자만 입력 가능합니다"), + smll : z.string() + .min(1, "냄새를 입력해주세요") + .regex(/^[0-9]+$/, "냄새는 숫자만 입력 가능합니다"), + cdtnv : z.string() + .min(1, "전기전도도를 입력해주세요") + .regex(/^[0-9]+$/, "전기전도도는 숫자만 입력 가능합니다"), + crmty : z.string() + .min(1, "색도를 입력해주세요") + .regex(/^[0-9]+$/, "색도는 숫자만 입력 가능합니다"), + rmrk : z.string().optional(), + /* telno: z.string() + .min(8, "전화번호는 8자 이상 입력해주세요") + .max(12, "전화번호는 12자 이내로 입력해주세요") + .regex(/^[0-9-]+$/, "전화번호는 숫자와 하이픈(-)만 입력 가능합니다"), */ + candidateTitle: z.string().optional(), +}); + +export type FormData = z.infer<ReturnType<typeof createFormSchema>>; + +function SdmInfoInsertContent() { + const searchParams = useSearchParams(); + const {insertSdmInfoData, Sdm} = useDmStore((state) => state); + const {successLogin} = Sdm; + const [ismoneyDisabled, setIsmoneyDisabled] = useState(false); + const formSchema = createFormSchema(); + + const [isLogin, setIsLogin] = useState(false); + const [facilityName, setFacilityName] = useState<string>(''); + const [defaultValues, setDefaultValues] = useState<FormData>({ + sdmNm: '', + demandYmd: new Date(), + demandQnty: '', + usePurpose: '', + money: '', + /**요구수질 */ + reqWaterQlty: '', + grssClfg: '', + tn: '', + combResdch: '', + tph: '', + trbt: '', + ph: '', + bod: '', + /**컬럼확인필요 */ + clIonQnt: '', + smll: '', + cdtnv: '', + crmty: '', + rmrk: '', + /** 후보지 제목 */ + candidateTitle: '', + }); + + useEffect(() => { + const user = sessionStorage.getItem('user'); + setDefaultValues(prev => ({ + ...prev, + sdmNm: searchParams.get('dmdCoNm') || '', + })); + sessionStorage.setItem('user', searchParams.get('dmdInfoId') || ''); + setIsLogin(user || searchParams.get('dmdInfoId') ? true : false); + }, [searchParams]); + + useEffect(() => { + const local = localStorage.getItem('user'); + if(local) { + setIsLogin(true); + } + }, []); + + const form = useForm<FormData>({ + resolver: zodResolver(formSchema), + defaultValues, + mode: "onChange", + }); + + // 로그인 성공 시 모달 닫기 및 폼 업데이트 + useEffect(() => { + if (successLogin) { + setIsLogin(true); + const user = sessionStorage.getItem('user'); + if (user) { + try { + const userData = JSON.parse(user); + if (userData.dmdCoNm) { + setFacilityName(userData.dmdCoNm); + form.setValue('sdmNm', userData.dmdCoNm); + } + } catch (error) { + console.error('Error parsing user data:', error); + } + } + } + }, [successLogin, form]); + + // 체크박스 상태가 변경될 때마다 money 필드의 에러를 초기화 + useEffect(() => { + if (ismoneyDisabled) { + form.clearErrors('money'); + form.setValue('money', ''); + } + }, [ismoneyDisabled, form]); + + const onSubmit: SubmitHandler<FormData> = async (data) => { + try { + // Transform form data to match API schema + const apiData : DemandCompType = { + dmdInfoId: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdInfoId?.toString() || searchParams.get('dmdInfoId') + (searchParams.get('bznmTotNo') || ''), + dmdInfoFk: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdInfoId?.toString() || searchParams.get('dmdInfoId') + (searchParams.get('bznmTotNo') || ''), + dmdCoNm: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdCoNm?.toString() || data.sdmNm, + ctpvCd: JSON.parse(sessionStorage.getItem('user') || '{}')?.ctpvCd?.toString() || searchParams.get('ctpvCd') || '', + sggCd: JSON.parse(sessionStorage.getItem('user') || '{}')?.sggCd?.toString() || searchParams.get('sggCd') || '', + daddr: JSON.parse(sessionStorage.getItem('user') || '{}')?.daddr?.toString() || searchParams.get('daddr') || '', + telno: JSON.parse(sessionStorage.getItem('user') || '{}')?.telno?.toString() || searchParams.get('telno') || '', + rgtr: JSON.parse(sessionStorage.getItem('user') || '{}')?.rgtr?.toString() || searchParams.get('rgtr') || '', + useUsgCd: data.usePurpose || '', + wntUntprc: Number(data.money) || '', + dmndTmng: data.demandYmd || new Date(), + dmndQty: Number(data.demandQnty) || '', + grssClfg: Number(data.grssClfg) || '', + tn: Number(data.tn) || '', + combResdch: Number(data.combResdch) || '', + tph: Number(data.tph) || '', + trbt: Number(data.trbt) || '', + ph: Number(data.ph) || '', + blgcOxygDmndQnt: Number(data.bod) || '', + clIonQnt: Number(data.clIonQnt) || '', + smll: Number(data.smll) || '', + cdtnv: Number(data.cdtnv) || '', + crmty: Number(data.crmty) || '', + rmrk: data.rmrk || '', + bzmnNo: "", + dmdAplyPswd: "", + index: 0, + reqDt: "", + dmdInfoSn: 0 + }; + + await insertSdmInfoData(apiData); + } catch (error) { + console.error('Error submitting form:', error); + } + }; + + return ( + <div className="min-h-screen bg-gray-50"> + <div className="container mx-auto px-4 py-8"> + <div className="text-center mb-8"> + <h1 className="text-4xl font-bold text-gray-900 underline"> + 수요정보 등록 + </h1> + </div> + + <div className="max-w-3xl mx-auto bg-white rounded-lg shadow-md p-6"> + <FormProvider {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> + <SdmInfoRegForm + form={form} + onCheckboxChange={setIsmoneyDisabled} + facilityName={facilityName} + /> + <div className="flex justify-center pt-4"> + <Button + type="submit" + className="w-40 bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-md transition-colors duration-200" + > + 수요정보 등록 + </Button> + </div> + </form> + </FormProvider> + </div> + </div> + + {!isLogin? + <UiModal + body={<SdmLogin/>} + bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]" + open={!isLogin} + onOpenChange={(open) => !open && setIsLogin(false)} + noHeaderYn="Y" + noFooterYn="Y" + /> + :null} + <Toaster position="top-center" /> + </div> + ); +} + +export default function MyForm() { + return ( + <Suspense fallback={<div>Loading...</div>}> + <SdmInfoInsertContent /> + </Suspense> + ); +} + diff --git a/apps/dm/app/pages/sdmMng/sdmInsert/page.tsx b/apps/dm/app/pages/sdmMng/sdmInsert/page.tsx new file mode 100644 index 000000000..89f6988a6 --- /dev/null +++ b/apps/dm/app/pages/sdmMng/sdmInsert/page.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { useDmStore } from "@/app/store"; +import type { DemandCompType } from "@/app/store/sdmMng"; +import SdmInsertForm from '@common/business_components/ui/sdmInsertForm'; +import { useRouter } from "next/navigation"; +import { useCallback, useEffect, useRef } from "react"; +import { Toaster } from "react-hot-toast"; + +export default function MyForm() { + const {Common, initializeCommonData, getDemandCompList, insertSdmData, Sdm} = useDmStore((state) => state); + const { selectSidoData, selectSigunData } = Common; + const { demandCompList, successNextPage } = Sdm; + const router = useRouter(); + const formDataRef = useRef<DemandCompType | null>(null); + + useEffect(() => { + initializeCommonData(); + }, [initializeCommonData]); + + useEffect(() => { + if (successNextPage && formDataRef.current) { + const data = formDataRef.current; + const queryParams = new URLSearchParams({ + dmdCoNm: data.dmdCoNm || '', + ctpvCd: data.ctpvCd || '', + sggCd: data.sggCd || '', + dmdInfoId: data.dmdInfoId || '', + daddr: data.daddr || '', + telno: data.telno || '', + }); + router.push(`/pages/sdmMng/sdmInfoInsert?${queryParams.toString()}`); + } + }, [successNextPage, router]); + + const callback = useCallback((searchText: string, pageCnt: number) => { + searchText = searchText.trim(); + if(searchText.length > 0) { + getDemandCompList(searchText, pageCnt); + } + }, [getDemandCompList]); + + const handleInsertSdmData = useCallback((data: import('@common/business_components/ui/sdmInsertForm/form').DemandCompType) => { + const convertedData: import('@/app/store/sdmMng').DemandCompType = { + ...data, + dmdInfoSn: '' + }; + formDataRef.current = convertedData; + insertSdmData(convertedData).catch(error => { + console.error('Error inserting SDM data:', error); + }); + }, [insertSdmData]); + + return ( + <div className="min-h-screen bg-gray-50"> + <SdmInsertForm + selectSidoData={selectSidoData} + selectSigunData={selectSigunData} + dataList={demandCompList} + rtnValue={callback} + rtnInputData={handleInsertSdmData} + /> + <Toaster position="top-center" /> + </div> + ); +} + diff --git a/apps/dm/app/pages/sdmMng/sdmLogin/page.tsx b/apps/dm/app/pages/sdmMng/sdmLogin/page.tsx new file mode 100644 index 000000000..2fd6fc461 --- /dev/null +++ b/apps/dm/app/pages/sdmMng/sdmLogin/page.tsx @@ -0,0 +1,18 @@ +'use client' + +import { SdmLoginForm } from '@/components/ui/sdmLoginForm/component'; + +export default function SdmLogin() { + return ( + <div className="container mx-auto px-4 py-8"> + <div className="text-center mb-8"> + <h1 className="text-4xl font-bold text-gray-900 underline"> + 수요처 로그인 + </h1> + </div> + <div className="max-w-3xl mx-auto bg-white rounded-lg shadow-md p-6"> + <SdmLoginForm /> + </div> + </div> + ); +} \ No newline at end of file diff --git a/apps/dm/app/pages/wrAbout/page.tsx b/apps/dm/app/pages/wrAbout/page.tsx new file mode 100644 index 000000000..e52549e66 --- /dev/null +++ b/apps/dm/app/pages/wrAbout/page.tsx @@ -0,0 +1,335 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; +import Image from 'next/image'; + +const NAVS = [ + { id: 'card1', label: '물재이용이란' }, + { id: 'card2', label: '물재이용 필요성' }, + { id: 'card3', label: '빗물 재이용' }, + { id: 'card4', label: '중수 재이용' }, + { id: 'card5', label: '하수 재이용' }, + { id: 'card6', label: '물재이용 목표' }, +]; + +export default function WrAboutPage() { + const [activeIdx, setActiveIdx] = useState(0); + const navRefs = useRef<(HTMLLIElement | null)[]>([]); + const cardRefs = useRef<(HTMLElement | null)[]>([]); + + // 클릭 시 이동 + const handleNavClick = (idx: number) => { + setActiveIdx(idx); + const targetCard = cardRefs.current[idx]; + if (targetCard) { + targetCard.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }; + + // 키보드 이벤트 처리 + const handleKeyDown = (e: React.KeyboardEvent, idx: number) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleNavClick(idx); + } + }; + + useEffect(() => { + const observerOptions = { + root: null, + rootMargin: '-200px 0px 0px 0px', // 상단에서 200px 지점을 기준으로 + threshold: 0 + }; + + const observer = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const idx = cardRefs.current.findIndex(ref => ref === entry.target); + if (idx !== -1) { + setActiveIdx(idx); + } + } + }); + }, observerOptions); + + // 각 카드 요소를 관찰 + cardRefs.current.forEach((card) => { + if (card) { + observer.observe(card); + } + }); + + return () => { + cardRefs.current.forEach((card) => { + if (card) { + observer.unobserve(card); + } + }); + }; + }, []); + + return ( + <main className="relative w-[950px] mx-auto pt-[100px] font-sans"> + {/* 사이드바 */} + <nav className="fixed left-[290px] top-[195px] w-[210px] z-50" aria-label="물재이용 정보 섹션 내비게이션"> + <ul role="list" className="relative space-y-6"> + {NAVS.map((nav, idx) => ( + <li + key={nav.id} + ref={(el) => { navRefs.current[idx] = el; }} + role="listitem" + tabIndex={0} + aria-current={idx === activeIdx ? 'true' : 'false'} + aria-label={`현재 위치: ${nav.label}`} + onClick={() => handleNavClick(idx)} + onKeyDown={(e) => handleKeyDown(e, idx)} + className={`nav-item relative pl-6 text-2xl font-bold text-gray-700 cursor-pointer transition-all duration-300 + after:content-[''] after:absolute after:top-6 after:left-[0.5rem] after:w-[2px] after:h-[24px] after:bg-[#C5C5C5] + ${idx === activeIdx ? 'text-4xl font-bold text-gray-900' : ''}`} + > + <span className={`absolute left-[0.5rem] top-1 w-4 h-4 rounded-full -translate-x-1/2 transition-all duration-300 + ${idx === activeIdx ? 'bg-blue-500 border-blue-500' : 'bg-white border-gray-400 border'}`}></span> + {nav.label} + </li> + ))} + </ul> + </nav> + + <article ref={(el) => { cardRefs.current[0] = el; }} className="card-wrap pl-[100px] mb-24 !pt-0" id="card1" aria-labelledby="card1-title"> + <h2 id="card1-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 물재이용이란 + </h2> + <section aria-labelledby="card1-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] "> + <h1 id="card1-desc" className="text-[#269DDA] text-4xl font-bold mb-8">물 재이용이 무엇인가요?</h1> + <p className="text-2xl font-bold mb-10 leading-relaxed"> + 물 재이용은 빗물, 오수, 하수처리수, 폐수처리수 및 발전소 온배수를<br /> + 물 재이용시설을 이용하여 처리하고, 그 처리된 물을 생활, 공업, 농업, 조경,<br /> + 하천 유지 등의 용도로 이용하는 것을 말합니다. + </p> + <Image + src="/img/wrAbout/img_01_1.png" + alt="이 이미지는 물 재이용 시스템의 개요를 나타냅니다. 가정, 공공기관, 하수처리장 등에서 발생한 생활하수, 빗물, 하수처리수를 물 재이용 시설에서 처리한 후, 청소나 화장실, 농업, 살수차, 하천 유지, 공장, 공원 및 골프장 등 다양한 용도로 활용하는 과정을 도식화한 것입니다." + width={800} + height={400} + className="w-[90%] mx-auto mb-8 rounded" + /> + <p className="text-center"> + 환경부는 2011년에 '물의 재이용 촉진 및 지원에 관한 법률'을 제정하여<br /> + 한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다. + </p> + </section> + </article> + + {/* 카드2 */} + <article ref={(el) => { cardRefs.current[1] = el; }} className="card-wrap pl-[100px] mb-24" id="card2" aria-labelledby="card2-title"> + <h2 id="card2-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 물재이용 필요성 + </h2> + <section aria-labelledby="card2-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] "> + <h1 id="card2-desc" className="text-[#EE7B62] text-4xl font-bold mb-8">물 재이용, 왜 필요한가요?</h1> + <p className="text-2xl font-bold mb-10 leading-relaxed"> + 빗물, 중수, 하수처리수는 지속적으로 이용 가능한 순환자원입니다.<br /> + 물 재이용을 통한 효율적인 수자원 활용은 물 스트레스국가인 우리나라에서<br />가뭄대응능력 향상 및 하천수질보전을 위해 반드시 필요한 일입니다. + </p> + <Image + src="/img/wrAbout/img_02_1.png" + alt="이 이미지는 기후변화로 인한 온도 상승 추이를 설명한다. 왼쪽에는 지구 그림과 온도계가 함께 표시되어 있으며, 지구 평균 기온은 지난 100년간 약 0.9℃ 상승했음을 나타낸다. 오른쪽에는 대한민국 지도와 함께 온도계가 있으며, 지난 100년간 약 1.8℃ 상승했음을 보여준다. 이는 지구 평균보다 2배 높은 수치이며, 자료 출처는 「기후변화 평가보고서 2020, 환경부·기상청」이다." + width={800} + height={400} + className="w-[90%] mx-auto mb-8 w-[80%]" + /> + <p className="text-2xl font-bold mb-10 leading-relaxed"> + - 우리나라의 기후변화 진행속도는 세계 평균을 상회하고 있습니다.<br /> + - 기후변화로 인한 국지적인 가뭄은 더욱 자주, 더욱 심하게 일어나고 있으며, 우리의 물 안보를 위협하고 있습니다. + </p> + <Image + src="/img/wrAbout/img_02_2.png" + alt="세계 평균 1인당 강수량은 15,044m³이며, 대한민국은 2,546m³로 약 1/6 수준입니다. 계절별 강수량은 봄 10%, 여름 70%, 가을 15%, 겨울 5%입니다. 이는 여름철에 강수량이 집중되는 문제를 나타냅니다." + width={800} + height={400} + className="w-[90%] mx-auto mb-8" + /> + <p className="text-2xl font-bold mb-10 leading-relaxed"> + - 우리나라의 1인당 강수량은 2,546㎥ 으로 세계 평균 15,044㎥의 약 1/6 수준입니다.<br /> + - 우리나라 강수량의 70%는 여름철에 집중되어 있어 이용 가능한 수자원확보가 어렵습니다. + </p> + <Image + src="/img/wrAbout/img_02_3.png" + alt="물 재이용은 환경을 보호하며 지속 가능한 발전이 가능하게 하고, 안정적인 수자원 확보, 깨끗하고 경제적인 공업용수 공급, 하천 수질 개선을 통한 생태계 회복에 기여합니다." + width={800} + height={400} + className="w-[93%] mx-auto" + /> + </section> + </article> + + {/* 카드3 */} + <article ref={(el) => { cardRefs.current[2] = el; }} className="card-wrap pl-[100px] mb-24" id="card3" aria-labelledby="card3-title"> + <h2 id="card3-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 빗물 재이용 + </h2> + <section aria-labelledby="card3-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] "> + <h1 id="card3-desc" className="text-[#269DDA] text-4xl font-bold mb-6">친환경 수자원 빗물,<br />편리하게 사용할 수 있어요!</h1> + <p className="text-2xl font-bold mb-10 leading-relaxed">가뭄을 극복하고 홍수를 예방하기 위해<br />비가 내릴 때 빗물을 모아두었다가 사용합니다.</p> + <Image + src="/img/wrAbout/img_03_1.png" + alt="주택 지붕에 내린 빗물을 저장조에 모아 조경용수, 화장실용수, 세척·살수용수로 재활용하는 과정을 보여줍니다." + width={800} + height={400} + className="w-[90%] mx-auto mb-6" + /> + <Image + src="/img/wrAbout/img_03_2.png" + alt="빗물이용" + width={200} + height={100} + className="mb-5 w-[20%]" + /> + <p className="text-2xl font-bold leading-relaxed"> + - 의무 설치대상: 지붕면적 1,000㎡ 이상의 운동장, 체육관, 공공업무시설 등<br /> + - 설 치 기 준: 집수시설, 처리시설, 빗물 저류조(지붕 집수면적 x 0.05m 이상)<br /> + - 활 용 용 도: 골프장 등의 조경용수, 화장실용수, 세척·살수용수 등 + </p> + </section> + </article> + + {/* 카드4 */} + <article ref={(el) => { cardRefs.current[3] = el; }} className="card-wrap pl-[100px] mb-24" id="card4" aria-labelledby="card4-title"> + <h2 id="card4-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 중수 재이용 + </h2> + <section aria-labelledby="card4-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] "> + <h1 id="card4-desc" className="text-[#9771B0] text-4xl font-bold mb-6">요모조모 쓸모 많은 중수,<br />알뜰하게 재이용할 수 있어요!</h1> + <p className="text-2xl font-bold mb-10 leading-relaxed">한번 사용한 물을 중수도 시설을 통해 재생하여<br />개별적 또는 지역적으로 재이용할 수 있습니다.</p> + <Image + src="/img/wrAbout/img_04_1.png" + alt="욕실, 세면대, 주방, 세탁기 등에서 발생한 생활용수를 중수도 시설에서 처리하여 화장실 변기용수와 조경용수로 재이용하는 시스템 흐름도입니다." + width={800} + height={400} + className="w-[90%] mx-auto mb-6" + /> + <Image + src="/img/wrAbout/img_04_2.png" + alt="중수재이용" + width={200} + height={100} + className="mb-5 w-[20%]" + /> + <p className="text-2xl font-bold leading-relaxed"> + - 의무 설치대상: 일정규모 이상의 대규모점포, 숙박/목욕장, 공장, 각종 개발사업 등<br /> + - 의무 재이용량: 물 사용량의 10% 이상<br /> + - 활 용 용 도: 청소·화장실용수, 세척·살수용수, 조경용수, 공업용수 등 + </p> + </section> + </article> + + {/* 카드5 */} + <article ref={(el) => { cardRefs.current[4] = el; }} className="card-wrap pl-[100px] mb-24" id="card5" aria-labelledby="card5-title"> + <h2 id="card5-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 하수 재이용 + </h2> + <section aria-labelledby="card5-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] "> + <h1 id="card5-desc" className="text-[#00A5B1] text-4xl font-bold mb-6">아직 할 일이 많은 하수처리수,<br />풍족하게 재이용할 수 있어요!</h1> + <p className="text-2xl font-bold mb-10 leading-relaxed">사용하고 버려져 공공하수처리장으로 모여든 물은<br />용도에 맞는 처리 후 재이용 할 수 있습니다.</p> + <Image + src="/img/wrAbout/img_05_1.png" + alt="주택과 건물에서 발생한 하수가 공공하수처리장을 거쳐 처리된 후, 물 재이용 시설에서 정화되어 공업용수, 하천유지용수, 농업용수, 세척·살수용수로 재활용됩니다." + width={800} + height={400} + className="w-[90%] mx-auto mb-6" + /> + <Image + src="/img/wrAbout/img_05_2.png" + alt="하수처리 재이용" + width={200} + height={100} + className="mb-5 w-[20%]" + /> + <p className="text-2xl font-bold leading-relaxed"> + - 의무 이용대상: 공공하수처리장(시설용량 5,000㎥/일 이상)<br /> + - 의무 재이용량: 일 하수처리량의 10% 이상<br /> + - 활 용 용 도: 공업용수, 하천유지용수, 농업용수 등 + </p> + </section> + </article> + + {/* 카드6 */} + <article ref={(el) => { cardRefs.current[5] = el; }} className="card-wrap pl-[100px] mb-24" id="card6" aria-labelledby="card6-title"> + <h2 id="card6-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 물재이용 목표 + </h2> + <section aria-labelledby="card6-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] "> + <h1 id="card6-desc" className="text-[#269DDA] text-4xl font-bold mb-6">2030년 물 재이용 목표를 위해<br />모두의 관심과 노력이 필요합니다.</h1> + <p className="text-2xl font-bold mb-6 leading-relaxed"> + 환경부는 2030년까지의 제2차 물 재이용 기본계획을 수립하여<br />한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다. + </p> + <Image + src="/img/wrAbout/img_06_1.png" + alt="지속가능한 물 재이용 정착으로 건전한 물순환 확산" + width={800} + height={400} + className="w-[80%] mx-auto mb-6" + /> + <div className="flex gap-3 flex-wrap justify-between bg-gray-100 p-6 rounded-lg mb-8"> + <div className="flex-1"> + <Image + src="/img/wrAbout/icon_01.png" + alt="" + width={70} + height={70} + className="mx-auto mb-4 w-[auto] h-[70px]" + /> + <h3 className="text-center font-semibold text-nowrap mb-2">도시 물 순환이용<br />체계로의 전환</h3> + <ul className="text-sm text-gray-600 leading-relaxed space-y-1"> + <li className="text-nowrap">- 지속가능한 물 순환이용체계 구축</li> + <li>- 물 재이용시설 관리체계 강화</li> + </ul> + </div> + <div className="flex-1 text-center"> + <Image + src="/img/wrAbout/icon_02.png" + alt="" + width={70} + height={70} + className="mx-auto mb-4 w-[auto] h-[70px]" + /> + <h3 className="text-center font-semibold text-nowrap mb-2">유역기반 하수처리수 재이용<br />수요확대 및 공급체계 구축</h3> + <ul className="text-sm text-gray-600 leading-relaxed space-y-1"> + <li>- 유역기반 통합물관리 연계 강화</li> + <li>- 하수처리수 재이용 공업용수 공급 활성화</li> + <li>- 하수처리수 재이용을 활용한 물순환 촉진</li> + </ul> + </div> + <div className="flex-1 text-center"> + <Image + src="/img/wrAbout/icon_03.png" + alt="" + width={70} + height={70} + className="mx-auto mb-4 w-[auto] h-[70px]" + /> + <h3 className="text-center font-semibold text-nowrap mb-2">물 재이용 인식제고<br />및 산업발전 기반 마련</h3> + <ul className="text-sm text-gray-600 leading-relaxed space-y-1"> + <li>- 물 재이용 교육 및 대국민 홍보 강화</li> + <li>- 핵심기술 육성 및 산업발전 지원 강화</li> + </ul> + </div> + </div> + <Image + src="/img/wrAbout/img_06_2.png" + alt="물 재이용량은 2014년 13.3억톤에서 2030년 25.9억톤까지 증가할 것으로 예상됩니다. 그에 따른 기대 효과로는 연간 상수대체 17.9억톤, 물산업 시장성장 약 2조3,541억 원, 건천화된 73개 하천 해소, 약 20,550명의 고용 효과가 포함됩니다." + width={800} + height={400} + className="w-[90%] mx-auto" + /> + </section> + </article> + </main> + ); +} diff --git a/apps/dm/app/pages/wrExample/page.tsx b/apps/dm/app/pages/wrExample/page.tsx new file mode 100644 index 000000000..066e0abe2 --- /dev/null +++ b/apps/dm/app/pages/wrExample/page.tsx @@ -0,0 +1,364 @@ +import Image from "next/image"; + +export default function WrExamplePage() { + return ( + <div className="bg-[#F1F6F9]"> + <div className="relative w-[950px] mx-auto pt-[50px] font-sans"> + <h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 rounded-full"></span> + 물 재이용사례 + </h2> + </div> + <div className="bg-[#F1F6F9] text-gray-800"> + <div className="relative w-[950px] mx-auto pt-[50px] font-sans"> + {/* 카드 1 */} + <div className="card-wrap mb-24" id="card1"> + <h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 사업사례 1 + </h2> + <div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]"> + {/* 사업명 */} + <section> + <h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2> + <p className="text-2xl font-semibold text-gray-800 leading-relaxed"> + 경기권역 반도체 사업장 물 재이용 사업<br /> + : 삼성전자 반도체 사업장 하수재이용수 공급(재이용시설 설치, 공급망 구축 등) + </p> + </section> + {/* 사업 배경 및 목적 */} + <section className="mt-8"> + <h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 배경 및 목적</h2> + <p className="text-2xl font-semibold text-gray-800 leading-relaxed"> + 삼성전자는 환경경영전략 실현과 반도체 공장의 안정적 용수 공급을 위해 하수 재이용수를 공업용수로 도입하기로 결정 + </p> + </section> + {/* 사업 주요 내용 */} + <section className="mt-8"> + <h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2> + <p className="text-2xl font-semibold text-gray-800 leading-relaxed"> + 1단계 사업[화성·오산(12만㎥/일) → 삼성 화성·기흥(29년 공급)]<br /> + 2단계 사업[수원(21만㎥/일) → 삼성 평택(30년 공급)] + </p> + </section> + {/* 특이사항 */} + <section className="mt-8"> + <h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2> + <p className="text-2xl font-semibold text-gray-800 leading-relaxed"> + 본 사업은 광역 하수 재이용 공급망 구축의 첫 사업으로 초기 민관협의체를 구성·운영 후 관계기관(환경부, 경기도, 지자체, 공단, 수공, 삼성전자) 간 MOU 체결을 통해 사업을 추진하였으며, 반도체 사업장에 하수 재이용수를 공업용수로 공급하는 국내 첫 사업으로 위협부담 해소를 위해 사업을 단계별로 시행 + </p> + </section> + {/* 단계별 시각 정보 */} + <section className="bg-white px-4 mt-8"> + <Image + src="/img/wrExample/img_ex_01_1.png" + alt="사업단계 설명" + width={800} + height={400} + className="w-[100%] mx-auto mb-8" + /> + </section> + </div> + </div> + + {/* 카드2 */} + <div className="card-wrap mb-24" id="card2"> + <h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 사업사례 2 + </h2> + <div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]"> + {/* 사업명 */} + <section> + <h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2> + <p className="text-2xl font-semibold text-gray-800 leading-relaxed"> + 경기도 파주시 하수재이용<br /> + : 운정·금촌하수처리시설의 하수처리수를 막공법(U/F, R/O)으로 재처리하여 파주 LCD 산업단지내 3개 업체*에 공업용수 공급<br /><br /> + *LG디스플레이(P10공장), 희성전자, 엔비텍 등 + </p> + </section> + {/* 사업 방식 */} + <section className="mt-8"> + <h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 방식</h2> + <p className="text-2xl font-semibold text-gray-800 leading-relaxed"> + 민간투자사업(시행사 : 파주그린허브(주), 운영사 : 테크로스) + </p> + </section> + {/* 사업 주요 내용 */} + <section className="mt-8"> + <h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2> + <p className="text-2xl font-semibold text-gray-800 leading-relaxed"> + 처리공정 : 전처리분리막(U/F) + 역삼투서비(R/O)<br /> + 공급량 : 4만m³/일<br /> + 관로 : 13.6km<br /> + 사업비 : 651억원 + </p> + </section> + {/* 특이사항 */} + <section className="mt-8"> + <h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2> + <p className="text-2xl font-semibold text-gray-800 leading-relaxed"> + 하수처리수 재이용시설 자체 농축폐수 처리공정 개발 및 적용 성공사례 + </p> + <div className="grid grid-cols-1 gap-0 items-start"> + {/* 좌측 이미지 */} + <div> + <Image + src="/img/wrExample/img_ex_02_1.png" + alt="적용 성공사례 지도" + width={1000} + height={1200} + className="w-[100%] mx-auto" + /> + </div> + + {/* 우측 표 */} + <div className="space-y-3"> + {/* 하수처리수 공급 */} + <div> + <table className="min-w-full h-[500px]text-center border border-black"> + <colgroup> + <col style={{ width: "20%" }} /> + <col style={{ width: "40%" }} /> + <col style={{ width: "40%" }} /> + </colgroup> + <tbody> + <tr> + <td colSpan={3} className="bg-[#ffc000] h-[30px] text-xl text-white font-semibold text-center pt-[6px] pb-[6px] px-0">하수처리수 공급</td> + </tr> + <tr className="text-xl h-[30px]"> + <td className="text-xl h-[30px] border-black border p-1 font-bold">위치</td> + <td className="text-xl h-[30px] border-black border p-1 font-bold">금촌하수</td> + <td className="text-xl h-[30px] border-black border p-1 font-bold">운정하수</td> + </tr> + <tr className="text-sm"> + <td className="text-xl h-[30px] border-black border p-1 font-bold">시설용량</td> + <td className="text-xl h-[30px] border-black border p-1 font-bold">44,000㎥/일</td> + <td className="text-xl h-[30px] border-black border p-1 font-bold">60,000㎥/일</td> + </tr> + <tr className="text-sm"> + <td className="text-xl h-[30px] border-black border p-1 font-bold">유입량</td> + <td className="text-xl h-[30px] border-black border p-1 font-bold">27,000㎥/일<br />자연유하</td> + <td className="text-xl h-[30px] border-black border p-1 font-bold">30,000㎥/일<br />이송펌프장</td> + </tr> + </tbody> + </table> + {/* 첫 화살표 (자연유하 / 관로이송) */} + <div className="flex h-[30px] justify-between items-center my-3 px-4"> + <div className="flex text-xl items-center gap-1 text-orange-600 font-bold pl-6"> + <span>자연유하: 161m</span> + <Image + src="/img/wrExample/img_arrow_down_1.png" + alt="오른쪽 화살표" + width={24} + height={24} + className="h-6 w-6" + /> + </div> + <div className="flex text-xl items-center gap-1 text-orange-600 font-bold"> + <Image + src="/img/wrExample/img_arrow_down_1.png" + alt="왼쪽 화살표" + width={24} + height={24} + className="h-6 w-6" + /> + <span>관로이송: 3.2km</span> + </div> + </div> + </div> + + {/* 물재이용 처리시설 */} + <div> + <table className="min-w-full text-center border border-black"> + <colgroup> + <col style={{ width: "50%" }} /> + <col style={{ width: "50%" }} /> + </colgroup> + <tbody> + <tr> + <td colSpan={2} className="bg-[#00b050] text-xl h-[30px] text-white font-semibold text-center pt-[6px] pb-[6px] px-0">물재이용 처리시설</td> + </tr> + <tr className="text-sm"> + <td className="text-xl h-[30px] border-black border p-1 font-bold">유입유량</td> + <td className="text-xl h-[30px] border-black border p-1 font-bold">57,000㎥/일</td> + </tr> + <tr className="text-sm"> + <td className="text-xl h-[30px] border-black border p-1 font-bold">재이용시설</td> + <td className="text-xl h-[30px] border-black border p-1 font-bold">40,000㎥/일</td> + </tr> + <tr className="text-sm"> + <td className="text-xl h-[30px] border-black border p-1 font-bold">농축폐수처리</td> + <td className="text-xl h-[30px] border-black border p-1 font-bold">17,000㎥/일</td> + </tr> + </tbody> + </table> + {/* 두 번째 화살표 (공급관로) */} + <div className="flex flex-col items-center my-3"> + <div className="flex h-[30px] items-center gap-1 text-xl text-blue-600 font-semibold"> + <span>공급관로 L=10.4km</span> + <Image + src="/img/wrExample/img_arrow_down_2.png" + alt="아래 화살표" + width={24} + height={24} + className="h-6 w-6" + /> + </div> + </div> + </div> + + {/* 수요처 공급 */} + <div> + <table className="min-w-full text-center border border-black"> + <colgroup> + <col style={{ width: "50%" }} /> + <col style={{ width: "50%" }} /> + </colgroup> + <tbody> + <tr> + <td colSpan={2} className="bg-[#3399ff] h-[30px] text-xl text-white font-semibold text-center pt-[6px] pb-[6px] px-0">수요처 공급</td> + </tr> + <tr className="text-sm"> + <td className="text-xl h-[30px] border-black border p-1 font-bold">수요처</td> + <td className="text-xl h-[30px] border-black border p-1 font-bold">LG디스플레이 외 2개사</td> + </tr> + <tr className="text-sm"> + <td className="text-xl h-[30px] border-black border p-1 font-bold">수요량</td> + <td className="text-xl h-[30px] border-black border p-1 font-bold">40,000㎥/일</td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + </section> + </div> + </div> + + {/* 카드3 */} + <div className="card-wrap mb-24" id="card3"> + <h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 사업사례 3 + </h2> + <div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]"> + {/* 사업명 */} + <section> + <h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2> + <p className="text-2xl font-semibold text-gray-800 leading-relaxed"> + 충남 보령 하수재이용<br /> + : 보령·신보령화력발전소에 하루 1만톤의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업 추진 + </p> + </section> + {/* 사업 방식 */} + <section className="mt-8"> + <h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 방식</h2> + <p className="text-2xl font-semibold text-gray-800 leading-relaxed"> + 2017년 한국환경공단과 사업대행 위 · 수탁 협약 체결 + </p> + </section> + {/* 사업 주요 내용 */} + <section className="mt-8"> + <h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2> + <p className="text-2xl font-semibold text-gray-800 leading-relaxed"> + 처리공정 : 전처리분리막(U/F) + 역삼투서비(R/O)<br /> + 공급량 : 1만m3/일<br /> + 관로 : 8.7km<br /> + 사업비 : 315억원(국비 216억원, 재원협의(안)) + </p> + </section> + {/* 특이사항 */} + <section className="mt-8"> + <h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2> + <p className="text-2xl font-semibold text-gray-800 leading-relaxed"> + 국내 유일 하수재이용 공업용수 공급 재정사업으로 2016년 충남 서부권 가뭄 시 지역사회 기여 차원에서 기관간 MOU (환경부, 충청남도, 보령시, 한국중부발전㈜)를 바탕 + 으로 추진 되어 경제성보다 공익성을 바탕으로 국가전력 기반시설의 안정적 운영 도모 + </p> + <div className="p-2"> + <Image + src="/img/wrExample/img_ex_03_1.png" + alt="국가전력 기반시설 설명" + width={700} + height={350} + className="w-[100%] mx-auto" + /> + </div> + </section> + </div> + </div> + + {/* 카드4 */} + <div className="card-wrap mb-24" id="card4"> + <h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 사업사례 4 + </h2> + <div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]"> + {/* 사업명 */} + <section> + <h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2> + <p className="text-2xl font-semibold text-gray-800 leading-relaxed"> + 경기도 수원시 빗물이용과 중수도 + </p> + </section> + {/* 사업 내용 */} + <section className="mt-8"> + <h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 내용</h2> + <p className="text-2xl font-semibold text-gray-800 leading-relaxed"> + (빗물) 월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물 공급기를 설치하여 혹서기 도시 열섬효과 및 도로 비산먼지 저감<br /> +   ※ 시설 가동중 도로 노면 온도의 7.2 ~ 26.3℃ 감소 효과 확인(21.7월, 수원시청).<br /> +   다만, 가뭄기나 강우량이 적을 경우 빗물을 원활히 공급하기 다소 곤란<br /><br /> + - (중수도) 수원시청, 장안구청, 월드컵경기장에 빗물 연계 중수도를 화장실·조경용수 등으로 사용하여 수도요금 절감(연 7백~30백만원) + </p> + <div className="grid grid-cols-1 md:grid-cols-2 gap-2 items-start p-2"> + <div> + <Image + src="/img/wrExample/img_ex_04_1.png" + alt="월드컵경기장 인근도로 노면 살수 이미지" + width={350} + height={250} + className="w-[100%] mx-auto" + /> + <span className="text-xl block text-center font-semibold my-2"><월드컵경기장 인근도로 노면 살수></span> + </div> + <div> + <Image + src="/img/wrExample/img_ex_04_2.png" + alt="주유기 모양의 빗물 공급기 이미지" + width={350} + height={250} + className="w-[100%] mx-auto" + /> + <span className="text-xl block text-center font-semibold my-2"><주유기 모양의 빗물 공급기></span> + </div> + <div> + <Image + src="/img/wrExample/img_ex_04_3.png" + alt="빗물주유기에서 살수용수 공급 이미지" + width={350} + height={250} + className="w-[100%] mx-auto" + /> + <span className="text-xl block text-center font-semibold my-2"><빗물주유기에서 살수용수 공급></span> + </div> + <div> + <Image + src="/img/wrExample/img_ex_04_4.png" + alt="빗물 공급기 및 빗물분사장치 위치" + width={350} + height={250} + className="w-[100%] mx-auto" + /> + <span className="text-xl block text-center font-semibold my-2"><빗물 공급기 및 빗물분사장치 위치></span> + </div> + </div> + </section> + </div> + </div> + </div> + </div> + </div> + ); +} + diff --git a/apps/dm/app/pages/wrOverview/page.tsx b/apps/dm/app/pages/wrOverview/page.tsx new file mode 100644 index 000000000..f8aa3b933 --- /dev/null +++ b/apps/dm/app/pages/wrOverview/page.tsx @@ -0,0 +1,113 @@ +import Image from "next/image"; + +export default function WrOverviewPage() { + return ( + <div className="bg-[#F1F6F9]"> + <div className="relative w-[950px] mx-auto pt-[50px] font-sans"> + {/* 카드 1 - 물재이용 사업 추진 절차 */} + <div className="card-wrap mb-24" id="card1"> + <h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 물재이용 사업 추진 절차 + </h2> + <div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]"> + <table className="table-fixed w-full text-center text-sm"> + <caption className="sr-only">물 재이용 추진 절차 상세 표</caption> + <colgroup> + <col style={{ width: "28%" }} /> + <col style={{ width: "2%" }} /> + <col style={{ width: "35%" }} /> + <col style={{ width: "5%" }} /> + <col style={{ width: "30%" }} /> + </colgroup> + <tbody> + {/* 1단계 */} + <tr><td></td><td></td><td className="text-xl bg-gray-100 font-bold py-3">타당성 조사</td><td></td><td></td></tr> + <tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr> + {/* 2단계 */} + <tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">매칭시스템 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">사업계획(안) 수립</td><td></td><td></td></tr> + <tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr> + {/* 3단계 */} + <tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">하수도정보시스템 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">국고지원 신청</td><td className="text-lg">→</td><td className="text-xl bg-green-200 font-bold py-3">기술검토<br />(한국환경공단)</td></tr> + <tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr> + {/* 4단계 */} + <tr><td></td><td></td><td className="text-xl bg-gray-100 font-bold py-3">국고지원 확정 통보</td><td></td><td></td></tr> + <tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr> + {/* 5단계 */} + <tr><td></td><td></td><td className="text-xl bg-gray-100 font-bold py-3">사업계획 수립(실시설계)</td><td></td><td></td></tr> + <tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr> + {/* 6단계 */} + <tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">(행정관) 기술검토 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">재원협의<br />(지방환경관서장)</td><td className="text-lg">→</td><td className="text-xl bg-green-200 font-bold py-3">기술검토<br />(한국환경공단)</td></tr> + <tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr> + {/* 7단계 */} + <tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">(행정관) 기술검토 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">설치승인<br />(시도지사)</td><td className="text-lg">→</td><td className="text-xl bg-green-200 font-bold py-3">기술검토<br />(한국환경공단)</td></tr> + <tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr> + {/* 8단계 */} + <tr><td></td><td></td><td className="text-xl bg-gray-100 font-bold py-3">공사시행</td><td></td><td></td></tr> + <tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr> + {/* 9단계 */} + <tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">(행정관) 기술검토 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">준공(국고정산)</td><td className="text-lg">→</td><td className="text-xl bg-green-200 font-bold py-3">기술검토<br />(한국환경공단)</td></tr> + </tbody> + </table> + </div> + </div> + + {/* 카드 2 - 물재이용 매칭 시스템 */} + <div className="card-wrap mb-24" id="card2"> + <h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 물재이용 매칭 시스템이란? + </h2> + <div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]"> + {/* 개요 */} + <section> + <h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">개요</h2> + <p className="text-2xl font-bold text-gray-800 leading-relaxed"> + 기후변화 및 지자체의 개발속도 차이에 따라 물 자원의 수요-공급 불균형이 발생하여 빗물, 중수도, 하수처리 재처리수 등의 공급 여유가 있는 지자체의 재이용수를 부족한 지자체 수요처에 공급하여 불균형 해소를 통한 수자원 절약 및 물 자원 순환 + </p> + </section> + + {/* 구성 */} + <section className="mt-8"> + <h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">구성</h2> + <ul className="list-[square] pl-6 text-2xl font-bold text-gray-800 leading-relaxed space-y-2"> + <li>공급자: 하수처리장, 빗물이용시설, 중수도 등</li> + <li>수요자: 산업체(냉각수), 공공시설(조경, 도로청소), 농민(농업용수) 등</li> + <li>시스템: 수질, 수량, 거리 기반 매칭, 중개</li> + <li>운영주체: 한국환경공단</li> + </ul> + </section> + + {/* 절차 */} + <section className="mt-8"> + <h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2> + <p className="text-2xl font-bold text-gray-800 leading-relaxed"> + ① 공급자 등록 → ② 수요자 조건 입력 → ③ 매칭 검토 → ④ 시스템 매칭 → ⑤ 계약·공급 이행 + </p> + </section> + + {/* 기대효과 */} + <section className="mt-8"> + <h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">기대효과</h2> + <ul className="list-[square] pl-6 text-2xl font-bold text-gray-800 leading-relaxed space-y-2"> + <li>물 자원 수요-공급 조정을 통한 효율적 분배</li> + <li>물 재이용시설 데이터 수집으로 체계적 관리</li> + <li>지자체, 사업시행자 등 재이용 정보 획득 용이</li> + </ul> + <div className="text-center my-6"> + <Image + src="/img/wrOverview/img_ov_01_1.png" + alt="하수 재이용 시스템 흐름도. 저장탱크에서 재처리된 물이 손 씻기, 수도, 세정 등에 공급되며 재이용 활성화를 통해 최적의 물공급 방안이 마련되는 과정을 시각화함." + width={650} + height={500} + className="h-[500px] mx-auto mb-4 rounded" + /> + <p className="text-2xl font-bold text-gray-800 mt-10">(하수 재이용 시스템 개요도)</p> + </div> + </section> + </div> + </div> + </div> + </div> + ); +} \ No newline at end of file diff --git a/apps/dm/app/pages/wrStatus/page.tsx b/apps/dm/app/pages/wrStatus/page.tsx new file mode 100644 index 000000000..4e830608f --- /dev/null +++ b/apps/dm/app/pages/wrStatus/page.tsx @@ -0,0 +1,558 @@ +'use client'; + +import React, { useState, useEffect, useRef } from 'react'; +import Image from 'next/image'; +import { CHART_FORM, MainChart, type ChartConfig } from '@common/business_components'; + +interface RegionData { + name: string; + x: number; + y: number; + values: number[]; +} + +interface TableData { + region: string; + sewage: number; + rainwater: number; + reclaimed: number; + thermal: number; +} + + +const WrStatusPage: React.FC = () => { + const [selectedRegion, setSelectedRegion] = useState<string | null>(null); + const [originalFills, setOriginalFills] = useState<Record<string, string>>({}); + const [hoveredRegion, setHoveredRegion] = useState<string | null>(null); + const [tooltipPosition, setTooltipPosition] = useState<{ x: number; y: number } | null>(null); + const [favorites, setFavorites] = useState<Record<string, boolean>>({}); + const svgContainerRef = useRef<HTMLDivElement>(null); + const [chartData, setChartData] = useState<any[]>([ + { + label : '서울', + 하수 : 4, + 빗물 : 816, + 중수도 : 371, + 온배수 : 0 + }, + { + label : '부산', + 하수 : 13, + 빗물 : 68, + 중수도 : 20, + 온배수 : 1 + }, + { + label : '대구', + 하수 : 9, + 빗물 : 294, + 중수도 : 14, + 온배수 : 2 + }, + { + label : '인천', + 하수 : 17, + 빗물 : 165, + 중수도 : 35, + 온배수 : 15 + }, + { + label : '광주', + 하수 : 3, + 빗물 : 74, + 중수도 : 15, + 온배수 : 15 + }, + { + label : '대전', + 하수 : 2, + 빗물 : 52, + 중수도 : 11, + 온배수 : 20 + }, + { + label : '울산', + 하수 : 8, + 빗물 : 111, + 중수도 : 19, + 온배수 : 30 + }, + { + label : '세종', + 하수 : 9, + 빗물 : 65, + 중수도 : 0, + 온배수 : 0 + }, + { + label : '경기', + 하수 : 176, + 빗물 : 1009, + 중수도 : 252, + 온배수 : 40 + }, + { + label : '강원', + 하수 : 69, + 빗물 : 69, + 중수도 : 22, + 온배수 : 0 + }, + { + label : '충북', + 하수 : 38, + 빗물 : 17, + 중수도 : 91, + 온배수 : 0 + }, + { + label : '충남', + 하수 : 67, + 빗물 : 54, + 중수도 : 188, + 온배수 : 0 + }, + { + label : '전북', + 하수 : 55, + 빗물 : 33, + 중수도 : 220, + 온배수 : 0 + }, + { + label : '전남', + 하수 : 83, + 빗물 : 16, + 중수도 : 75, + 온배수 : 0 + }, + { + label : '경북', + 하수 : 81, + 빗물 : 37, + 중수도 : 70, + 온배수 : 0 + }, + { + label : '경남', + 하수 : 75, + 빗물 : 27, + 중수도 : 140, + 온배수 : 0 + }, + { + label : '제주', + 하수 : 8, + 빗물 : 47, + 중수도 : 64, + 온배수 : 0 + } + ]); + + const chartConfig = chartData.reduce((config, element) => { + config[element.label] = { + label: element.label, + color : "hsl(var(--chart-1))" + }; + return config; + }, {} as Record<string, { label: string; color: string; }>) satisfies ChartConfig; + + const chartOption = { + chartForm : CHART_FORM.BAR, + chartConfig : chartConfig, + chartData : chartData, + chartTitle : '', + chartClassName : "h-[300px] w-full", + graphSize : 40, + yFieldText : '개소수(건)', + showLegend : true + } + + const colorLegend = [ + { label: '하수', color: '#FF6B6B', key: 'sewage' }, + { label: '빗물', color: '#4ECDC4', key: 'rainwater' }, + { label: '중수도', color: '#45B7D1', key: 'reclaimed' }, + { label: '온배수', color: '#FED16A', key: 'thermal' }, + ]; + + const regionIdMap: Record<string, string> = { + 서울: '서울특별시', + 부산: '부산광역시', + 대구: '대구광역시', + 인천: '인천광역시', + 광주: '광주광역시', + 대전: '대전광역시', + 울산: '울산광역시', + 세종: '세종특별자치시', + 경기: '경기도', + 강원: '강원특별자치도', + 충북: '충청북도', + 충남: '충청남도', + 전북: '전라북도', + 전남: '전라남도', + 경북: '경상북도', + 경남: '경상남도', + 제주: '제주특별자치도' + }; + + const tableData: TableData[] = [ + { region: '서울특별시', sewage: 4, rainwater: 816, reclaimed: 371, thermal: 0 }, + { region: '부산광역시', sewage: 13, rainwater: 68, reclaimed: 20, thermal: 1 }, + { region: '대구광역시', sewage: 9, rainwater: 294, reclaimed: 14, thermal: 2 }, + { region: '인천광역시', sewage: 17, rainwater: 165, reclaimed: 35, thermal: 15 }, + { region: '광주광역시', sewage: 3, rainwater: 74, reclaimed: 15, thermal: 15 }, + { region: '대전광역시', sewage: 2, rainwater: 52, reclaimed: 11, thermal: 20 }, + { region: '울산광역시', sewage: 8, rainwater: 111, reclaimed: 19, thermal: 30 }, + { region: '세종특별자치시', sewage: 9, rainwater: 65, reclaimed: 0, thermal: 0 }, + { region: '경기도', sewage: 176, rainwater: 1009, reclaimed: 252, thermal: 40 }, + { region: '강원특별자치도', sewage: 69, rainwater: 69, reclaimed: 22, thermal: 0 }, + { region: '충청북도', sewage: 38, rainwater: 17, reclaimed: 91, thermal: 0 }, + { region: '충청남도', sewage: 67, rainwater: 54, reclaimed: 188, thermal: 0 }, + { region: '전라북도', sewage: 55, rainwater: 33, reclaimed: 220, thermal: 0 }, + { region: '전라남도', sewage: 83, rainwater: 16, reclaimed: 75, thermal: 0 }, + { region: '경상북도', sewage: 81, rainwater: 37, reclaimed: 70, thermal: 0 }, + { region: '경상남도', sewage: 75, rainwater: 27, reclaimed: 140, thermal: 0 }, + { region: '제주특별자치도', sewage: 8, rainwater: 47, reclaimed: 64, thermal: 0 }, + + ]; + + const regionData: RegionData[] = [ + { name: '서울', x: 170, y: 140, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '인천', x: 120, y: 150, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '경기', x: 210, y: 210, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '강원', x: 330, y: 120, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '충북', x: 260, y: 280, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '충남', x: 100, y: 320, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '세종', x: 180, y: 305, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '대전', x: 195, y: 360, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '전북', x: 170, y: 450, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '전남', x: 160, y: 600, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '광주', x: 120, y: 530, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '경북', x: 400, y: 320, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '경남', x: 310, y: 530, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '대구', x: 350, y: 450, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '울산', x: 450, y: 500, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '부산', x: 420, y: 550, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '제주', x: 60, y: 750, values: [0.3, 0.3, 0.2, 0.2] }, + ]; + + const handleMarkerClick = (regionId: string) => { + if (selectedRegion === regionId) { + setSelectedRegion(null); + } else { + setSelectedRegion(regionId); + } + }; + + const handleFavoriteClick = (region: string) => { + setFavorites(prev => ({ + ...prev, + [region]: !prev[region] + })); + }; + + const createDonutMarker = (svg: SVGSVGElement, x: number, y: number, name: string, values: number[]) => { + const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FED16A']; + const radius = 26; + const innerRadius = 18; + // const regionId = regionIdMap[name]; + const regionId = regionIdMap[name]; + if (!regionId) { + console.warn(`Invalid region name: ${name}`); + return; + } + + const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + g.setAttribute('transform', `translate(${x}, ${y})`); + g.setAttribute('id', `marker-${name}`); + g.style.cursor = 'pointer'; + g.setAttribute('role', 'button'); + g.setAttribute('tabindex', '0'); + g.setAttribute('aria-label', `${name} 지역 선택`); + + const bg = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + bg.setAttribute('r', radius.toString()); + bg.setAttribute('fill', 'white'); + bg.setAttribute('pointer-events', 'all'); + g.appendChild(bg); + + let startAngle = 0; + values.forEach((value, idx) => { + const endAngle = startAngle + value * 2 * Math.PI; + const x1 = radius * Math.sin(startAngle); + const y1 = -radius * Math.cos(startAngle); + const x2 = radius * Math.sin(endAngle); + const y2 = -radius * Math.cos(endAngle); + const largeArc = endAngle - startAngle > Math.PI ? 1 : 0; + const d = `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} L 0 0 Z`; + + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('d', d); + path.setAttribute('fill', colors[idx % colors.length] as string); + g.appendChild(path); + + startAngle = endAngle; + }); + + const center = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + center.setAttribute('r', innerRadius.toString()); + center.setAttribute('fill', 'white'); + g.appendChild(center); + + const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + text.setAttribute('x', '0'); + text.setAttribute('y', '2'); + text.setAttribute('text-anchor', 'middle'); + text.setAttribute('font-size', '13'); + text.setAttribute('font-weight', 'bold'); + text.setAttribute('dominant-baseline', 'middle'); + text.setAttribute('fill', 'black'); + text.textContent = name || ''; + g.appendChild(text); + + g.addEventListener('mouseenter', (e) => { + setHoveredRegion(regionId); + setTooltipPosition({ x: e.clientX, y: e.clientY }); + }); + + g.addEventListener('mouseleave', () => { + setHoveredRegion(null); + setTooltipPosition(null); + }); + + g.addEventListener('click', () => { + handleMarkerClick(regionId); + }); + + g.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + + const matrix = g.getScreenCTM(); + if (!matrix) return; // 또는 console.warn('matrix is null'); return; + + const point = svg.createSVGPoint(); + point.x = 0; + point.y = 0; + const transformed = point.matrixTransform(matrix); + + setTooltipPosition({ x: transformed.x, y: transformed.y }); + setHoveredRegion(regionId); + handleMarkerClick(regionId); + } + }); + + svg.appendChild(g); + }; + + useEffect(() => { + const loadSVG = async () => { + try { + const response = await fetch('/img/wrStatus/southKoreaMap.svg'); + const svgText = await response.text(); + + if (svgContainerRef.current) { + svgContainerRef.current.innerHTML = svgText; + + const svg = svgContainerRef.current.querySelector('svg') as SVGSVGElement; + if (svg) { + svg.style.width = '58%'; + svg.style.height = 'auto'; + svg.style.objectFit = 'contain'; + + // Set default fills + const paths = svg.querySelectorAll('path'); + paths.forEach(path => { + if (!path.getAttribute('fill') || path.getAttribute('fill') === 'none') { + path.setAttribute('fill', '#ffffff'); + } + }); + + // Create markers + regionData.forEach(region => { + createDonutMarker(svg, region.x, region.y, region.name, region.values); + }); + } + } + } catch (error) { + console.error('SVG 로딩 실패:', error); + if (svgContainerRef.current) { + svgContainerRef.current.innerHTML = "<p className='text-red-500'>SVG 로딩 실패</p>"; + } + } + }; + + loadSVG(); + }, []); + + useEffect(() => { + const svg = svgContainerRef.current?.querySelector('svg'); + if (svg) { + const paths = svg.querySelectorAll('path'); + paths.forEach(path => { + const pathId = path.getAttribute('id'); + if (pathId) { + if (selectedRegion === pathId) { + path.setAttribute('fill', '#2563eb'); + } else { + const original = originalFills[pathId] || '#ffffff'; + path.setAttribute('fill', original); + } + } + }); + } + }, [selectedRegion, originalFills]); + + return ( + <div className="flex h-screen bg-[#F4F7FB] text-gray-800"> + {/* 우측 콘텐츠 영역 */} + <main className="flex-1 p-6 overflow-auto"> + {/* 상단 제목 */} + <div className="mb-6 flex justify-between items-center"> + <h2 className="text-2xl font-bold">물재이용 현황</h2> + <div className="text-sm text-gray-500">관리자 ▼</div> + </div> + + {/* 콘텐츠 본문 */} + <div className="grid grid-cols-1 xl:grid-cols-2 gap-4"> + {/* 지도 카드 */} + <div className="bg-white rounded-xl shadow border border-[#DBE2E6] overflow-hidden max-h-[900px]"> + {/* 타이틀 영역 */} + <div className="border-b border-[#DBE2E6] px-4 py-3"> + <h3 className="text-[12px] font-semibold">재이용시설개소수(2023)</h3> + <p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설 개소수 조회</p> + </div> + + {/* 본문 콘텐츠 */} + <div className="p-4"> + <div className="bg-[#E2EDF3] p-4 rounded shadow relative"> + <div className="flex justify-center"> + <div + ref={svgContainerRef} + className="w-full max-w-5xl max-h-[680px] overflow-y-auto block mx-auto" + aria-label="대한민국 지역별 재이용시설 분포도" + role="region" + tabIndex={0} + /> + </div> + + {typeof window !== 'undefined' && hoveredRegion && tooltipPosition && ( + <div + className="fixed bg-white text-[12px] text-gray-700 border border-gray-300 shadow-lg rounded px-3 py-2 z-50" + style={{ + top: tooltipPosition.y + 10, + left: tooltipPosition.x + 10, + pointerEvents: 'none', + }} + > + <div className="font-semibold mb-1">{hoveredRegion}</div> + <div className="space-y-1"> + {colorLegend.map(({ label, color, key }) => ( + <div key={key} className="flex items-center space-x-2"> + <span className="text-[12px] inline-block w-3 h-3 rounded-sm" style={{ backgroundColor: color }}></span> + <span className="text-[12px]"> + {label}:{' '} + {tableData.find(d => d.region === hoveredRegion)?.[key as keyof TableData] ?? '-'} + </span> + </div> + ))} + </div> + </div> + )} + + {/* 범례 */} + <div className="absolute bottom-2 right-2 bg-white rounded-lg shadow-md p-4 text-sm space-y-2"> + <div className="flex items-center space-x-2"> + <span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#FF6B6B'}}></span> + <span className="text-[12px] text-gray-900">하수재이용시설 (총 717건)</span> + </div> + <div className="flex items-center space-x-2"> + <span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#4ECDC4'}}></span> + <span className="text-[12px] text-gray-900">빗물이용시설 (총 3,730건)</span> + </div> + <div className="flex items-center space-x-2"> + <span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#45B7D1'}}></span> + <span className="text-[12px] text-gray-900">중수도시설 (총 1,074건)</span> + </div> + <div className="flex items-center space-x-2"> + <span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#FED16A'}}></span> + <span className="text-[12px] text-gray-900">온배수시설 (총 201건)</span> + </div> + </div> + </div> + </div> + </div> + + {/* 우측 콘텐츠 (표 + 차트) */} + <div className="col-span-1 flex flex-col space-y-4"> + {/* 재이용시설현황 표 */} + <div className="bg-white rounded-xl shadow border border-[#DBE2E6] overflow-hidden"> + {/* 타이틀 + 버튼 영역 */} + <div className="flex justify-between items-center border-b border-[#DBE2E6] px-4 py-3"> + <div> + <h3 className="text-[12px] font-semibold">재이용시설현황(2023)</h3> + <p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설 현황 비교</p> + </div> + </div> + <div className="p-4"> + <div className="rounded border border-[#DBE2E6] shadow overflow-auto max-h-[292px] p-4"> + <table className="min-w-full text-sm text-center"> + <caption className="sr-only text-[12px]">행정구역별 하수, 빗물, 중수도, 온배수 재이용시설 수</caption> + <thead className="bg-gray-50 text-xs text-gray-600 uppercase border-b"> + <tr> + <th className="text-[12px] font-semibold px-4 py-2">번호</th> + <th className="text-[12px] font-semibold px-4 py-2">시도</th> + <th className="text-[12px] font-semibold px-4 py-2">하수</th> + <th className="text-[12px] font-semibold px-4 py-2">빗물</th> + <th className="text-[12px] font-semibold px-4 py-2">중수도</th> + <th className="text-[12px] font-semibold px-4 py-2">온배수</th> + </tr> + </thead> + <tbody className="divide-y"> + {tableData.map((row, index) => ( + <tr key={index} className="hover:bg-gray-50"> + <td className="text-[12px] px-4 py-2 text-center"> + {index+1} + </td> + <td className="text-[12px]">{row.region}</td> + <td className="text-[12px]">{row.sewage}</td> + <td className="text-[12px]">{row.rainwater}</td> + <td className="text-[12px]">{row.reclaimed}</td> + <td className="text-[12px]">{row.thermal}</td> + </tr> + ))} + </tbody> + </table> + </div> + </div> + </div> + + {/* 재이용통계 차트 */} + <div className="bg-white rounded-xl shadow border border-[#DBE2E6] overflow-hidden"> + <div className="border-b border-[#DBE2E6] px-4 py-3"> + <h3 className="text-[12px] font-semibold">재이용통계(2023)</h3> + <p className="text-[12px] text-[#999] font-medium">항목별 통계 보기</p> + </div> + <div className="p-4 bg-[#f9fbfd]"> + <div className="h-[305px] text-gray-500"> + <MainChart + chartForm={chartOption.chartForm} + chartConfig={chartOption.chartConfig} + chartData={chartOption.chartData} + chartTitle={chartOption.chartTitle} + chartClassName={chartOption.chartClassName} + graphSize={chartOption.graphSize} + yFieldText={chartOption.yFieldText} + showLegend={chartOption.showLegend} + /> + </div> + </div> + </div> + </div> + </div> + </main> + </div> + ); +}; + +export default WrStatusPage; \ No newline at end of file diff --git a/apps/dm/app/store/common/index.tsx b/apps/dm/app/store/common/index.tsx new file mode 100644 index 000000000..c62ebdd0a --- /dev/null +++ b/apps/dm/app/store/common/index.tsx @@ -0,0 +1,380 @@ +/**==================================== + * 보편적으로 쓰는 store 공간 + ====================================**/ + +import { fetchType, getFetch } from '@/components/ui/api'; + +/** + * STORE - TYPE + */ + +// selectBox +export type SelectDataType = { + text: string; + val: string; +} + +// grid header +export type TableUpperProps = { + id: string; + title: string; + upName?: string; + upSequnce?: number; + align? : string; + row? : number; + rowSpan?: number; + colSpan?: number; +} + +// 시도 데이터 +export type sidoDataType = { + adstDstrCd: number, + sido: string, +} + +// 시군구 데이터 +export type sigunDataType = { + adstDstrCd: number, + gugun: string +} + +// 권역 데이터 +export type regionDataType = { + rvbsnCd: string, + rvbsn: string, +} + +// 단위유역 데이터 +export type unitWaterShedDataType = { + rvbsnCd: string, + lgbsn: string, +} + +// 중권역 데이터 +export type middleRegionDataType = { + rvbsnCd: string, + mdmbsn: string, +} + +// 환경청 데이터 +export type envPartDataType = { + lclgvrnmntCd: string, + lclgvrnmntNm: string, + adstDliStrCd: string, + adstDstrCd: string, +} + +export type envPartSelectDataType = SelectDataType & { + adstDliStrCd: string, +} + +/** + * STORE - DATA + */ + +// 시도 데이터 +let selectSidoData: SelectDataType[] = [ + { text: "전체", val: '0' } +]; + +// 시군구 데이터 +let selectSigunData: SelectDataType[] = [ + { text: "전체", val: '0' } +]; + +// 권역 데이터 +const selectRegionData: SelectDataType[] = [ + { text: "전체", val: '0' } +]; + +// 단위유역 데이터 +const selectUnitWaterShedData: SelectDataType[] = [ +]; + +// 환경청 데이터 +let selectEnvPartData: envPartSelectDataType[] = [ + { text: "전체", val: '0', adstDliStrCd: '' } +]; + +// 환경청 내 시도 데이터 +const selectEnvPartSidoData: SelectDataType[] = [ + { text: "전체", val: '0' } +]; + +// 환경청 내 시군구 데이터 +const selectEnvPartSigunData: SelectDataType[] = [ + { text: "전체", val: '0' } +]; + +// 구분 데이터 +// const selectPartData:SelectDataType[] = [ +// { text: "행정별", val: '0' }, +// { text: "유역별", val: '01' }, +// { text: "환경청별", val: '02' }, +// ]; + +// 상향 하향 데이터 +const selectUpdownData:SelectDataType[] = [ + { text: "전체", val: '0' }, + { text: "500이하", val: '499' }, + { text: "500이상", val: '500' }, +]; + +// 운영 상태 데이터 +const selectOperationData:SelectDataType[] = [ + { text: "전체", val: '0' }, + { text: "가동중", val: '1' }, + { text: "재가동", val: '2' }, + { text: "가동중지", val: '3' }, + { text: "시설폐쇄", val: '4' }, +]; + +// 시설 구분 데이터 +const selectFacilityPartData:SelectDataType[] = [ + { text: "전체", val: '0' }, + { text: "공공하수처리시설", val: 'SW' }, + { text: "오수처리장", val: 'SS' }, + { text: "개인하수처리시설", val: 'PB' }, + { text: "분뇨처리장", val: 'NS' }, + { text: "폐수처리장", val: 'LW' }, + { text: "침출수처리장", val: 'LC' }, + { text: "기타기초시설", val: 'ET' }, +]; + +// 년도 데이터 +const getYearData = () => { + const year = new Date().getFullYear(); + const year1 = year - 1; + const year2 = year - 2; + const year3 = year - 3; + const year4 = year - 4; + const year5 = year - 5; + return [ + { text: year1 + "년", val: year1.toString() }, + { text: year2 + "년", val: year2.toString() }, + { text: year3 + "년", val: year3.toString() }, + { text: year4 + "년", val: year4.toString() }, + { text: year5 + "년", val: year5.toString() }, + ] +} + +const selectSearchYear:SelectDataType[] = getYearData(); + +// 유량 기간 구분 +const selectFlowTerm : SelectDataType[] = [ + { text: "연간", val: '0' }, + { text: "하절기", val: '01' }, + { text: "동절기", val: '02' }, + { text: "청천시", val: '03' }, + { text: "강우시", val: '04' }, +]; + +// 유량 기간 구분 +const selectFlowPanel : SelectDataType[] = [ + { text: "전체", val: '0'}, + { text: "3mm미만", val: '1'}, + { text: "3~10mm미만", val: '2'}, + { text: "10~20mm미만", val: '3'}, + { text: "20~30mm미만", val: '4'}, + { text: "30~40mm미만", val: '5'}, + { text: "40~50mm미만", val: '6'}, + { text: "50~60mm미만", val: '7'}, + { text: "60mm이상", val: '8'}, +]; + +// 중권역 데이터 +const selectMiddleRegionData: SelectDataType[] = [ + { text: "전체", val: '0' } +]; + +/** + * STORE - API + */ + +// 시도 API 호출 함수 추가 +const fetchSidoData = async (): Promise<SelectDataType[]> => { + try { + const res = await getFetch(fetchType.COMMON, '/addr/city'); + const data = res.data; + selectSidoData = [ + { text: "전체", val: '0'}, + ...data.map((item: sidoDataType) => ({ + text: item.sido || '', + val: String(item.adstDstrCd) || '' + })) + ]; + return selectSidoData; + } catch (error) { + console.error('시도 데이터를 가져오는데 실패했습니다:', error); + return selectSidoData; + } +}; + +// 시군구 API 호출 함수 추가 +const fetchSigunData = async (): Promise<SelectDataType[]> => { + try { + const res = await getFetch(fetchType.COMMON, '/addr/sgg'); + const data = res.data; + selectSigunData = [ + { text: "전체", val: '0' }, + ...data + .map((item: sigunDataType) => ({ + text: item.gugun || '', + val: String(item.adstDstrCd) || '' + })) + ]; + return selectSigunData; + } catch (error) { + console.error('시군구 데이터를 가져오는데 실패했습니다:', error); + return selectSigunData; + } +}; + +// 권역 API 호출 함수 추가 +const fetchRegionData = async (): Promise<SelectDataType[]> => { + try { + const res = await getFetch(fetchType.COMMON, '/addr/sarea'); + const data = res.data; + return [ + { text: "전체", val: '0'}, + ...data.map((item: regionDataType) => ({ + text: item.rvbsn || '', + val: String(item.rvbsnCd) || '' + })) + ]; + } catch (error) { + console.error('권역 데이터를 가져오는데 실패했습니다:', error); + return selectRegionData; + } +}; + +// 중권역 API 호출 함수 추가 +const fetchMiddleRegionData = async (): Promise<SelectDataType[]> => { + try { + const res = await getFetch(fetchType.COMMON, '/addr/midArea'); + const data = res.data; + return [ + { text: "전체", val: '0' }, + ...data + .map((item: middleRegionDataType) => ({ + text: item.mdmbsn || '', + val: String(item.rvbsnCd) || '' + })) + ]; + } catch (error) { + console.error('중권역 데이터를 가져오는데 실패했습니다:', error); + return selectMiddleRegionData; + } +}; + +// 단위유역 API 호출 함수 추가 +const fetchUnitWaterShedData = async (): Promise<SelectDataType[]> => { + try { + const res = await getFetch(fetchType.COMMON, '/addr/largeArea'); + const data = res.data; + return [ + { text: "전체", val: '0' }, + ...data + .map((item: unitWaterShedDataType) => ({ + text: item.lgbsn || '', + val: String(item.rvbsnCd) || '' + })) + ]; + } catch (error) { + console.error('단위유역 데이터를 가져오는데 실패했습니다:', error); + return selectUnitWaterShedData; + } +}; + +// 환경청 데이터 +const fetchEnvPartData = async (): Promise<envPartSelectDataType[]> => { + try { + const res = await getFetch(fetchType.COMMON, '/addr/govArea'); + const data = res.data; + selectEnvPartData = [ + { text: "전체", val: '0', adstDliStrCd: '' }, + ...data + .map((item: envPartDataType) => ({ + text: item.lclgvrnmntNm || '', + val: String(item.lclgvrnmntCd) || '', + adstDliStrCd: item.adstDliStrCd || '' + })) + ]; + return selectEnvPartData; + } catch (error) { + console.error('환경청 데이터를 가져오는데 실패했습니다:', error); + return selectEnvPartData; + } +}; + +/**==================================== + * Store - expaort + ====================================*/ +export type CommonAction = { + initializeCommonData: () => Promise<void>; +} + +export type CommonType = { + Common: { + isInit: boolean; + selectSidoData: SelectDataType[]; + selectSigunData: SelectDataType[]; + selectUpdownData: SelectDataType[]; + selectOperationData: SelectDataType[]; + selectFacilityPartData: SelectDataType[]; + selectSearchYear: SelectDataType[]; + selectUnitWaterShedData: SelectDataType[]; + selectRegionData: SelectDataType[]; + selectMiddleRegionData: SelectDataType[]; + selectEnvPartSidoData: SelectDataType[]; + selectEnvPartSigunData: SelectDataType[]; + selectFlowTerm: SelectDataType[]; + selectFlowPanel: SelectDataType[]; + selectEnvPartData: envPartSelectDataType[]; + btnClass: string; + } +} + +export const CommonState:CommonType = { + Common: { + isInit: false, + selectSidoData: selectSidoData, + selectSigunData: selectSigunData, + selectUpdownData: selectUpdownData, + selectOperationData: selectOperationData, + selectFacilityPartData: selectFacilityPartData, + selectSearchYear: selectSearchYear, + selectRegionData: selectRegionData, + selectUnitWaterShedData: selectUnitWaterShedData, + selectMiddleRegionData: selectMiddleRegionData, + selectEnvPartSidoData : selectEnvPartSidoData, + selectEnvPartSigunData : selectEnvPartSigunData, + selectFlowTerm: selectFlowTerm, + selectFlowPanel: selectFlowPanel, + selectEnvPartData : selectEnvPartData, + btnClass: "p-4 m-2 text-white rounded-sm text-lg font-medium" + } +} + +export const CommonReducer:(set:any)=>CommonAction=(set: any) => { + return { + // 시도 변경 이벤트 + initializeCommonData: async () => { + const promiseSet = await Promise.all([fetchSidoData(), fetchSigunData(), fetchRegionData(), fetchUnitWaterShedData(), fetchMiddleRegionData(), fetchEnvPartData()]); + const [sidoData, sggData, regionData, unitData, midData, envData] = promiseSet; + set((state: CommonType) => ({ + Common: { + ...state.Common, + selectSidoData: sidoData, + selectSigunData: sggData, + selectRegionData: regionData, + selectMiddleRegionData: midData, + selectUnitWaterShedData: unitData, + selectEnvPartData: envData + } + }) + )}, + } +} + +export type CommonStore = CommonType & CommonAction; diff --git a/apps/dm/app/store/index.tsx b/apps/dm/app/store/index.tsx new file mode 100644 index 000000000..47170b3db --- /dev/null +++ b/apps/dm/app/store/index.tsx @@ -0,0 +1,42 @@ +'use client' + +import { type ReactNode, createContext, useContext, useRef } from 'react' +import { useStore } from 'zustand' +import { type DmStore, StoreZus } from './store' + + +export type StoreApi = ReturnType<typeof StoreZus> + +export const StoreContext = createContext<StoreApi | undefined>( + undefined, +) + +export interface StoreProviderProps { + children: ReactNode +} + +export const StoreProvider = ({ + children, +}: StoreProviderProps) => { + const storeRef = useRef<StoreApi | null>(null) + if (!storeRef.current) { + // Create store instance if not initialized + storeRef.current = StoreZus() as StoreApi + } + return ( + <StoreContext.Provider value={storeRef.current}> + {children} + </StoreContext.Provider> + ) +} + +export const useDmStore = <T,>( + selector: (store: DmStore) => T, +): T => { + const storeContext = useContext(StoreContext) + if (!storeContext) { + throw new Error(`useStore must be used within StoreProvider`) + } + + return useStore(storeContext, selector) +} \ No newline at end of file diff --git a/apps/dm/app/store/readme.md b/apps/dm/app/store/readme.md new file mode 100644 index 000000000..0710aa69b --- /dev/null +++ b/apps/dm/app/store/readme.md @@ -0,0 +1,121 @@ +# Zustand store +## Zustand 등록방법 +**-- index.tsx --** +### context 등록 +기본적으로 createContext를 통해 react에 context를 생성해서 react에서 사용할 수 있도록 해야함. +``` js +//StoreZus는 내부적으로 사용하기 위한 store파일에서 따로 생성함. +export type StoreApi = ReturnType<typeof StoreZus> +//react에서 사용하기 위한 context 생성. +export const StoreContext = createContext<StoreApi | undefined>( + undefined, +) +``` + +### component로 작성 +등록된 context를 component화 시키기 위한 작업 +```js +//외부에서 호출시 <StoreProvider> 형태로 호출. +//app이나 home에서 전체적인 사용이 되도록 작성해야함. +export const StoreProvider = ({ + children, +}: StoreProviderProps) => { + const storeRef = useRef<StoreApi>(null) + if (!storeRef.current) { + //StoreZus는 store.tsx를 봐야함. + storeRef.current = StoreZus() + } + return ( + <StoreContext.Provider value={storeRef.current}> + {children} + </StoreContext.Provider> + ) +} +``` + +### store 등록 +react에서 사용하는 store에 현재 context 등록 +```js +export const useWrStore = <T,>( + selector: (store: WrStore) => T, +): T => { + const storeContext = useContext(StoreContext) + if (!storeContext) { + throw new Error(`useStore must be used within StoreProvider`) + } + + return useStore(storeContext, selector) +} +``` + +## Zustand 사용방법 +**-- store.tsx, 폴더별 index.tsx --** +### 기본 store 생성 +index.tsx의 context에 사용되는 StoreZus를 추가하여 사용됨. + +외부에서 생성된 state, actions를 사용 가능한 구조 +```js +import { createStore } from 'zustand/vanilla'; +import { tableActionsExport, tableStateExport, type tableState, type tableType } from './publicReuseFaciltyInsert/index';//외부 파일 위치. +export type WrStore = tableType;//typescript이므로 사용되는 스크립트를 type1 & type2 형식으로 넣어야함. +//index.tsx에서 사용하는 store 생성. +export const StoreZus = (initState: tableState = tableStateExport) => { + return createStore<WrStore>()((set) => { + return { + //아래에서 사용하는 state & actions은 외부 폴더에서 가져다가 사용. + ...initState,//외부에 있는 기본 초기값 state + ...tableActionsExport(set)//외부에 있는 actions + } + }) +} +``` +### 외부 스토어 작성 방법 +외부 스토어는 export를 어떻게 할건지만 정하면됨. + +사용하는 store.tsx에서 import를 받기만 하면 사용가능. +#### 타입을 외부로 빼는 방법 +```js +export type tableState = { + isInit: boolean; + upHeadList: TableUpperProps[]; + publicReuseFacilityUper: PublicReuseFacility[]; +} +type tableActions = { + decrementList: () => void +} + +export type tableType = tableState & tableActions; +``` +#### 초기값 state를 외부로 빼는 방법 +```js +export const tableStateExport:tableState= { + isInit:false, + upHeadList: upHeadList, + publicReuseFacilityUper: publicReuseFacilityUper, +} +``` +#### actions를 외부로 빼는 방법 +```js +//tableActionsExport를 import 받아서 실행하여 return 된 값을 사용 +export const tableActionsExport:(set:any)=>tableActions= +//set을 통해 react store에 등록 +(set: any) => { + return { + //decrementList Object 형태로 전달 + decrementList: + //set 함수를 실행해서 react store에 등록 + ()=>set( + // store에 등록되는 함수 구조를 가지고 있음. + (state:tableState) => { + //actions 실질적인 로직 작성하는 곳. + return ( + { + upHeadList: state.upHeadList, + publicReuseFacilityUper: + state.publicReuseFacilityUper.slice(0, state.publicReuseFacilityUper.length - 1) + })} + ) + } +} +``` + diff --git a/apps/dm/app/store/sdmMng/index.tsx b/apps/dm/app/store/sdmMng/index.tsx new file mode 100644 index 000000000..a2475970b --- /dev/null +++ b/apps/dm/app/store/sdmMng/index.tsx @@ -0,0 +1,528 @@ +import { fetchType, getFetch, postFetch } from "@/components/ui/api"; +import type { SelectDataType } from "@common/business_components/ui"; +import type { TableUpperProps } from "../common"; +import toast from "react-hot-toast"; + +/**==================================== + * Store - 타입 +=====================================*/ +// 수요처 정보 +export type DemandCompType = { + index : number | ''; + dmdInfoId : string | ''; + dmdInfoFk : string | ''; + bzmnNo : string | ''; + dmdCoNm : string | ''; + daddr : string | ''; + telno : string | ''; + rgtr : string | ''; + useUsgCd : string | ''; + ctpvCd : string | ''; + sggCd : string | ''; + dmdAplyPswd : string | ''; + wntUntprc : number | ''; + dmndTmng : Date | ''; + dmndQty : number | ''; + grssClfg : number | ''; + tn : number | ''; + combResdch : number | ''; + tph : number | ''; + trbt : number | ''; + ph : number | ''; + blgcOxygDmndQnt : number | ''; + clIonQnt : number | ''; + smll : number | ''; + cdtnv : number | ''; + crmty : number | ''; + rmrk : string | ''; + reqDt : string | ''; + dmdInfoSn : number | ''; + status : string | ''; +} + +export type DemandMatchings = { + splyFcltFk : string | ''; + splyFcltFlg : string | ''; + pbnstRuseFcltBizFk : number | ''; + splyBgngDay : string | ''; + splyEndDay : string | ''; + splyCpct : number | ''; + splyFmYn : string | ''; + useUsgCd : string | ''; + dmdInfoSn : number | ''; + splyCndtList : string | ''; + mtchnCmptnYmd : string | ''; + rvwCmptnYmd : string | ''; + regDt : string | ''; + rgtr : string | ''; + mdfcnDt : string | ''; + mdfr : string | ''; +} + +// 유사사례 +export type SimilarCaseType = { + ruseBizNm : string | ''; + bizBgngDate : string | ''; + bizEndDate : string | ''; + splyFcltKnd : string | ''; + splyFcltNm : string | ''; + splyFcltCpct : string | ''; + dmdFcltNm : string | ''; + dmdFcltAddr : string | ''; + ruseFcltCpct : string | ''; + ruseFcltPplLen : string | ''; + ruseFcltUsg : string | ''; + ruseFcltSplyCpct : string | ''; + ruseFcltPrcsMsrc : string | ''; + grnteWqUsg : string | ''; + grnteWqGrssClfg : string | ''; + grnteWqCombResdch : string | ''; + grnteWqTrbt : string | ''; + grnteWqBod : string | ''; + grnteWqSmll : string | ''; + grnteWqCrmty : string | ''; + grnteWqTn : string | ''; + grnteWqTp : string | ''; + grnteWqPh : string | ''; + grnteWqClIonQnt : string | ''; + grnteWqCndctStat : string | ''; + grnteWqRmrk : string | ''; + reqBizReuseFcltCst : string | ''; + reqBizPplCst : string | ''; + reqBizArchCst : string | ''; + reqBizMchnCst : string | ''; + reqBizMtrngCst : string | ''; + reqBizGisCst : string | ''; + reqBizCctvCst : string | ''; + reqBizCmcnCst : string | ''; + reqBizWstCst : string | ''; + reqBizDesignCst : string | ''; + reqBizSpvsCst : string | ''; + reqBizSbsdCst : string | ''; + reqBizEtc : string | ''; + reqBizGrssCst : string | ''; + prcmPlanNlep : string | ''; + prcmPlanLcep : string | ''; + prcmPlanBrdnCst : string | ''; + prcmPlanEcqrRt : string | ''; + prcmPlanGrssCst : string | ''; + clclnNlep : string | ''; + clclnLcep : string | ''; + clclnBrdnCst : string | ''; + clclnEcqrRt : string | ''; + clclnGrssCst : string | ''; +} + +// 사업비 상세정보 +export type MoneyDetailType = { + reqBizGrssCst : string | ''; + reqBizReuseFcltCst : string | ''; + reqBizPplCst : string | ''; + reqBizArchCst : string | ''; + reqBizMchnCst : string | ''; + reqBizMtrngCst : string | ''; + reqBizGisCst : string | ''; + reqBizCctvCst : string | ''; + reqBizCmcnCst : string | ''; + reqBizWstCst : string | ''; + reqBizDesignCst : string | ''; + reqBizSpvsCst : string | ''; + reqBizSbsdCst : string | ''; + reqBizEtc : string | ''; + + /** 재원조달계획(백만원원) */ + prcmPlanGrssCst : string | ''; + prcmPlanNlep : string | ''; + prcmPlanLcep : string | ''; + prcmPlanBrdnCst : string | ''; + prcmPlanEcqrRt : string | ''; + /** 사업비 정산(백만원) */ + clclnGrssCst : string | ''; + clclnNlep : string | ''; + clclnLcep : string | ''; + clclnBrdnCst : string | ''; + clclnEcqrRt : string | ''; +} + +/**==================================== + * Store - 데이터 + ====================================*/ +// 수요처 정보 +const demandCompList : DemandCompType[] = [ +] + +// 사용용도 +const useTypesData : SelectDataType[] = [ + { text: '청소', val: "청소" }, + { text: "화장실용수", val: "화장실용수" }, + { text: "세척", val: "세척" }, + { text: "살수", val: "살수" }, + { text: "조경", val: "조경" }, + { text: "농업", val: "농업" }, + { text: "하천", val: "하천" }, + { text: "지하수", val: "지하수" }, + { text: "공업", val: "공업" }, +]; + +// 유사사례 모달 헤더 +export const similarCaseModalHeader: TableUpperProps[] = [ + { id: "ruseBizNm", title: "사업명", row: 0, rowSpan: 2 }, + { id: "bizBgngDate", title: "사업시작일", row: 0, rowSpan: 2 }, + { id: "bizEndDate", title: "사업종료일", row: 0, rowSpan: 2 }, + { id: "A", title: "공급처", row: 0, colSpan: 3 }, + { id: "splyFcltKnd", title: "시설종류", row: 1 }, + { id: "splyFcltNm", title: "공급처명", row: 1 }, + { id: "splyFcltCpct", title: "시설용량\n(㎥/일)", row: 1 }, + { id: "B", title: "수요처", row: 0, colSpan: 2 }, + { id: "dmdFcltNm", title: "수요처명", row: 1 }, + { id: "dmdFcltAddr", title: "주소", row: 1 }, + { id: "C", title: "재이용시설", row: 0, colSpan: 5 }, + { id: "ruseFcltCpct", title: "시설용량\n(㎥/일)", row: 1 }, + { id: "ruseFcltPplLen", title: "관로\n(km)", row: 1 }, + { id: "ruseFcltUsg", title: "용도", row: 1 }, + { id: "ruseFcltSplyCpct", title: "공급량\n(㎥/일)", row: 1 }, + { id: "ruseFcltPrcsMsrc", title: "처리공법", row: 1 }, + { id: "D", title: "보증수질", row: 0, colSpan: 13 }, + { id: "grnteWqUsg", title: "용도별", row: 1 }, + { id: "grnteWqGrssClfg", title: "총대장균군수\n(개/100ml)", row: 1 }, + { id: "grnteWqCombResdch", title: "결합잔류염소\n(mg/L)", row: 1 }, + { id: "grnteWqTrbt", title: "탁도\n(NTU)", row: 1 }, + { id: "grnteWqBod", title: "생물학적산소요구량\n(BOD)\n(mg/L)", row: 1 }, + { id: "grnteWqSmll", title: "냄새\n(개/100ml)", row: 1 }, + { id: "grnteWqCrmty", title: "색도\n(도)", row: 1 }, + { id: "grnteWqTn", title: "총질소\n(T-N)\n(mg/L)", row: 1 }, + { id: "grnteWqTp", title: "총인\n(T-P)\n(mg/L)", row: 1 }, + { id: "grnteWqPh", title: "수소이온농도\n(pH)", row: 1 }, + { id: "grnteWqClIonQnt", title: "염화물\n(mgCL/L)", row: 1 }, + { id: "grnteWqCndctStat", title: "전기전도도\n(μS/cm)", row: 1 }, + { id: "grnteWqRmrk", title: "비고", row: 1 }, + { id: "reqBizGrssCst", title: "소요사업비\n(백만원)", row: 0, rowSpan: 2 }, +] + +// 유사사례 데이터 +const similarCaseList : SimilarCaseType[] = [ + +] + +// 사업비 상세정보 모달 헤더 +export const moneyDetailModalHeader: TableUpperProps[] = [ + { id: "A", title: "소요사업비(백만원)", row: 0, colSpan: 14 }, + { id: "reqBizGrssCst", title: "총", row: 1, rowSpan: 2 }, + { id: "B", title: "토목", row: 1, colSpan: 2 }, + { id: "reqBizReuseFcltCst", title: "재이용시설", row: 2 }, + { id: "reqBizPplCst", title: "관로", row: 2 }, + { id: "reqBizArchCst", title: "건축", row: 1, rowSpan: 2 }, + { id: "reqBizMchnCst", title: "기계", row: 1, rowSpan: 2 }, + { id: "reqBizMtrngCst", title: "전기,계측", row: 1, rowSpan: 2 }, + { id: "reqBizGisCst", title: "GIS/DB", row: 1, rowSpan: 2 }, + { id: "reqBizCctvCst", title: "CCTV", row: 1, rowSpan: 2 }, + { id: "reqBizCmcnCst", title: "시공 및 준공", row: 1, rowSpan: 2 }, + { id: "reqBizWstCst", title: "폐기물", row: 1, rowSpan: 2 }, + { id: "reqBizDesignCst", title: "설계비", row: 1, rowSpan: 2 }, + { id: "reqBizSpvsCst", title: "감리비", row: 1, rowSpan: 2 }, + { id: "reqBizSbsdCst", title: "시설 부대비", row: 1, rowSpan: 2 }, + { id: "reqBizEtc", title: "기타", row: 1, rowSpan: 2 }, + { id: "C", title: "재원조달계획(백만원)", row: 0, colSpan: 5 }, + { id: "prcmPlanGrssCst", title: "총", row: 1, rowSpan: 2 }, + { id: "prcmPlanNlep", title: "국비", row: 1, rowSpan: 2 }, + { id: "prcmPlanLcep", title: "지방비", row: 1, rowSpan: 2 }, + { id: "prcmPlanBrdnCst", title: "원인자부담", row: 1, rowSpan: 2 }, + { id: "prcmPlanEcqrRt", title: "국고비율", row: 1, rowSpan: 2 }, + { id: "D", title: "사업비 정산(백만원)", row: 0, colSpan: 5 }, + { id: "clclnGrssCst", title: "총", row: 1, rowSpan: 2 }, + { id: "clclnNlep", title: "국비", row: 1, rowSpan: 2 }, + { id: "clclnLcep", title: "지방비", row: 1, rowSpan: 2 }, + { id: "clclnBrdnCst", title: "원인자부담", row: 1, rowSpan: 2 }, + { id: "clclnEcqrRt", title: "국고비율", row: 1, rowSpan: 2 }, +] + +// 사업비 상세정보 +const moneyDetailList : MoneyDetailType[] = [ +] + +// 수요정보 신청현황 헤더 +export const upHeadSdmInfoStatus: TableUpperProps[] = [ + { id: "index", title: "No."}, + { id: "ctpvCd", title: "시도", row: 0, rowSpan: 2 }, + { id: "sggCd", title: "시군구", row: 0, rowSpan: 2 }, + { id: "dmdCoNm", title: "회사명", row: 0, rowSpan: 2 }, + { id: "daddr", title: "공장주소", row: 0, rowSpan: 2 }, + { id: "telno", title: "전화번호", row: 0, rowSpan: 2 }, + { id: "dmndTmng", title: "신청일자", row: 0, rowSpan: 2 }, + { id: "status", title: "진행상태", row: 0, rowSpan: 2 }, +] + +// 수요정보 신청현황 데이터 +const sdmInfoStatusList : DemandCompType[] = [ +] + +/**==================================== + * Store - Api +=====================================*/ +// 수요처 등록 +const postSdmData = async (data: DemandCompType) => { + try { + await postFetch("/reuseFc/request", data); + toast.success("수요처 등록에 성공하였습니다."); + return true; + } catch (error) { + console.log(error); + toast.error("수요처 등록에 실패하였습니다."); + return false; + } +} + +// 수요정보 등록 +const postSdmInfoData = async (data: DemandCompType) => { + try { + await postFetch("/reuseFc/request/demandInfo", data); + toast.success('수요 정보 등록에 성공하였습니다.'); + } catch (error) { + console.log(error); + toast.error('수요 정보 등록에 실패하였습니다.'); + } +} + +const postSdmLoginData = async (data: DemandCompType) => { + try { + const res = await postFetch("/reuseFc/request/login", data); + const jsonData = await res.json(); + + const status = jsonData.status; + const loginData = jsonData.data; + let loginSuccess = false; + if(status === 200 && loginData != null) { + alert("수요처 로그인에 성공하였습니다."); + sessionStorage.setItem("user", JSON.stringify(loginData)); + loginSuccess = true; + } else if(status === 401) { + alert("패스워드가 틀렸습니다."); + loginSuccess = false; + } else { + alert("등록된 수요처가 없습니다."); + loginSuccess = false; + } + + return loginSuccess; + } catch (error) { + console.log(error); + alert("수요처 로그인에 실패하였습니다."); + } +} + +const getDemandCompList = async (searchText: string, pageCnt: number) => { + try { + const url = '/reuseFc/request/' + (searchText ? searchText : '') + '/' + pageCnt; + const res = await getFetch(fetchType.WR, url); + return { + data: res?.data || [], + count: res?.count || 0 + } + } catch (error) { + console.error(error+' 수요처 정보를 불러오지 못했습니다.'); + return { data: [], count: 0 }; + } +} + +const getSdmInfoStatusList = async (dmdInfoId : string) => { + try { + const url = '/reuseFc/request/' + dmdInfoId; + const res = await getFetch(fetchType.WR, url); + return res?.data || []; + } catch (error) { + console.error(error+' 수요정보 신청현황을 불러오지 못했습니다.'); + return []; + } +} + +// 유사사례 조회 +const fetchGetSimilarCaseList = async (part: string, value: number|string) => { + try { + const url = '/reuseFc/pubFcRuseCase/dm/' + part + '/' + (value ? value : 0); + const res = await getFetch(fetchType.WR, url); + return res?.data || []; + } catch (error) { + console.error(error+' 유사사례 조회에 실패하였습니다.'); + return []; + } +} + +/**==================================== + * Store - expaort + ====================================*/ +export type SdmActions = { + initializeSdmData: () => void; + insertSdmData: (data: DemandCompType) => Promise<void>; + insertSdmInfoData: (data: DemandCompType) => Promise<void>; + getDemandCompList: (searchText: string, pageCnt: number) => Promise<void>; + actionLogin: (data: DemandCompType) => Promise<void>; + getSdmInfoStatusList: (dmdInfoId: string) => Promise<void>; + getSimilarCaseList: (part: string, value: number|string) => Promise<void>; +} + +export type SdmType ={ + Sdm :{ + isInit: boolean; + useTypesData: SelectDataType[]; + demandCompList: DemandCompType[]; + upHeadSimilarCaseList: TableUpperProps[]; + similarCaseList: SimilarCaseType[]; + upHeadMoneyDetailList : TableUpperProps[]; + moneyDetailList: MoneyDetailType[]; + + // 수요정보 신청현황 + sdmInfoStatusList: DemandCompType[]; + upHeadSdmInfoStatus: TableUpperProps[]; + + // 로그인관련 + successNextPage : boolean; + successLogin: boolean; + } +} + +export const SdmState: SdmType = { + Sdm:{ + isInit:false, + useTypesData: useTypesData, + demandCompList: demandCompList, + upHeadSimilarCaseList: similarCaseModalHeader, + similarCaseList: similarCaseList, + upHeadMoneyDetailList : moneyDetailModalHeader, + moneyDetailList: moneyDetailList, + + // 수요정보 신청현황 + sdmInfoStatusList: [], + upHeadSdmInfoStatus: upHeadSdmInfoStatus, + + // 로그인관련 + successNextPage : false, + successLogin: false, + } +} + +export const SdmReducer = (set: any): SdmActions => { + return { + initializeSdmData: async () => { + }, + + insertSdmData: async (data: DemandCompType) => { + const preRes = await getSdmInfoStatusList(data.dmdInfoId); + if(preRes?.dmdInfoId) { + toast.error('이미 등록된 수요처입니다.'); + return; + } + + const res = await postSdmData(data); + if(res) { + set((state: SdmType) => ({ + Sdm: { + ...state.Sdm, + successNextPage: true, + } + })) + } + }, + + insertSdmInfoData: async (data: DemandCompType) => { + postSdmInfoData(data); + }, + + // 수요처 정보 + getDemandCompList: async (searchText: string, pageCnt: number) => { + const res = await getDemandCompList(searchText, pageCnt); + if(res.data.length === 0) { + set((state: SdmType) => ({ + Sdm: { + ...state.Sdm, + demandCompList: [], + } + })) + return; + } else { + set((state: SdmType) => ({ + Sdm: { + ...state.Sdm, + demandCompList: res.data, + } + })) + } + }, + + // 수요정보 신청현황 + getSdmInfoStatusList: async (dmdInfoId: string) => { + const res = await getSdmInfoStatusList(dmdInfoId); + if(res.length === 0) { + set((state: SdmType) => ({ + Sdm: { + ...state.Sdm, + sdmInfoStatusList: [], + } + })) + return; + } + const sdmInfoStatusList : DemandCompType[] = []; + + res.demandInfo.map((el: DemandCompType, index: number) => { + const matchingInfo = res.demandMatchings.find((matching: DemandMatchings) => matching.dmdInfoSn === el.dmdInfoSn); + const status = matchingInfo?.splyFcltFlg?.trim() === 'Sub' ? '검토' : matchingInfo?.splyFcltFlg?.trim() === 'matched' ? '매칭완료' : '신청'; + + const tmpData = { + ...el, + index: ++index, + bzmnNo: res.bzmnNo, + dmdCoNm: res.dmdCoNm, + dmdInfoId: res.dmdInfoId, + daddr: res.daddr, + telno: res.telno.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3'), + rgtr: res.rgtr, + ctpvCd: res.ctpvCd, + sggCd: res.sggCd, + useUsgCd: el.useUsgCd, + dmndTmng: el.dmndTmng, + dmdInfoSn : el.dmdInfoSn, + status : status + } + sdmInfoStatusList.push(tmpData); + }) + set((state: SdmType) => ({ + Sdm: { + ...state.Sdm, + sdmInfoStatusList: sdmInfoStatusList, + } + })) + }, + + actionLogin: async (data: DemandCompType) => { + const loginSuccess = await postSdmLoginData(data); + set((state: SdmType) => ({ + Sdm: { + ...state.Sdm, + successLogin: loginSuccess, + } + })) + }, + + // 유사사례 조회 + getSimilarCaseList: async (part: string, value: number|string) => { + const res = await fetchGetSimilarCaseList(part, value); + if(res && res.length !== 0) { + set((state: SdmType) => ({ + Sdm: { + ...state.Sdm, + similarCaseList: res, + } + })) + return; + }else{ + set((state: SdmType) => ({ + Sdm: { + ...state.Sdm, + similarCaseList: [], + } + })) + return; + } + } + } +} +export type SdmStore = SdmType & SdmActions; diff --git a/apps/dm/app/store/store.tsx b/apps/dm/app/store/store.tsx new file mode 100644 index 000000000..66b626aa3 --- /dev/null +++ b/apps/dm/app/store/store.tsx @@ -0,0 +1,19 @@ +import { createStore } from 'zustand/vanilla'; +import { CommonReducer, CommonState, type CommonStore, type CommonType } from './common'; +import { SdmReducer, SdmState, type SdmStore, type SdmType } from './sdmMng'; + +export type DmStore = CommonStore & SdmStore; +export type DmState = CommonType & SdmType; + +export const StoreZus = (initState: DmState= { + ...CommonState, + ...SdmState, +}) => { + return createStore<DmStore>()((set) => { + return { + ...initState, + ...CommonReducer(set), + ...SdmReducer(set) + } + }) +} \ No newline at end of file diff --git a/apps/dm/app/style/PublicReuseFaciltyInsert.module.css b/apps/dm/app/style/PublicReuseFaciltyInsert.module.css new file mode 100644 index 000000000..6fc6f75ee --- /dev/null +++ b/apps/dm/app/style/PublicReuseFaciltyInsert.module.css @@ -0,0 +1,9 @@ +.formInsideTitle{ + background-color: gray; + text-align: center; +} + + + +.smallTitle{ +} \ No newline at end of file diff --git a/apps/dm/components.json b/apps/dm/components.json new file mode 100644 index 000000000..d9ef0ae53 --- /dev/null +++ b/apps/dm/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/apps/dm/components/toastFromUi.tsx b/apps/dm/components/toastFromUi.tsx new file mode 100644 index 000000000..24b0b985f --- /dev/null +++ b/apps/dm/components/toastFromUi.tsx @@ -0,0 +1,53 @@ + +import { Button } from "@common/components/ui"; +import Link from "next/link"; +import toast from "react-hot-toast"; + +export function ToastFormUi(body:string,title:string,url:string,buttonName:string){ + + //const router = useRouter(); + + return( + + toast.custom((t) => ( + + <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`} + > + <div className="w-full flex-1 py-10"> + <div className="flex items-start"> + <div className="flex-shrink-0 pt-0.5"> + + </div> + <div className="ml-3 flex-1"> + <p className="text-xl font-medium underline"> + {title} + </p> + <p className="mt-1 text-2xl"> + {body} + </p> + </div> + </div> + </div> + <div className="flex py-20"> + <Button + size='sm' + onClick={() => toast.dismiss(t.id)} + > + <Link href={url}> {buttonName}</Link> + </Button> + </div> + <div className="flex pl-4 pr-4 py-20"> + <Button + onClick={() => toast.dismiss(t.id)} + size='sm' + > + 닫기 + </Button> + </div> + </div> + )) + ) +} \ No newline at end of file diff --git a/apps/dm/components/ui/api/index.ts b/apps/dm/components/ui/api/index.ts new file mode 100644 index 000000000..51d06e700 --- /dev/null +++ b/apps/dm/components/ui/api/index.ts @@ -0,0 +1,72 @@ +//8880 +import { DEV_URL_FO, DEV_URL_WR } from "../config"; + +export const fetchType = { + COMMON: "common", + WR: "wr" +}; + +interface RequestParams { + [key: string]: unknown; +} + +export const getFetch = async (type: string, url: string) => { + const baseUrl = type === fetchType.COMMON ? DEV_URL_FO : DEV_URL_WR; + const response = await fetch(baseUrl + url, { + method: "GET", + headers: { + accept: "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`오류 발생: ${response.status}`); + } + const res = await response.json(); + return res; +}; + +export const postFetch = async (url: string, params: RequestParams) => { + const response = await fetch(DEV_URL_WR + url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(params), + }); + + if (!response.ok) { + throw new Error(`오류 발생: ${response.status}`); + } + return response; +}; + +export const putFetch = async (url: string, params: RequestParams) => { + const response = await fetch(DEV_URL_WR + url, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(params), + }); + + if (!response.ok) { + throw new Error(`오류 발생: ${response.status}`); + } + return response; +}; + +export const deleteFetch = async (url: string, params: RequestParams) => { + const response = await fetch(DEV_URL_WR + url, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(params), + }); + + if (!response.ok) { + throw new Error(`오류 발생: ${response.status}`); + } + return response; +}; \ No newline at end of file diff --git a/apps/dm/components/ui/common/searhDemandInput/component.tsx b/apps/dm/components/ui/common/searhDemandInput/component.tsx new file mode 100644 index 000000000..5598129a7 --- /dev/null +++ b/apps/dm/components/ui/common/searhDemandInput/component.tsx @@ -0,0 +1,103 @@ +import { useDmStore } from '@/app/store'; +import type { DemandCompType } from '@/app/store/sdmMng'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; + +type thisProps = { + label: string | undefined; + initValue: string | undefined; + setValue : (dmdCoNm?: string) => void; + setRtnData : (rtnData: DemandCompType[]) => void; +} + +export default function SearchInputText({label=undefined, initValue=undefined, setValue=()=>{}, setRtnData=()=>{}} :thisProps) { + + const { getDemandCompList, Sdm } = useDmStore((state) => state); + + const { + demandCompList + } = Sdm; + + const [isOpen, setIsOpen] = useState(false); + const [textValue, setTextValue] = useState(initValue || ''); + const wrapperRef = useRef<HTMLDivElement>(null); + const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { + const value = e.target.value; + setTextValue(value); + setValue(value); + if (value) { + setIsOpen(true); + getDemandCompList(value, 20); + } else { + setIsOpen(false); + } + }, [getDemandCompList, setValue]); + + const handleSuggestionClick = useCallback((dmdCoNm: string) => { + setTextValue(dmdCoNm); + setValue(dmdCoNm); + setIsOpen(false); + setRtnData(demandCompList); + }, [demandCompList, setRtnData, setValue]); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + return ( + <div ref={wrapperRef}> + <input + type="text" + 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" + placeholder={`${label}을(를) 입력하세요`} + /> + {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="max-h-72"> + {demandCompList && demandCompList.length > 0 ? demandCompList.map((item : DemandCompType, index : number) => ( + <div + key={index} + className="!text-xl w-full text-left px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto" + onClick={() => handleSuggestionClick(item.dmdCoNm)} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + handleSuggestionClick(item.dmdCoNm); + } + }} + > + {item.dmdInfoId.substring(10,14) !== '0000' ? ( + <> + <span className="text-gray-500">사업자번호 : </span> {item.dmdInfoId.substring(0,10)}  + <span className="text-gray-500">종사업자번호 : </span> {item.dmdInfoId.substring(10,14)}  + </> + ) : ( + <> + <span className="text-gray-500">사업자번호 : </span> {item.dmdInfoId.substring(0,10)}  + </> + )} + <span className="float-right">{item.dmdCoNm}</span> + </div> + )) : ( + <div className="!text-xl w-full px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto"> + 검색 결과가 없습니다. + </div> + )} + </div> + </div> + )} + </div> + ) +} \ No newline at end of file diff --git a/apps/dm/components/ui/config/index.ts b/apps/dm/components/ui/config/index.ts new file mode 100644 index 000000000..8bf23935c --- /dev/null +++ b/apps/dm/components/ui/config/index.ts @@ -0,0 +1,13 @@ +const LOCAL_PORT = "3000"; +const DEV_PORT = "11080"; +const DEV_PORT_FO = "11082"; + +export const LOCAL_URL_WR = `http://localhost:${LOCAL_PORT}/api/v1`; +export const DEV_URL_WR = `http://con.mind-one.co.kr:${DEV_PORT}/api/v1`; + +//export const DEV_URL_FO = `http://con.mind-one.co.kr:${DEV_PORT_FO}/api/v1`; +export const LOCAL_URL_FO = `http://192.168.1.129:${DEV_PORT_FO}/api/v1`; +export const DEV_URL_FO = `http://con.mind-one.co.kr:${DEV_PORT_FO}/api/v1`; + +export const CS_URL= 'http://dev.onasys.co.kr:8000' +export const CS_TUS_UPLOAD= "/attach-service/api/v1/tus/upload"; \ No newline at end of file diff --git a/apps/dm/components/ui/sdmInfoInsertForm/AccessibleFormField.tsx b/apps/dm/components/ui/sdmInfoInsertForm/AccessibleFormField.tsx new file mode 100644 index 000000000..cfe985396 --- /dev/null +++ b/apps/dm/components/ui/sdmInfoInsertForm/AccessibleFormField.tsx @@ -0,0 +1,41 @@ +import * as React from "react"; +import type { ReactNode } from "react"; +import type { ControllerRenderProps, FieldError, FieldValues } from "react-hook-form"; +import { FormUi } from "@common/business_components/ui"; + + +interface AccessibleFormFieldProps<T extends FieldValues> { + field: ControllerRenderProps<T, any>; + label: string; + required?: boolean; + error?: FieldError; + inputComponent: ReactNode; +} + +export function AccessibleFormField<T extends FieldValues>({ + field, + label, + required = false, + error, + inputComponent, +}: AccessibleFormFieldProps<T>) { + return ( + <FormUi + label={ + <span> + <span className="text-black">{label}</span> + {required && <span className="text-red-500"> *</span>} + </span> + } + > + <div> + {inputComponent} + {error?.message && Array.from(new Set(error.message.split('\n'))).map((msg, idx) => ( + <p key={idx} className="text-red-500 text-sm mt-1" role="alert"> + {msg} + </p> + ))} + </div> + </FormUi> + ); +} diff --git a/apps/dm/components/ui/sdmInfoInsertForm/component.tsx b/apps/dm/components/ui/sdmInfoInsertForm/component.tsx new file mode 100644 index 000000000..79d53db80 --- /dev/null +++ b/apps/dm/components/ui/sdmInfoInsertForm/component.tsx @@ -0,0 +1,572 @@ +import ModalSimilarPage from "@/app/pages/sdmMng/sdmInfoInsert/modalSimilar/page"; +import type { FormData } from "@/app/pages/sdmMng/sdmInfoInsert/page"; +import { useDmStore } from "@/app/store"; +import { CANDIDATE, DatePicker, FormField, FormLabel, FormUi, SelectBox, UiModal } from "@common/business_components"; +import { Input } from "@common/components"; +import { Button } from "@common/components/ui"; +import { useSearchParams } from "next/navigation"; +import { useCallback, useEffect, useState } from "react"; +import { AccessibleFormField } from "./AccessibleFormField"; +import type { ControllerRenderProps, UseFormReturn } from "react-hook-form"; +import SearchInputText from "../common/searhDemandInput/component"; +import type { DemandCompType } from "@/app/store/sdmMng"; + +export interface SdmInfoRegFormProps { + form: UseFormReturn<FormData>; + onCheckboxChange?: (checked: boolean) => void; + facilityName?: string; +} + +type FieldComponent = (field: ControllerRenderProps<FormData, keyof FormData>) => JSX.Element; + +export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfoRegFormProps) { + const { initializeCommonData, Sdm } = useDmStore((state) => state); + const { useTypesData } = Sdm; + const searchParams = useSearchParams(); + + const [sdmNmValue, setSdmNmValue] = useState<string>(searchParams.get('dmdCoNm') || ''); + const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(false); + const [isSimilarCase, setIsSimilarCase] = useState(false); + const [key, setKey] = useState(0); + + //모달 조회에 필요한 데이터 + const [sply, setSply] = useState<number>(0); + const [usageCd, setUsageCd] = useState<string>('청소'); + const [money, setMoney] = useState<number>(0); + + useEffect(() => { + initializeCommonData(); + }, [initializeCommonData]); + + useEffect(() => { + if (useTypesData && useTypesData.length > 0) { + setTimeout(() => { + form.setValue('usePurpose', useTypesData[0]?.val?.toString() || ''); + }, 500); + } + }, [useTypesData, form]); + + useEffect(() => { + if (facilityName) { + setSdmNmValue(facilityName); + form.setValue('sdmNm', facilityName); + setKey(prev => prev + 1); + } + }, [facilityName, form]); + + const searchCandidate = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + } + + const searchSimilarCase = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsSimilarCase(true); + } + + const handleCheckboxChange = (checked: boolean) => { + setIsBznmTotNoDisabled(checked); + onCheckboxChange?.(checked); + }; + + // 시설 선택시 이벤트 + const selectComp = useCallback((rtnData: DemandCompType[]) => { + const rtnCompData = rtnData?.[0]; + form.setValue('sdmNm', rtnCompData?.dmdCoNm || ''); + }, [form]); + + const formFields: Array<{ + name: keyof FormData; + label: string; + component: FieldComponent | undefined; + formIndex: number; + required?: boolean; + }> = [ + { + name: "sdmNm", + label: "수요처선택", + formIndex: 0, + required: true, + component: (field) => ( + <div key={key}> + <SearchInputText + label="수요처명" + setValue={(val) => { + setSdmNmValue(val || ''); + field.onChange(val || ''); + }} + setRtnData={selectComp} + initValue={facilityName || sdmNmValue} + {...field} + /> + </div> + ) + }, + { + name: "demandYmd", + label: "요구시기", + formIndex: 0, + required: true, + component: (field) => ( + <div className="w-full"> + <DatePicker + mode="single" + selected={field.value as Date} + onSelect={(date) => { + field.onChange(date as Date); + }} + className="w-full" + defaultMonth={field.value as Date} + field={{ + value: field.value as Date, + onChange: (date: Date | undefined) => { + field.onChange(date as Date); + } + }} + selectClass={undefined} + /> + </div> + ) + }, + { + name: "demandQnty", + label: "요구수량(㎥/d)", + formIndex: 0, + required: true, + component: (field) => ( + <Input + placeholder="요구수량(㎥/d)을 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={(e) => { + field.onChange(e); + setSply(Number(e.target.value)); + }} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ) + }, + { + name: "usePurpose", + label: "사용용도", + formIndex: 0, + required: true, + component: (field) => ( + <SelectBox + selectClass={undefined} + label="" + selectArray={useTypesData} + onSelectValue={(val) => { + field.onChange(val.toString()); + setUsageCd(val.toString()); + }} + className="" + value={field.value as string} + /> + ) + }, + { + name: "money", + label: "희망단가(원/m3)", + formIndex: 0, + required: true, + component: (field) => ( + <AccessibleFormField + field={field} + label="희망단가(원/㎥)" + required={true} + error={form.formState.errors.money} + inputComponent={ + <FormUi + onCheckboxChange={handleCheckboxChange} + isDisabled={isBznmTotNoDisabled} + > + <Input + placeholder="희망단가(원/㎥)을 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={(e) => { + field.onChange(e); + setMoney(Number(e.target.value)); + }} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + </FormUi> + } + /> + ) + }, + { + name: "reqWaterQlty", + label: "요구수질", + formIndex: 0, + required: true, + component: undefined + }, + { + name: "grssClfg", + label: "총대장균군수(개/100ml)", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="총대장균군수를 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ) + }, + { + name: "tn", + label: "총질소(T-N)(mg/L)", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="총질소를 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ) + }, + { + name: "combResdch", + label: "결합잔류염소(mg/L)", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="결합잔류염소를 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ) + }, + { + name: "tph", + label: "총인(T-P)(mg/L)", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="총인을 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ) + }, + { + name: "trbt", + label: "탁도(NTU)", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="탁도를 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ) + }, + { + name: "ph", + label: "수소이온농도(pH)", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="수소이온농도를 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ) + }, + { + name: "bod", + label: "생물학적 산소요구량(BOD)(mg/L)", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="생물학적 산소요구량을 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ) + }, + { + name: "clIonQnt", + label: "염화물(mgCL/L)", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="염화물을 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ) + }, + { + name: "smll", + label: "냄새", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="냄새를 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ) + }, + { + name: "cdtnv", + label: "전기전도도(㎲/cm)", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="전기전도도를 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ) + }, + { + name: "crmty", + label: "색도(도)", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="색도를 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ) + }, + { + name: "rmrk", + label: "비고", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="비고를 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ) + }, + { + name: "candidateTitle", + label: CANDIDATE || "공급처 후보지 선택(필수아님)", + formIndex: 2, + component: (field) => ( + <div> + <div className="-mt-14 float-right flex gap-10"> + <Button className={'bg-gray-200'} onClick={searchCandidate}> + 후보지 조회 + </Button> + <Button className={'bg-gray-200'} onClick={searchSimilarCase}> + 유사사례 + </Button> + </div> + <div className="flex justify-center gap-10 mt-5"> + <Input + placeholder="후보지1" + type="text" + className="!text-xl w-[130px] text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + <Input + placeholder="후보지2" + type="text" + className="!text-xl w-[130px] text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + <Input + placeholder="후보지3" + type="text" + className="!text-xl w-[130px] text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + </div> + </div> + ) + }, + ]; + + const normalFields = formFields.filter(f => f.formIndex === 0 && f.component); + const groupFields = formFields.filter(f => f.formIndex === 1 && f.component); + const candidateFields = formFields.filter(f => f.formIndex === 2 && f.component); + + return ( + <> + {normalFields.map(({ name, label, component, required }) => ( + component ? ( + <FormField + key={name} + control={form.control} + name={name} + render={({ field }) => ( + name === 'money' ? component(field) : ( + // <FormUi label={label}> + // {component(field)} + // </FormUi> + <AccessibleFormField + field={field} + label={label} + required={required} + error={form.formState.errors[name]} + inputComponent={component(field)} + /> + ) + )} + /> + ) : null + ))} + + {groupFields.length > 0 && ( + <div className="mt-10"> + <FormLabel className="text-xl">요구수질</FormLabel> + <div className="flex flex-wrap justify-start gap-10 mt-3 mb-3"> + {groupFields.map(({ name, label, component, required }) => ( + <div key={name} className="min-w-[180px]"> + <FormField + control={form.control} + name={name} + render={({ field }) => ( + // <FormUi label={label}> + // {component?.(field)} + // </FormUi> + <AccessibleFormField + field={field} + label={label} + required={required} + error={form.formState.errors[name]} + inputComponent={component?.(field)} + /> + )} + /> + </div> + ))} + </div> + </div> + )} + + {candidateFields.map(({ name, label, component, required }, index) => ( + component ? ( + <FormField + key={name + index} + control={form.control} + name={name} + render={({ field }) => ( + // <FormUi label={label}> + // {component(field)} + // </FormUi> + <AccessibleFormField + field={field} + label={label} + required={required} + error={form.formState.errors[name]} + inputComponent={component?.(field)} + /> + )} + /> + ) : null + ))} + + {isSimilarCase && ( + <UiModal + body={<ModalSimilarPage sply={sply} usageCd={usageCd} money={money} />} + bodyClass="sm:max-w-[100vw] w-[1400px] h-[900px]" + open={isSimilarCase} + onOpenChange={(open) => !open && setIsSimilarCase(false)} + noHeaderYn="Y" + noFooterYn="Y" + /> + )} + </> + ); +} \ No newline at end of file diff --git a/apps/dm/components/ui/sdmLoginForm/component.tsx b/apps/dm/components/ui/sdmLoginForm/component.tsx new file mode 100644 index 000000000..b9cc305a9 --- /dev/null +++ b/apps/dm/components/ui/sdmLoginForm/component.tsx @@ -0,0 +1,148 @@ +import { useDmStore } from "@/app/store"; +import type { DemandCompType } from "@/app/store/sdmMng"; +import { BZNM_TOT_NO, FormField, FormUi } from "@common/business_components"; +import { Button, Input } from "@common/components"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { sha512 } from 'js-sha512'; +import { useEffect, useState } from "react"; +import { FormProvider, useForm, type ControllerRenderProps } from "react-hook-form"; +import * as z from "zod"; + +export function SdmLoginForm() { + type FormData = z.infer<ReturnType<typeof createFormSchema>>; + type FieldComponent = (field: ControllerRenderProps<FormData, keyof FormData>) => JSX.Element; + const createFormSchema = () => z.object({ + dmdInfoId: z.string() + .min(1, "사업자등록번호를 입력해주세요") + .regex(/^\d{10}$/, "사업자등록번호는 10자리 숫자만 입력 가능합니다"), + bznmTotNo: isBznmTotNoDisabled + ? z.string().optional() + : z.string().length(4, "종사업자등록번호는 4자리 숫자만 입력 가능합니다"), + password: z.string() + .min(1, "비밀번호를 입력해주세요") + .min(8, "비밀번호는 8자 이상 입력해주세요") + .max(16, "비밀번호는 16자 이내로 입력해주세요"), + rgtr: z.string().optional(), + dmdAplyPswd: z.string().optional(), + }); + + const { actionLogin, initializeCommonData } = useDmStore((state) => state); + const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(false); + const formSchema = createFormSchema(); + + useEffect(() => { + initializeCommonData(); + }, [initializeCommonData]); + + const defaultValues: FormData = { + dmdInfoId: '', + bznmTotNo: '', + password: '', + rgtr: '', + dmdAplyPswd: '', + }; + + const form = useForm<FormData>({ + resolver: zodResolver(formSchema), + defaultValues, + mode: "onChange", + }); + + const handleCheckboxChange = (checked: boolean) => { + setIsBznmTotNoDisabled(checked); + }; + + const handleLogin = () => { + const data = form.getValues(); + const passwordStr = sha512.hex(data.password) + data.dmdAplyPswd = passwordStr; + const param = { + dmdInfoId: data.dmdInfoId + (isBznmTotNoDisabled ? '0000' : data.bznmTotNo), + dmdAplyPswd: data.dmdAplyPswd, + } + actionLogin(param as unknown as DemandCompType); + }; + + const formFields: Array<{ + name: keyof FormData; + label: string; + component: FieldComponent | undefined; + }> = [ + { + name: "dmdInfoId", + label: "사업자등록번호", + component: (field) => ( + <Input + placeholder="사업자등록번호를 입력하세요" + type="text" + className="!mt-5 !mb-5 !text-xl text-gray-600" + {...field} + /> + ) + }, + { + name: "bznmTotNo", + label: BZNM_TOT_NO || "종사업자등록번호", + component: (field) => ( + <FormUi + label="종사업자등록번호" + onCheckboxChange={handleCheckboxChange} + isDisabled={isBznmTotNoDisabled} + > + <Input + placeholder="여러사업장이 있을 경우 입력해주세요" + type="text" + className="!mt-5 !text-xl text-gray-600" + disabled={isBznmTotNoDisabled} + {...field} + /> + </FormUi> + ) + }, + { + name: "password", + label: "비밀번호", + component: (field) => ( + <Input + placeholder="비밀번호를 입력하세요" + type="password" + className="!mt-5 !mb-5 !text-xl text-gray-600" + {...field} + /> + ) + } + ]; + + return ( + <> + <div> + <FormProvider {...form}> + {formFields.map(({ name, label, component }) => ( + <FormField + key={name} + control={form.control} + name={name} + render={({ field }) => ( + label==='종사업자등록번호' ? + <> + {component && component(field)} + </> + : <FormUi label={label}> + {component && component(field)} + </FormUi> + )} + /> + ))} + </FormProvider> + <div className="flex justify-center pt-4"> + <Button + onClick={handleLogin} + className="w-32 bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-md transition-colors duration-200" + > + 로그인 + </Button> + </div> + </div> + </> + ); +} \ No newline at end of file diff --git a/apps/dm/eslint.config.mjs b/apps/dm/eslint.config.mjs new file mode 100644 index 000000000..c85fb67c4 --- /dev/null +++ b/apps/dm/eslint.config.mjs @@ -0,0 +1,16 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), +]; + +export default eslintConfig; diff --git a/apps/dm/next.config.ts b/apps/dm/next.config.ts new file mode 100644 index 000000000..99301dab6 --- /dev/null +++ b/apps/dm/next.config.ts @@ -0,0 +1,41 @@ +import type { NextConfig } from "next"; +import webpack from "webpack"; + +const nextConfig: NextConfig = { + eslint: { + ignoreDuringBuilds: true, + }, + webpack: (config) => { + config.plugins.push( + new webpack.DefinePlugin({ + "import.meta.env.VITE_PORT": JSON.stringify( + process.env.NEXT_PUBLIC_PORT + ), + "import.meta.env.VITE_PROXY_HOST": JSON.stringify( + process.env.NEXT_PUBLIC_PROXY_HOST + ), + "import.meta.env.VITE_SERVER_API_URL": JSON.stringify( + process.env.NEXT_PUBLIC_SERVER_API_URL + ), + "import.meta.env.VITE_SITE_ID": JSON.stringify(process.env.SITE_ID), + "import.meta.env.VITE_CLAIM_NAME": JSON.stringify( + process.env.NEXT_PUBLIC_CLAIM_NAME + ), + "import.meta.env.VITE_ACCESS_TOKEN": JSON.stringify( + process.env.NEXT_PUBLIC_ACCESS_TOKEN + ), + "import.meta.env.VITE_REFRESH_TOKEN": JSON.stringify( + process.env.NEXT_PUBLIC_REFRESH_TOKEN + ), + "import.meta.env.VITE_AUTH_USER_ID": JSON.stringify( + process.env.NEXT_PUBLIC_AUTH_USER_ID + ), + "process.env.SITE_ID": JSON.stringify(process.env.NEXT_PUBLIC_SITE_ID), + }) + ); + + return config; + }, +}; + +export default nextConfig; diff --git a/apps/dm/package.json b/apps/dm/package.json new file mode 100644 index 000000000..2c9fe587b --- /dev/null +++ b/apps/dm/package.json @@ -0,0 +1,35 @@ +{ + "name": "dm", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@common/business_components": "workspace:*", + "@common/components": "workspace:*", + "@hookform/resolvers": "^4.1.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "js-sha512": "^0.9.0", + "lucide-react": "^0.474.0", + "next": "15.1.7", + "react-hook-form": "^7.54.2", + "react-hot-toast": "^2.5.2", + "tailwind-merge": "^3.0.1", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.24.2" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@types/webpack": "^5.28.5", + "eslint": "^9", + "eslint-config-next": "15.1.7", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } +} diff --git a/apps/dm/postcss.config.mjs b/apps/dm/postcss.config.mjs new file mode 100644 index 000000000..1a69fd2a4 --- /dev/null +++ b/apps/dm/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/apps/dm/public/file.svg b/apps/dm/public/file.svg new file mode 100644 index 000000000..004145cdd --- /dev/null +++ b/apps/dm/public/file.svg @@ -0,0 +1 @@ +<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg> \ No newline at end of file diff --git a/apps/dm/public/fonts/NotoSansKR-Black.otf b/apps/dm/public/fonts/NotoSansKR-Black.otf new file mode 100644 index 000000000..2b54d195e Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Black.otf differ diff --git a/apps/dm/public/fonts/NotoSansKR-Black.woff b/apps/dm/public/fonts/NotoSansKR-Black.woff new file mode 100644 index 000000000..90ef00381 Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Black.woff differ diff --git a/apps/dm/public/fonts/NotoSansKR-Black.woff2 b/apps/dm/public/fonts/NotoSansKR-Black.woff2 new file mode 100644 index 000000000..8953c8fa7 Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Black.woff2 differ diff --git a/apps/dm/public/fonts/NotoSansKR-Bold.otf b/apps/dm/public/fonts/NotoSansKR-Bold.otf new file mode 100644 index 000000000..2b78dd845 Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Bold.otf differ diff --git a/apps/dm/public/fonts/NotoSansKR-Bold.woff b/apps/dm/public/fonts/NotoSansKR-Bold.woff new file mode 100644 index 000000000..74b20d49c Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Bold.woff differ diff --git a/apps/dm/public/fonts/NotoSansKR-Bold.woff2 b/apps/dm/public/fonts/NotoSansKR-Bold.woff2 new file mode 100644 index 000000000..216f94dad Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Bold.woff2 differ diff --git a/apps/dm/public/fonts/NotoSansKR-Light.otf b/apps/dm/public/fonts/NotoSansKR-Light.otf new file mode 100644 index 000000000..ecab31abc Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Light.otf differ diff --git a/apps/dm/public/fonts/NotoSansKR-Light.woff b/apps/dm/public/fonts/NotoSansKR-Light.woff new file mode 100644 index 000000000..40aabd2a4 Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Light.woff differ diff --git a/apps/dm/public/fonts/NotoSansKR-Light.woff2 b/apps/dm/public/fonts/NotoSansKR-Light.woff2 new file mode 100644 index 000000000..6b7cd1307 Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Light.woff2 differ diff --git a/apps/dm/public/fonts/NotoSansKR-Medium.otf b/apps/dm/public/fonts/NotoSansKR-Medium.otf new file mode 100644 index 000000000..371959d51 Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Medium.otf differ diff --git a/apps/dm/public/fonts/NotoSansKR-Medium.woff b/apps/dm/public/fonts/NotoSansKR-Medium.woff new file mode 100644 index 000000000..e15aae09e Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Medium.woff differ diff --git a/apps/dm/public/fonts/NotoSansKR-Medium.woff2 b/apps/dm/public/fonts/NotoSansKR-Medium.woff2 new file mode 100644 index 000000000..b97668034 Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Medium.woff2 differ diff --git a/apps/dm/public/fonts/NotoSansKR-Regular.otf b/apps/dm/public/fonts/NotoSansKR-Regular.otf new file mode 100644 index 000000000..7ba76b49c Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Regular.otf differ diff --git a/apps/dm/public/fonts/NotoSansKR-Regular.woff b/apps/dm/public/fonts/NotoSansKR-Regular.woff new file mode 100644 index 000000000..40fab257c Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Regular.woff differ diff --git a/apps/dm/public/fonts/NotoSansKR-Regular.woff2 b/apps/dm/public/fonts/NotoSansKR-Regular.woff2 new file mode 100644 index 000000000..61d064695 Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Regular.woff2 differ diff --git a/apps/dm/public/fonts/NotoSansKR-Thin.otf b/apps/dm/public/fonts/NotoSansKR-Thin.otf new file mode 100644 index 000000000..06c566278 Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Thin.otf differ diff --git a/apps/dm/public/fonts/NotoSansKR-Thin.woff b/apps/dm/public/fonts/NotoSansKR-Thin.woff new file mode 100644 index 000000000..40723731f Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Thin.woff differ diff --git a/apps/dm/public/fonts/NotoSansKR-Thin.woff2 b/apps/dm/public/fonts/NotoSansKR-Thin.woff2 new file mode 100644 index 000000000..7835c23ed Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Thin.woff2 differ diff --git a/apps/dm/public/globe.svg b/apps/dm/public/globe.svg new file mode 100644 index 000000000..567f17b0d --- /dev/null +++ b/apps/dm/public/globe.svg @@ -0,0 +1 @@ +<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg> \ No newline at end of file diff --git a/apps/dm/public/img/wrAbout/icon_01.png b/apps/dm/public/img/wrAbout/icon_01.png new file mode 100644 index 000000000..30eff5745 Binary files /dev/null and b/apps/dm/public/img/wrAbout/icon_01.png differ diff --git a/apps/dm/public/img/wrAbout/icon_02.png b/apps/dm/public/img/wrAbout/icon_02.png new file mode 100644 index 000000000..36401729f Binary files /dev/null and b/apps/dm/public/img/wrAbout/icon_02.png differ diff --git a/apps/dm/public/img/wrAbout/icon_03.png b/apps/dm/public/img/wrAbout/icon_03.png new file mode 100644 index 000000000..87f48c444 Binary files /dev/null and b/apps/dm/public/img/wrAbout/icon_03.png differ diff --git a/apps/dm/public/img/wrAbout/img_01_1.png b/apps/dm/public/img/wrAbout/img_01_1.png new file mode 100644 index 000000000..9aab4e824 Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_01_1.png differ diff --git a/apps/dm/public/img/wrAbout/img_02_1.png b/apps/dm/public/img/wrAbout/img_02_1.png new file mode 100644 index 000000000..3d61c6e5d Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_02_1.png differ diff --git a/apps/dm/public/img/wrAbout/img_02_2.png b/apps/dm/public/img/wrAbout/img_02_2.png new file mode 100644 index 000000000..c8283053e Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_02_2.png differ diff --git a/apps/dm/public/img/wrAbout/img_02_3.png b/apps/dm/public/img/wrAbout/img_02_3.png new file mode 100644 index 000000000..f5ba9d81e Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_02_3.png differ diff --git a/apps/dm/public/img/wrAbout/img_03_1.png b/apps/dm/public/img/wrAbout/img_03_1.png new file mode 100644 index 000000000..a7534ae68 Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_03_1.png differ diff --git a/apps/dm/public/img/wrAbout/img_03_2.png b/apps/dm/public/img/wrAbout/img_03_2.png new file mode 100644 index 000000000..6b4faca7d Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_03_2.png differ diff --git a/apps/dm/public/img/wrAbout/img_04_1.png b/apps/dm/public/img/wrAbout/img_04_1.png new file mode 100644 index 000000000..a160072bd Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_04_1.png differ diff --git a/apps/dm/public/img/wrAbout/img_04_2.png b/apps/dm/public/img/wrAbout/img_04_2.png new file mode 100644 index 000000000..374fc55bf Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_04_2.png differ diff --git a/apps/dm/public/img/wrAbout/img_05_1.png b/apps/dm/public/img/wrAbout/img_05_1.png new file mode 100644 index 000000000..1418fa92d Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_05_1.png differ diff --git a/apps/dm/public/img/wrAbout/img_05_2.png b/apps/dm/public/img/wrAbout/img_05_2.png new file mode 100644 index 000000000..0c2f92396 Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_05_2.png differ diff --git a/apps/dm/public/img/wrAbout/img_06_1.png b/apps/dm/public/img/wrAbout/img_06_1.png new file mode 100644 index 000000000..08acf4971 Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_06_1.png differ diff --git a/apps/dm/public/img/wrAbout/img_06_2.png b/apps/dm/public/img/wrAbout/img_06_2.png new file mode 100644 index 000000000..19b150b7c Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_06_2.png differ diff --git a/apps/dm/public/img/wrExample/icon_01.png b/apps/dm/public/img/wrExample/icon_01.png new file mode 100644 index 000000000..30eff5745 Binary files /dev/null and b/apps/dm/public/img/wrExample/icon_01.png differ diff --git a/apps/dm/public/img/wrExample/icon_02.png b/apps/dm/public/img/wrExample/icon_02.png new file mode 100644 index 000000000..36401729f Binary files /dev/null and b/apps/dm/public/img/wrExample/icon_02.png differ diff --git a/apps/dm/public/img/wrExample/icon_03.png b/apps/dm/public/img/wrExample/icon_03.png new file mode 100644 index 000000000..87f48c444 Binary files /dev/null and b/apps/dm/public/img/wrExample/icon_03.png differ diff --git a/apps/dm/public/img/wrExample/img_01_1.png b/apps/dm/public/img/wrExample/img_01_1.png new file mode 100644 index 000000000..9aab4e824 Binary files /dev/null and b/apps/dm/public/img/wrExample/img_01_1.png differ diff --git a/apps/dm/public/img/wrExample/img_02_1.png b/apps/dm/public/img/wrExample/img_02_1.png new file mode 100644 index 000000000..3d61c6e5d Binary files /dev/null and b/apps/dm/public/img/wrExample/img_02_1.png differ diff --git a/apps/dm/public/img/wrExample/img_02_2.png b/apps/dm/public/img/wrExample/img_02_2.png new file mode 100644 index 000000000..c8283053e Binary files /dev/null and b/apps/dm/public/img/wrExample/img_02_2.png differ diff --git a/apps/dm/public/img/wrExample/img_02_3.png b/apps/dm/public/img/wrExample/img_02_3.png new file mode 100644 index 000000000..f5ba9d81e Binary files /dev/null and b/apps/dm/public/img/wrExample/img_02_3.png differ diff --git a/apps/dm/public/img/wrExample/img_03_1.png b/apps/dm/public/img/wrExample/img_03_1.png new file mode 100644 index 000000000..a7534ae68 Binary files /dev/null and b/apps/dm/public/img/wrExample/img_03_1.png differ diff --git a/apps/dm/public/img/wrExample/img_03_2.png b/apps/dm/public/img/wrExample/img_03_2.png new file mode 100644 index 000000000..6b4faca7d Binary files /dev/null and b/apps/dm/public/img/wrExample/img_03_2.png differ diff --git a/apps/dm/public/img/wrExample/img_04_1.png b/apps/dm/public/img/wrExample/img_04_1.png new file mode 100644 index 000000000..a160072bd Binary files /dev/null and b/apps/dm/public/img/wrExample/img_04_1.png differ diff --git a/apps/dm/public/img/wrExample/img_04_2.png b/apps/dm/public/img/wrExample/img_04_2.png new file mode 100644 index 000000000..374fc55bf Binary files /dev/null and b/apps/dm/public/img/wrExample/img_04_2.png differ diff --git a/apps/dm/public/img/wrExample/img_05_1.png b/apps/dm/public/img/wrExample/img_05_1.png new file mode 100644 index 000000000..1418fa92d Binary files /dev/null and b/apps/dm/public/img/wrExample/img_05_1.png differ diff --git a/apps/dm/public/img/wrExample/img_05_2.png b/apps/dm/public/img/wrExample/img_05_2.png new file mode 100644 index 000000000..0c2f92396 Binary files /dev/null and b/apps/dm/public/img/wrExample/img_05_2.png differ diff --git a/apps/dm/public/img/wrExample/img_06_1.png b/apps/dm/public/img/wrExample/img_06_1.png new file mode 100644 index 000000000..08acf4971 Binary files /dev/null and b/apps/dm/public/img/wrExample/img_06_1.png differ diff --git a/apps/dm/public/img/wrExample/img_06_2.png b/apps/dm/public/img/wrExample/img_06_2.png new file mode 100644 index 000000000..19b150b7c Binary files /dev/null and b/apps/dm/public/img/wrExample/img_06_2.png differ diff --git a/apps/dm/public/img/wrExample/img_arrow_down_1.png b/apps/dm/public/img/wrExample/img_arrow_down_1.png new file mode 100644 index 000000000..9e8fb76c6 Binary files /dev/null and b/apps/dm/public/img/wrExample/img_arrow_down_1.png differ diff --git a/apps/dm/public/img/wrExample/img_arrow_down_2.png b/apps/dm/public/img/wrExample/img_arrow_down_2.png new file mode 100644 index 000000000..2002bb607 Binary files /dev/null and b/apps/dm/public/img/wrExample/img_arrow_down_2.png differ diff --git a/apps/dm/public/img/wrExample/img_ex_01_1.png b/apps/dm/public/img/wrExample/img_ex_01_1.png new file mode 100644 index 000000000..9d6bfb002 Binary files /dev/null and b/apps/dm/public/img/wrExample/img_ex_01_1.png differ diff --git a/apps/dm/public/img/wrExample/img_ex_02_1.png b/apps/dm/public/img/wrExample/img_ex_02_1.png new file mode 100644 index 000000000..70c1f8ed2 Binary files /dev/null and b/apps/dm/public/img/wrExample/img_ex_02_1.png differ diff --git a/apps/dm/public/img/wrExample/img_ex_03_1.png b/apps/dm/public/img/wrExample/img_ex_03_1.png new file mode 100644 index 000000000..c981fa9fb Binary files /dev/null and b/apps/dm/public/img/wrExample/img_ex_03_1.png differ diff --git a/apps/dm/public/img/wrExample/img_ex_04_1.png b/apps/dm/public/img/wrExample/img_ex_04_1.png new file mode 100644 index 000000000..f5547820e Binary files /dev/null and b/apps/dm/public/img/wrExample/img_ex_04_1.png differ diff --git a/apps/dm/public/img/wrExample/img_ex_04_2.png b/apps/dm/public/img/wrExample/img_ex_04_2.png new file mode 100644 index 000000000..b102942ee Binary files /dev/null and b/apps/dm/public/img/wrExample/img_ex_04_2.png differ diff --git a/apps/dm/public/img/wrExample/img_ex_04_3.png b/apps/dm/public/img/wrExample/img_ex_04_3.png new file mode 100644 index 000000000..50ca8045f Binary files /dev/null and b/apps/dm/public/img/wrExample/img_ex_04_3.png differ diff --git a/apps/dm/public/img/wrExample/img_ex_04_4.png b/apps/dm/public/img/wrExample/img_ex_04_4.png new file mode 100644 index 000000000..6e6d0c294 Binary files /dev/null and b/apps/dm/public/img/wrExample/img_ex_04_4.png differ diff --git a/apps/dm/public/img/wrOverview/img_ov_01_1.png b/apps/dm/public/img/wrOverview/img_ov_01_1.png new file mode 100644 index 000000000..e09893ef9 Binary files /dev/null and b/apps/dm/public/img/wrOverview/img_ov_01_1.png differ diff --git a/apps/dm/public/img/wrStatus/icon_favorit_off.png b/apps/dm/public/img/wrStatus/icon_favorit_off.png new file mode 100644 index 000000000..4de0b3ebc Binary files /dev/null and b/apps/dm/public/img/wrStatus/icon_favorit_off.png differ diff --git a/apps/dm/public/img/wrStatus/icon_favorit_on.png b/apps/dm/public/img/wrStatus/icon_favorit_on.png new file mode 100644 index 000000000..f2edfa4e9 Binary files /dev/null and b/apps/dm/public/img/wrStatus/icon_favorit_on.png differ diff --git a/apps/dm/public/img/wrStatus/icon_right.png b/apps/dm/public/img/wrStatus/icon_right.png new file mode 100644 index 000000000..fef56226c Binary files /dev/null and b/apps/dm/public/img/wrStatus/icon_right.png differ diff --git a/apps/dm/public/img/wrStatus/southKoreaMap.svg b/apps/dm/public/img/wrStatus/southKoreaMap.svg new file mode 100644 index 000000000..7b3c170e3 --- /dev/null +++ b/apps/dm/public/img/wrStatus/southKoreaMap.svg @@ -0,0 +1,19 @@ +<svg width="506" height="785" viewBox="0 0 506 785" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path id="전라북도" d="M273.567 425.516L274.188 429.893L276.776 436.366L272.497 444.123L262.388 453.507L253.164 454.538L241.456 466.408L234.164 484.969L234.199 489.884L228.425 500.23L234.578 510.92L233.336 516.076L236.637 519.971L228.736 529.825L229.207 534.786L222.445 540.171L212.531 530.741L203.135 532.139L199.017 538.372L191.864 536.768L186.861 538.819L178.752 536.126L178.43 533.686L170.644 532.093L159.695 536.699L152.358 532.517L155.785 526.261L152.174 521.77L153.301 515.491L150.678 509.923L146.09 512.799L143.398 518.848L135.29 516.167L133.266 509.43L123.122 503.816L114.117 508.754L114.416 513.612L109.16 518.184L110.057 521.77L99.1308 527.9L94.1739 524.096L89.24 525.711L90.4706 530.077L82.9604 529.126L82.5924 520.2L79.1881 518.573L78.9811 511.447L73.0925 508.709L79.9126 497.056L93.1848 491.912L98.1302 491.935L102.064 487.913L106.906 492.153L107.423 488.944L103.754 484.98L84.6165 484.969L78.9811 480.489L80.7752 474.141L98.1762 464.196L104.306 451.112L124.123 456.68L124.364 451.33L120.201 449.978L113.634 443.859L122.673 440.548L127.63 441.453L130.552 436.286L130.149 434.155L121.098 437.409L112.541 435.621L109.608 438.749L102.627 436.057L103.168 425.779L97.0031 422.823L98.6938 419.707L117.038 419.237L140.604 407.436L141.846 399.336L147.608 396.54L155.164 396.987L161.248 399.45L167.044 409.808L173.347 412.283L175.969 408.822L181.513 407.757L188.08 409.132L193.911 406.187L199.305 404.835L211.634 423.27L214.555 422.606L217.936 428.438L226.436 427.853L227.85 421.93L235.429 423.018L236.499 426.776L240.11 422.606L239.455 417.221L243.837 416.854L245.033 420.097L246.447 420.452L247.678 417.897L250.461 417.771L253.969 423.362L256.798 423.408L260.49 425.837L270.254 423.305L273.567 425.516Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="서울특별시" d="M200.823 161.685L192.036 164.378L194.072 156.747L192.485 153.378L193.393 150.686L190.909 149.792L191.588 144.407L187.309 142.391L180.777 141.039L177.177 150.021L175.153 150.239L172.22 146.195L167.24 149.403L165.687 156.747L152.829 155.315L150.828 157.641L147.895 163.473L153.531 165.054L154.209 170.427L152.852 171.55L153.531 174.919L158.706 174.243L162.985 182.32L167.263 181.873L171.093 183.225L181.007 179.41L181.455 182.549L182.352 184.566L189.333 181.209H191.818L194.072 179.628L196.993 178.287L195.187 173.349L200.823 167.07V161.685Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="인천광역시" d="M126.722 162.453L125.376 165.054L126.285 167.517L129.068 166.875L130.782 169.98L126.423 172.1L128.079 180.303L131.702 186.914L140.259 190.214L146.768 178.951L143.398 173.578L143.168 168.411H145.422L147.895 163.472L150.828 157.641L146.55 157.423L134.381 146.871L130.103 148.899V150.468L123.582 153.608L126.722 162.453Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="경기도" d="M286.172 172.455L282.618 168.537L274.475 166.394L258.259 155.177H251.036L253.521 145.977L246.769 140.363L252.393 134.313L252.163 123.715L263.653 116.359L262.756 104.466L256.235 100.88L254.613 97.1337L244.285 90.3394L245.194 80.463L243.837 75.5249L228.299 78.2174L224.469 65.8777L210.725 60.275L199.029 45.0137L185.504 55.3368L181.904 64.984L169.517 74.4021L165.687 81.3567L155.336 89.6634L146.55 93.9256L142.72 111.421L133.933 122.42L120.201 123.543L118.165 152.255L123.582 153.607L130.103 150.468V148.898L134.381 146.87L146.55 157.423L150.828 157.64L152.829 155.315L165.687 156.747L167.24 149.403L172.22 146.194L175.153 150.239L177.177 150.021L180.777 141.039L187.309 142.391L191.588 144.407L190.909 149.792L193.393 150.686L192.485 153.378L194.072 156.747L192.036 164.377L200.823 161.685V167.07L195.187 173.349L196.993 178.287L194.072 179.627L191.818 181.208H189.333L182.352 184.566L181.455 182.549L181.007 179.41L171.093 183.225L167.263 181.873L162.985 182.32L158.706 174.242L153.531 174.918L152.852 171.55L154.209 170.427L153.531 165.053L147.895 163.472L145.422 168.41H143.168L143.398 173.578L146.768 178.951L140.258 190.214L135.405 195.748L153.082 208.569L146.09 211.937L138.211 210.815L129.206 212.167L130.552 216.876L127.182 223.83L130.782 226.305L129.885 230.785L138.441 228.539L142.49 224.953L147.447 228.986L141.593 231.232L142.49 242.678L145.422 249.403L144.065 254.124L149.252 260.62L154.209 273.418L163.859 268.698L177.844 266.91L186.642 262.648L195.866 266.234L206.228 275.435L209.161 270.496L216.993 268.847L235.061 253.677L244.745 255.464L256.177 245.198L259.375 238.416L263.078 241.417L265.873 236.25L271.543 231.908L272.21 222.261L275.142 219.121L275.372 204.765L281.595 187.659L281.226 179.857L284.378 177.164L286.172 172.455Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-miterlimit="10"/> +<path id="강원특별자치도" d="M210.047 41.6452L215.912 42.3212L220.858 38.2882L231.669 45.2429L235.05 40.7515L253.291 44.3377L274.234 40.0755L292.256 42.3212L300.583 47.7062L308.472 45.4606L320.629 47.0302L337.064 46.5834L349.462 40.3047L358.916 29.7638L363.873 7.99466L369.612 1.81909L372.545 7.88009L377.502 18.4324L378.399 28.9733L386.622 42.7681L396.754 72.1679L401.481 87.6469L412.971 107.617L425.357 133.419L450.579 165.054L449.452 169.98L458.009 180.533L456.882 187.029L483.23 235.952L486.83 254.101L472.419 262.648L463 270.726L451.258 261.972L442.022 264.665L429.406 259.956L421.079 267.586L409.141 259.727L404.863 267.357L392.027 262.946L386.622 260.254L380.538 259.807L370.635 251.649H359.813L353.062 244.775L341.572 244.694L348.105 233.707L323.102 230.785L312.072 236.399L307.564 226.076L296.534 225.893L292.923 238.267L279.639 239.092L274.464 240.432L271.543 231.908L272.21 222.261L275.143 219.121L275.373 204.765L281.595 187.659L281.227 179.857L284.378 177.164L286.172 172.455L282.618 168.537L274.475 166.394L258.248 155.177H251.036L253.521 145.977L246.758 140.363L252.394 134.313L252.164 123.715L263.653 116.359L262.756 104.466L256.235 100.88L254.613 97.1337L244.285 90.3394L245.194 80.4631L243.837 75.5249L228.299 78.2174L224.458 65.8777L210.725 60.275L199.029 45.0137L210.047 41.6452Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="경상북도" d="M481.264 504.297L493.903 466.82L494.57 458.742L501.551 447.972L504.346 437.535L500.206 434.052L496.594 439.219L484.012 447.205L476.813 439.322L483.759 430.019L480.378 392.77L488.268 377.074L489.843 341.395L497.721 321.196L492.995 303.482V271.619L486.83 254.101L472.419 262.648L463 270.726L451.257 261.972L442.022 264.665L429.405 259.956L421.079 267.586L409.141 259.726L404.862 267.357L392.027 262.946L382.194 265.111L374.902 272.742L364.77 277.99L358.479 289.206L361.17 295.588L350.163 302.841L333.464 290.467L327 298.556L319.295 295.623L314.763 298.544L308.725 298.922L304.424 307.378L308.898 313.599L304.286 315.616L299.685 311.422L282.871 316.945L289.243 323.155L288.012 328.471L281.893 323.522L270.254 330.304L279.053 336.205L283.446 343.733L279.421 348.671L277.845 365.605L279.869 370.176L273.567 377.612L273.498 381.783L278.075 384.762L280.41 380.912L287.598 388.382L297.995 386.87L299.801 398.155L290.519 397.491L290.002 406.92L282.791 421.804L273.636 425.527L274.188 429.893L276.775 436.366L272.497 444.123L279.041 453.357L291.209 459.602L300.479 459.682L303.492 466.809L311.612 474.829L312.693 483.995L306.586 488.841L312.82 492.84L330.393 492.164L339.56 499.164L335.5 483.903L343.067 485.702L344.689 483.181L339.111 476.272L343.389 468.424L350.14 467.53L352.004 464.471L340.077 458.96L347.886 446.437L354.097 443.894L352.165 452.269L355.546 454.286L358.479 452.04L362.079 440.823L369.957 439.7L375.592 437.237L382.803 436.332L388.301 439.104L389.474 443.951L389.784 450.883L391.13 454.068L388.784 462.535L384.598 463.257L383.24 466.408L382.93 470.681L379.871 474.027L380.101 477.178L378.513 484.35L369.06 485.702V482.104L361.618 482.631L355.316 490.697L358.099 499.313L387.3 511.252L404.264 502.006L418.675 505.81L422.033 502.716L430.372 501.376L435.674 491.568L446.358 490.617L456.939 494.421L455.593 500.929L462.126 502.728L466.853 500.482L473.592 500.253L481.264 504.297Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="충청북도" d="M274.464 240.432L279.639 239.092L292.923 238.267L296.534 225.893L307.564 226.042L312.072 236.399L323.102 230.785L348.105 233.707L341.572 244.694L353.062 244.774L359.813 251.649H370.635L380.538 259.807L386.622 260.253L392.027 262.946L382.194 265.203L374.902 272.742L364.77 277.99L358.479 289.206L361.17 295.588L350.164 302.841L333.464 290.467L327 298.556L319.295 295.623L314.763 298.544L308.725 298.922L304.424 307.378L308.898 313.599L304.286 315.616L299.686 311.423L282.871 316.945L289.243 323.155L288.012 328.471L281.893 323.522L270.254 330.304L279.053 336.205L283.446 343.733L279.421 348.671L277.845 365.605L279.869 370.176L273.567 377.612L273.498 381.783L278.075 384.762L280.41 380.912L287.598 388.382L297.995 386.87L299.801 398.155L290.462 397.594L290.002 406.92L282.791 421.804L273.567 425.608L270.254 423.305L260.49 425.837L256.798 423.408L253.969 423.362L250.461 417.771L247.678 417.897L246.447 420.452L245.033 420.096L243.837 416.854L240.122 412.225L238.144 402.67L240.053 401.467L237.994 394.191L233.014 385.896L226.505 385.415L224.584 382.172L231.554 362.42L234.946 359.75L234.268 357.046L228.954 356.084V350.527L225.93 346.047L221.18 349.988L217.821 349.037L216.016 343.32L212.013 342.713V335.586L209.863 332.607L210.058 330.866L207.091 327.818L203.71 320.852L201.007 318.618L204.561 309.234L213.819 300.779L219.581 301.626L222.042 298.315L210.323 278.517L206.228 275.435L209.161 270.496L216.993 268.847L235.061 253.677L244.745 255.464L256.177 245.198L259.375 238.416L263.078 241.417L265.873 236.25L271.543 231.908L274.464 240.432Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="충청남도" d="M226.505 385.415L219.397 392.942L215.567 391.04L214.762 388.038L212.197 386.434V380.603L210.392 380.144L209.092 385.713L204.308 392.266L198.81 386.331L198.672 379.927L195.164 377.521L195.682 371.322L198.672 367.587L197.994 365.502L200.052 356.118L194.244 346.207L197.407 341.727L194.681 339.997L192.899 329.835L185.239 318.835L187.493 311.434L185.239 304.479L188.206 302.841L195.153 306.496L204.561 309.234L213.819 300.778L219.581 301.626L222.042 298.315L210.323 278.517L206.228 275.435L195.866 266.234L186.642 262.648L177.844 266.91L163.859 268.698L154.209 273.418L145.917 273.132L137.13 255.418L131.955 258.317L129.609 252.898L118.66 250.251L107.4 241.498L103.685 241.624L100.465 246.103L101.443 252.004L103.478 257.126L102.374 257.882L98.1419 250.664L91.6668 247.604L85.6403 247.364L80.6259 250.48L86.2844 253.746L82.857 255.877L87.9865 260.047L90.3672 260.849L90.7928 263.748L84.5707 269.568L76.198 280.281L72.3681 278.425L78.5787 265.501L77.0375 263.702L78.1301 256.163L76.1405 253.299L71.5861 267.128L68.6878 268.812L65.0765 263.324L60.8211 265.936L61.2696 273.968L59.8435 275.606L56.6922 274.827L52.6554 277.898L47.7214 291.097L48.1585 292.506L51.8618 292.323L52.0228 286.147L57.3593 288.565L60.108 293.606L52.9659 295.863L52.8969 298.762L58.6474 299.931L62.8338 298.498L64.858 295.313L68.7108 294.992L67.3997 300.767L71.3675 304.342L69.0558 313.29L73.1732 319.145L74.1853 344.958L85.3643 346.952L79.0272 321.413L81.2354 317.483L75.2089 312.843L76.888 311.044L77.7966 304.433L79.7173 302.99L81.799 311.56L86.0313 314.653L88.9871 312.797L91.9544 306.965L93.668 308.421L91.8739 316.681L95.7037 343.606L92.9664 349.782L100.879 357.287V359.292L94.2776 362.053L97.0493 382.86L89.7692 391.052L90.9883 393.618L94.105 391.04L101.949 393.756L107.964 401.1L106.779 403.77L111.702 416.281L117.038 419.237L140.604 407.436L141.846 399.336L147.608 396.54L155.164 396.987L161.248 399.45L167.102 409.773L173.405 412.237L175.969 408.822L181.513 407.757L188.08 409.132L193.911 406.187L199.305 404.835L211.634 423.27L214.555 422.606L217.937 428.437L226.436 427.853L227.85 421.93L235.43 423.018L236.499 426.776L240.11 422.606L239.455 417.221L243.837 416.854L240.122 412.225L238.144 402.67L240.053 401.467L237.994 394.191L233.014 385.896L226.505 385.415Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="세종특별자치시" d="M204.561 309.234L201.007 318.618L203.71 320.852L207.091 327.818L210.058 330.866L209.863 332.607L212.013 335.586V342.713L210.84 349.633L209.253 350.928L207.919 354.8L200.052 356.118L194.244 346.207L197.407 341.727L194.681 339.997L192.899 329.835L185.239 318.835L187.493 311.434L185.239 304.479L188.206 302.841L195.153 306.496L204.561 309.234Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="대전광역시" d="M200.052 356.118L207.919 354.8L209.253 350.928L210.84 349.633L212.013 342.713L216.016 343.32L217.821 349.037L221.18 349.988L225.93 346.047L228.954 350.527V356.084L234.268 357.046L234.946 359.75L231.554 362.42L224.584 382.172L226.505 385.415L219.397 392.942L215.567 391.04L214.762 388.039L212.197 386.435V380.603L210.392 380.144L209.092 385.713L204.308 392.266L198.81 386.331L198.672 379.927L195.164 377.521L195.682 371.322L198.672 367.587L197.994 365.502L200.052 356.118Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="대구광역시" d="M361.619 482.631L369.06 482.104V485.702L378.514 484.35L380.101 477.178L379.871 474.027L382.93 470.681L383.24 466.408L384.598 463.257L388.784 462.535L391.13 454.068L389.785 450.883L389.474 443.951L388.301 439.105L382.803 436.332L375.592 437.237L369.957 439.7L362.079 440.823L358.479 452.04L355.546 454.286L352.165 452.269L354.097 443.894L347.886 446.437L340.077 458.96L352.004 464.471L350.141 467.53L343.389 468.424L339.111 476.273L344.689 483.181L343.067 485.702L335.5 483.903L339.56 499.164L350.819 496.472L355.316 490.697L361.619 482.631Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="울산광역시" d="M459.136 556.349L456.663 554.332L454.19 547.377L446.496 545.327L444.161 538.326L439.204 536.699L432.361 528.06L420.515 521.724L416.858 516.465L422.769 511.183L422.033 502.716L430.372 501.376L435.674 491.568L446.358 490.617L456.939 494.421L455.593 500.929L462.126 502.728L466.853 500.482L473.592 500.253L481.264 504.297L481.206 506.761L483.012 511.252L481.666 515.743L481.885 522.469L478.963 528.977L474.455 532.116L473.109 526.731L471.683 524.13L471.752 531.669L465.898 535.702L467.704 538.624L467.474 544.685L465.001 548.718L466.347 550.517L462.597 552.797L459.136 556.349Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="부산광역시" d="M446.496 545.327L454.19 547.378L456.663 554.332L459.136 556.349L453.857 558.216L449.233 567.336L449.452 570.258L438.871 583.503L432.338 581.922L429.187 585.737L430.993 588.876L430.935 591.569L426.484 591.351L423.103 587.983L415.673 597.412L412.522 595.384L411.395 600.322L408.244 601.216L406.887 596.954L408.692 585.29L402.93 591.5L399.434 589.151L395.409 592.245L391.004 591.122L391.13 586.722L386.909 580.959L403.908 570.682L411.959 570.854L416.203 568.253L418.077 564.197L433.132 556.039L435.754 549.554L443.046 549.99L446.496 545.327Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="경상남도" d="M272.497 444.123L262.388 453.506L253.164 454.538L241.456 466.408L234.164 484.969L234.199 489.884L228.425 500.23L234.578 510.92L233.336 516.076L236.637 519.971L228.736 529.825L229.207 534.786L222.445 540.171L225.907 549.085L227.712 563.888L233.589 569.742L239.639 582.506L245.251 586.676L246.769 593.001L242.652 598.684L255.2 603.026L258.892 602.797L264.838 597.286L265.321 595.075L267.793 596.198L268.242 598.89L272.095 597.676L274.326 598.89L277.132 597.263L274.498 590.824L276.799 589.919L278.846 590.492L280.145 586.39L285.091 584.946L282.434 589.243L282.216 599.784L280.881 606.509L289.795 611.218L291.968 615.297L293.245 615.939L297.098 614.243L299.778 615.492L301.319 615.263L303.849 609.076L309.692 609.557L312.613 614.369L316.742 612.376L320.042 607.185L322.297 613.693L317.041 616.088L318.248 619.525L323.067 621.003L324.999 619.525L327.024 621.278L326.794 628.725L321.492 627.373L320.042 628.954L324.585 632.793L327.208 639.369L329.738 640.538L332.061 638.636L333.165 632.735L331.854 628.806L334.787 627.488L338.628 620.155L336.259 617.222L333.947 621.324L331.808 620.419L334.545 606.532L334.235 603.828L341.216 602.476L344.252 598.936L338.962 593.952L330.635 595.522L328.887 598.214L325.448 598.661L328.312 593.345L340.296 589.403L339.18 585.794L351.578 584.981L354.12 586.699L355.857 594.181L361.998 591.706L355.857 572.194L359.422 567.565L361.032 568.379L359.917 574.841L362.159 579.813L366.645 580.478L369.807 584.076L371.21 580.26L373.189 581.841L372.2 587.318L377.697 589.69L385.196 588.384L386.035 590.583L391.004 591.122L391.13 586.722L386.909 580.959L403.908 570.682L411.924 570.842L416.202 568.253L418.077 564.197L433.097 556.039L435.754 549.554L443.011 549.978L446.496 545.327L444.161 538.326L439.204 536.699L432.361 528.06L420.515 521.724L416.858 516.465L422.77 511.183L422.033 502.716L418.675 505.81L404.264 502.006L387.3 511.252L358.099 499.313L355.316 490.697L350.647 496.598L339.56 499.164L330.393 492.164L312.843 492.771L306.586 488.841L312.693 483.995L311.612 474.829L303.492 466.809L300.479 459.682L291.209 459.602L279.041 453.358L272.497 444.123Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="전라남도" d="M238.224 616.271L235.165 615.859L225.619 619.64L224.043 615.824L217.81 608.721L222.019 601.239L224.055 605.272L229.127 606.991L239.593 598.1L242.675 598.695L246.781 593.012L245.263 586.677L239.65 582.517L233.601 569.754L227.724 563.899L225.918 549.096L222.456 540.182L212.554 530.764L203.146 532.162L199.04 538.395L191.875 536.791L186.872 538.842L178.764 536.149L178.442 533.709L170.656 532.105L159.707 536.711L152.369 532.529L155.796 526.273L152.197 521.781L153.324 515.503L150.69 509.934L146.113 512.81L143.41 518.871L135.302 516.179L133.277 509.442L123.145 503.828L114.14 508.766L114.439 513.796L109.183 518.195L110.08 521.781L99.1539 527.911L94.197 524.096L89.2515 525.723L90.4821 530.077L82.972 529.126L82.6039 520.2L79.2111 518.573L79.0041 511.447L73.1156 508.709L68.4347 513.509L69.5618 516.649L72.1725 518.39L71.0914 519.353L66.445 518.172L61.0855 529.068L56.7611 531.222L51.9997 541.992L60.89 543.608L58.0838 548.271L64.7314 560.279L69.3433 562.18V565.767L64.1678 571.828L62.6152 565.526L54.4839 564.655L53.4143 566.03L55.8296 569.364L59.7744 571.679L58.7508 574.073L54.2539 575.643L53.3453 578.782L47.8364 580.822L46.8243 586.631L52.4483 590.446L54.9325 588.212L57.6352 581.246L58.7623 588.647L59.6594 600.987L54.2539 608.618L54.2884 613.155L52.4368 618.047L64.6048 623.879L76.4279 634.408L73.1616 634.878L70.4589 632.862L69.5618 636.906L63.2592 627.488L54.0239 623.455L52.4483 629.516L55.151 634.901L58.7623 636.241L61.4651 642.967L57.1867 642.52L50.8726 634.443L47.7213 621.656L43.5465 615.95L37.1289 627.488L40.2572 641.019L44.5586 647.012L46.8128 646.336L50.5736 648.879L51.7697 652.649L54.4724 652.397L57.1752 650.151L63.2592 658.457L61.9251 669.537L66.859 671.015L63.9263 677.74L60.9015 680.8L61.6836 682.908L66.6405 683.584L66.7785 691.535L76.7729 690.321L80.8328 684.042L80.6028 680.674L95.0135 672.825L93.6564 667.44L99.959 665.87L103.375 660.039L102.869 645.877L105.376 643.654L107.182 647.47L106.963 656.441L104.939 665.859L108.09 671.244L116.877 675.277L118.901 671.691L125.204 673.49L127.458 668.998L130.609 669.674L129.885 660.382L133.542 658.904L134.255 656.235L133.082 654.081L136.245 645.442L142.996 643.643L148.183 638.934L155.382 635.348L159.96 636.837L163.939 635.577L167.09 629.069L172.266 627.946L174.29 630.41H181.501L182.18 636.688L180.156 641.844L175.67 642.44L174.52 636.688L169.575 636.917L167.55 644.101L169.103 648.02L165.975 653.978L162.145 653.302L163.272 648.363L161.627 646.714L156.509 663.843L169.115 666.088L171.599 672.596L175.199 677.305L183.307 675.506L186.688 669.228L192.324 665.641L193.221 661.597L188.494 658.973L189.172 655.994L196.832 656.67L199.753 658.469L202.824 655.88L201.329 647.023L188.379 634.397L188.724 630.203L191.875 627.064L189.621 624.372L194.348 619.88L190.944 617.509L192.542 616.294H200.65L206.056 614.278L206.504 610.015L209.207 612.032L210.104 621.003L214.348 624.177L216.407 630.203L212.807 636.711L210.817 644.8L213.025 649.28L216.694 649.223L221.352 651.961L222.249 647.47L221.122 643.654L223.595 635.806L227.425 632.438L232.336 636.998L238.684 634.019L239.363 629.081L242.066 625.265L243.871 619.204L241.629 615.836L238.247 616.283L238.224 616.271ZM145.871 561.275L143.617 563.968L142.938 567.554C141.604 568.883 135.727 571.816 135.727 571.816H131.449L128.516 569.8C126.779 570.59 121.305 571.816 121.305 571.816L118.372 573.833L110.494 572.939L110.275 569.8L111.563 568.562L107.124 564.415L95.1861 563.074L97.0492 558.995L96.9917 554.767L100.373 553.198L98.5674 548.707C99.8555 548.328 105.997 546.908 105.997 546.908V544.215L110.505 541.523H113.887V545.109L116.589 546.461L117.602 545.349L134.163 542.646L138.671 548.707V552.293L142.559 555.146L145.883 558.571V561.264L145.871 561.275Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-miterlimit="10"/> +<path id="광주광역시" d="M98.5788 548.707C99.8669 548.329 106.008 546.908 106.008 546.908V544.215L110.517 541.523H113.898V545.109L116.601 546.461L117.613 545.35L134.174 542.646L138.683 548.707V552.293L142.57 555.146L145.894 558.572V561.264L143.64 563.957L142.961 567.543C141.627 568.872 135.762 571.805 135.762 571.805H131.483L128.55 569.789C126.814 570.579 121.339 571.805 121.339 571.805L118.407 573.822L110.528 572.928L110.31 569.789L111.598 568.551L107.159 564.403L95.2205 563.063L97.0836 558.984L97.0261 554.756L100.407 553.187L98.6018 548.695L98.5788 548.707Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="제주특별자치도" d="M50.8266 732.613L44.9726 734.641H36.1858L33.2646 738.009L20.1994 742.718L14.794 748.321L1.49878 759.091L3.75298 772.107L17.0482 783.095L21.5451 778.615L45.1911 778.844L50.8266 781.983L64.1103 779.062L69.2973 776.151L84.3866 775.693L91.8163 772.336L95.1976 771.878L99.9245 763.353L109.16 754.165L108.481 740.026L96.0947 730.379L83.248 729.027L77.854 730.597L65.2374 730.826L50.8266 732.613Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/apps/dm/public/next.svg b/apps/dm/public/next.svg new file mode 100644 index 000000000..5174b28c5 --- /dev/null +++ b/apps/dm/public/next.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg> \ No newline at end of file diff --git a/apps/dm/public/vercel.svg b/apps/dm/public/vercel.svg new file mode 100644 index 000000000..770539603 --- /dev/null +++ b/apps/dm/public/vercel.svg @@ -0,0 +1 @@ +<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg> \ No newline at end of file diff --git a/apps/dm/public/window.svg b/apps/dm/public/window.svg new file mode 100644 index 000000000..b2b2a44f6 --- /dev/null +++ b/apps/dm/public/window.svg @@ -0,0 +1 @@ +<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg> \ No newline at end of file diff --git a/apps/dm/tailwind.config.ts b/apps/dm/tailwind.config.ts new file mode 100644 index 000000000..d3eabd953 --- /dev/null +++ b/apps/dm/tailwind.config.ts @@ -0,0 +1,66 @@ +import baseConfig from "../../packages/common/components/tailwind.config"; + +import type { Config } from "tailwindcss"; + +export default { + darkMode: ["class"], + presets: [baseConfig], + content: [ + "./app/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "../../packages/common/components/src/ui/**/*.{js,ts,jsx,tsx,mdx}", + "../../packages/common/business_components/src/ui/**/*.{js,ts,jsx,tsx,mdx}", + ], + plugins: [require("tailwindcss-animate")], + theme: { + extend: { + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + colors: { + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + '1': 'hsl(var(--chart-1))', + '2': 'hsl(var(--chart-2))', + '3': 'hsl(var(--chart-3))', + '4': 'hsl(var(--chart-4))', + '5': 'hsl(var(--chart-5))' + } + } + } + } +} satisfies Config; diff --git a/apps/dm/tsconfig.json b/apps/dm/tsconfig.json new file mode 100644 index 000000000..d897e147f --- /dev/null +++ b/apps/dm/tsconfig.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "verbatimModuleSyntax": true, + "strictNullChecks": true, + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"], + "@common/components/*": ["../../packages/common/components/src/*"], + "@common/components": ["../../packages/common/components/src/index"], + "@common/business_components/*": [ + "../../packages/common/business_components/src/*" + ], + "@common/business_components": [ + "../../packages/common/business_components/src/index" + ], + "@common/network": ["../../packages/common/network"] + } + }, + "include": [ + ".eslintrc.*", + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "**/*.mts", + "**/*.js", + "**/*.cjs", + "**/*.mjs", + "**/*.jsx", + "**/*.json", + ".next/types/**/*.ts" + ], + "exclude": ["**/node_modules", "**/.*/*"] +}