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 (
+ <>
+
+
+
+
+
+ onClickSdmStatus(index)}
+ onPageChange={() => {}}
+ />
+
+
+
+
+
+
수요신청 상세현황
+
+
+
+
+
+ 요구시기
+ {sdmApplyDetail?.dmndTmng?.toString()}
+ 요구수량(m³/일)
+ { }
+
+
+ 사용용도
+ {sdmApplyDetail?.useUsgCd}
+ 희망단가(원/m³)
+ { }
+
+
+ 요구수량
+
+
+ 총대장균군수(개/100ml)
+ { }
+ 결합잔류염소(mg/L)
+ { }
+ 탁도(NTU)
+ { }
+
+
+ 생물학적 산소요구량(BOD)(mg/L)
+ { }
+ 냄새
+ { }
+ 색도(도)
+ { }
+
+
+ 총질소(T-N)(mg/L)
+ { }
+ 총인(T-P)(mg/L)
+ { }
+ 수소이온농도(pH)
+ { }
+
+
+ 염화물(mgCL/L)
+ { }
+ 전기전도도(㎲/cm)
+ { }
+ 비고
+ {sdmApplyDetail?.rmrk}
+
+
+ 공급처 후보지
+
+
+ 후보지1
+
+ 후보지2
+
+ 후보지3
+
+
+
+
+
+
+
+
+
수요신청 진행상태
+
+
+
+
+
+ 진행상태
+ {sdmApplyDetail?.status?.toString()}
+ 공급처매칭결과
+
+
+
+
+
+
+
+ {!isLogin?
+ }
+ bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]"
+ open={!isLogin}
+ onOpenChange={(open) => !open && setIsLogin(false)}
+ noHeaderYn="Y"
+ noFooterYn="Y"
+ />
+ :null}
+
+ >
+ );
+}
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([]);
+ 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 (
+
+
+
+
+
+
유사사례
+ {
+ setSearchUseType({text: searchUseTypeList.find(item => item.val === val)?.text ?? '', value: val});
+ }}
+ className="w-[140px] ml-20"
+ label=""
+ selectClass="w-[140px]"
+ />
+
+
+
+
+
+
+
+ {similarCaseList.length > 0 ? similarCaseList?.map((el: SimilarCaseType, index) => (
+
+ {upHeadSimilarCaseList.map((head) => (
+ head.id && head.id.length > 1 && (
+ 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, ",")}
+
+ )
+ ))}
+
+ )) : <>>}
+
+
+
+
+
+
+
+ )
+}
\ 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>;
+
+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('');
+ const [defaultValues, setDefaultValues] = useState({
+ 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({
+ 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 = 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 (
+
+
+
+ {!isLogin?
+
}
+ bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]"
+ open={!isLogin}
+ onOpenChange={(open) => !open && setIsLogin(false)}
+ noHeaderYn="Y"
+ noFooterYn="Y"
+ />
+ :null}
+
+
+ );
+}
+
+export default function MyForm() {
+ return (
+ Loading...}>
+
+
+ );
+}
+
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(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 (
+
+
+
+
+ );
+}
+
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 (
+
+
+
+ 수요처 로그인
+
+
+
+
+
+
+ );
+}
\ 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 (
+
+ {/* 사이드바 */}
+
+
+ {NAVS.map((nav, idx) => (
+ { 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' : ''}`}
+ >
+
+ {nav.label}
+
+ ))}
+
+
+
+ { cardRefs.current[0] = el; }} className="card-wrap pl-[100px] mb-24 !pt-0" id="card1" aria-labelledby="card1-title">
+
+
+ 물재이용이란
+
+
+ 물 재이용이 무엇인가요?
+
+ 물 재이용은 빗물, 오수, 하수처리수, 폐수처리수 및 발전소 온배수를
+ 물 재이용시설을 이용하여 처리하고, 그 처리된 물을 생활, 공업, 농업, 조경,
+ 하천 유지 등의 용도로 이용하는 것을 말합니다.
+
+
+
+ 환경부는 2011년에 '물의 재이용 촉진 및 지원에 관한 법률'을 제정하여
+ 한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다.
+
+
+
+
+ {/* 카드2 */}
+ { cardRefs.current[1] = el; }} className="card-wrap pl-[100px] mb-24" id="card2" aria-labelledby="card2-title">
+
+
+ 물재이용 필요성
+
+
+ 물 재이용, 왜 필요한가요?
+
+ 빗물, 중수, 하수처리수는 지속적으로 이용 가능한 순환자원입니다.
+ 물 재이용을 통한 효율적인 수자원 활용은 물 스트레스국가인 우리나라에서 가뭄대응능력 향상 및 하천수질보전을 위해 반드시 필요한 일입니다.
+
+
+
+ - 우리나라의 기후변화 진행속도는 세계 평균을 상회하고 있습니다.
+ - 기후변화로 인한 국지적인 가뭄은 더욱 자주, 더욱 심하게 일어나고 있으며, 우리의 물 안보를 위협하고 있습니다.
+
+
+
+ - 우리나라의 1인당 강수량은 2,546㎥ 으로 세계 평균 15,044㎥의 약 1/6 수준입니다.
+ - 우리나라 강수량의 70%는 여름철에 집중되어 있어 이용 가능한 수자원확보가 어렵습니다.
+
+
+
+
+
+ {/* 카드3 */}
+ { cardRefs.current[2] = el; }} className="card-wrap pl-[100px] mb-24" id="card3" aria-labelledby="card3-title">
+
+
+ 빗물 재이용
+
+
+ 친환경 수자원 빗물, 편리하게 사용할 수 있어요!
+ 가뭄을 극복하고 홍수를 예방하기 위해 비가 내릴 때 빗물을 모아두었다가 사용합니다.
+
+
+
+ - 의무 설치대상: 지붕면적 1,000㎡ 이상의 운동장, 체육관, 공공업무시설 등
+ - 설 치 기 준: 집수시설, 처리시설, 빗물 저류조(지붕 집수면적 x 0.05m 이상)
+ - 활 용 용 도: 골프장 등의 조경용수, 화장실용수, 세척·살수용수 등
+
+
+
+
+ {/* 카드4 */}
+ { cardRefs.current[3] = el; }} className="card-wrap pl-[100px] mb-24" id="card4" aria-labelledby="card4-title">
+
+
+ 중수 재이용
+
+
+ 요모조모 쓸모 많은 중수, 알뜰하게 재이용할 수 있어요!
+ 한번 사용한 물을 중수도 시설을 통해 재생하여 개별적 또는 지역적으로 재이용할 수 있습니다.
+
+
+
+ - 의무 설치대상: 일정규모 이상의 대규모점포, 숙박/목욕장, 공장, 각종 개발사업 등
+ - 의무 재이용량: 물 사용량의 10% 이상
+ - 활 용 용 도: 청소·화장실용수, 세척·살수용수, 조경용수, 공업용수 등
+
+
+
+
+ {/* 카드5 */}
+ { cardRefs.current[4] = el; }} className="card-wrap pl-[100px] mb-24" id="card5" aria-labelledby="card5-title">
+
+
+ 하수 재이용
+
+
+ 아직 할 일이 많은 하수처리수, 풍족하게 재이용할 수 있어요!
+ 사용하고 버려져 공공하수처리장으로 모여든 물은 용도에 맞는 처리 후 재이용 할 수 있습니다.
+
+
+
+ - 의무 이용대상: 공공하수처리장(시설용량 5,000㎥/일 이상)
+ - 의무 재이용량: 일 하수처리량의 10% 이상
+ - 활 용 용 도: 공업용수, 하천유지용수, 농업용수 등
+
+
+
+
+ {/* 카드6 */}
+ { cardRefs.current[5] = el; }} className="card-wrap pl-[100px] mb-24" id="card6" aria-labelledby="card6-title">
+
+
+ 물재이용 목표
+
+
+ 2030년 물 재이용 목표를 위해 모두의 관심과 노력이 필요합니다.
+
+ 환경부는 2030년까지의 제2차 물 재이용 기본계획을 수립하여 한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다.
+
+
+
+
+
+
도시 물 순환이용 체계로의 전환
+
+ - 지속가능한 물 순환이용체계 구축
+ - 물 재이용시설 관리체계 강화
+
+
+
+
+
유역기반 하수처리수 재이용 수요확대 및 공급체계 구축
+
+ - 유역기반 통합물관리 연계 강화
+ - 하수처리수 재이용 공업용수 공급 활성화
+ - 하수처리수 재이용을 활용한 물순환 촉진
+
+
+
+
+
물 재이용 인식제고 및 산업발전 기반 마련
+
+ - 물 재이용 교육 및 대국민 홍보 강화
+ - 핵심기술 육성 및 산업발전 지원 강화
+
+
+
+
+
+
+
+ );
+}
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 (
+
+
+
+
+ 물 재이용사례
+
+
+
+
+ {/* 카드 1 */}
+
+
+
+ 사업사례 1
+
+
+ {/* 사업명 */}
+
+ 사업명
+
+ 경기권역 반도체 사업장 물 재이용 사업
+ : 삼성전자 반도체 사업장 하수재이용수 공급(재이용시설 설치, 공급망 구축 등)
+
+
+ {/* 사업 배경 및 목적 */}
+
+ 사업 배경 및 목적
+
+ 삼성전자는 환경경영전략 실현과 반도체 공장의 안정적 용수 공급을 위해 하수 재이용수를 공업용수로 도입하기로 결정
+
+
+ {/* 사업 주요 내용 */}
+
+ 사업 주요 내용
+
+ 1단계 사업[화성·오산(12만㎥/일) → 삼성 화성·기흥(29년 공급)]
+ 2단계 사업[수원(21만㎥/일) → 삼성 평택(30년 공급)]
+
+
+ {/* 특이사항 */}
+
+ 특이사항
+
+ 본 사업은 광역 하수 재이용 공급망 구축의 첫 사업으로 초기 민관협의체를 구성·운영 후 관계기관(환경부, 경기도, 지자체, 공단, 수공, 삼성전자) 간 MOU 체결을 통해 사업을 추진하였으며, 반도체 사업장에 하수 재이용수를 공업용수로 공급하는 국내 첫 사업으로 위협부담 해소를 위해 사업을 단계별로 시행
+
+
+ {/* 단계별 시각 정보 */}
+
+
+
+
+ {/* 카드2 */}
+
+
+
+ 사업사례 2
+
+
+ {/* 사업명 */}
+
+ 사업명
+
+ 경기도 파주시 하수재이용
+ : 운정·금촌하수처리시설의 하수처리수를 막공법(U/F, R/O)으로 재처리하여 파주 LCD 산업단지내 3개 업체*에 공업용수 공급
+ *LG디스플레이(P10공장), 희성전자, 엔비텍 등
+
+
+ {/* 사업 방식 */}
+
+ 사업 방식
+
+ 민간투자사업(시행사 : 파주그린허브(주), 운영사 : 테크로스)
+
+
+ {/* 사업 주요 내용 */}
+
+ 사업 주요 내용
+
+ 처리공정 : 전처리분리막(U/F) + 역삼투서비(R/O)
+ 공급량 : 4만m³/일
+ 관로 : 13.6km
+ 사업비 : 651억원
+
+
+ {/* 특이사항 */}
+
+ 특이사항
+
+ 하수처리수 재이용시설 자체 농축폐수 처리공정 개발 및 적용 성공사례
+
+
+ {/* 좌측 이미지 */}
+
+
+
+
+ {/* 우측 표 */}
+
+ {/* 하수처리수 공급 */}
+
+
+
+
+
+
+
+
+
+ 하수처리수 공급
+
+
+ 위치
+ 금촌하수
+ 운정하수
+
+
+ 시설용량
+ 44,000㎥/일
+ 60,000㎥/일
+
+
+ 유입량
+ 27,000㎥/일 자연유하
+ 30,000㎥/일 이송펌프장
+
+
+
+ {/* 첫 화살표 (자연유하 / 관로이송) */}
+
+
+ 자연유하: 161m
+
+
+
+
+ 관로이송: 3.2km
+
+
+
+
+ {/* 물재이용 처리시설 */}
+
+
+
+
+
+
+
+
+ 물재이용 처리시설
+
+
+ 유입유량
+ 57,000㎥/일
+
+
+ 재이용시설
+ 40,000㎥/일
+
+
+ 농축폐수처리
+ 17,000㎥/일
+
+
+
+ {/* 두 번째 화살표 (공급관로) */}
+
+
+
+ {/* 수요처 공급 */}
+
+
+
+
+
+
+
+
+ 수요처 공급
+
+
+ 수요처
+ LG디스플레이 외 2개사
+
+
+ 수요량
+ 40,000㎥/일
+
+
+
+
+
+
+
+
+
+
+ {/* 카드3 */}
+
+
+
+ 사업사례 3
+
+
+ {/* 사업명 */}
+
+ 사업명
+
+ 충남 보령 하수재이용
+ : 보령·신보령화력발전소에 하루 1만톤의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업 추진
+
+
+ {/* 사업 방식 */}
+
+ 사업 방식
+
+ 2017년 한국환경공단과 사업대행 위 · 수탁 협약 체결
+
+
+ {/* 사업 주요 내용 */}
+
+ 사업 주요 내용
+
+ 처리공정 : 전처리분리막(U/F) + 역삼투서비(R/O)
+ 공급량 : 1만m3/일
+ 관로 : 8.7km
+ 사업비 : 315억원(국비 216억원, 재원협의(안))
+
+
+ {/* 특이사항 */}
+
+ 특이사항
+
+ 국내 유일 하수재이용 공업용수 공급 재정사업으로 2016년 충남 서부권 가뭄 시 지역사회 기여 차원에서 기관간 MOU (환경부, 충청남도, 보령시, 한국중부발전㈜)를 바탕
+ 으로 추진 되어 경제성보다 공익성을 바탕으로 국가전력 기반시설의 안정적 운영 도모
+
+
+
+
+
+
+
+
+ {/* 카드4 */}
+
+
+
+ 사업사례 4
+
+
+ {/* 사업명 */}
+
+ 사업명
+
+ 경기도 수원시 빗물이용과 중수도
+
+
+ {/* 사업 내용 */}
+
+ 사업 내용
+
+ (빗물) 월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물 공급기를 설치하여 혹서기 도시 열섬효과 및 도로 비산먼지 저감
+ ※ 시설 가동중 도로 노면 온도의 7.2 ~ 26.3℃ 감소 효과 확인(21.7월, 수원시청).
+ 다만, 가뭄기나 강우량이 적을 경우 빗물을 원활히 공급하기 다소 곤란
+ - (중수도) 수원시청, 장안구청, 월드컵경기장에 빗물 연계 중수도를 화장실·조경용수 등으로 사용하여 수도요금 절감(연 7백~30백만원)
+
+
+
+
+ <월드컵경기장 인근도로 노면 살수>
+
+
+
+ <주유기 모양의 빗물 공급기>
+
+
+
+ <빗물주유기에서 살수용수 공급>
+
+
+
+ <빗물 공급기 및 빗물분사장치 위치>
+
+
+
+
+
+
+
+
+ );
+}
+
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 (
+
+
+ {/* 카드 1 - 물재이용 사업 추진 절차 */}
+
+
+
+ 물재이용 사업 추진 절차
+
+
+
+ 물 재이용 추진 절차 상세 표
+
+
+
+
+
+
+
+
+ {/* 1단계 */}
+ 타당성 조사
+ ↓
+ {/* 2단계 */}
+ 매칭시스템 신청 사업계획(안) 수립
+ ↓
+ {/* 3단계 */}
+ 하수도정보시스템 신청 국고지원 신청 → 기술검토 (한국환경공단)
+ ↓
+ {/* 4단계 */}
+ 국고지원 확정 통보
+ ↓
+ {/* 5단계 */}
+ 사업계획 수립(실시설계)
+ ↓
+ {/* 6단계 */}
+ (행정관) 기술검토 신청 재원협의 (지방환경관서장) → 기술검토 (한국환경공단)
+ ↓
+ {/* 7단계 */}
+ (행정관) 기술검토 신청 설치승인 (시도지사) → 기술검토 (한국환경공단)
+ ↓
+ {/* 8단계 */}
+ 공사시행
+ ↓
+ {/* 9단계 */}
+ (행정관) 기술검토 신청 준공(국고정산) → 기술검토 (한국환경공단)
+
+
+
+
+
+ {/* 카드 2 - 물재이용 매칭 시스템 */}
+
+
+
+ 물재이용 매칭 시스템이란?
+
+
+ {/* 개요 */}
+
+ 개요
+
+ 기후변화 및 지자체의 개발속도 차이에 따라 물 자원의 수요-공급 불균형이 발생하여 빗물, 중수도, 하수처리 재처리수 등의 공급 여유가 있는 지자체의 재이용수를 부족한 지자체 수요처에 공급하여 불균형 해소를 통한 수자원 절약 및 물 자원 순환
+
+
+
+ {/* 구성 */}
+
+ 구성
+
+ 공급자: 하수처리장, 빗물이용시설, 중수도 등
+ 수요자: 산업체(냉각수), 공공시설(조경, 도로청소), 농민(농업용수) 등
+ 시스템: 수질, 수량, 거리 기반 매칭, 중개
+ 운영주체: 한국환경공단
+
+
+
+ {/* 절차 */}
+
+ 사업 주요 내용
+
+ ① 공급자 등록 → ② 수요자 조건 입력 → ③ 매칭 검토 → ④ 시스템 매칭 → ⑤ 계약·공급 이행
+
+
+
+ {/* 기대효과 */}
+
+ 기대효과
+
+ 물 자원 수요-공급 조정을 통한 효율적 분배
+ 물 재이용시설 데이터 수집으로 체계적 관리
+ 지자체, 사업시행자 등 재이용 정보 획득 용이
+
+
+
+
+
+
+
+ );
+}
\ 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(null);
+ const [originalFills, setOriginalFills] = useState>({});
+ const [hoveredRegion, setHoveredRegion] = useState(null);
+ const [tooltipPosition, setTooltipPosition] = useState<{ x: number; y: number } | null>(null);
+ const [favorites, setFavorites] = useState>({});
+ const svgContainerRef = useRef(null);
+ const [chartData, setChartData] = useState([
+ {
+ 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) 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 = {
+ 서울: '서울특별시',
+ 부산: '부산광역시',
+ 대구: '대구광역시',
+ 인천: '인천광역시',
+ 광주: '광주광역시',
+ 대전: '대전광역시',
+ 울산: '울산광역시',
+ 세종: '세종특별자치시',
+ 경기: '경기도',
+ 강원: '강원특별자치도',
+ 충북: '충청북도',
+ 충남: '충청남도',
+ 전북: '전라북도',
+ 전남: '전라남도',
+ 경북: '경상북도',
+ 경남: '경상남도',
+ 제주: '제주특별자치도'
+ };
+
+ 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 = "SVG 로딩 실패
";
+ }
+ }
+ };
+
+ 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 (
+
+ {/* 우측 콘텐츠 영역 */}
+
+ {/* 상단 제목 */}
+
+
+ {/* 콘텐츠 본문 */}
+
+ {/* 지도 카드 */}
+
+ {/* 타이틀 영역 */}
+
+
재이용시설개소수(2023)
+
행정구역별 재이용시설 개소수 조회
+
+
+ {/* 본문 콘텐츠 */}
+
+
+
+
+ {typeof window !== 'undefined' && hoveredRegion && tooltipPosition && (
+
+
{hoveredRegion}
+
+ {colorLegend.map(({ label, color, key }) => (
+
+
+
+ {label}:{' '}
+ {tableData.find(d => d.region === hoveredRegion)?.[key as keyof TableData] ?? '-'}
+
+
+ ))}
+
+
+ )}
+
+ {/* 범례 */}
+
+
+
+ 하수재이용시설 (총 717건)
+
+
+
+ 빗물이용시설 (총 3,730건)
+
+
+
+ 중수도시설 (총 1,074건)
+
+
+
+ 온배수시설 (총 201건)
+
+
+
+
+
+
+ {/* 우측 콘텐츠 (표 + 차트) */}
+
+ {/* 재이용시설현황 표 */}
+
+ {/* 타이틀 + 버튼 영역 */}
+
+
+
재이용시설현황(2023)
+
행정구역별 재이용시설 현황 비교
+
+
+
+
+
+ 행정구역별 하수, 빗물, 중수도, 온배수 재이용시설 수
+
+
+ 번호
+ 시도
+ 하수
+ 빗물
+ 중수도
+ 온배수
+
+
+
+ {tableData.map((row, index) => (
+
+
+ {index+1}
+
+ {row.region}
+ {row.sewage}
+ {row.rainwater}
+ {row.reclaimed}
+ {row.thermal}
+
+ ))}
+
+
+
+
+
+
+ {/* 재이용통계 차트 */}
+
+
+
재이용통계(2023)
+
항목별 통계 보기
+
+
+
+
+
+
+
+ );
+};
+
+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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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;
+}
+
+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
+
+export const StoreContext = createContext(
+ undefined,
+)
+
+export interface StoreProviderProps {
+ children: ReactNode
+}
+
+export const StoreProvider = ({
+ children,
+}: StoreProviderProps) => {
+ const storeRef = useRef(null)
+ if (!storeRef.current) {
+ // Create store instance if not initialized
+ storeRef.current = StoreZus() as StoreApi
+ }
+ return (
+
+ {children}
+
+ )
+}
+
+export const useDmStore = (
+ 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
+//react에서 사용하기 위한 context 생성.
+export const StoreContext = createContext(
+ undefined,
+)
+```
+
+### component로 작성
+등록된 context를 component화 시키기 위한 작업
+```js
+//외부에서 호출시 형태로 호출.
+//app이나 home에서 전체적인 사용이 되도록 작성해야함.
+export const StoreProvider = ({
+ children,
+}: StoreProviderProps) => {
+ const storeRef = useRef(null)
+ if (!storeRef.current) {
+ //StoreZus는 store.tsx를 봐야함.
+ storeRef.current = StoreZus()
+ }
+ return (
+
+ {children}
+
+ )
+}
+```
+
+### store 등록
+react에서 사용하는 store에 현재 context 등록
+```js
+export const useWrStore = (
+ 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()((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;
+ insertSdmInfoData: (data: DemandCompType) => Promise;
+ getDemandCompList: (searchText: string, pageCnt: number) => Promise;
+ actionLogin: (data: DemandCompType) => Promise;
+ getSdmInfoStatusList: (dmdInfoId: string) => Promise;
+ getSimilarCaseList: (part: string, value: number|string) => Promise;
+}
+
+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()((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) => (
+
+
+
+
+
+
+
+
+
+ {title}
+
+
+ {body}
+
+
+
+
+
+ toast.dismiss(t.id)}
+ >
+ {buttonName}
+
+
+
+ toast.dismiss(t.id)}
+ size='sm'
+ >
+ 닫기
+
+
+
+ ))
+ )
+}
\ 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(null);
+ const handleInputChange = useCallback((e: React.ChangeEvent) => {
+ 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 (
+
+
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 && (
+
+
+ {demandCompList && demandCompList.length > 0 ? demandCompList.map((item : DemandCompType, index : number) => (
+
handleSuggestionClick(item.dmdCoNm)}
+ role="button"
+ tabIndex={0}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ handleSuggestionClick(item.dmdCoNm);
+ }
+ }}
+ >
+ {item.dmdInfoId.substring(10,14) !== '0000' ? (
+ <>
+ 사업자번호 : {item.dmdInfoId.substring(0,10)}
+ 종사업자번호 : {item.dmdInfoId.substring(10,14)}
+ >
+ ) : (
+ <>
+ 사업자번호 : {item.dmdInfoId.substring(0,10)}
+ >
+ )}
+ {item.dmdCoNm}
+
+ )) : (
+
+ 검색 결과가 없습니다.
+
+ )}
+
+
+ )}
+
+ )
+}
\ 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 {
+ field: ControllerRenderProps;
+ label: string;
+ required?: boolean;
+ error?: FieldError;
+ inputComponent: ReactNode;
+}
+
+export function AccessibleFormField({
+ field,
+ label,
+ required = false,
+ error,
+ inputComponent,
+}: AccessibleFormFieldProps) {
+ return (
+
+ {label}
+ {required && * }
+
+ }
+ >
+
+ {inputComponent}
+ {error?.message && Array.from(new Set(error.message.split('\n'))).map((msg, idx) => (
+
+ {msg}
+
+ ))}
+
+
+ );
+}
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;
+ onCheckboxChange?: (checked: boolean) => void;
+ facilityName?: string;
+}
+
+type FieldComponent = (field: ControllerRenderProps) => 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(searchParams.get('dmdCoNm') || '');
+ const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(false);
+ const [isSimilarCase, setIsSimilarCase] = useState(false);
+ const [key, setKey] = useState(0);
+
+ //모달 조회에 필요한 데이터
+ const [sply, setSply] = useState(0);
+ const [usageCd, setUsageCd] = useState('청소');
+ const [money, setMoney] = useState(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) => (
+
+ {
+ setSdmNmValue(val || '');
+ field.onChange(val || '');
+ }}
+ setRtnData={selectComp}
+ initValue={facilityName || sdmNmValue}
+ {...field}
+ />
+
+ )
+ },
+ {
+ name: "demandYmd",
+ label: "요구시기",
+ formIndex: 0,
+ required: true,
+ component: (field) => (
+
+ {
+ 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}
+ />
+
+ )
+ },
+ {
+ name: "demandQnty",
+ label: "요구수량(㎥/d)",
+ formIndex: 0,
+ required: true,
+ component: (field) => (
+ {
+ 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) => (
+ {
+ field.onChange(val.toString());
+ setUsageCd(val.toString());
+ }}
+ className=""
+ value={field.value as string}
+ />
+ )
+ },
+ {
+ name: "money",
+ label: "희망단가(원/m3)",
+ formIndex: 0,
+ required: true,
+ component: (field) => (
+
+ {
+ field.onChange(e);
+ setMoney(Number(e.target.value));
+ }}
+ onBlur={field.onBlur}
+ name={field.name}
+ ref={field.ref}
+ />
+
+ }
+ />
+ )
+ },
+ {
+ name: "reqWaterQlty",
+ label: "요구수질",
+ formIndex: 0,
+ required: true,
+ component: undefined
+ },
+ {
+ name: "grssClfg",
+ label: "총대장균군수(개/100ml)",
+ formIndex: 1,
+ required: true,
+ component: (field) => (
+
+ )
+ },
+ {
+ name: "tn",
+ label: "총질소(T-N)(mg/L)",
+ formIndex: 1,
+ required: true,
+ component: (field) => (
+
+ )
+ },
+ {
+ name: "combResdch",
+ label: "결합잔류염소(mg/L)",
+ formIndex: 1,
+ required: true,
+ component: (field) => (
+
+ )
+ },
+ {
+ name: "tph",
+ label: "총인(T-P)(mg/L)",
+ formIndex: 1,
+ required: true,
+ component: (field) => (
+
+ )
+ },
+ {
+ name: "trbt",
+ label: "탁도(NTU)",
+ formIndex: 1,
+ required: true,
+ component: (field) => (
+
+ )
+ },
+ {
+ name: "ph",
+ label: "수소이온농도(pH)",
+ formIndex: 1,
+ required: true,
+ component: (field) => (
+
+ )
+ },
+ {
+ name: "bod",
+ label: "생물학적 산소요구량(BOD)(mg/L)",
+ formIndex: 1,
+ required: true,
+ component: (field) => (
+
+ )
+ },
+ {
+ name: "clIonQnt",
+ label: "염화물(mgCL/L)",
+ formIndex: 1,
+ required: true,
+ component: (field) => (
+
+ )
+ },
+ {
+ name: "smll",
+ label: "냄새",
+ formIndex: 1,
+ required: true,
+ component: (field) => (
+
+ )
+ },
+ {
+ name: "cdtnv",
+ label: "전기전도도(㎲/cm)",
+ formIndex: 1,
+ required: true,
+ component: (field) => (
+
+ )
+ },
+ {
+ name: "crmty",
+ label: "색도(도)",
+ formIndex: 1,
+ required: true,
+ component: (field) => (
+
+ )
+ },
+ {
+ name: "rmrk",
+ label: "비고",
+ formIndex: 1,
+ required: true,
+ component: (field) => (
+
+ )
+ },
+ {
+ name: "candidateTitle",
+ label: CANDIDATE || "공급처 후보지 선택(필수아님)",
+ formIndex: 2,
+ component: (field) => (
+
+ )
+ },
+ ];
+
+ 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 ? (
+ (
+ name === 'money' ? component(field) : (
+ //
+ // {component(field)}
+ //
+
+ )
+ )}
+ />
+ ) : null
+ ))}
+
+ {groupFields.length > 0 && (
+
+
요구수질
+
+ {groupFields.map(({ name, label, component, required }) => (
+
+
(
+ //
+ // {component?.(field)}
+ //
+
+ )}
+ />
+
+ ))}
+
+
+ )}
+
+ {candidateFields.map(({ name, label, component, required }, index) => (
+ component ? (
+ (
+ //
+ // {component(field)}
+ //
+
+ )}
+ />
+ ) : null
+ ))}
+
+ {isSimilarCase && (
+ }
+ 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>;
+ type FieldComponent = (field: ControllerRenderProps) => 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({
+ 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) => (
+
+ )
+ },
+ {
+ name: "bznmTotNo",
+ label: BZNM_TOT_NO || "종사업자등록번호",
+ component: (field) => (
+
+
+
+ )
+ },
+ {
+ name: "password",
+ label: "비밀번호",
+ component: (field) => (
+
+ )
+ }
+ ];
+
+ return (
+ <>
+
+
+ {formFields.map(({ name, label, component }) => (
+ (
+ label==='종사업자등록번호' ?
+ <>
+ {component && component(field)}
+ >
+ :
+ {component && component(field)}
+
+ )}
+ />
+ ))}
+
+
+
+ 로그인
+
+
+
+ >
+ );
+}
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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", "**/.*/*"]
+}