Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70db7662bb | ||
|
|
bb57ff72e7 | ||
|
|
9eef99a435 | ||
|
|
daff1610e3 | ||
|
|
5e3d263750 | ||
|
|
ced62ec0c4 | ||
|
|
3c65e6f8f5 | ||
|
|
1efe2dbe89 | ||
|
|
36e56dcaef | ||
|
|
e42e28d062 | ||
|
|
0786053022 | ||
|
|
791ec1ae2d | ||
|
|
4e4942313a | ||
|
|
e6fcbfbde4 | ||
|
|
ed4d1b73cb | ||
|
|
e3dac52d7e | ||
|
|
d1d8a1eff3 | ||
|
|
4502d55ce1 | ||
|
|
3d3b978070 | ||
|
|
117082c862 | ||
|
|
bfa03aabc0 | ||
|
|
f535cd1010 | ||
|
|
2f6d4ebec1 | ||
|
|
1c3cc5b61b | ||
|
|
db7584cb01 | ||
|
|
b842408f41 | ||
|
|
6e33a62ed9 | ||
|
|
36231a5eca | ||
|
|
84fb81b245 | ||
|
|
e18f759fab | ||
|
|
ce9c7b17a2 | ||
|
|
c2d6f7fe37 | ||
|
|
9b18aa9cef | ||
|
|
5eccb58aa9 | ||
|
|
acce1364fa | ||
|
|
625c5f5fe2 | ||
|
|
dffa9d74a0 | ||
|
|
668a8ef844 | ||
|
|
df4d708981 | ||
|
|
c48a46c7ab | ||
|
|
da844fb3a7 | ||
|
|
3e1f89d90f | ||
|
|
aaf898fb4c | ||
|
|
533e89c919 | ||
|
|
aab8cf9622 |
@@ -19,11 +19,15 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"],
|
"@/*": ["./src/*"]
|
||||||
"@common/components": ["../../packages/common/components/dist/"],
|
|
||||||
"@common/network": ["../../packages/common/network/"]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
"../../packages/common/types/**/*.d.ts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules", ".next", "dist", "coverage", "storybook-static", "public"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { DesignEditPage } from "@/pages/design-cnstn";
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Page({ params }: PageProps) {
|
||||||
|
const { id } = await params;
|
||||||
|
return <DesignEditPage id={id} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { DesignViewPage } from "@/pages/design-cnstn";
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Page({ params }: PageProps) {
|
||||||
|
const { id } = await params;
|
||||||
|
return <DesignViewPage id={id} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { DesignListPage } from "@/pages/design-cnstn";
|
||||||
|
|
||||||
|
export default DesignListPage;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { DesignRegistPage } from "@/pages/design-cnstn";
|
||||||
|
import { parseTaskSeComCdFromApi } from "@/pages/design-cnstn/lib/design-cnstn-regist-schema";
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
searchParams: Promise<{ taskSeComCd?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Page({ searchParams }: PageProps) {
|
||||||
|
const params = await searchParams;
|
||||||
|
const taskSeComCd = parseTaskSeComCdFromApi(params.taskSeComCd);
|
||||||
|
return <DesignRegistPage taskSeComCd={taskSeComCd.length ? taskSeComCd : undefined} />;
|
||||||
|
}
|
||||||
@@ -32,7 +32,6 @@
|
|||||||
"next": "15.5.18",
|
"next": "15.5.18",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"tus-js-client": "^4.3.1",
|
"tus-js-client": "^4.3.1",
|
||||||
"uuid": "^14.0.1",
|
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config";
|
import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config";
|
||||||
import { type AuthPayload, type AuthService, getRemainingMs } from "@common/auth";
|
import {
|
||||||
|
type AuthPayload,
|
||||||
|
type AuthService,
|
||||||
|
clearDeviceId,
|
||||||
|
getOrCreateDeviceId,
|
||||||
|
} from "@common/auth";
|
||||||
|
|
||||||
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
|
import { clearRefreshTimer, setToken, tokenManager } from "../lib/token-manager";
|
||||||
import { clearRefreshTimer, setToken, setupRefreshTimer, tokenManager } from "../lib/token-manager";
|
|
||||||
|
|
||||||
async function login(payload: AuthPayload): Promise<void> {
|
async function login(payload: AuthPayload): Promise<void> {
|
||||||
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
|
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
|
||||||
@@ -26,10 +30,10 @@ async function login(payload: AuthPayload): Promise<void> {
|
|||||||
setToken(CLAIM_NAME, accessToken);
|
setToken(CLAIM_NAME, accessToken);
|
||||||
setToken(AUTH_USER_ID, tokenId);
|
setToken(AUTH_USER_ID, tokenId);
|
||||||
|
|
||||||
const remainingMs = getRemainingMs(accessToken);
|
// const remainingMs = getRemainingMs(accessToken);
|
||||||
if (remainingMs > 0) {
|
// if (remainingMs > 0) {
|
||||||
setupRefreshTimer(silentRefresh, remainingMs);
|
// setupRefreshTimer(silentRefresh, remainingMs);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function silentRefresh(): Promise<void> {
|
async function silentRefresh(): Promise<void> {
|
||||||
@@ -58,10 +62,10 @@ async function silentRefresh(): Promise<void> {
|
|||||||
setToken(CLAIM_NAME, accessToken);
|
setToken(CLAIM_NAME, accessToken);
|
||||||
setToken(AUTH_USER_ID, tokenId);
|
setToken(AUTH_USER_ID, tokenId);
|
||||||
|
|
||||||
const remainingMs = getRemainingMs(accessToken);
|
// const remainingMs = getRemainingMs(accessToken);
|
||||||
if (remainingMs > 0) {
|
// if (remainingMs > 0) {
|
||||||
setupRefreshTimer(silentRefresh, remainingMs);
|
// setupRefreshTimer(silentRefresh, remainingMs);
|
||||||
}
|
// }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[silentRefresh] Failed at ${new Date().toISOString()}`, error);
|
console.error(`[silentRefresh] Failed at ${new Date().toISOString()}`, error);
|
||||||
tokenManager.clearAuthToken();
|
tokenManager.clearAuthToken();
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
|
|
||||||
const DEVICE_ID_KEY = "x-device-id";
|
|
||||||
|
|
||||||
export function getOrCreateDeviceId(): string {
|
|
||||||
if (typeof window === "undefined") return "";
|
|
||||||
const existing = localStorage.getItem(DEVICE_ID_KEY);
|
|
||||||
if (existing) return existing;
|
|
||||||
const id = uuidv4();
|
|
||||||
localStorage.setItem(DEVICE_ID_KEY, id);
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearDeviceId(): void {
|
|
||||||
if (typeof window === "undefined") return;
|
|
||||||
localStorage.removeItem(DEVICE_ID_KEY);
|
|
||||||
}
|
|
||||||
@@ -18,13 +18,7 @@ interface CodeSelectProps extends React.ComponentPropsWithoutRef<typeof Select>
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CodeSelect({
|
function CodeSelect({ upCd, useYn, placeholder = "선택", className, ...props }: CodeSelectProps) {
|
||||||
upCd,
|
|
||||||
useYn,
|
|
||||||
placeholder = "선택",
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: CodeSelectProps) {
|
|
||||||
const listQuery = useCodeList(useYn ? undefined : upCd);
|
const listQuery = useCodeList(useYn ? undefined : upCd);
|
||||||
const listByUseYnQuery = useCodeListByUseYn(useYn ? upCd : undefined, useYn ?? "");
|
const listByUseYnQuery = useCodeListByUseYn(useYn ? upCd : undefined, useYn ?? "");
|
||||||
|
|
||||||
@@ -34,7 +28,7 @@ function CodeSelect({
|
|||||||
return (
|
return (
|
||||||
<Select {...props} disabled={props.disabled || isLoading}>
|
<Select {...props} disabled={props.disabled || isLoading}>
|
||||||
<SelectTrigger className={cn("w-[12rem]", className)}>
|
<SelectTrigger className={cn("w-[12rem]", className)}>
|
||||||
<SelectValue placeholder={isLoading ? "로딩 중…" : placeholder} />
|
<SelectValue placeholder={placeholder} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{codes?.map((item) => (
|
{codes?.map((item) => (
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { client } from "@/shared/api";
|
||||||
|
import { ENDPOINT } from "@/shared/config";
|
||||||
|
import type { PageResponse } from "@/shared/model";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
DesignCnstnBizCstId,
|
||||||
|
DesignCnstnBizCstResponseDto,
|
||||||
|
DesignCnstnBizCstSaveRequestDto,
|
||||||
|
DesignCnstnBizCstSearchDto,
|
||||||
|
DesignCnstnBizCstService,
|
||||||
|
DesignCnstnBizCstUpdateRequestDto,
|
||||||
|
} from "../model/design-cnstn-biz-cst-interface";
|
||||||
|
|
||||||
|
const BASE = ENDPOINT.AS_SERVICE.DESIGN_CNSTN_BIZ_CST;
|
||||||
|
|
||||||
|
async function findPage(
|
||||||
|
searchDto: DesignCnstnBizCstSearchDto,
|
||||||
|
): Promise<PageResponse<DesignCnstnBizCstResponseDto>> {
|
||||||
|
const response = await client.get<PageResponse<DesignCnstnBizCstResponseDto>>(BASE, {
|
||||||
|
params: searchDto,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findById(
|
||||||
|
designCnstnDocNo: string,
|
||||||
|
acntgSeCd: string,
|
||||||
|
): Promise<DesignCnstnBizCstResponseDto> {
|
||||||
|
const response = await client.get<DesignCnstnBizCstResponseDto>(
|
||||||
|
`${BASE}/${designCnstnDocNo}/${acntgSeCd}`,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save(body: DesignCnstnBizCstSaveRequestDto): Promise<DesignCnstnBizCstId> {
|
||||||
|
const response = await client.post<DesignCnstnBizCstId>(BASE, body);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update(
|
||||||
|
designCnstnDocNo: string,
|
||||||
|
acntgSeCd: string,
|
||||||
|
body: DesignCnstnBizCstUpdateRequestDto,
|
||||||
|
): Promise<void> {
|
||||||
|
await client.put<void>(`${BASE}/${designCnstnDocNo}/${acntgSeCd}`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteDesignCnstnBizCst(designCnstnDocNo: string, acntgSeCd: string): Promise<void> {
|
||||||
|
await client.delete<void>(`${BASE}/${designCnstnDocNo}/${acntgSeCd}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const designCnstnBizCstService = {
|
||||||
|
findPage,
|
||||||
|
findById,
|
||||||
|
save,
|
||||||
|
update,
|
||||||
|
delete: deleteDesignCnstnBizCst,
|
||||||
|
} satisfies DesignCnstnBizCstService;
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { client } from "@/shared/api";
|
||||||
|
import { ENDPOINT } from "@/shared/config";
|
||||||
|
import type { PageResponse } from "@/shared/model";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
DesignCnstnBizSclId,
|
||||||
|
DesignCnstnBizSclResponseDto,
|
||||||
|
DesignCnstnBizSclSaveRequestDto,
|
||||||
|
DesignCnstnBizSclSearchDto,
|
||||||
|
DesignCnstnBizSclService,
|
||||||
|
DesignCnstnBizSclUpdateRequestDto,
|
||||||
|
} from "../model/design-cnstn-biz-scl-interface";
|
||||||
|
|
||||||
|
const BASE = ENDPOINT.AS_SERVICE.DESIGN_CNSTN_BIZ_SCL;
|
||||||
|
|
||||||
|
async function findPage(
|
||||||
|
searchDto: DesignCnstnBizSclSearchDto,
|
||||||
|
): Promise<PageResponse<DesignCnstnBizSclResponseDto>> {
|
||||||
|
const response = await client.get<PageResponse<DesignCnstnBizSclResponseDto>>(BASE, {
|
||||||
|
params: searchDto,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findById(
|
||||||
|
designCnstnDocNo: string,
|
||||||
|
cnstpSeCd: string,
|
||||||
|
): Promise<DesignCnstnBizSclResponseDto> {
|
||||||
|
const response = await client.get<DesignCnstnBizSclResponseDto>(
|
||||||
|
`${BASE}/${designCnstnDocNo}/${cnstpSeCd}`,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save(body: DesignCnstnBizSclSaveRequestDto): Promise<DesignCnstnBizSclId> {
|
||||||
|
const response = await client.post<DesignCnstnBizSclId>(BASE, body);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update(
|
||||||
|
designCnstnDocNo: string,
|
||||||
|
cnstpSeCd: string,
|
||||||
|
body: DesignCnstnBizSclUpdateRequestDto,
|
||||||
|
): Promise<void> {
|
||||||
|
await client.put<void>(`${BASE}/${designCnstnDocNo}/${cnstpSeCd}`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteDesignCnstnBizScl(designCnstnDocNo: string, cnstpSeCd: string): Promise<void> {
|
||||||
|
await client.delete<void>(`${BASE}/${designCnstnDocNo}/${cnstpSeCd}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const designCnstnBizSclService = {
|
||||||
|
findPage,
|
||||||
|
findById,
|
||||||
|
save,
|
||||||
|
update,
|
||||||
|
delete: deleteDesignCnstnBizScl,
|
||||||
|
} satisfies DesignCnstnBizSclService;
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { client } from "@/shared/api";
|
||||||
|
import { ENDPOINT } from "@/shared/config";
|
||||||
|
import type { PageResponse } from "@/shared/model";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
DesignCnstnEsntlAtchFileId,
|
||||||
|
DesignCnstnEsntlAtchFileResponseDto,
|
||||||
|
DesignCnstnEsntlAtchFileSaveRequestDto,
|
||||||
|
DesignCnstnEsntlAtchFileSearchDto,
|
||||||
|
DesignCnstnEsntlAtchFileService,
|
||||||
|
DesignCnstnEsntlAtchFileUpdateRequestDto,
|
||||||
|
} from "../model/design-cnstn-esntl-atch-file-interface";
|
||||||
|
|
||||||
|
const BASE = ENDPOINT.AS_SERVICE.DESIGN_CNSTN_ESNTL_ATCH_FILE;
|
||||||
|
|
||||||
|
async function findPage(
|
||||||
|
searchDto: DesignCnstnEsntlAtchFileSearchDto,
|
||||||
|
): Promise<PageResponse<DesignCnstnEsntlAtchFileResponseDto>> {
|
||||||
|
const response = await client.get<PageResponse<DesignCnstnEsntlAtchFileResponseDto>>(BASE, {
|
||||||
|
params: searchDto,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findById(
|
||||||
|
designCnstnDocNo: string,
|
||||||
|
esntlAtchFileId: string,
|
||||||
|
): Promise<DesignCnstnEsntlAtchFileResponseDto> {
|
||||||
|
const response = await client.get<DesignCnstnEsntlAtchFileResponseDto>(
|
||||||
|
`${BASE}/${designCnstnDocNo}/${esntlAtchFileId}`,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save(
|
||||||
|
body: DesignCnstnEsntlAtchFileSaveRequestDto,
|
||||||
|
): Promise<DesignCnstnEsntlAtchFileId> {
|
||||||
|
const response = await client.post<DesignCnstnEsntlAtchFileId>(BASE, body);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update(
|
||||||
|
designCnstnDocNo: string,
|
||||||
|
esntlAtchFileId: string,
|
||||||
|
body: DesignCnstnEsntlAtchFileUpdateRequestDto,
|
||||||
|
): Promise<void> {
|
||||||
|
await client.put<void>(`${BASE}/${designCnstnDocNo}/${esntlAtchFileId}`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteDesignCnstnEsntlAtchFile(
|
||||||
|
designCnstnDocNo: string,
|
||||||
|
esntlAtchFileId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await client.delete<void>(`${BASE}/${designCnstnDocNo}/${esntlAtchFileId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const designCnstnEsntlAtchFileService = {
|
||||||
|
findPage,
|
||||||
|
findById,
|
||||||
|
save,
|
||||||
|
update,
|
||||||
|
delete: deleteDesignCnstnEsntlAtchFile,
|
||||||
|
} satisfies DesignCnstnEsntlAtchFileService;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { client } from "@/shared/api";
|
||||||
|
import { ENDPOINT } from "@/shared/config";
|
||||||
|
import type { PageResponse } from "@/shared/model";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
DesignCnstnApplySaveRequestDto,
|
||||||
|
DesignCnstnMngDetResponseDto,
|
||||||
|
DesignCnstnMngResponseDto,
|
||||||
|
DesignCnstnMngSaveRequestDto,
|
||||||
|
DesignCnstnMngSearchDto,
|
||||||
|
DesignCnstnMngService,
|
||||||
|
DesignCnstnMngUpdateRequestDto,
|
||||||
|
} from "../model/design-cnstn-mng-interface";
|
||||||
|
|
||||||
|
const BASE = ENDPOINT.AS_SERVICE.DESIGN_CNSTN;
|
||||||
|
|
||||||
|
async function findPage(
|
||||||
|
searchDto: DesignCnstnMngSearchDto,
|
||||||
|
): Promise<PageResponse<DesignCnstnMngResponseDto>> {
|
||||||
|
const response = await client.get<PageResponse<DesignCnstnMngResponseDto>>(BASE, {
|
||||||
|
params: searchDto,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findById(id: string): Promise<DesignCnstnMngDetResponseDto> {
|
||||||
|
const response = await client.get<DesignCnstnMngDetResponseDto>(`${BASE}/${id}`);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save(body: DesignCnstnMngSaveRequestDto): Promise<string> {
|
||||||
|
const response = await client.post<string>(BASE, body);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveApply(body: DesignCnstnApplySaveRequestDto): Promise<string> {
|
||||||
|
const response = await client.post<string>(`${BASE}/save-apply`, body);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update(id: string, body: DesignCnstnMngUpdateRequestDto): Promise<void> {
|
||||||
|
await client.put<void>(`${BASE}/${id}`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function techReview(id: string, body: DesignCnstnApplySaveRequestDto): Promise<void> {
|
||||||
|
await client.post<void>(`${BASE}/${id}/tech-review`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteDesignCnstnMng(id: string): Promise<void> {
|
||||||
|
await client.delete<void>(`${BASE}/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const designCnstnMngService = {
|
||||||
|
findPage,
|
||||||
|
findById,
|
||||||
|
save,
|
||||||
|
saveApply,
|
||||||
|
update,
|
||||||
|
techReview,
|
||||||
|
delete: deleteDesignCnstnMng,
|
||||||
|
} satisfies DesignCnstnMngService;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { client } from "@/shared/api";
|
||||||
|
import { ENDPOINT } from "@/shared/config";
|
||||||
|
import type { PageResponse } from "@/shared/model";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
DesignCnstnRvwOpnnId,
|
||||||
|
DesignCnstnRvwOpnnResponseDto,
|
||||||
|
DesignCnstnRvwOpnnSaveRequestDto,
|
||||||
|
DesignCnstnRvwOpnnSearchDto,
|
||||||
|
DesignCnstnRvwOpnnService,
|
||||||
|
DesignCnstnRvwOpnnUpdateRequestDto,
|
||||||
|
} from "../model/design-cnstn-rvw-opnn-interface";
|
||||||
|
|
||||||
|
const BASE = ENDPOINT.AS_SERVICE.DESIGN_CNSTN_RVW_OPNN;
|
||||||
|
|
||||||
|
async function findPage(
|
||||||
|
searchDto: DesignCnstnRvwOpnnSearchDto,
|
||||||
|
): Promise<PageResponse<DesignCnstnRvwOpnnResponseDto>> {
|
||||||
|
const response = await client.get<PageResponse<DesignCnstnRvwOpnnResponseDto>>(BASE, {
|
||||||
|
params: searchDto,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findById(
|
||||||
|
designCnstnDocNo: string,
|
||||||
|
rvwOpnnSn: number,
|
||||||
|
): Promise<DesignCnstnRvwOpnnResponseDto> {
|
||||||
|
const response = await client.get<DesignCnstnRvwOpnnResponseDto>(
|
||||||
|
`${BASE}/${designCnstnDocNo}/${rvwOpnnSn}`,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save(body: DesignCnstnRvwOpnnSaveRequestDto): Promise<DesignCnstnRvwOpnnId> {
|
||||||
|
const response = await client.post<DesignCnstnRvwOpnnId>(BASE, body);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update(
|
||||||
|
designCnstnDocNo: string,
|
||||||
|
rvwOpnnSn: number,
|
||||||
|
body: DesignCnstnRvwOpnnUpdateRequestDto,
|
||||||
|
): Promise<void> {
|
||||||
|
await client.put<void>(`${BASE}/${designCnstnDocNo}/${rvwOpnnSn}`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteDesignCnstnRvwOpnn(
|
||||||
|
designCnstnDocNo: string,
|
||||||
|
rvwOpnnSn: number,
|
||||||
|
): Promise<void> {
|
||||||
|
await client.delete<void>(`${BASE}/${designCnstnDocNo}/${rvwOpnnSn}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const designCnstnRvwOpnnService = {
|
||||||
|
findPage,
|
||||||
|
findById,
|
||||||
|
save,
|
||||||
|
update,
|
||||||
|
delete: deleteDesignCnstnRvwOpnn,
|
||||||
|
} satisfies DesignCnstnRvwOpnnService;
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { ENDPOINT } from "@/shared/config";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
DesignCnstnBizCstSaveRequestDto,
|
||||||
|
DesignCnstnBizCstSearchDto,
|
||||||
|
DesignCnstnBizCstUpdateRequestDto,
|
||||||
|
} from "../model/design-cnstn-biz-cst-interface";
|
||||||
|
import type {
|
||||||
|
DesignCnstnBizSclSaveRequestDto,
|
||||||
|
DesignCnstnBizSclSearchDto,
|
||||||
|
DesignCnstnBizSclUpdateRequestDto,
|
||||||
|
} from "../model/design-cnstn-biz-scl-interface";
|
||||||
|
import type {
|
||||||
|
DesignCnstnEsntlAtchFileSaveRequestDto,
|
||||||
|
DesignCnstnEsntlAtchFileSearchDto,
|
||||||
|
DesignCnstnEsntlAtchFileUpdateRequestDto,
|
||||||
|
} from "../model/design-cnstn-esntl-atch-file-interface";
|
||||||
|
import type {
|
||||||
|
DesignCnstnApplySaveRequestDto,
|
||||||
|
DesignCnstnMngSaveRequestDto,
|
||||||
|
DesignCnstnMngSearchDto,
|
||||||
|
DesignCnstnMngUpdateRequestDto,
|
||||||
|
} from "../model/design-cnstn-mng-interface";
|
||||||
|
import type {
|
||||||
|
DesignCnstnRvwOpnnSaveRequestDto,
|
||||||
|
DesignCnstnRvwOpnnSearchDto,
|
||||||
|
DesignCnstnRvwOpnnUpdateRequestDto,
|
||||||
|
} from "../model/design-cnstn-rvw-opnn-interface";
|
||||||
|
|
||||||
|
import { designCnstnBizCstService } from "./design-cnstn-biz-cst-service";
|
||||||
|
import { designCnstnBizSclService } from "./design-cnstn-biz-scl-service";
|
||||||
|
import { designCnstnEsntlAtchFileService } from "./design-cnstn-esntl-atch-file-service";
|
||||||
|
import { designCnstnMngService } from "./design-cnstn-mng-service";
|
||||||
|
import { designCnstnRvwOpnnService } from "./design-cnstn-rvw-opnn-service";
|
||||||
|
|
||||||
|
export const designCnstnMngBaseKey = [ENDPOINT.AS_SERVICE.DESIGN_CNSTN] as const;
|
||||||
|
export const designCnstnBizCstBaseKey = [ENDPOINT.AS_SERVICE.DESIGN_CNSTN_BIZ_CST] as const;
|
||||||
|
export const designCnstnBizSclBaseKey = [ENDPOINT.AS_SERVICE.DESIGN_CNSTN_BIZ_SCL] as const;
|
||||||
|
export const designCnstnEsntlAtchFileBaseKey = [
|
||||||
|
ENDPOINT.AS_SERVICE.DESIGN_CNSTN_ESNTL_ATCH_FILE,
|
||||||
|
] as const;
|
||||||
|
export const designCnstnRvwOpnnBaseKey = [ENDPOINT.AS_SERVICE.DESIGN_CNSTN_RVW_OPNN] as const;
|
||||||
|
|
||||||
|
/* ─── DesignCnstnMng ─── */
|
||||||
|
|
||||||
|
export function useDesignCnstnMngPage(params: DesignCnstnMngSearchDto) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [...designCnstnMngBaseKey, "findPage", params],
|
||||||
|
queryFn: () => designCnstnMngService.findPage(params),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDesignCnstnMng(id: string | null | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [...designCnstnMngBaseKey, "findById", id],
|
||||||
|
queryFn: () => designCnstnMngService.findById(id!),
|
||||||
|
enabled: Boolean(id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSaveDesignCnstnMng() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (body: DesignCnstnMngSaveRequestDto) => designCnstnMngService.save(body),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSaveApplyDesignCnstnMng() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (body: DesignCnstnApplySaveRequestDto) => designCnstnMngService.saveApply(body),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateDesignCnstnMng() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, body }: { id: string; body: DesignCnstnMngUpdateRequestDto }) =>
|
||||||
|
designCnstnMngService.update(id, body),
|
||||||
|
onSuccess: (_data, { id }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [...designCnstnMngBaseKey, "findById", id],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTechReviewDesignCnstnMng() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, body }: { id: string; body: DesignCnstnApplySaveRequestDto }) =>
|
||||||
|
designCnstnMngService.techReview(id, body),
|
||||||
|
onSuccess: (_data, { id }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [...designCnstnMngBaseKey, "findById", id],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteDesignCnstnMng() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => designCnstnMngService.delete(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── DesignCnstnBizCst ─── */
|
||||||
|
|
||||||
|
export function useDesignCnstnBizCstPage(params: DesignCnstnBizCstSearchDto) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [...designCnstnBizCstBaseKey, "findPage", params],
|
||||||
|
queryFn: () => designCnstnBizCstService.findPage(params),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDesignCnstnBizCst(
|
||||||
|
designCnstnDocNo: string | null | undefined,
|
||||||
|
acntgSeCd: string | null | undefined,
|
||||||
|
) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [...designCnstnBizCstBaseKey, "findById", designCnstnDocNo, acntgSeCd],
|
||||||
|
queryFn: () => designCnstnBizCstService.findById(designCnstnDocNo!, acntgSeCd!),
|
||||||
|
enabled: Boolean(designCnstnDocNo && acntgSeCd),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSaveDesignCnstnBizCst() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (body: DesignCnstnBizCstSaveRequestDto) => designCnstnBizCstService.save(body),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnBizCstBaseKey });
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateDesignCnstnBizCst() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
designCnstnDocNo,
|
||||||
|
acntgSeCd,
|
||||||
|
body,
|
||||||
|
}: {
|
||||||
|
designCnstnDocNo: string;
|
||||||
|
acntgSeCd: string;
|
||||||
|
body: DesignCnstnBizCstUpdateRequestDto;
|
||||||
|
}) => designCnstnBizCstService.update(designCnstnDocNo, acntgSeCd, body),
|
||||||
|
onSuccess: (_data, { designCnstnDocNo, acntgSeCd }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnBizCstBaseKey });
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [...designCnstnBizCstBaseKey, "findById", designCnstnDocNo, acntgSeCd],
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteDesignCnstnBizCst() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
designCnstnDocNo,
|
||||||
|
acntgSeCd,
|
||||||
|
}: {
|
||||||
|
designCnstnDocNo: string;
|
||||||
|
acntgSeCd: string;
|
||||||
|
}) => designCnstnBizCstService.delete(designCnstnDocNo, acntgSeCd),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnBizCstBaseKey });
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── DesignCnstnBizScl ─── */
|
||||||
|
|
||||||
|
export function useDesignCnstnBizSclPage(params: DesignCnstnBizSclSearchDto) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [...designCnstnBizSclBaseKey, "findPage", params],
|
||||||
|
queryFn: () => designCnstnBizSclService.findPage(params),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDesignCnstnBizScl(
|
||||||
|
designCnstnDocNo: string | null | undefined,
|
||||||
|
cnstpSeCd: string | null | undefined,
|
||||||
|
) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [...designCnstnBizSclBaseKey, "findById", designCnstnDocNo, cnstpSeCd],
|
||||||
|
queryFn: () => designCnstnBizSclService.findById(designCnstnDocNo!, cnstpSeCd!),
|
||||||
|
enabled: Boolean(designCnstnDocNo && cnstpSeCd),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSaveDesignCnstnBizScl() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (body: DesignCnstnBizSclSaveRequestDto) => designCnstnBizSclService.save(body),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnBizSclBaseKey });
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateDesignCnstnBizScl() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
designCnstnDocNo,
|
||||||
|
cnstpSeCd,
|
||||||
|
body,
|
||||||
|
}: {
|
||||||
|
designCnstnDocNo: string;
|
||||||
|
cnstpSeCd: string;
|
||||||
|
body: DesignCnstnBizSclUpdateRequestDto;
|
||||||
|
}) => designCnstnBizSclService.update(designCnstnDocNo, cnstpSeCd, body),
|
||||||
|
onSuccess: (_data, { designCnstnDocNo, cnstpSeCd }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnBizSclBaseKey });
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [...designCnstnBizSclBaseKey, "findById", designCnstnDocNo, cnstpSeCd],
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteDesignCnstnBizScl() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
designCnstnDocNo,
|
||||||
|
cnstpSeCd,
|
||||||
|
}: {
|
||||||
|
designCnstnDocNo: string;
|
||||||
|
cnstpSeCd: string;
|
||||||
|
}) => designCnstnBizSclService.delete(designCnstnDocNo, cnstpSeCd),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnBizSclBaseKey });
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── DesignCnstnEsntlAtchFile ─── */
|
||||||
|
|
||||||
|
export function useDesignCnstnEsntlAtchFilePage(params: DesignCnstnEsntlAtchFileSearchDto) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [...designCnstnEsntlAtchFileBaseKey, "findPage", params],
|
||||||
|
queryFn: () => designCnstnEsntlAtchFileService.findPage(params),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDesignCnstnEsntlAtchFile(
|
||||||
|
designCnstnDocNo: string | null | undefined,
|
||||||
|
esntlAtchFileId: string | null | undefined,
|
||||||
|
) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [
|
||||||
|
...designCnstnEsntlAtchFileBaseKey,
|
||||||
|
"findById",
|
||||||
|
designCnstnDocNo,
|
||||||
|
esntlAtchFileId,
|
||||||
|
],
|
||||||
|
queryFn: () =>
|
||||||
|
designCnstnEsntlAtchFileService.findById(designCnstnDocNo!, esntlAtchFileId!),
|
||||||
|
enabled: Boolean(designCnstnDocNo && esntlAtchFileId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSaveDesignCnstnEsntlAtchFile() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (body: DesignCnstnEsntlAtchFileSaveRequestDto) =>
|
||||||
|
designCnstnEsntlAtchFileService.save(body),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnEsntlAtchFileBaseKey });
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateDesignCnstnEsntlAtchFile() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
designCnstnDocNo,
|
||||||
|
esntlAtchFileId,
|
||||||
|
body,
|
||||||
|
}: {
|
||||||
|
designCnstnDocNo: string;
|
||||||
|
esntlAtchFileId: string;
|
||||||
|
body: DesignCnstnEsntlAtchFileUpdateRequestDto;
|
||||||
|
}) => designCnstnEsntlAtchFileService.update(designCnstnDocNo, esntlAtchFileId, body),
|
||||||
|
onSuccess: (_data, { designCnstnDocNo, esntlAtchFileId }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnEsntlAtchFileBaseKey });
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [
|
||||||
|
...designCnstnEsntlAtchFileBaseKey,
|
||||||
|
"findById",
|
||||||
|
designCnstnDocNo,
|
||||||
|
esntlAtchFileId,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteDesignCnstnEsntlAtchFile() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
designCnstnDocNo,
|
||||||
|
esntlAtchFileId,
|
||||||
|
}: {
|
||||||
|
designCnstnDocNo: string;
|
||||||
|
esntlAtchFileId: string;
|
||||||
|
}) => designCnstnEsntlAtchFileService.delete(designCnstnDocNo, esntlAtchFileId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnEsntlAtchFileBaseKey });
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── DesignCnstnRvwOpnn ─── */
|
||||||
|
|
||||||
|
export function useDesignCnstnRvwOpnnPage(params: DesignCnstnRvwOpnnSearchDto) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [...designCnstnRvwOpnnBaseKey, "findPage", params],
|
||||||
|
queryFn: () => designCnstnRvwOpnnService.findPage(params),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDesignCnstnRvwOpnn(
|
||||||
|
designCnstnDocNo: string | null | undefined,
|
||||||
|
rvwOpnnSn: number | null | undefined,
|
||||||
|
) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [...designCnstnRvwOpnnBaseKey, "findById", designCnstnDocNo, rvwOpnnSn],
|
||||||
|
queryFn: () => designCnstnRvwOpnnService.findById(designCnstnDocNo!, rvwOpnnSn!),
|
||||||
|
enabled: Boolean(designCnstnDocNo && rvwOpnnSn != null),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSaveDesignCnstnRvwOpnn() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (body: DesignCnstnRvwOpnnSaveRequestDto) => designCnstnRvwOpnnService.save(body),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnRvwOpnnBaseKey });
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateDesignCnstnRvwOpnn() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
designCnstnDocNo,
|
||||||
|
rvwOpnnSn,
|
||||||
|
body,
|
||||||
|
}: {
|
||||||
|
designCnstnDocNo: string;
|
||||||
|
rvwOpnnSn: number;
|
||||||
|
body: DesignCnstnRvwOpnnUpdateRequestDto;
|
||||||
|
}) => designCnstnRvwOpnnService.update(designCnstnDocNo, rvwOpnnSn, body),
|
||||||
|
onSuccess: (_data, { designCnstnDocNo, rvwOpnnSn }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnRvwOpnnBaseKey });
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [...designCnstnRvwOpnnBaseKey, "findById", designCnstnDocNo, rvwOpnnSn],
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteDesignCnstnRvwOpnn() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
designCnstnDocNo,
|
||||||
|
rvwOpnnSn,
|
||||||
|
}: {
|
||||||
|
designCnstnDocNo: string;
|
||||||
|
rvwOpnnSn: number;
|
||||||
|
}) => designCnstnRvwOpnnService.delete(designCnstnDocNo, rvwOpnnSn),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnRvwOpnnBaseKey });
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
export { designCnstnBizCstService } from "./api/design-cnstn-biz-cst-service";
|
||||||
|
export { designCnstnBizSclService } from "./api/design-cnstn-biz-scl-service";
|
||||||
|
export { designCnstnEsntlAtchFileService } from "./api/design-cnstn-esntl-atch-file-service";
|
||||||
|
export { designCnstnMngService } from "./api/design-cnstn-mng-service";
|
||||||
|
export { designCnstnRvwOpnnService } from "./api/design-cnstn-rvw-opnn-service";
|
||||||
|
export {
|
||||||
|
designCnstnBizCstBaseKey,
|
||||||
|
designCnstnBizSclBaseKey,
|
||||||
|
designCnstnEsntlAtchFileBaseKey,
|
||||||
|
designCnstnMngBaseKey,
|
||||||
|
designCnstnRvwOpnnBaseKey,
|
||||||
|
useDeleteDesignCnstnBizCst,
|
||||||
|
useDeleteDesignCnstnBizScl,
|
||||||
|
useDeleteDesignCnstnEsntlAtchFile,
|
||||||
|
useDeleteDesignCnstnMng,
|
||||||
|
useDeleteDesignCnstnRvwOpnn,
|
||||||
|
useDesignCnstnBizCst,
|
||||||
|
useDesignCnstnBizCstPage,
|
||||||
|
useDesignCnstnBizScl,
|
||||||
|
useDesignCnstnBizSclPage,
|
||||||
|
useDesignCnstnEsntlAtchFile,
|
||||||
|
useDesignCnstnEsntlAtchFilePage,
|
||||||
|
useDesignCnstnMng,
|
||||||
|
useDesignCnstnMngPage,
|
||||||
|
useDesignCnstnRvwOpnn,
|
||||||
|
useDesignCnstnRvwOpnnPage,
|
||||||
|
useSaveApplyDesignCnstnMng,
|
||||||
|
useSaveDesignCnstnBizCst,
|
||||||
|
useSaveDesignCnstnBizScl,
|
||||||
|
useSaveDesignCnstnEsntlAtchFile,
|
||||||
|
useSaveDesignCnstnMng,
|
||||||
|
useSaveDesignCnstnRvwOpnn,
|
||||||
|
useTechReviewDesignCnstnMng,
|
||||||
|
useUpdateDesignCnstnBizCst,
|
||||||
|
useUpdateDesignCnstnBizScl,
|
||||||
|
useUpdateDesignCnstnEsntlAtchFile,
|
||||||
|
useUpdateDesignCnstnMng,
|
||||||
|
useUpdateDesignCnstnRvwOpnn,
|
||||||
|
} from "./api/queries";
|
||||||
|
export type {
|
||||||
|
DesignCnstnBizCstId,
|
||||||
|
DesignCnstnBizCstResponseDto,
|
||||||
|
DesignCnstnBizCstSaveRequestDto,
|
||||||
|
DesignCnstnBizCstSearchDto,
|
||||||
|
DesignCnstnBizCstService,
|
||||||
|
DesignCnstnBizCstUpdateRequestDto,
|
||||||
|
} from "./model/design-cnstn-biz-cst-interface";
|
||||||
|
export type {
|
||||||
|
DesignCnstnBizSclId,
|
||||||
|
DesignCnstnBizSclResponseDto,
|
||||||
|
DesignCnstnBizSclSaveRequestDto,
|
||||||
|
DesignCnstnBizSclSearchDto,
|
||||||
|
DesignCnstnBizSclService,
|
||||||
|
DesignCnstnBizSclUpdateRequestDto,
|
||||||
|
} from "./model/design-cnstn-biz-scl-interface";
|
||||||
|
export type {
|
||||||
|
DesignCnstnEsntlAtchFileId,
|
||||||
|
DesignCnstnEsntlAtchFileResponseDto,
|
||||||
|
DesignCnstnEsntlAtchFileSaveRequestDto,
|
||||||
|
DesignCnstnEsntlAtchFileSearchDto,
|
||||||
|
DesignCnstnEsntlAtchFileService,
|
||||||
|
DesignCnstnEsntlAtchFileUpdateRequestDto,
|
||||||
|
} from "./model/design-cnstn-esntl-atch-file-interface";
|
||||||
|
export type {
|
||||||
|
AtflMngClientResponseDto,
|
||||||
|
BizCstItem,
|
||||||
|
BizCstTreeDto,
|
||||||
|
BizSclTreeDto,
|
||||||
|
BzScaleItem,
|
||||||
|
DesignCnstnApplySaveRequestDto,
|
||||||
|
DesignCnstnMngDetResponseDto,
|
||||||
|
DesignCnstnMngResponseDto,
|
||||||
|
DesignCnstnMngSaveRequestDto,
|
||||||
|
DesignCnstnMngSearchDto,
|
||||||
|
DesignCnstnMngService,
|
||||||
|
DesignCnstnMngUpdateRequestDto,
|
||||||
|
DesignCnstnRvwOpnnResponseDto,
|
||||||
|
DesignCnstnRvwOpnnSaveRequestDto,
|
||||||
|
ElaprDocPrgrsClientResponseDto,
|
||||||
|
EsntlAtchFileItem,
|
||||||
|
EsntlAtchFileMngTreeResponseDto,
|
||||||
|
} from "./model/design-cnstn-mng-interface";
|
||||||
|
export type {
|
||||||
|
DesignCnstnRvwOpnnId,
|
||||||
|
DesignCnstnRvwOpnnSearchDto,
|
||||||
|
DesignCnstnRvwOpnnService,
|
||||||
|
DesignCnstnRvwOpnnUpdateRequestDto,
|
||||||
|
} from "./model/design-cnstn-rvw-opnn-interface";
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import type { PageableParams, PageResponse } from "@/shared/model";
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnBizCstId */
|
||||||
|
export interface DesignCnstnBizCstId {
|
||||||
|
designCnstnMng: string;
|
||||||
|
acntgSeCd: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnBizCstSaveRequestDto */
|
||||||
|
export interface DesignCnstnBizCstSaveRequestDto {
|
||||||
|
designCnstnDocNo: string;
|
||||||
|
acntgSeCd: string;
|
||||||
|
bscPlanAmt?: number;
|
||||||
|
bgtRfltAmt?: number;
|
||||||
|
tstmDesignAmt?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnBizCstUpdateRequestDto */
|
||||||
|
export interface DesignCnstnBizCstUpdateRequestDto {
|
||||||
|
bscPlanAmt?: number;
|
||||||
|
bgtRfltAmt?: number;
|
||||||
|
tstmDesignAmt?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnBizCstSearchDto + Pageable */
|
||||||
|
export interface DesignCnstnBizCstSearchDto extends PageableParams {
|
||||||
|
designCnstnDocNo?: string;
|
||||||
|
acntgSeCd?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnBizCstResponseDto */
|
||||||
|
export interface DesignCnstnBizCstResponseDto {
|
||||||
|
designCnstnDocNo: string;
|
||||||
|
acntgSeCd: string;
|
||||||
|
bscPlanAmt?: number;
|
||||||
|
bgtRfltAmt?: number;
|
||||||
|
tstmDesignAmt?: number;
|
||||||
|
regDt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Controller public 메서드명·시그니처와 동일 */
|
||||||
|
export interface DesignCnstnBizCstService {
|
||||||
|
findPage(
|
||||||
|
searchDto: DesignCnstnBizCstSearchDto,
|
||||||
|
): Promise<PageResponse<DesignCnstnBizCstResponseDto>>;
|
||||||
|
findById(designCnstnDocNo: string, acntgSeCd: string): Promise<DesignCnstnBizCstResponseDto>;
|
||||||
|
save(body: DesignCnstnBizCstSaveRequestDto): Promise<DesignCnstnBizCstId>;
|
||||||
|
update(
|
||||||
|
designCnstnDocNo: string,
|
||||||
|
acntgSeCd: string,
|
||||||
|
body: DesignCnstnBizCstUpdateRequestDto,
|
||||||
|
): Promise<void>;
|
||||||
|
delete(designCnstnDocNo: string, acntgSeCd: string): Promise<void>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import type { PageableParams, PageResponse } from "@/shared/model";
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnBizSclId */
|
||||||
|
export interface DesignCnstnBizSclId {
|
||||||
|
designCnstnMng: string;
|
||||||
|
cnstpSeCd: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnBizSclSaveRequestDto */
|
||||||
|
export interface DesignCnstnBizSclSaveRequestDto {
|
||||||
|
designCnstnDocNo: string;
|
||||||
|
cnstpSeCd: string;
|
||||||
|
bscPlanQty?: number;
|
||||||
|
bgtRfltQty?: number;
|
||||||
|
tstmDesignQty?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnBizSclUpdateRequestDto */
|
||||||
|
export interface DesignCnstnBizSclUpdateRequestDto {
|
||||||
|
bscPlanQty?: number;
|
||||||
|
bgtRfltQty?: number;
|
||||||
|
tstmDesignQty?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnBizSclSearchDto + Pageable */
|
||||||
|
export interface DesignCnstnBizSclSearchDto extends PageableParams {
|
||||||
|
designCnstnDocNo?: string;
|
||||||
|
cnstpSeCd?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnBizSclResponseDto */
|
||||||
|
export interface DesignCnstnBizSclResponseDto {
|
||||||
|
designCnstnDocNo: string;
|
||||||
|
cnstpSeCd: string;
|
||||||
|
bscPlanQty?: number;
|
||||||
|
bgtRfltQty?: number;
|
||||||
|
tstmDesignQty?: number;
|
||||||
|
regDt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Controller public 메서드명·시그니처와 동일 */
|
||||||
|
export interface DesignCnstnBizSclService {
|
||||||
|
findPage(
|
||||||
|
searchDto: DesignCnstnBizSclSearchDto,
|
||||||
|
): Promise<PageResponse<DesignCnstnBizSclResponseDto>>;
|
||||||
|
findById(designCnstnDocNo: string, cnstpSeCd: string): Promise<DesignCnstnBizSclResponseDto>;
|
||||||
|
save(body: DesignCnstnBizSclSaveRequestDto): Promise<DesignCnstnBizSclId>;
|
||||||
|
update(
|
||||||
|
designCnstnDocNo: string,
|
||||||
|
cnstpSeCd: string,
|
||||||
|
body: DesignCnstnBizSclUpdateRequestDto,
|
||||||
|
): Promise<void>;
|
||||||
|
delete(designCnstnDocNo: string, cnstpSeCd: string): Promise<void>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import type { PageableParams, PageResponse } from "@/shared/model";
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnEsntlAtchFileId */
|
||||||
|
export interface DesignCnstnEsntlAtchFileId {
|
||||||
|
designCnstnMng: string;
|
||||||
|
esntlAtchFileId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnEsntlAtchFileSaveRequestDto */
|
||||||
|
export interface DesignCnstnEsntlAtchFileSaveRequestDto {
|
||||||
|
designCnstnDocNo: string;
|
||||||
|
esntlAtchFileId: string;
|
||||||
|
atchFileGroupId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnEsntlAtchFileUpdateRequestDto */
|
||||||
|
export interface DesignCnstnEsntlAtchFileUpdateRequestDto {
|
||||||
|
atchFileGroupId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnEsntlAtchFileSearchDto + Pageable */
|
||||||
|
export interface DesignCnstnEsntlAtchFileSearchDto extends PageableParams {
|
||||||
|
designCnstnDocNo?: string;
|
||||||
|
esntlAtchFileId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnEsntlAtchFileResponseDto */
|
||||||
|
export interface DesignCnstnEsntlAtchFileResponseDto {
|
||||||
|
designCnstnDocNo: string;
|
||||||
|
esntlAtchFileId: string;
|
||||||
|
atchFileGroupId?: string;
|
||||||
|
regDt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Controller public 메서드명·시그니처와 동일 */
|
||||||
|
export interface DesignCnstnEsntlAtchFileService {
|
||||||
|
findPage(
|
||||||
|
searchDto: DesignCnstnEsntlAtchFileSearchDto,
|
||||||
|
): Promise<PageResponse<DesignCnstnEsntlAtchFileResponseDto>>;
|
||||||
|
findById(
|
||||||
|
designCnstnDocNo: string,
|
||||||
|
esntlAtchFileId: string,
|
||||||
|
): Promise<DesignCnstnEsntlAtchFileResponseDto>;
|
||||||
|
save(body: DesignCnstnEsntlAtchFileSaveRequestDto): Promise<DesignCnstnEsntlAtchFileId>;
|
||||||
|
update(
|
||||||
|
designCnstnDocNo: string,
|
||||||
|
esntlAtchFileId: string,
|
||||||
|
body: DesignCnstnEsntlAtchFileUpdateRequestDto,
|
||||||
|
): Promise<void>;
|
||||||
|
delete(designCnstnDocNo: string, esntlAtchFileId: string): Promise<void>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import type {
|
||||||
|
BizCstSaveBase,
|
||||||
|
BzScaleSaveBase,
|
||||||
|
EsntlAtchFileSaveBase,
|
||||||
|
PageableParams,
|
||||||
|
PageResponse,
|
||||||
|
PbadmsApplySaveHeader,
|
||||||
|
PbadmsMngDetResponseBase,
|
||||||
|
} from "@/shared/model";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
DesignCnstnRvwOpnnResponseDto,
|
||||||
|
DesignCnstnRvwOpnnSaveRequestDto,
|
||||||
|
} from "./design-cnstn-rvw-opnn-interface";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
DesignCnstnRvwOpnnResponseDto,
|
||||||
|
DesignCnstnRvwOpnnSaveRequestDto,
|
||||||
|
} from "./design-cnstn-rvw-opnn-interface";
|
||||||
|
export type {
|
||||||
|
AtflMngClientResponseDto,
|
||||||
|
BizCstTreeDto,
|
||||||
|
BizSclTreeDto,
|
||||||
|
ElaprDocPrgrsClientResponseDto,
|
||||||
|
EsntlAtchFileMngTreeResponseDto,
|
||||||
|
} from "@/shared/model";
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnMngSaveRequestDto */
|
||||||
|
export interface DesignCnstnMngSaveRequestDto {
|
||||||
|
designCnstnDocNo?: string;
|
||||||
|
admdstCd?: string;
|
||||||
|
envoSeComCd?: string;
|
||||||
|
taskSeComCd?: string;
|
||||||
|
bizNm?: string;
|
||||||
|
bizBgngYr?: string;
|
||||||
|
bizEndYr?: string;
|
||||||
|
bizCn?: string;
|
||||||
|
aplcntNm?: string;
|
||||||
|
aplcntMblTelno?: string;
|
||||||
|
aplcntOgdpNm?: string;
|
||||||
|
aplcntEmlAddr?: string;
|
||||||
|
dsgnrNm?: string;
|
||||||
|
dsgnrTelno?: string;
|
||||||
|
dsgnrOgdpNm?: string;
|
||||||
|
dsgnrEmlAddr?: string;
|
||||||
|
atchFileGroupId?: string;
|
||||||
|
prgrsSttsComCd?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnMngUpdateRequestDto */
|
||||||
|
export type DesignCnstnMngUpdateRequestDto = Omit<DesignCnstnMngSaveRequestDto, "designCnstnDocNo">;
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnMngSearchDto + Pageable */
|
||||||
|
export interface DesignCnstnMngSearchDto extends PageableParams {
|
||||||
|
designCnstnDocNo?: string;
|
||||||
|
admdstCd?: string;
|
||||||
|
envoSeComCd?: string;
|
||||||
|
taskSeComCd?: string;
|
||||||
|
bizNm?: string;
|
||||||
|
bizBgngYr?: string;
|
||||||
|
bizEndYr?: string;
|
||||||
|
bizCn?: string;
|
||||||
|
aplcntNm?: string;
|
||||||
|
aplcntMblTelno?: string;
|
||||||
|
aplcntOgdpNm?: string;
|
||||||
|
aplcntEmlAddr?: string;
|
||||||
|
dsgnrNm?: string;
|
||||||
|
dsgnrTelno?: string;
|
||||||
|
dsgnrOgdpNm?: string;
|
||||||
|
dsgnrEmlAddr?: string;
|
||||||
|
atchFileGroupId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DesignCnstnMngResponseExtension {
|
||||||
|
designCnstnDocNo: string;
|
||||||
|
envoSeCdNm?: string;
|
||||||
|
taskSeCdNm?: string;
|
||||||
|
prgrsSttsCdNm?: string;
|
||||||
|
regDt?: string;
|
||||||
|
rgtr?: string;
|
||||||
|
rgtrNm?: string;
|
||||||
|
mdfcnDt?: string;
|
||||||
|
sido?: string;
|
||||||
|
gugun?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnMngResponseDto */
|
||||||
|
export type DesignCnstnMngResponseDto = Omit<DesignCnstnMngSaveRequestDto, "designCnstnDocNo"> &
|
||||||
|
DesignCnstnMngResponseExtension;
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnApplySaveRequestDto.BzScaleItem */
|
||||||
|
export type BzScaleItem = BzScaleSaveBase;
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnApplySaveRequestDto.BizCstItem */
|
||||||
|
export type BizCstItem = BizCstSaveBase;
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnApplySaveRequestDto.EsntlAtchFileItem */
|
||||||
|
export type EsntlAtchFileItem = EsntlAtchFileSaveBase;
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnMngDetResponseDto */
|
||||||
|
export type DesignCnstnMngDetResponseDto = PbadmsMngDetResponseBase<
|
||||||
|
DesignCnstnMngResponseDto,
|
||||||
|
DesignCnstnRvwOpnnResponseDto
|
||||||
|
>;
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnApplySaveRequestDto */
|
||||||
|
export interface DesignCnstnApplySaveRequestDto extends PbadmsApplySaveHeader {
|
||||||
|
designCnstnMng: DesignCnstnMngSaveRequestDto;
|
||||||
|
bzScaleList?: BzScaleItem[];
|
||||||
|
bizCstList?: BizCstItem[];
|
||||||
|
esntlAtchFileList?: EsntlAtchFileItem[];
|
||||||
|
rvwOpnnList?: DesignCnstnRvwOpnnSaveRequestDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Controller public 메서드명·시그니처와 동일 */
|
||||||
|
export interface DesignCnstnMngService {
|
||||||
|
findPage(searchDto: DesignCnstnMngSearchDto): Promise<PageResponse<DesignCnstnMngResponseDto>>;
|
||||||
|
findById(id: string): Promise<DesignCnstnMngDetResponseDto>;
|
||||||
|
save(body: DesignCnstnMngSaveRequestDto): Promise<string>;
|
||||||
|
saveApply(body: DesignCnstnApplySaveRequestDto): Promise<string>;
|
||||||
|
update(id: string, body: DesignCnstnMngUpdateRequestDto): Promise<void>;
|
||||||
|
techReview(id: string, body: DesignCnstnApplySaveRequestDto): Promise<void>;
|
||||||
|
delete(id: string): Promise<void>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import type { PageableParams, PageResponse, RvwOpnnResponseExtension } from "@/shared/model";
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnRvwOpnnId */
|
||||||
|
export interface DesignCnstnRvwOpnnId {
|
||||||
|
designCnstnMng: string;
|
||||||
|
rvwOpnnSn: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnRvwOpnnSaveRequestDto */
|
||||||
|
export interface DesignCnstnRvwOpnnSaveRequestDto {
|
||||||
|
designCnstnDocNo: string;
|
||||||
|
rvwOpnnSn?: number;
|
||||||
|
opnrSeComCd?: string;
|
||||||
|
opnrNm?: string;
|
||||||
|
rvwRsltCn?: string;
|
||||||
|
rvwRsltAnmlyYn?: string;
|
||||||
|
atchFileGroupId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnRvwOpnnUpdateRequestDto */
|
||||||
|
export interface DesignCnstnRvwOpnnUpdateRequestDto {
|
||||||
|
opnrSeComCd?: string;
|
||||||
|
opnrNm?: string;
|
||||||
|
rvwRsltCn?: string;
|
||||||
|
rvwRsltAnmlyYn?: string;
|
||||||
|
atchFileGroupId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnRvwOpnnSearchDto + Pageable */
|
||||||
|
export interface DesignCnstnRvwOpnnSearchDto extends PageableParams {
|
||||||
|
designCnstnDocNo?: string;
|
||||||
|
opnrNm?: string;
|
||||||
|
opnrSeComCd?: string;
|
||||||
|
rvwRsltCn?: string;
|
||||||
|
rvwRsltAnmlyYn?: string;
|
||||||
|
atchFileGroupId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 백엔드 DesignCnstnRvwOpnnResponseDto */
|
||||||
|
export type DesignCnstnRvwOpnnResponseDto = Partial<
|
||||||
|
Pick<DesignCnstnRvwOpnnSaveRequestDto, "designCnstnDocNo" | "rvwOpnnSn">
|
||||||
|
> &
|
||||||
|
Omit<DesignCnstnRvwOpnnSaveRequestDto, "designCnstnDocNo" | "rvwOpnnSn"> &
|
||||||
|
RvwOpnnResponseExtension;
|
||||||
|
|
||||||
|
/** Controller public 메서드명·시그니처와 동일 */
|
||||||
|
export interface DesignCnstnRvwOpnnService {
|
||||||
|
findPage(
|
||||||
|
searchDto: DesignCnstnRvwOpnnSearchDto,
|
||||||
|
): Promise<PageResponse<DesignCnstnRvwOpnnResponseDto>>;
|
||||||
|
findById(designCnstnDocNo: string, rvwOpnnSn: number): Promise<DesignCnstnRvwOpnnResponseDto>;
|
||||||
|
save(body: DesignCnstnRvwOpnnSaveRequestDto): Promise<DesignCnstnRvwOpnnId>;
|
||||||
|
update(
|
||||||
|
designCnstnDocNo: string,
|
||||||
|
rvwOpnnSn: number,
|
||||||
|
body: DesignCnstnRvwOpnnUpdateRequestDto,
|
||||||
|
): Promise<void>;
|
||||||
|
delete(designCnstnDocNo: string, rvwOpnnSn: number): Promise<void>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export { DesignEditPage } from "./ui/design-edit-page";
|
||||||
|
export { DesignFormPage } from "./ui/design-form-page";
|
||||||
|
export { DesignListPage } from "./ui/design-list-page";
|
||||||
|
export { DesignRegistPage } from "./ui/design-regist-page";
|
||||||
|
export { DesignViewPage } from "./ui/design-view-page";
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
export const DESIGN_CNSTN_VIEW_BUTTON_ACTION = {
|
||||||
|
strg: "strg",
|
||||||
|
aply: "aply",
|
||||||
|
del: "del",
|
||||||
|
rqstRvw: "rqstRvw",
|
||||||
|
rvwStrg: "rvwStrg",
|
||||||
|
rvwCmptn: "rvwCmptn",
|
||||||
|
atrzAprv: "atrzAprv",
|
||||||
|
atrzRply: "atrzRply",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type DesignCnstnViewButtonAction =
|
||||||
|
(typeof DESIGN_CNSTN_VIEW_BUTTON_ACTION)[keyof typeof DESIGN_CNSTN_VIEW_BUTTON_ACTION];
|
||||||
|
|
||||||
|
const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, DesignCnstnViewButtonAction[]>> = {
|
||||||
|
ROLE_USER05: {
|
||||||
|
"": [
|
||||||
|
DESIGN_CNSTN_VIEW_BUTTON_ACTION.strg,
|
||||||
|
DESIGN_CNSTN_VIEW_BUTTON_ACTION.aply,
|
||||||
|
DESIGN_CNSTN_VIEW_BUTTON_ACTION.del,
|
||||||
|
],
|
||||||
|
CV: [
|
||||||
|
DESIGN_CNSTN_VIEW_BUTTON_ACTION.strg,
|
||||||
|
DESIGN_CNSTN_VIEW_BUTTON_ACTION.aply,
|
||||||
|
DESIGN_CNSTN_VIEW_BUTTON_ACTION.del,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ROLE_USER01_ATRZ_STATUS_CODES = ["CT", "CS"] as const;
|
||||||
|
|
||||||
|
const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
|
||||||
|
string,
|
||||||
|
Record<string, DesignCnstnViewButtonAction[]>
|
||||||
|
> = {
|
||||||
|
CT: {
|
||||||
|
ATRZ01: [DESIGN_CNSTN_VIEW_BUTTON_ACTION.rqstRvw],
|
||||||
|
},
|
||||||
|
CS: {
|
||||||
|
ATRZ02: [DESIGN_CNSTN_VIEW_BUTTON_ACTION.rvwStrg, DESIGN_CNSTN_VIEW_BUTTON_ACTION.rvwCmptn],
|
||||||
|
ATRZ03: [DESIGN_CNSTN_VIEW_BUTTON_ACTION.atrzAprv],
|
||||||
|
ATRZ99: [DESIGN_CNSTN_VIEW_BUTTON_ACTION.atrzRply],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveButtonConfigs(
|
||||||
|
actions: DesignCnstnViewButtonAction[],
|
||||||
|
): DesignCnstnViewButtonAction[] {
|
||||||
|
return actions.map((action) => DESIGN_CNSTN_VIEW_BUTTON_ACTION[action]);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetButtonConfigsOptions {
|
||||||
|
hasPendingApprovalRequest?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRoleUser01AtrzButtonActions(
|
||||||
|
prgrsSttsComCd: string,
|
||||||
|
atrzSeComCdList: string[],
|
||||||
|
options?: GetButtonConfigsOptions,
|
||||||
|
): DesignCnstnViewButtonAction[] {
|
||||||
|
if (prgrsSttsComCd === "CS" && options?.hasPendingApprovalRequest === false) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const atrzStatusMap = ROLE_USER01_ATRZ_STATUS_ACTIONS[prgrsSttsComCd];
|
||||||
|
if (!atrzStatusMap || atrzSeComCdList.length === 0) return [];
|
||||||
|
|
||||||
|
const seen = new Set<DesignCnstnViewButtonAction>();
|
||||||
|
const actions: DesignCnstnViewButtonAction[] = [];
|
||||||
|
|
||||||
|
for (const atrzSeComCd of atrzSeComCdList) {
|
||||||
|
const mappedActions = atrzStatusMap[atrzSeComCd];
|
||||||
|
if (!mappedActions) continue;
|
||||||
|
|
||||||
|
for (const action of mappedActions) {
|
||||||
|
if (seen.has(action)) continue;
|
||||||
|
seen.add(action);
|
||||||
|
actions.push(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function needsRoleUser01AtrzResolve(
|
||||||
|
dtlInstCd: string | undefined,
|
||||||
|
prgrsSttsComCd: string,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
dtlInstCd === "ROLE_USER01" &&
|
||||||
|
ROLE_USER01_ATRZ_STATUS_CODES.includes(
|
||||||
|
prgrsSttsComCd as (typeof ROLE_USER01_ATRZ_STATUS_CODES)[number],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getButtonConfigs(
|
||||||
|
dtlInstCd: string | undefined,
|
||||||
|
prgrsSttsComCd: string,
|
||||||
|
atrzSeComCdList?: string[],
|
||||||
|
options?: GetButtonConfigsOptions,
|
||||||
|
): DesignCnstnViewButtonAction[] {
|
||||||
|
if (!dtlInstCd) return [];
|
||||||
|
|
||||||
|
if (needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd)) {
|
||||||
|
return resolveButtonConfigs(
|
||||||
|
resolveRoleUser01AtrzButtonActions(prgrsSttsComCd, atrzSeComCdList ?? [], options),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusMap = ROLE_STATUS_BUTTON_ACTIONS[dtlInstCd];
|
||||||
|
if (!statusMap) return [];
|
||||||
|
|
||||||
|
const actions = statusMap[prgrsSttsComCd];
|
||||||
|
if (!actions) return [];
|
||||||
|
|
||||||
|
return resolveButtonConfigs(actions);
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const designCnstnRegistSchema = z.object({
|
||||||
|
designCnstnMng: z.object({
|
||||||
|
bizNm: z.string().min(1, "사업명은 필수입니다"),
|
||||||
|
bizBgngYr: z.string().min(1, "사업시작연도는 필수입니다"),
|
||||||
|
bizEndYr: z.string().min(1, "사업종료연도는 필수입니다"),
|
||||||
|
bizCn: z.string().min(1, "사업내용은 필수입니다"),
|
||||||
|
taskSeComCd: z.array(z.string()).min(1, "구분은 필수입니다"),
|
||||||
|
dsgnrNm: z.string().min(1, "설계자 이름은 필수입니다"),
|
||||||
|
dsgnrTelno: z
|
||||||
|
.string()
|
||||||
|
.min(1, "설계자 전화번호는 필수입니다")
|
||||||
|
.regex(/^[0-9-]+$/, "숫자와 하이픈(-)만 입력 가능합니다"),
|
||||||
|
dsgnrOgdpNm: z.string().min(1, "설계자 소속은 필수입니다"),
|
||||||
|
dsgnrEmlAddr: z
|
||||||
|
.string()
|
||||||
|
.min(1, "설계자 이메일은 필수입니다")
|
||||||
|
.email("올바른 이메일 형식이 아닙니다"),
|
||||||
|
aplcntNm: z.string().optional(),
|
||||||
|
aplcntMblTelno: z.string().optional(),
|
||||||
|
aplcntOgdpNm: z.string().optional(),
|
||||||
|
aplcntEmlAddr: z.string().optional(),
|
||||||
|
designCnstnDocNo: z.string().optional(),
|
||||||
|
admdstCd: z.string().optional(),
|
||||||
|
envoSeComCd: z.string().optional(),
|
||||||
|
prgrsSttsComCd: z.string().optional(),
|
||||||
|
atchFileGroupId: z.string().optional(),
|
||||||
|
envoSeCdNm: z.string().optional(),
|
||||||
|
taskSeCdNm: z.string().optional(),
|
||||||
|
prgrsSttsCdNm: z.string().optional(),
|
||||||
|
sido: z.string().optional(),
|
||||||
|
gugun: z.string().optional(),
|
||||||
|
regDt: z.string().optional(),
|
||||||
|
rgtr: z.string().optional(),
|
||||||
|
rgtrNm: z.string().optional(),
|
||||||
|
mdfcnDt: z.string().optional(),
|
||||||
|
}),
|
||||||
|
bzScaleList: z.array(
|
||||||
|
z.object({
|
||||||
|
cnstpSeCd: z.string(),
|
||||||
|
bscPl: z.number(),
|
||||||
|
budgetReflct: z.number(),
|
||||||
|
thtiDsgn: z.number(),
|
||||||
|
diff: z.number().optional(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
esntlAtchFileList: z.array(
|
||||||
|
z.object({
|
||||||
|
esntlAtchFileId: z.string(),
|
||||||
|
atchFileGroupId: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
privacyConsent: z.enum(["Y", "N"], {
|
||||||
|
required_error: "개인정보 수집/이용 동의는 필수입니다",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type DesignCnstnRegistFormData = z.infer<typeof designCnstnRegistSchema>;
|
||||||
|
|
||||||
|
export function formatTaskSeComCdForApi(taskSeComCd: string[]): string {
|
||||||
|
return taskSeComCd.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseTaskSeComCdFromApi(taskSeComCd?: string): string[] {
|
||||||
|
if (!taskSeComCd) return [];
|
||||||
|
return taskSeComCd.split(",").filter(Boolean);
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import type { IUserInfo } from "@/shared/store";
|
||||||
|
import type { DesignCnstnMngDetResponseDto } from "@/entities/design-cnstn";
|
||||||
|
|
||||||
|
import type { DesignCnstnRegistFormData } from "./design-cnstn-regist-schema";
|
||||||
|
import { parseTaskSeComCdFromApi } from "./design-cnstn-regist-schema";
|
||||||
|
|
||||||
|
function mapEsntlAtchFileList(esntlFileList: DesignCnstnMngDetResponseDto["esntlFileList"]) {
|
||||||
|
const esntlMap = new Map<string, string>();
|
||||||
|
|
||||||
|
for (const item of esntlFileList ?? []) {
|
||||||
|
if (!item.esntlAtchFileId || !item.atflCd) continue;
|
||||||
|
esntlMap.set(item.esntlAtchFileId, item.atflCd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({
|
||||||
|
esntlAtchFileId,
|
||||||
|
atchFileGroupId,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapDetailToFormData(data: DesignCnstnMngDetResponseDto): DesignCnstnRegistFormData {
|
||||||
|
const detDto = data.detDto;
|
||||||
|
if (!detDto) {
|
||||||
|
throw new Error("상세 정보(detDto)가 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const bizSclList = data.bizSclList ?? [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
designCnstnMng: {
|
||||||
|
designCnstnDocNo: detDto.designCnstnDocNo,
|
||||||
|
bizNm: detDto.bizNm ?? "",
|
||||||
|
bizBgngYr: detDto.bizBgngYr ?? "",
|
||||||
|
bizEndYr: detDto.bizEndYr ?? "",
|
||||||
|
bizCn: detDto.bizCn ?? "",
|
||||||
|
taskSeComCd: parseTaskSeComCdFromApi(detDto.taskSeComCd),
|
||||||
|
dsgnrNm: detDto.dsgnrNm ?? "",
|
||||||
|
dsgnrTelno: detDto.dsgnrTelno ?? "",
|
||||||
|
dsgnrOgdpNm: detDto.dsgnrOgdpNm ?? "",
|
||||||
|
dsgnrEmlAddr: detDto.dsgnrEmlAddr ?? "",
|
||||||
|
aplcntNm: detDto.aplcntNm ?? "",
|
||||||
|
aplcntMblTelno: detDto.aplcntMblTelno ?? "",
|
||||||
|
aplcntOgdpNm: detDto.aplcntOgdpNm ?? "",
|
||||||
|
aplcntEmlAddr: detDto.aplcntEmlAddr ?? "",
|
||||||
|
admdstCd: detDto.admdstCd,
|
||||||
|
envoSeComCd: detDto.envoSeComCd,
|
||||||
|
prgrsSttsComCd: detDto.prgrsSttsComCd,
|
||||||
|
atchFileGroupId: detDto.atchFileGroupId ?? "",
|
||||||
|
envoSeCdNm: detDto.envoSeCdNm,
|
||||||
|
taskSeCdNm: detDto.taskSeCdNm,
|
||||||
|
prgrsSttsCdNm: detDto.prgrsSttsCdNm,
|
||||||
|
sido: detDto.sido,
|
||||||
|
gugun: detDto.gugun,
|
||||||
|
regDt: detDto.regDt,
|
||||||
|
rgtr: detDto.rgtr,
|
||||||
|
rgtrNm: detDto.rgtrNm,
|
||||||
|
mdfcnDt: detDto.mdfcnDt,
|
||||||
|
},
|
||||||
|
bzScaleList: bizSclList.map((item) => ({
|
||||||
|
cnstpSeCd: item.cnstpSeCd,
|
||||||
|
bscPl: item.bscPlanQty,
|
||||||
|
budgetReflct: item.bgtRfltQty,
|
||||||
|
thtiDsgn: item.tstmDesignQty,
|
||||||
|
diff: item.tstmDesignQty - item.bgtRfltQty,
|
||||||
|
})),
|
||||||
|
esntlAtchFileList: mapEsntlAtchFileList(data.esntlFileList),
|
||||||
|
privacyConsent: "Y",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCreateDefaultValues(
|
||||||
|
userInfo: IUserInfo | null,
|
||||||
|
taskSeComCd?: string[],
|
||||||
|
): DesignCnstnRegistFormData {
|
||||||
|
return {
|
||||||
|
designCnstnMng: {
|
||||||
|
bizNm: "",
|
||||||
|
bizBgngYr: "",
|
||||||
|
bizEndYr: "",
|
||||||
|
bizCn: "",
|
||||||
|
taskSeComCd: taskSeComCd ?? [],
|
||||||
|
dsgnrNm: "",
|
||||||
|
dsgnrTelno: "",
|
||||||
|
dsgnrOgdpNm: "",
|
||||||
|
dsgnrEmlAddr: "",
|
||||||
|
aplcntNm: userInfo?.userName ?? "",
|
||||||
|
aplcntMblTelno: userInfo?.mphn ?? "",
|
||||||
|
aplcntOgdpNm: userInfo?.ogdpSeNm ?? "",
|
||||||
|
aplcntEmlAddr: userInfo?.email ?? "",
|
||||||
|
prgrsSttsComCd: "CV",
|
||||||
|
atchFileGroupId: "",
|
||||||
|
},
|
||||||
|
bzScaleList: [],
|
||||||
|
esntlAtchFileList: [],
|
||||||
|
privacyConsent: "N",
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import type {
|
||||||
|
DesignCnstnMngDetResponseDto,
|
||||||
|
DesignCnstnRvwOpnnResponseDto,
|
||||||
|
DesignCnstnRvwOpnnSaveRequestDto,
|
||||||
|
} from "@/entities/design-cnstn";
|
||||||
|
|
||||||
|
export interface ReviewOpinionFormData {
|
||||||
|
rvwRsltCn: string;
|
||||||
|
rvwRsltAnmlyYn: "Y" | "N" | "";
|
||||||
|
atchFileGroupId: string;
|
||||||
|
opnrSeComCd: string;
|
||||||
|
rvwOpnnSn?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReviewOpinionGetFormDataResult {
|
||||||
|
formData: ReviewOpinionFormData;
|
||||||
|
rvwList: DesignCnstnRvwOpnnResponseDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasReviewOpinionContent(formData: ReviewOpinionFormData) {
|
||||||
|
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapServerItemToSaveDto(
|
||||||
|
item: DesignCnstnRvwOpnnResponseDto,
|
||||||
|
designCnstnDocNo: string,
|
||||||
|
): DesignCnstnRvwOpnnSaveRequestDto | null {
|
||||||
|
if (item.rvwOpnnSn == null) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
designCnstnDocNo,
|
||||||
|
rvwOpnnSn: item.rvwOpnnSn,
|
||||||
|
opnrSeComCd: item.opnrSeComCd,
|
||||||
|
opnrNm: item.opnrNm,
|
||||||
|
rvwRsltCn: item.rvwRsltCn,
|
||||||
|
rvwRsltAnmlyYn: item.rvwRsltAnmlyYn || undefined,
|
||||||
|
atchFileGroupId: item.atchFileGroupId || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapFormDataToSaveDto(
|
||||||
|
formData: ReviewOpinionFormData,
|
||||||
|
designCnstnDocNo: string,
|
||||||
|
opnrNm?: string,
|
||||||
|
): DesignCnstnRvwOpnnSaveRequestDto {
|
||||||
|
const base = {
|
||||||
|
designCnstnDocNo,
|
||||||
|
opnrSeComCd: formData.opnrSeComCd,
|
||||||
|
opnrNm: opnrNm ?? "",
|
||||||
|
rvwRsltCn: formData.rvwRsltCn,
|
||||||
|
rvwRsltAnmlyYn: formData.rvwRsltAnmlyYn || undefined,
|
||||||
|
atchFileGroupId: formData.atchFileGroupId || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (formData.rvwOpnnSn != null) {
|
||||||
|
return { ...base, rvwOpnnSn: formData.rvwOpnnSn };
|
||||||
|
}
|
||||||
|
|
||||||
|
return base as DesignCnstnRvwOpnnSaveRequestDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toRvwOpnnSaveDto(
|
||||||
|
data: DesignCnstnMngDetResponseDto,
|
||||||
|
result: ReviewOpinionGetFormDataResult | null | undefined,
|
||||||
|
opnrNm?: string,
|
||||||
|
): DesignCnstnRvwOpnnSaveRequestDto[] | undefined {
|
||||||
|
if (!result || !data.detDto) return undefined;
|
||||||
|
|
||||||
|
const { formData, rvwList } = result;
|
||||||
|
const designCnstnDocNo = data.detDto.designCnstnDocNo;
|
||||||
|
|
||||||
|
const list: DesignCnstnRvwOpnnSaveRequestDto[] = rvwList
|
||||||
|
.map((item) => mapServerItemToSaveDto(item, designCnstnDocNo))
|
||||||
|
.filter((item): item is DesignCnstnRvwOpnnSaveRequestDto => item != null);
|
||||||
|
|
||||||
|
if (!hasReviewOpinionContent(formData)) {
|
||||||
|
return list.length > 0 ? list : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formDto = mapFormDataToSaveDto(formData, designCnstnDocNo, opnrNm);
|
||||||
|
|
||||||
|
if (formData.rvwOpnnSn != null) {
|
||||||
|
const existingIndex = list.findIndex((item) => item.rvwOpnnSn === formData.rvwOpnnSn);
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
list[existingIndex] = formDto;
|
||||||
|
} else {
|
||||||
|
list.push(formDto);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
list.push(formDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.length > 0 ? list : undefined;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { ReviewStepCode } from "../model/view-types";
|
||||||
|
|
||||||
|
export function getEditableReviewStep(
|
||||||
|
dtlInstCd?: string,
|
||||||
|
prgrsSttsComCd?: string,
|
||||||
|
): ReviewStepCode | undefined {
|
||||||
|
if (dtlInstCd === "ROLE_USER01" && prgrsSttsComCd === "CS") {
|
||||||
|
return "KECO";
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import type {
|
||||||
|
BizSclTreeDto,
|
||||||
|
BzScaleItem,
|
||||||
|
DesignCnstnApplySaveRequestDto,
|
||||||
|
DesignCnstnMngDetResponseDto,
|
||||||
|
EsntlAtchFileItem,
|
||||||
|
EsntlAtchFileMngTreeResponseDto,
|
||||||
|
} from "@/entities/design-cnstn";
|
||||||
|
|
||||||
|
import type { ReviewStepCode } from "../model/view-types";
|
||||||
|
|
||||||
|
import { type ReviewOpinionGetFormDataResult, toRvwOpnnSaveDto } from "./review-opinion-utils";
|
||||||
|
|
||||||
|
export function toBzScaleSaveDto(bizSclList: BizSclTreeDto[]): BzScaleItem[] {
|
||||||
|
return bizSclList
|
||||||
|
.filter((node) => node.useYn === "Y")
|
||||||
|
.map((node) => ({
|
||||||
|
cnstpSeCd: node.cnstpSeCd,
|
||||||
|
bscPl: node.bscPlanQty,
|
||||||
|
budgetReflct: node.bgtRfltQty,
|
||||||
|
thtiDsgn: node.tstmDesignQty,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toEsntlAtchFileSaveDto(
|
||||||
|
esntlFileList: EsntlAtchFileMngTreeResponseDto[],
|
||||||
|
): EsntlAtchFileItem[] {
|
||||||
|
const esntlMap = new Map<string, string>();
|
||||||
|
|
||||||
|
for (const item of esntlFileList) {
|
||||||
|
if (!item.esntlAtchFileId || !item.atflCd) continue;
|
||||||
|
esntlMap.set(item.esntlAtchFileId, item.atflCd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({
|
||||||
|
esntlAtchFileId,
|
||||||
|
atchFileGroupId,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSavePayload(
|
||||||
|
data: DesignCnstnMngDetResponseDto,
|
||||||
|
prgrsSttsComCd: string,
|
||||||
|
): DesignCnstnApplySaveRequestDto {
|
||||||
|
const detDto = data.detDto;
|
||||||
|
if (!detDto) {
|
||||||
|
throw new Error("상세 정보(detDto)가 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
designCnstnMng: { ...detDto, prgrsSttsComCd },
|
||||||
|
bzScaleList: toBzScaleSaveDto(data.bizSclList ?? []),
|
||||||
|
esntlAtchFileList: toEsntlAtchFileSaveDto(data.esntlFileList ?? []),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTechReviewPayload(
|
||||||
|
data: DesignCnstnMngDetResponseDto,
|
||||||
|
prgrsSttsComCd: string,
|
||||||
|
techReViewSaveCd: string,
|
||||||
|
reviewStep: ReviewStepCode,
|
||||||
|
reviewOpinionFormData?: ReviewOpinionGetFormDataResult | null,
|
||||||
|
opnrNm?: string,
|
||||||
|
): DesignCnstnApplySaveRequestDto {
|
||||||
|
return {
|
||||||
|
...buildSavePayload(data, prgrsSttsComCd),
|
||||||
|
techReViewSaveCd,
|
||||||
|
rvwStpCd: reviewStep,
|
||||||
|
rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Fragment } from "react";
|
||||||
|
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table";
|
||||||
|
|
||||||
|
import { dayjs } from "@/shared/lib";
|
||||||
|
import type { DesignCnstnMngResponseDto } from "@/entities/design-cnstn";
|
||||||
|
import { Badge } from "@common/components";
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<DesignCnstnMngResponseDto>();
|
||||||
|
|
||||||
|
const MAIN_STEP_LABELS = {
|
||||||
|
save: "저장",
|
||||||
|
apply: "신청완료",
|
||||||
|
review: "설계자문 검토",
|
||||||
|
reply: "설계검토 회신",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const REVIEW_STATUS_CODES = new Set(["CS", "CR"]);
|
||||||
|
|
||||||
|
type BadgeState = "completed" | "current" | "pending" | "action";
|
||||||
|
|
||||||
|
interface ProgressBadge {
|
||||||
|
label: string;
|
||||||
|
state: BadgeState;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): ProgressBadge[] {
|
||||||
|
const reviewLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.review;
|
||||||
|
const replyLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.reply;
|
||||||
|
|
||||||
|
if (prgrsSttsComCd === "CV") {
|
||||||
|
return [
|
||||||
|
{ label: MAIN_STEP_LABELS.save, state: "current" },
|
||||||
|
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||||
|
{ label: MAIN_STEP_LABELS.review, state: "pending" },
|
||||||
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prgrsSttsComCd === "CT") {
|
||||||
|
return [
|
||||||
|
{ label: MAIN_STEP_LABELS.save, state: "completed" },
|
||||||
|
{ label: MAIN_STEP_LABELS.apply, state: "current" },
|
||||||
|
{ label: MAIN_STEP_LABELS.review, state: "pending" },
|
||||||
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (REVIEW_STATUS_CODES.has(prgrsSttsComCd)) {
|
||||||
|
return [
|
||||||
|
{ label: MAIN_STEP_LABELS.save, state: "completed" },
|
||||||
|
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||||
|
{ label: reviewLabel, state: "action" },
|
||||||
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prgrsSttsComCd === "CP" || prgrsSttsComCd === "EP") {
|
||||||
|
return [
|
||||||
|
{ label: MAIN_STEP_LABELS.save, state: "completed" },
|
||||||
|
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||||
|
{ label: MAIN_STEP_LABELS.review, state: "completed" },
|
||||||
|
{ label: replyLabel, state: "action" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{ label: prgrsSttsCdNm || prgrsSttsComCd || "-", state: "current" }];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBadgeColor(state: BadgeState) {
|
||||||
|
switch (state) {
|
||||||
|
case "completed":
|
||||||
|
return "green";
|
||||||
|
case "current":
|
||||||
|
return "blue";
|
||||||
|
case "pending":
|
||||||
|
return "lightGray";
|
||||||
|
case "action":
|
||||||
|
return "orange";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatOptional(value: string | undefined) {
|
||||||
|
return value?.trim() ? value : "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProgressStepBadgeProps {
|
||||||
|
prgrsSttsComCd: string;
|
||||||
|
prgrsSttsCdNm: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProgressStepBadge({ prgrsSttsComCd, prgrsSttsCdNm }: ProgressStepBadgeProps) {
|
||||||
|
const badges = buildProgressBadges(prgrsSttsComCd, prgrsSttsCdNm);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap items-center justify-start">
|
||||||
|
{badges.map((badge, index) => (
|
||||||
|
<Fragment key={`${badge.label}-${index}`}>
|
||||||
|
{index > 0 && <span className="mx-2">></span>}
|
||||||
|
<Badge color={getBadgeColor(badge.state)}>{badge.label}</Badge>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDesignCnstnColumns() {
|
||||||
|
return [
|
||||||
|
columnHelper.accessor("sido", {
|
||||||
|
header: "시도",
|
||||||
|
meta: { align: "center" },
|
||||||
|
cell: ({ getValue }) => formatOptional(getValue()),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("gugun", {
|
||||||
|
header: "시군구",
|
||||||
|
meta: { align: "center" },
|
||||||
|
cell: ({ getValue }) => formatOptional(getValue()),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("bizNm", {
|
||||||
|
header: "사업명",
|
||||||
|
meta: { align: "left" },
|
||||||
|
cell: ({ getValue }) => formatOptional(getValue()),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("taskSeCdNm", {
|
||||||
|
header: "업무구분",
|
||||||
|
meta: { align: "center" },
|
||||||
|
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("aplcntNm", {
|
||||||
|
header: "담당자",
|
||||||
|
meta: { align: "center" },
|
||||||
|
cell: ({ getValue }) => formatOptional(getValue()),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("envoSeCdNm", {
|
||||||
|
header: "해당기관",
|
||||||
|
meta: { align: "center" },
|
||||||
|
cell: ({ getValue }) => formatOptional(getValue()),
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "progress",
|
||||||
|
header: "진행상황",
|
||||||
|
meta: { align: "center" },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<ProgressStepBadge
|
||||||
|
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
|
||||||
|
prgrsSttsCdNm={row.original.prgrsSttsCdNm || ""}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("regDt", {
|
||||||
|
header: "신청일",
|
||||||
|
meta: { align: "center" },
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const value = getValue();
|
||||||
|
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export type ReviewStepCode = "LOCAL" | "ENV" | "KECO";
|
||||||
|
|
||||||
|
export const REVIEW_STEP_LABELS: Record<ReviewStepCode, string> = {
|
||||||
|
LOCAL: "지자체",
|
||||||
|
ENV: "환경청",
|
||||||
|
KECO: "환경공단",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DESIGN_PROGRESS_ORDER = ["CV", "CT", "CS", "CR", "CP", "EP"] as const;
|
||||||
|
|
||||||
|
export const ROLE_TO_REVIEW_STEP: Record<string, ReviewStepCode> = {
|
||||||
|
ROLE_USER05: "LOCAL",
|
||||||
|
ROLE_USER03: "ENV",
|
||||||
|
ROLE_USER01: "KECO",
|
||||||
|
};
|
||||||
@@ -0,0 +1,391 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
|
import { ENDPOINT } from "@/shared/config";
|
||||||
|
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||||
|
import {
|
||||||
|
type DesignCnstnApplySaveRequestDto,
|
||||||
|
designCnstnMngBaseKey,
|
||||||
|
type DesignCnstnMngDetResponseDto,
|
||||||
|
designCnstnMngService,
|
||||||
|
} from "@/entities/design-cnstn";
|
||||||
|
import {
|
||||||
|
buildElaprPrgrsSttsListFromAplnRows,
|
||||||
|
ElaprApprovalDialog,
|
||||||
|
type ElaprDmndSaveRequestDto,
|
||||||
|
elaprDmndService,
|
||||||
|
type ElaprTmpltPickerConfirmPayload,
|
||||||
|
ElaprTmpltPickerDialog,
|
||||||
|
hasCurrentUserPendingApprovalRequest,
|
||||||
|
useElaprDocPrgrsByDocNo,
|
||||||
|
} from "@/entities/elapr";
|
||||||
|
import { Button } from "@common/components";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DESIGN_CNSTN_VIEW_BUTTON_ACTION,
|
||||||
|
type DesignCnstnViewButtonAction,
|
||||||
|
getButtonConfigs,
|
||||||
|
needsRoleUser01AtrzResolve,
|
||||||
|
} from "../../lib/button-config";
|
||||||
|
import { buildSavePayload, buildTechReviewPayload } from "../../lib/save-payload-utils";
|
||||||
|
import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opinion-section";
|
||||||
|
|
||||||
|
interface FloatingActionButtonsProps {
|
||||||
|
data: DesignCnstnMngDetResponseDto;
|
||||||
|
docSeComCd: string;
|
||||||
|
designCnstnDocNo: string;
|
||||||
|
prgrsSttsComCd: string;
|
||||||
|
dtlInstCd: string | undefined;
|
||||||
|
atrzSeComCdList: string[];
|
||||||
|
isRvwLoginUserLoading: boolean;
|
||||||
|
techReviewOpinionRef?: React.RefObject<TechReviewOpinionSectionHandle | null>;
|
||||||
|
opnrNm?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveDialog = "elaprTmplt" | "elaprApproval" | null;
|
||||||
|
|
||||||
|
function FloatingActionButtons({
|
||||||
|
data,
|
||||||
|
docSeComCd,
|
||||||
|
designCnstnDocNo,
|
||||||
|
prgrsSttsComCd,
|
||||||
|
dtlInstCd,
|
||||||
|
atrzSeComCdList,
|
||||||
|
isRvwLoginUserLoading,
|
||||||
|
techReviewOpinionRef,
|
||||||
|
opnrNm,
|
||||||
|
}: FloatingActionButtonsProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const { confirm } = useConfirm();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const userInfo = useUserStore((state) => state.userInfo);
|
||||||
|
const alert = useAlertStore((state) => state.setMessage);
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const [activeDialog, setActiveDialog] = useState<ActiveDialog>(null);
|
||||||
|
|
||||||
|
const docNo = data.detDto?.designCnstnDocNo ?? designCnstnDocNo;
|
||||||
|
const isRegister = data.detDto?.rgtr === userInfo?.userUnqId;
|
||||||
|
|
||||||
|
const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd);
|
||||||
|
const needsCsApprovalCheck = needsAtrzResolve && prgrsSttsComCd === "CS";
|
||||||
|
const { data: elaprDocPrgrsList = [], isLoading: isElaprDocPrgrsLoading } =
|
||||||
|
useElaprDocPrgrsByDocNo(needsCsApprovalCheck ? docNo : null);
|
||||||
|
|
||||||
|
const hasPendingApprovalRequest = hasCurrentUserPendingApprovalRequest(
|
||||||
|
elaprDocPrgrsList,
|
||||||
|
userInfo?.userUnqId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const actionButtons = useMemo(() => {
|
||||||
|
if (needsAtrzResolve && isRvwLoginUserLoading) return [];
|
||||||
|
if (needsCsApprovalCheck && isElaprDocPrgrsLoading) return [];
|
||||||
|
return getButtonConfigs(dtlInstCd, prgrsSttsComCd, atrzSeComCdList, {
|
||||||
|
hasPendingApprovalRequest: needsCsApprovalCheck ? hasPendingApprovalRequest : undefined,
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
atrzSeComCdList,
|
||||||
|
dtlInstCd,
|
||||||
|
hasPendingApprovalRequest,
|
||||||
|
isElaprDocPrgrsLoading,
|
||||||
|
isRvwLoginUserLoading,
|
||||||
|
needsAtrzResolve,
|
||||||
|
needsCsApprovalCheck,
|
||||||
|
prgrsSttsComCd,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const canEdit =
|
||||||
|
isRegister && actionButtons.some((button) => button === DESIGN_CNSTN_VIEW_BUTTON_ACTION.strg);
|
||||||
|
|
||||||
|
function handleGoToList() {
|
||||||
|
router.push("/design-cnstn");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEdit() {
|
||||||
|
router.push(`/design-cnstn/${designCnstnDocNo}/edit`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldShowButton(action: DesignCnstnViewButtonAction) {
|
||||||
|
return actionButtons.some((button) => button === action);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeAction({
|
||||||
|
action,
|
||||||
|
request,
|
||||||
|
successMessage,
|
||||||
|
}: {
|
||||||
|
action: DesignCnstnViewButtonAction;
|
||||||
|
request?: unknown;
|
||||||
|
successMessage?: string;
|
||||||
|
}) {
|
||||||
|
const handler = buttonActionHandlers[action];
|
||||||
|
if (!handler) return;
|
||||||
|
|
||||||
|
setIsProcessing(true);
|
||||||
|
try {
|
||||||
|
await handler({
|
||||||
|
request,
|
||||||
|
data,
|
||||||
|
onSuccess: () => {
|
||||||
|
if (successMessage) alert(successMessage);
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [...designCnstnMngBaseKey, "findById", designCnstnDocNo],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
let errorMessage = "";
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
errorMessage = error.response?.data?.message ?? "";
|
||||||
|
} else {
|
||||||
|
errorMessage = error.message ?? "처리 중 오류가 발생했습니다.";
|
||||||
|
}
|
||||||
|
alert(errorMessage);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStrgClick() {
|
||||||
|
const payload: DesignCnstnApplySaveRequestDto = buildSavePayload(data, "CV");
|
||||||
|
await executeAction({
|
||||||
|
action: DESIGN_CNSTN_VIEW_BUTTON_ACTION.strg,
|
||||||
|
request: payload,
|
||||||
|
successMessage: "저장되었습니다.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAplyClick() {
|
||||||
|
const payload: DesignCnstnApplySaveRequestDto = buildSavePayload(data, "CT");
|
||||||
|
await executeAction({
|
||||||
|
action: DESIGN_CNSTN_VIEW_BUTTON_ACTION.aply,
|
||||||
|
request: payload,
|
||||||
|
successMessage: "신청 완료되었습니다.",
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelClick() {
|
||||||
|
if (!(await confirm("삭제하시겠습니까?"))) return;
|
||||||
|
await executeAction({
|
||||||
|
action: DESIGN_CNSTN_VIEW_BUTTON_ACTION.del,
|
||||||
|
successMessage: "삭제되었습니다.",
|
||||||
|
});
|
||||||
|
router.push("/design-cnstn");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReviewOpinionFormData() {
|
||||||
|
return techReviewOpinionRef?.current?.getFormData() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleElaprTmpltConfirm({ aplnRows }: ElaprTmpltPickerConfirmPayload) {
|
||||||
|
const payload: ElaprDmndSaveRequestDto = {
|
||||||
|
elaprDocNo: docNo,
|
||||||
|
dmndTtl: data.detDto?.bizNm ?? "",
|
||||||
|
dmndCn: data.detDto?.bizCn ?? "",
|
||||||
|
elaprPrgrsSttsList: buildElaprPrgrsSttsListFromAplnRows(aplnRows),
|
||||||
|
};
|
||||||
|
await executeAction({
|
||||||
|
action: DESIGN_CNSTN_VIEW_BUTTON_ACTION.rqstRvw,
|
||||||
|
request: payload,
|
||||||
|
successMessage: "검토자지정이 완료되었습니다.",
|
||||||
|
});
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", docNo],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRvwStrgClick() {
|
||||||
|
const payload = buildTechReviewPayload(
|
||||||
|
data,
|
||||||
|
"CS",
|
||||||
|
"SV",
|
||||||
|
"KECO",
|
||||||
|
getReviewOpinionFormData(),
|
||||||
|
opnrNm,
|
||||||
|
);
|
||||||
|
|
||||||
|
await executeAction({
|
||||||
|
action: DESIGN_CNSTN_VIEW_BUTTON_ACTION.rvwStrg,
|
||||||
|
request: payload,
|
||||||
|
successMessage: "저장되었습니다.",
|
||||||
|
});
|
||||||
|
techReviewOpinionRef?.current?.resetForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRvwCmptnClick() {
|
||||||
|
const payload = buildTechReviewPayload(
|
||||||
|
data,
|
||||||
|
"CS",
|
||||||
|
"CP",
|
||||||
|
"KECO",
|
||||||
|
getReviewOpinionFormData(),
|
||||||
|
opnrNm,
|
||||||
|
);
|
||||||
|
|
||||||
|
await executeAction({
|
||||||
|
action: DESIGN_CNSTN_VIEW_BUTTON_ACTION.rvwCmptn,
|
||||||
|
request: payload,
|
||||||
|
});
|
||||||
|
techReviewOpinionRef?.current?.resetForm();
|
||||||
|
setActiveDialog("elaprApproval");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="sticky bottom-0 z-40 !-mb-2 border-t bg-white pb-8 pt-6 shadow-[0_-4px_12px_rgba(0,0,0,0.06)]">
|
||||||
|
<div className="mx-4 flex flex-wrap justify-center gap-3 sm:gap-4">
|
||||||
|
<Button color="white" type="button" onClick={handleGoToList} disabled={isProcessing}>
|
||||||
|
목록
|
||||||
|
</Button>
|
||||||
|
{canEdit && (
|
||||||
|
<Button color="gray" onClick={handleEdit} disabled={isProcessing}>
|
||||||
|
수정
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{shouldShowButton(DESIGN_CNSTN_VIEW_BUTTON_ACTION.strg) && (
|
||||||
|
<Button onClick={handleStrgClick} disabled={isProcessing}>
|
||||||
|
저장
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{shouldShowButton(DESIGN_CNSTN_VIEW_BUTTON_ACTION.aply) && (
|
||||||
|
<Button color="green" onClick={handleAplyClick} disabled={isProcessing}>
|
||||||
|
신청완료
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{shouldShowButton(DESIGN_CNSTN_VIEW_BUTTON_ACTION.del) && (
|
||||||
|
<Button color="red" onClick={handleDelClick} disabled={isProcessing}>
|
||||||
|
삭제
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{shouldShowButton(DESIGN_CNSTN_VIEW_BUTTON_ACTION.rqstRvw) && (
|
||||||
|
<Button onClick={() => setActiveDialog("elaprTmplt")} disabled={isProcessing}>
|
||||||
|
검토자지정
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{shouldShowButton(DESIGN_CNSTN_VIEW_BUTTON_ACTION.rvwStrg) && (
|
||||||
|
<Button onClick={handleRvwStrgClick} disabled={isProcessing}>
|
||||||
|
저장
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{shouldShowButton(DESIGN_CNSTN_VIEW_BUTTON_ACTION.rvwCmptn) && (
|
||||||
|
<Button color="green" onClick={handleRvwCmptnClick} disabled={isProcessing}>
|
||||||
|
검토완료
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{shouldShowButton(DESIGN_CNSTN_VIEW_BUTTON_ACTION.atrzAprv) && (
|
||||||
|
<Button onClick={() => setActiveDialog("elaprApproval")} disabled={isProcessing}>
|
||||||
|
결재(승인)
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{shouldShowButton(DESIGN_CNSTN_VIEW_BUTTON_ACTION.atrzRply) && (
|
||||||
|
<Button onClick={() => setActiveDialog("elaprApproval")} disabled={isProcessing}>
|
||||||
|
결재(회신)
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElaprTmpltPickerDialog
|
||||||
|
open={activeDialog === "elaprTmplt"}
|
||||||
|
onClose={() => setActiveDialog(null)}
|
||||||
|
docSeComCd={docSeComCd}
|
||||||
|
onConfirm={handleElaprTmpltConfirm}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ElaprApprovalDialog
|
||||||
|
open={activeDialog === "elaprApproval"}
|
||||||
|
onOpenChange={(open) => setActiveDialog(open ? "elaprApproval" : null)}
|
||||||
|
elaprDocNo={data.detDto?.designCnstnDocNo ?? designCnstnDocNo}
|
||||||
|
currentUserUuid={userInfo?.userUnqId ?? ""}
|
||||||
|
onSuccess={() => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [...designCnstnMngBaseKey, "findById", designCnstnDocNo],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { FloatingActionButtons };
|
||||||
|
|
||||||
|
async function handleSaveApply(
|
||||||
|
context: ActionHandlerContext<DesignCnstnApplySaveRequestDto>,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await designCnstnMngService.saveApply(context.request);
|
||||||
|
context.onSuccess?.();
|
||||||
|
} catch (error) {
|
||||||
|
context.onError?.(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDel(context: ActionHandlerContext): Promise<void> {
|
||||||
|
try {
|
||||||
|
await designCnstnMngService.delete(context.data.detDto?.designCnstnDocNo ?? "");
|
||||||
|
context.onSuccess?.();
|
||||||
|
} catch (error) {
|
||||||
|
context.onError?.(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTechReview(
|
||||||
|
context: ActionHandlerContext<DesignCnstnApplySaveRequestDto>,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await designCnstnMngService.techReview(
|
||||||
|
context.data.detDto?.designCnstnDocNo ?? "",
|
||||||
|
context.request,
|
||||||
|
);
|
||||||
|
context.onSuccess?.();
|
||||||
|
} catch (error) {
|
||||||
|
context.onError?.(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRqstRvw(
|
||||||
|
context: ActionHandlerContext<ElaprDmndSaveRequestDto>,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await elaprDmndService.save(context.request);
|
||||||
|
context.onSuccess?.();
|
||||||
|
} catch (error) {
|
||||||
|
context.onError?.(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAtrzAprv(context: ActionHandlerContext): Promise<void> {
|
||||||
|
context.onSuccess?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAtrzRply(context: ActionHandlerContext): Promise<void> {
|
||||||
|
context.onSuccess?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttonActionHandlers: Record<DesignCnstnViewButtonAction, ActionHandler> = {
|
||||||
|
strg: handleSaveApply,
|
||||||
|
aply: handleSaveApply,
|
||||||
|
del: handleDel,
|
||||||
|
rqstRvw: handleRqstRvw,
|
||||||
|
rvwStrg: handleTechReview,
|
||||||
|
rvwCmptn: handleTechReview,
|
||||||
|
atrzAprv: handleAtrzAprv,
|
||||||
|
atrzRply: handleAtrzRply,
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
interface ActionHandlerContext<T = any> {
|
||||||
|
request: T;
|
||||||
|
data: DesignCnstnMngDetResponseDto;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionHandler = (context: ActionHandlerContext) => Promise<void>;
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useDesignCnstnMng } from "@/entities/design-cnstn";
|
||||||
|
import { Skeleton } from "@common/components";
|
||||||
|
|
||||||
|
import { DesignFormPage } from "./design-form-page";
|
||||||
|
|
||||||
|
interface DesignEditPageProps {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DesignEditPage({ id }: DesignEditPageProps) {
|
||||||
|
const { data, isLoading } = useDesignCnstnMng(id);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="mt-6 space-y-6 px-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Skeleton className="h-[4rem] w-full" />
|
||||||
|
<Skeleton className="h-[20rem] w-full" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Skeleton className="h-[4rem] w-full" />
|
||||||
|
<Skeleton className="h-[20rem] w-full" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Skeleton className="h-[4rem] w-full" />
|
||||||
|
<Skeleton className="h-[20rem] w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data?.detDto) {
|
||||||
|
return <div className="mt-6 text-[1.4rem]">상세 정보를 찾을 수 없습니다.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <DesignFormPage id={id} mode="edit" initialData={data} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DesignEditPage };
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||||
|
import {
|
||||||
|
type DesignCnstnApplySaveRequestDto,
|
||||||
|
designCnstnMngBaseKey,
|
||||||
|
type DesignCnstnMngDetResponseDto,
|
||||||
|
designCnstnMngService,
|
||||||
|
} from "@/entities/design-cnstn";
|
||||||
|
import {
|
||||||
|
AttachmentsFormSection,
|
||||||
|
BusinessScaleFormSection,
|
||||||
|
ContactInfoFormSection,
|
||||||
|
PrivacyConsentSection,
|
||||||
|
} from "@/widgets/pbadms-sprt";
|
||||||
|
import { Button } from "@common/components";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type DesignCnstnRegistFormData,
|
||||||
|
designCnstnRegistSchema,
|
||||||
|
formatTaskSeComCdForApi,
|
||||||
|
} from "../lib/design-cnstn-regist-schema";
|
||||||
|
import { getCreateDefaultValues, mapDetailToFormData } from "../lib/form-data-mapper";
|
||||||
|
|
||||||
|
import { BasicInfoFormSection } from "./sections/basic-info-form-section";
|
||||||
|
|
||||||
|
interface DesignFormPageProps {
|
||||||
|
id?: string;
|
||||||
|
mode: "create" | "edit";
|
||||||
|
initialData?: DesignCnstnMngDetResponseDto;
|
||||||
|
taskSeComCd?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function DesignFormPage({ id, mode, initialData, taskSeComCd }: DesignFormPageProps) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const router = useRouter();
|
||||||
|
const { confirm } = useConfirm();
|
||||||
|
const userInfo = useUserStore((state) => state.userInfo);
|
||||||
|
const alert = useAlertStore((state) => state.setMessage);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const isEditMode = mode === "edit";
|
||||||
|
|
||||||
|
const form = useForm<DesignCnstnRegistFormData>({
|
||||||
|
resolver: zodResolver(designCnstnRegistSchema),
|
||||||
|
mode: "onChange",
|
||||||
|
defaultValues:
|
||||||
|
isEditMode && initialData
|
||||||
|
? mapDetailToFormData(initialData)
|
||||||
|
: getCreateDefaultValues(userInfo, taskSeComCd),
|
||||||
|
});
|
||||||
|
|
||||||
|
function buildFormPayload(
|
||||||
|
data: DesignCnstnRegistFormData,
|
||||||
|
prgrsSttsComCd: "CV" | "CT",
|
||||||
|
): DesignCnstnApplySaveRequestDto {
|
||||||
|
const { taskSeComCd: formTaskSeComCd, ...designCnstnMngRest } = data.designCnstnMng;
|
||||||
|
|
||||||
|
return {
|
||||||
|
designCnstnMng: {
|
||||||
|
...designCnstnMngRest,
|
||||||
|
taskSeComCd: formatTaskSeComCdForApi(formTaskSeComCd),
|
||||||
|
designCnstnDocNo: isEditMode ? initialData?.detDto?.designCnstnDocNo : undefined,
|
||||||
|
prgrsSttsComCd,
|
||||||
|
},
|
||||||
|
bzScaleList: data.bzScaleList,
|
||||||
|
esntlAtchFileList: data.esntlAtchFileList,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitForm(
|
||||||
|
data: DesignCnstnRegistFormData,
|
||||||
|
prgrsSttsComCd: "CV" | "CT",
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
return designCnstnMngService.saveApply(buildFormPayload(data, prgrsSttsComCd));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSave(data: DesignCnstnRegistFormData) {
|
||||||
|
if (isSubmitting) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
const designCnstnDocNo = await submitForm(data, "CV");
|
||||||
|
if (!designCnstnDocNo) return;
|
||||||
|
|
||||||
|
alert("저장되었습니다.");
|
||||||
|
router.push(`/design-cnstn/${designCnstnDocNo}/edit`);
|
||||||
|
if (isEditMode && id) {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [...designCnstnMngBaseKey, "findById", id],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("저장 실패:", error);
|
||||||
|
alert("저장 중 오류가 발생했습니다.");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onApply(data: DesignCnstnRegistFormData) {
|
||||||
|
if (isSubmitting) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
const designCnstnDocNo = await submitForm(data, "CT");
|
||||||
|
if (!designCnstnDocNo) return;
|
||||||
|
|
||||||
|
alert("신청 완료되었습니다.");
|
||||||
|
router.push(`/design-cnstn/${designCnstnDocNo}`);
|
||||||
|
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||||
|
if (id) {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [...designCnstnMngBaseKey, "findById", id],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("신청 실패:", error);
|
||||||
|
alert("신청 중 오류가 발생했습니다.");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleApplyClick() {
|
||||||
|
form.handleSubmit(onApply)();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelClick() {
|
||||||
|
if (isSubmitting) return;
|
||||||
|
|
||||||
|
const docNo = initialData?.detDto?.designCnstnDocNo;
|
||||||
|
if (!docNo) {
|
||||||
|
alert("문서번호가 없습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await confirm("삭제하시겠습니까?"))) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
await designCnstnMngService.delete(docNo);
|
||||||
|
alert("삭제되었습니다.");
|
||||||
|
router.push("/design-cnstn");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("삭제 실패:", error);
|
||||||
|
alert("삭제 중 오류가 발생했습니다.");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSave)} className="mt-6 space-y-4">
|
||||||
|
<BasicInfoFormSection />
|
||||||
|
<ContactInfoFormSection fieldPrefix="designCnstnMng" />
|
||||||
|
<PrivacyConsentSection />
|
||||||
|
<BusinessScaleFormSection />
|
||||||
|
<AttachmentsFormSection
|
||||||
|
docSeComCd="CNFM"
|
||||||
|
fieldPrefix="designCnstnMng"
|
||||||
|
initialEsntlFiles={isEditMode ? initialData?.esntlFileList : undefined}
|
||||||
|
initialEtcFiles={isEditMode ? initialData?.etcFileList : undefined}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="sticky bottom-0 z-40 !-mb-2 border-t bg-white pb-8 pt-6 shadow-[0_-4px_12px_rgba(0,0,0,0.06)]">
|
||||||
|
<div className="mx-4 flex flex-wrap justify-center gap-3 sm:gap-4">
|
||||||
|
<Button
|
||||||
|
color="white"
|
||||||
|
type="button"
|
||||||
|
onClick={() => router.push("/design-cnstn")}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
목록
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
|
저장
|
||||||
|
</Button>
|
||||||
|
{isEditMode && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
color="green"
|
||||||
|
type="button"
|
||||||
|
onClick={handleApplyClick}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
신청완료
|
||||||
|
</Button>
|
||||||
|
<Button color="red" type="button" onClick={handleDelClick} disabled={isSubmitting}>
|
||||||
|
삭제
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DesignFormPage };
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import type { PaginationState } from "@tanstack/react-table";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type CmCdAdstDstrComboResponseDto,
|
||||||
|
useFindGugunCombo,
|
||||||
|
useFindSidoCombo,
|
||||||
|
} from "@/entities/adstdstr";
|
||||||
|
import { CodeSelect } from "@/entities/code";
|
||||||
|
import type { DesignCnstnMngSearchDto } from "@/entities/design-cnstn";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@common/components";
|
||||||
|
|
||||||
|
import { DesignList } from "./design-list";
|
||||||
|
|
||||||
|
const DEFAULT_PAGE_INDEX = 0;
|
||||||
|
const DEFAULT_PAGE_SIZE = 10;
|
||||||
|
const DEFAULT_SORT = "designCnstnDocNo,desc";
|
||||||
|
const EMPTY_ADST_LIST: CmCdAdstDstrComboResponseDto[] = [];
|
||||||
|
|
||||||
|
const DEFAULT_SEARCH_PARAMS: DesignCnstnMngSearchDto = {
|
||||||
|
bizNm: "",
|
||||||
|
taskSeComCd: "",
|
||||||
|
admdstCd: "",
|
||||||
|
page: DEFAULT_PAGE_INDEX,
|
||||||
|
size: DEFAULT_PAGE_SIZE,
|
||||||
|
sort: DEFAULT_SORT,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ALL_SELECT_VALUE = "__ALL__";
|
||||||
|
|
||||||
|
function DesignListPage() {
|
||||||
|
const [bizNm, setBizNm] = useState("");
|
||||||
|
const [taskSeComCd, setTaskSeComCd] = useState("");
|
||||||
|
const [sidoComCd, setSidoComCd] = useState("");
|
||||||
|
const [sggComCd, setSggComCd] = useState("");
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: DEFAULT_PAGE_INDEX,
|
||||||
|
pageSize: DEFAULT_PAGE_SIZE,
|
||||||
|
});
|
||||||
|
const [searchParams, setSearchParams] = useState<DesignCnstnMngSearchDto>(DEFAULT_SEARCH_PARAMS);
|
||||||
|
|
||||||
|
const { data: sidoData } = useFindSidoCombo();
|
||||||
|
const { data: gugunData } = useFindGugunCombo(sidoComCd);
|
||||||
|
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
|
||||||
|
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
|
||||||
|
|
||||||
|
const handleSearch = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setPagination((prev) => ({ ...prev, pageIndex: DEFAULT_PAGE_INDEX }));
|
||||||
|
setSearchParams({
|
||||||
|
...DEFAULT_SEARCH_PARAMS,
|
||||||
|
bizNm,
|
||||||
|
taskSeComCd,
|
||||||
|
admdstCd: sggComCd || sidoComCd || "",
|
||||||
|
page: DEFAULT_PAGE_INDEX,
|
||||||
|
size: pagination.pageSize,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 px-[1.8rem] pb-6">
|
||||||
|
<section className="card card-border mx-0 bg-background">
|
||||||
|
<form className="flex flex-wrap items-center gap-4 p-2" onSubmit={handleSearch}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Label htmlFor="sidoComCd" className="whitespace-nowrap">
|
||||||
|
시도
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={sidoComCd}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setSidoComCd(value === ALL_SELECT_VALUE ? "" : value);
|
||||||
|
setSggComCd("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="sidoComCd" className="w-[12rem]">
|
||||||
|
<SelectValue placeholder="전체" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={ALL_SELECT_VALUE}>전체</SelectItem>
|
||||||
|
{sidoOptions.map((option) => (
|
||||||
|
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||||
|
{option.adstDstrNm}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Label htmlFor="sggComCd" className="whitespace-nowrap">
|
||||||
|
시군구
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={sggComCd}
|
||||||
|
onValueChange={(value) => setSggComCd(value === ALL_SELECT_VALUE ? "" : value)}
|
||||||
|
disabled={!sidoComCd}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="sggComCd" className="w-[12rem]">
|
||||||
|
<SelectValue placeholder="전체" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={ALL_SELECT_VALUE}>전체</SelectItem>
|
||||||
|
{gugunOptions.map((option) => (
|
||||||
|
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||||
|
{option.adstDstrNm}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Label htmlFor="taskSeComCd" className="whitespace-nowrap">
|
||||||
|
구분
|
||||||
|
</Label>
|
||||||
|
<CodeSelect upCd="CNFM" value={taskSeComCd} onValueChange={setTaskSeComCd} />
|
||||||
|
</div>
|
||||||
|
<div className="flex w-[24rem] items-center gap-2">
|
||||||
|
<Label htmlFor="bizNm" className="whitespace-nowrap">
|
||||||
|
사업명
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="bizNm"
|
||||||
|
value={bizNm}
|
||||||
|
onChange={(event) => setBizNm(event.target.value)}
|
||||||
|
placeholder="사업명"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button type="submit">조회</Button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<DesignList
|
||||||
|
searchParams={searchParams}
|
||||||
|
pagination={pagination}
|
||||||
|
onPaginationChange={setPagination}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DesignListPage };
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||||
|
|
||||||
|
import { useDataTable, useUserStore } from "@/shared/lib";
|
||||||
|
import {
|
||||||
|
type DesignCnstnMngResponseDto,
|
||||||
|
type DesignCnstnMngSearchDto,
|
||||||
|
useDesignCnstnMngPage,
|
||||||
|
} from "@/entities/design-cnstn";
|
||||||
|
import { TaskSePickerDialog } from "@/widgets/pbadms-sprt";
|
||||||
|
import { Button } from "@common/components";
|
||||||
|
|
||||||
|
import { formatTaskSeComCdForApi } from "../lib/design-cnstn-regist-schema";
|
||||||
|
import { createDesignCnstnColumns } from "../model/columns";
|
||||||
|
|
||||||
|
interface DesignListProps {
|
||||||
|
searchParams: DesignCnstnMngSearchDto;
|
||||||
|
pagination: PaginationState;
|
||||||
|
onPaginationChange: (pagination: PaginationState) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DesignList({ searchParams, pagination, onPaginationChange }: DesignListProps) {
|
||||||
|
const userInfo = useUserStore((state) => state.userInfo);
|
||||||
|
const router = useRouter();
|
||||||
|
const [isTaskSeDialogOpen, setIsTaskSeDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const queryParams = useMemo(
|
||||||
|
() => ({
|
||||||
|
bizNm: searchParams.bizNm,
|
||||||
|
taskSeComCd: searchParams.taskSeComCd,
|
||||||
|
admdstCd: searchParams.admdstCd,
|
||||||
|
page: pagination.pageIndex,
|
||||||
|
size: pagination.pageSize,
|
||||||
|
sort: searchParams.sort ?? "designCnstnDocNo,desc",
|
||||||
|
}),
|
||||||
|
[searchParams, pagination],
|
||||||
|
);
|
||||||
|
|
||||||
|
const pageQuery = useDesignCnstnMngPage(queryParams);
|
||||||
|
const columns = useMemo(() => createDesignCnstnColumns(), []);
|
||||||
|
|
||||||
|
const handleRowClick = (row: DesignCnstnMngResponseDto) => {
|
||||||
|
const canEdit = row.prgrsSttsComCd === "CV" && row.rgtr === userInfo?.userUnqId;
|
||||||
|
|
||||||
|
router.push(
|
||||||
|
canEdit
|
||||||
|
? `/design-cnstn/${row.designCnstnDocNo}/edit`
|
||||||
|
: `/design-cnstn/${row.designCnstnDocNo}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { tableComponent } = useDataTable({
|
||||||
|
query: pageQuery,
|
||||||
|
tableOptions: {
|
||||||
|
columns,
|
||||||
|
state: { pagination },
|
||||||
|
onPaginationChange: (updater) => {
|
||||||
|
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||||
|
onPaginationChange(next);
|
||||||
|
},
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
manualPagination: true,
|
||||||
|
},
|
||||||
|
dataTableOptions: {
|
||||||
|
viewPagination: true,
|
||||||
|
onRowClick: handleRowClick,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleConfirmTaskSe = (selectedCodes: string[]) => {
|
||||||
|
setIsTaskSeDialogOpen(false);
|
||||||
|
router.push(`/design-cnstn/regist?taskSeComCd=${formatTaskSeComCdForApi(selectedCodes)}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="card card-border mx-0 bg-background pb-8 pt-0">
|
||||||
|
<div className="card-header flex items-center justify-between">
|
||||||
|
<h3 className="text-2xl font-semibold">설계자문 목록</h3>
|
||||||
|
<Button type="button" color="green" onClick={() => setIsTaskSeDialogOpen(true)}>
|
||||||
|
신청
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{tableComponent}
|
||||||
|
</section>
|
||||||
|
<TaskSePickerDialog
|
||||||
|
open={isTaskSeDialogOpen}
|
||||||
|
onOpenChange={setIsTaskSeDialogOpen}
|
||||||
|
onConfirm={handleConfirmTaskSe}
|
||||||
|
title="설계자문 신청"
|
||||||
|
codeUpCd="CNFM"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DesignList };
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { DesignFormPage } from "./design-form-page";
|
||||||
|
|
||||||
|
interface DesignRegistPageProps {
|
||||||
|
taskSeComCd?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function DesignRegistPage({ taskSeComCd }: DesignRegistPageProps) {
|
||||||
|
return <DesignFormPage mode="create" taskSeComCd={taskSeComCd} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DesignRegistPage };
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo, useRef } from "react";
|
||||||
|
|
||||||
|
import { useUserStore } from "@/shared/lib";
|
||||||
|
import { useDesignCnstnMng } from "@/entities/design-cnstn";
|
||||||
|
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
|
||||||
|
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
||||||
|
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||||
|
import { Skeleton } from "@common/components";
|
||||||
|
|
||||||
|
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
|
||||||
|
import { getEditableReviewStep } from "../lib/review-step-utils";
|
||||||
|
|
||||||
|
import { FloatingActionButtons } from "./components/floating-action-buttons";
|
||||||
|
import { AttachmentsSection } from "./sections/attachments-section";
|
||||||
|
import { BasicInfoSection } from "./sections/basic-info-section";
|
||||||
|
import { BusinessScaleSection } from "./sections/business-scale-section";
|
||||||
|
import { ContactInfoSection } from "./sections/contact-info-section";
|
||||||
|
import {
|
||||||
|
TechReviewOpinionSection,
|
||||||
|
type TechReviewOpinionSectionHandle,
|
||||||
|
} from "./sections/tech-review-opinion-section";
|
||||||
|
|
||||||
|
interface DesignViewPageProps {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DesignViewPage({ id }: DesignViewPageProps) {
|
||||||
|
const userInfo = useUserStore((state) => state.userInfo);
|
||||||
|
const techReviewOpinionRef = useRef<TechReviewOpinionSectionHandle>(null);
|
||||||
|
const { data, isLoading, isError } = useDesignCnstnMng(id);
|
||||||
|
|
||||||
|
const prgrsSttsComCd = data?.detDto?.prgrsSttsComCd || "";
|
||||||
|
const needsAtrzResolve = needsRoleUser01AtrzResolve(userInfo?.dtlInstCd, prgrsSttsComCd);
|
||||||
|
const { data: rvwLoginUserData, isLoading: isRvwLoginUserLoading } = useRvwMnpwMngLoginUserList(
|
||||||
|
{},
|
||||||
|
{ enabled: needsAtrzResolve },
|
||||||
|
);
|
||||||
|
const atrzSeComCdList = useMemo(
|
||||||
|
() => rvwLoginUserData?.content?.map((item) => item.atrzSeComCd) ?? [],
|
||||||
|
[rvwLoginUserData],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="mt-6 flex flex-col gap-4 px-6">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Skeleton className="h-[4rem] rounded-xl" />
|
||||||
|
<Skeleton className="h-[16rem] rounded-xl" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Skeleton className="h-[4rem] rounded-xl" />
|
||||||
|
<Skeleton className="h-[16rem] rounded-xl" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Skeleton className="h-[4rem] rounded-xl" />
|
||||||
|
<Skeleton className="h-[32rem] rounded-xl" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError || !data?.detDto) {
|
||||||
|
return <div className="mt-6 text-[1.4rem]">상세 정보를 찾을 수 없습니다.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const detDto = data.detDto;
|
||||||
|
const bizSclList = data.bizSclList ?? [];
|
||||||
|
const esntlFileList = data.esntlFileList ?? [];
|
||||||
|
const etcFileList = data.etcFileList ?? [];
|
||||||
|
const rvwList = data.rvwList ?? [];
|
||||||
|
|
||||||
|
const editableReviewStep = getEditableReviewStep(
|
||||||
|
userInfo?.dtlInstCd,
|
||||||
|
detDto.prgrsSttsComCd || "",
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6 space-y-4">
|
||||||
|
<BasicInfoSection det={detDto} />
|
||||||
|
<ContactInfoSection det={detDto} />
|
||||||
|
<SectionCollapsible title="결재현황">
|
||||||
|
<ElaprDocPrgrsStatus elaprDocNo={detDto.designCnstnDocNo} />
|
||||||
|
</SectionCollapsible>
|
||||||
|
<BusinessScaleSection bizSclList={bizSclList} />
|
||||||
|
<AttachmentsSection esntlFileList={esntlFileList} etcFileList={etcFileList} />
|
||||||
|
<TechReviewOpinionSection
|
||||||
|
ref={techReviewOpinionRef}
|
||||||
|
rvwList={rvwList}
|
||||||
|
editableReviewStep={editableReviewStep}
|
||||||
|
atrzSeComCdList={atrzSeComCdList}
|
||||||
|
isAtrzLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
||||||
|
/>
|
||||||
|
<FloatingActionButtons
|
||||||
|
data={data}
|
||||||
|
docSeComCd="CNFM"
|
||||||
|
designCnstnDocNo={detDto.designCnstnDocNo}
|
||||||
|
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
|
||||||
|
dtlInstCd={userInfo?.dtlInstCd}
|
||||||
|
atrzSeComCdList={atrzSeComCdList}
|
||||||
|
isRvwLoginUserLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
||||||
|
techReviewOpinionRef={techReviewOpinionRef}
|
||||||
|
opnrNm={userInfo?.userName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DesignViewPage };
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { formatFileSize } from "@/shared/lib/file-utils";
|
||||||
|
import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
|
||||||
|
import type {
|
||||||
|
AtflMngClientResponseDto,
|
||||||
|
EsntlAtchFileMngTreeResponseDto,
|
||||||
|
} from "@/entities/design-cnstn";
|
||||||
|
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||||
|
|
||||||
|
interface AttachmentsSectionProps {
|
||||||
|
esntlFileList: EsntlAtchFileMngTreeResponseDto[];
|
||||||
|
etcFileList: AtflMngClientResponseDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionProps) {
|
||||||
|
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{esntlFileList.length > 0 && (
|
||||||
|
<SectionCollapsible title="필수 첨부파일">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table variant="secondary">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>필수 자료명</TableHead>
|
||||||
|
<TableHead>파일명</TableHead>
|
||||||
|
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{esntlFileList.map((file) => (
|
||||||
|
<TableRow key={file.esntlAtchFileId}>
|
||||||
|
<TableCell className="pl-2 text-left">
|
||||||
|
{file.esntlAtchFileNm}
|
||||||
|
{file.esntlAtchYn === "Y" ? (
|
||||||
|
<span className="ml-1 text-red-500">*</span>
|
||||||
|
) : null}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-left">
|
||||||
|
{file.atflCd ? (
|
||||||
|
<a
|
||||||
|
className="cursor-pointer text-blue-600 hover:underline"
|
||||||
|
onClick={() => downloadAttachment(file.atflCd ?? "")}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{file.orgnlFileNm || "다운로드"}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-500">첨부없음</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">{formatFileSize(file.atflFlieSz)}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</SectionCollapsible>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SectionCollapsible title="기타 첨부파일">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table variant="secondary">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>파일명</TableHead>
|
||||||
|
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{etcFileList.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
|
||||||
|
등록된 기타첨부파일이 없습니다.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
etcFileList.map((file) => (
|
||||||
|
<TableRow key={file.atflId}>
|
||||||
|
<TableCell className="text-left">
|
||||||
|
{file.atflCd ? (
|
||||||
|
<a
|
||||||
|
className="cursor-pointer text-blue-600 hover:underline"
|
||||||
|
onClick={() => downloadAttachment(file.atflCd)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{file.orgnlFileNm || file.physFileNm || file.atflId}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right text-gray-500">
|
||||||
|
{formatFileSize(file.atflFlieSz)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</SectionCollapsible>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AttachmentsSection };
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
import { useUserStore } from "@/shared/lib";
|
||||||
|
import { Field, FieldError, FieldGroup } from "@/shared/ui/field";
|
||||||
|
import { findCodeLabel, useCodeList } from "@/entities/code";
|
||||||
|
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
|
||||||
|
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Table,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@common/components";
|
||||||
|
|
||||||
|
import type { DesignCnstnRegistFormData } from "../../lib/design-cnstn-regist-schema";
|
||||||
|
|
||||||
|
function BasicInfoFormSection() {
|
||||||
|
const userInfo = useUserStore((state) => state.userInfo);
|
||||||
|
const { control, watch, setValue } = useFormContext<DesignCnstnRegistFormData>();
|
||||||
|
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
||||||
|
const taskSeComCd = watch("designCnstnMng.taskSeComCd");
|
||||||
|
const { data: taskOptions } = useCodeList("CNFM");
|
||||||
|
|
||||||
|
function handleBnsSelect(data: BnsApplyData) {
|
||||||
|
setValue("designCnstnMng.bizNm", data.bizNm);
|
||||||
|
setValue("designCnstnMng.bizBgngYr", data.bizBgngYr);
|
||||||
|
setValue("designCnstnMng.bizEndYr", data.bizEndYr);
|
||||||
|
setValue("designCnstnMng.bizCn", data.bizCn);
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskSeComCdLabel =
|
||||||
|
taskSeComCd
|
||||||
|
.map((code) => findCodeLabel(taskOptions, code))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(", ") || "-";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionCollapsible title="기본 정보">
|
||||||
|
<Table variant="secondary">
|
||||||
|
<colgroup>
|
||||||
|
<col width="15%" />
|
||||||
|
<col width="35%" />
|
||||||
|
<col width="15%" />
|
||||||
|
<col width="35%" />
|
||||||
|
</colgroup>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[20rem]" data-required>
|
||||||
|
사업명
|
||||||
|
</TableHead>
|
||||||
|
<TableCell colSpan={3}>
|
||||||
|
<Controller
|
||||||
|
name="designCnstnMng.bizNm"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Field data-invalid={fieldState.invalid}>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id={field.name}
|
||||||
|
placeholder="사업명 입력"
|
||||||
|
aria-invalid={fieldState.invalid}
|
||||||
|
className="w-[40rem]"
|
||||||
|
/>
|
||||||
|
<Button type="button" onClick={() => setIsPickerOpen(true)}>
|
||||||
|
사업찾기
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<FieldError errors={[fieldState.error]} />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead data-required>사업기간</TableHead>
|
||||||
|
<TableCell colSpan={3}>
|
||||||
|
<FieldGroup className="flex-row items-center gap-2">
|
||||||
|
<Controller
|
||||||
|
name="designCnstnMng.bizBgngYr"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Field data-invalid={fieldState.invalid} className="w-auto">
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id={field.name}
|
||||||
|
placeholder="시작연도"
|
||||||
|
className="max-w-[12rem]"
|
||||||
|
aria-invalid={fieldState.invalid}
|
||||||
|
/>
|
||||||
|
<FieldError errors={[fieldState.error]} />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span>~</span>
|
||||||
|
<Controller
|
||||||
|
name="designCnstnMng.bizEndYr"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Field data-invalid={fieldState.invalid} className="w-auto">
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id={field.name}
|
||||||
|
placeholder="종료연도"
|
||||||
|
className="max-w-[12rem]"
|
||||||
|
aria-invalid={fieldState.invalid}
|
||||||
|
/>
|
||||||
|
<FieldError errors={[fieldState.error]} />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FieldGroup>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead data-required>사업내용</TableHead>
|
||||||
|
<TableCell colSpan={3}>
|
||||||
|
<Controller
|
||||||
|
name="designCnstnMng.bizCn"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Field data-invalid={fieldState.invalid}>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id={field.name}
|
||||||
|
placeholder="사업내용 입력"
|
||||||
|
aria-invalid={fieldState.invalid}
|
||||||
|
/>
|
||||||
|
<FieldError errors={[fieldState.error]} />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead data-required>구분</TableHead>
|
||||||
|
<TableCell>{taskSeComCdLabel}</TableCell>
|
||||||
|
<TableHead>신청 지자체</TableHead>
|
||||||
|
<TableCell>{userInfo?.instSeNm}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<BnsPickerDialog
|
||||||
|
open={isPickerOpen}
|
||||||
|
onClose={() => setIsPickerOpen(false)}
|
||||||
|
onSelect={handleBnsSelect}
|
||||||
|
/>
|
||||||
|
</SectionCollapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { BasicInfoFormSection };
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { DesignCnstnMngResponseDto } from "@/entities/design-cnstn";
|
||||||
|
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||||
|
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||||
|
|
||||||
|
interface BasicInfoSectionProps {
|
||||||
|
det: DesignCnstnMngResponseDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||||
|
return (
|
||||||
|
<SectionCollapsible title="기본 정보">
|
||||||
|
<Table variant="secondary">
|
||||||
|
<colgroup>
|
||||||
|
<col width="15%" />
|
||||||
|
<col width="35%" />
|
||||||
|
<col width="15%" />
|
||||||
|
<col width="35%" />
|
||||||
|
</colgroup>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>사업명</TableHead>
|
||||||
|
<TableCell>{det.bizNm || "-"}</TableCell>
|
||||||
|
<TableHead>사업기간</TableHead>
|
||||||
|
<TableCell>{(det.bizBgngYr || "-") + " ~ " + (det.bizEndYr || "-")}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>사업내용</TableHead>
|
||||||
|
<TableCell colSpan={3}>{det.bizCn || "-"}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>환경청구분</TableHead>
|
||||||
|
<TableCell>{det.envoSeCdNm || "-"}</TableCell>
|
||||||
|
<TableHead>구분</TableHead>
|
||||||
|
<TableCell>{det.taskSeCdNm || "-"}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>신청지자체</TableHead>
|
||||||
|
<TableCell colSpan={3}>
|
||||||
|
{det.sido || "-"} {det.gugun || "-"}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
</Table>
|
||||||
|
</SectionCollapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { BasicInfoSection };
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { BizSclTreeDto } from "@/entities/design-cnstn";
|
||||||
|
import { fmtNumber, indentPx, SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||||
|
|
||||||
|
interface BusinessScaleSectionProps {
|
||||||
|
bizSclList: BizSclTreeDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) {
|
||||||
|
return (
|
||||||
|
<SectionCollapsible title="사업규모">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table variant="secondary">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[30rem]">구분</TableHead>
|
||||||
|
<TableHead>기본계획(A)</TableHead>
|
||||||
|
<TableHead>예산반영(B)</TableHead>
|
||||||
|
<TableHead>금회설계시(C)</TableHead>
|
||||||
|
<TableHead>증감(C-B)</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{bizSclList.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="py-12 text-center text-muted-foreground">
|
||||||
|
조회된 사업규모 항목이 없습니다.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
bizSclList.map((node) => {
|
||||||
|
const pl = indentPx(node.indCnstpSeCdNm);
|
||||||
|
const bscPlanQty = Number(node.bscPlanQty) || 0;
|
||||||
|
const budgetReflct = Number(node.bgtRfltQty) || 0;
|
||||||
|
const tstmDesignQty = Number(node.tstmDesignQty) || 0;
|
||||||
|
const diff = tstmDesignQty - budgetReflct;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow key={node.cnstpSeCd}>
|
||||||
|
<TableHead className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||||
|
{node.cnstpSeCdNm}
|
||||||
|
</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<div className="text-right">{fmtNumber(bscPlanQty)}</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="text-right">{fmtNumber(budgetReflct)}</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="text-right">{fmtNumber(tstmDesignQty)}</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="text-right">
|
||||||
|
{diff > 0 ? `+${fmtNumber(diff)}` : fmtNumber(diff)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</SectionCollapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { BusinessScaleSection };
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { DesignCnstnMngResponseDto } from "@/entities/design-cnstn";
|
||||||
|
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||||
|
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||||
|
|
||||||
|
interface ContactInfoSectionProps {
|
||||||
|
det: DesignCnstnMngResponseDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
||||||
|
return (
|
||||||
|
<SectionCollapsible title="담당자 정보">
|
||||||
|
<Table variant="secondary">
|
||||||
|
<TableHeader>
|
||||||
|
{det.aplcntNm && (
|
||||||
|
<>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead rowSpan={2}>신청자</TableHead>
|
||||||
|
<TableHead>이름</TableHead>
|
||||||
|
<TableCell>{det.aplcntNm || "-"}</TableCell>
|
||||||
|
<TableHead>연락처</TableHead>
|
||||||
|
<TableCell>{det.aplcntMblTelno || "-"}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>소속</TableHead>
|
||||||
|
<TableCell>{det.aplcntOgdpNm || "-"}</TableCell>
|
||||||
|
<TableHead>이메일</TableHead>
|
||||||
|
<TableCell>{det.aplcntEmlAddr || "-"}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{det.dsgnrNm && (
|
||||||
|
<>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead rowSpan={2}>설계자</TableHead>
|
||||||
|
<TableHead>이름</TableHead>
|
||||||
|
<TableCell>{det.dsgnrNm || "-"}</TableCell>
|
||||||
|
<TableHead>연락처</TableHead>
|
||||||
|
<TableCell>{det.dsgnrTelno || "-"}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>소속</TableHead>
|
||||||
|
<TableCell>{det.dsgnrOgdpNm || "-"}</TableCell>
|
||||||
|
<TableHead>이메일</TableHead>
|
||||||
|
<TableCell>{det.dsgnrEmlAddr || "-"}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TableHeader>
|
||||||
|
</Table>
|
||||||
|
</SectionCollapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ContactInfoSection };
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||||
|
|
||||||
|
import { PROXY_HOST } from "@/shared/config";
|
||||||
|
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
||||||
|
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||||
|
import { attachService } from "@/entities/attach";
|
||||||
|
import type { DesignCnstnRvwOpnnResponseDto } from "@/entities/design-cnstn";
|
||||||
|
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
RadioGroup,
|
||||||
|
RadioGroupItem,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
Textarea,
|
||||||
|
} from "@common/components";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ReviewOpinionFormData,
|
||||||
|
ReviewOpinionGetFormDataResult,
|
||||||
|
} from "../../lib/review-opinion-utils";
|
||||||
|
import type { ReviewStepCode } from "../../model/view-types";
|
||||||
|
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
||||||
|
|
||||||
|
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
||||||
|
|
||||||
|
interface TechReviewOpinionSectionProps {
|
||||||
|
rvwList: DesignCnstnRvwOpnnResponseDto[];
|
||||||
|
editableReviewStep?: ReviewStepCode;
|
||||||
|
atrzSeComCdList?: string[];
|
||||||
|
isAtrzLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TechReviewOpinionSectionHandle {
|
||||||
|
getFormData: () => ReviewOpinionGetFormDataResult | null;
|
||||||
|
resetForm: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TechReviewOpinionTableProps {
|
||||||
|
items: DesignCnstnRvwOpnnResponseDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UploadedOpinionFile {
|
||||||
|
fileName: string;
|
||||||
|
fileKey: string;
|
||||||
|
atflId?: string;
|
||||||
|
atflCd?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
||||||
|
rvwRsltCn: "",
|
||||||
|
rvwRsltAnmlyYn: "",
|
||||||
|
atchFileGroupId: "",
|
||||||
|
opnrSeComCd: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
function createRvwOpnnUploadKey(file: File) {
|
||||||
|
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasFormContent(formData: ReviewOpinionFormData) {
|
||||||
|
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TechReviewOpinionSection = forwardRef<
|
||||||
|
TechReviewOpinionSectionHandle,
|
||||||
|
TechReviewOpinionSectionProps
|
||||||
|
>(function TechReviewOpinionSection({
|
||||||
|
rvwList,
|
||||||
|
editableReviewStep,
|
||||||
|
atrzSeComCdList = [],
|
||||||
|
isAtrzLoading = false,
|
||||||
|
}, ref) {
|
||||||
|
const canWriteOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||||
|
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||||
|
...EMPTY_FORM_DATA,
|
||||||
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
|
});
|
||||||
|
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
|
}));
|
||||||
|
}, [editableReviewStep]);
|
||||||
|
|
||||||
|
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||||
|
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||||
|
if (!atflCd || !atflId) return;
|
||||||
|
|
||||||
|
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
||||||
|
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||||
|
},
|
||||||
|
onError: (_error, _file, fileKey) => {
|
||||||
|
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
getFormData: () => {
|
||||||
|
if (!canWriteOpinion || !editableReviewStep || !hasFormContent(formData)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
...formData,
|
||||||
|
opnrSeComCd: editableReviewStep,
|
||||||
|
},
|
||||||
|
rvwList,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
resetForm: () => {
|
||||||
|
setFormData({
|
||||||
|
...EMPTY_FORM_DATA,
|
||||||
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
|
});
|
||||||
|
setUploadedFile(null);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[canWriteOpinion, editableReviewStep, formData, rvwList],
|
||||||
|
);
|
||||||
|
|
||||||
|
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||||
|
const hasKecoSection =
|
||||||
|
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteOpinion);
|
||||||
|
|
||||||
|
if (!hasKecoSection) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFileUpload(files: FileList | null) {
|
||||||
|
if (!files || files.length === 0) return;
|
||||||
|
|
||||||
|
const file = files[0];
|
||||||
|
const fileKey = createRvwOpnnUploadKey(file);
|
||||||
|
const existingAtflCd = formData.atchFileGroupId;
|
||||||
|
|
||||||
|
setUploadedFile({ fileName: file.name, fileKey });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await uploadFileAsync(file, {
|
||||||
|
fileKey,
|
||||||
|
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// onError callback handles cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFileRemove() {
|
||||||
|
if (!uploadedFile) return;
|
||||||
|
|
||||||
|
uploads[uploadedFile.fileKey]?.abort();
|
||||||
|
|
||||||
|
if (uploadedFile.atflId) {
|
||||||
|
try {
|
||||||
|
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("검토의견 첨부파일 삭제 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploadedFile(null);
|
||||||
|
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||||
|
if (editableReviewStep !== reviewStep || !canWriteOpinion) return null;
|
||||||
|
|
||||||
|
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||||
|
<Table variant="secondary">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[12rem]">의견내용</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<Textarea
|
||||||
|
value={formData.rvwRsltCn}
|
||||||
|
onChange={(event) =>
|
||||||
|
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
||||||
|
}
|
||||||
|
placeholder="의견내용을 입력하세요"
|
||||||
|
rows={4}
|
||||||
|
className="min-h-[8rem] !text-xl"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>검토결과</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FieldSet>
|
||||||
|
<RadioGroup
|
||||||
|
value={formData.rvwRsltAnmlyYn}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex flex-wrap gap-4">
|
||||||
|
<FieldLabel
|
||||||
|
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
||||||
|
className="!w-auto border-none"
|
||||||
|
>
|
||||||
|
<Field orientation="horizontal" className="w-auto">
|
||||||
|
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
||||||
|
<span className="font-normal">정상</span>
|
||||||
|
</Field>
|
||||||
|
</FieldLabel>
|
||||||
|
<FieldLabel
|
||||||
|
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
||||||
|
className="!w-auto border-none"
|
||||||
|
>
|
||||||
|
<Field orientation="horizontal" className="w-auto">
|
||||||
|
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
||||||
|
<span className="font-normal">이상</span>
|
||||||
|
</Field>
|
||||||
|
</FieldLabel>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</FieldSet>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>첨부파일</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
{uploadedFile ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xl">{uploadedFile.fileName}</span>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
void handleFileRemove();
|
||||||
|
}}
|
||||||
|
disabled={uploading}
|
||||||
|
className="size-8 !bg-destructive"
|
||||||
|
>
|
||||||
|
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
onChange={(event) => {
|
||||||
|
void handleFileUpload(event.target.files);
|
||||||
|
event.target.value = "";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button size="sm" variant="outline" type="button" asChild>
|
||||||
|
<span>파일 선택</span>
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
|
||||||
|
{renderOpinionForm("KECO")}
|
||||||
|
</div>
|
||||||
|
</SectionCollapsible>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table variant="secondary">
|
||||||
|
<colgroup>
|
||||||
|
<col width="10%" />
|
||||||
|
<col width="55%" />
|
||||||
|
<col width="5%" />
|
||||||
|
<col width="20%" />
|
||||||
|
<col width="10%" />
|
||||||
|
</colgroup>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>의견자명</TableHead>
|
||||||
|
<TableHead>의견내용</TableHead>
|
||||||
|
<TableHead>검토결과</TableHead>
|
||||||
|
<TableHead>첨부파일</TableHead>
|
||||||
|
<TableHead>등록일시</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{items.map((item) => (
|
||||||
|
<TableRow key={`${item.designCnstnDocNo}_${item.rvwOpnnSn}`}>
|
||||||
|
<TableCell align="center">{item.opnrNm}</TableCell>
|
||||||
|
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{item.atchFileInfo?.atflCd ? (
|
||||||
|
<a
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${item.atchFileInfo.atflCd}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{item.atchFileInfo.orgnlFileNm || "다운로드"}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
"-"
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { TechReviewOpinionSection };
|
||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
type FnrsCnsltnMngDetResponseDto,
|
type FnrsCnsltnMngDetResponseDto,
|
||||||
fnrsCnsltnMngService,
|
fnrsCnsltnMngService,
|
||||||
} from "@/entities/fnrs-cnsltn";
|
} from "@/entities/fnrs-cnsltn";
|
||||||
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
|
||||||
import { SplmntDmndDialog, type SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt";
|
import { SplmntDmndDialog, type SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt";
|
||||||
import { Button } from "@common/components";
|
import { Button } from "@common/components";
|
||||||
|
|
||||||
@@ -52,6 +51,8 @@ interface FloatingActionButtonsProps {
|
|||||||
fnrsCnsltnDocNo: string;
|
fnrsCnsltnDocNo: string;
|
||||||
prgrsSttsComCd: string;
|
prgrsSttsComCd: string;
|
||||||
dtlInstCd: string | undefined;
|
dtlInstCd: string | undefined;
|
||||||
|
atrzSeComCdList: string[];
|
||||||
|
isRvwLoginUserLoading: boolean;
|
||||||
checklistFormState: ChecklistFormState;
|
checklistFormState: ChecklistFormState;
|
||||||
techReviewOpinionRef?: React.RefObject<TechReviewOpinionSectionHandle | null>;
|
techReviewOpinionRef?: React.RefObject<TechReviewOpinionSectionHandle | null>;
|
||||||
opnrNm?: string;
|
opnrNm?: string;
|
||||||
@@ -65,6 +66,8 @@ function FloatingActionButtons({
|
|||||||
fnrsCnsltnDocNo,
|
fnrsCnsltnDocNo,
|
||||||
prgrsSttsComCd,
|
prgrsSttsComCd,
|
||||||
dtlInstCd,
|
dtlInstCd,
|
||||||
|
atrzSeComCdList,
|
||||||
|
isRvwLoginUserLoading,
|
||||||
checklistFormState,
|
checklistFormState,
|
||||||
techReviewOpinionRef,
|
techReviewOpinionRef,
|
||||||
opnrNm,
|
opnrNm,
|
||||||
@@ -82,18 +85,9 @@ function FloatingActionButtons({
|
|||||||
|
|
||||||
const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd);
|
const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd);
|
||||||
const needsTeApprovalCheck = needsAtrzResolve && prgrsSttsComCd === "TE";
|
const needsTeApprovalCheck = needsAtrzResolve && prgrsSttsComCd === "TE";
|
||||||
const { data: rvwLoginUserData, isLoading: isRvwLoginUserLoading } = useRvwMnpwMngLoginUserList(
|
|
||||||
{},
|
|
||||||
{ enabled: needsAtrzResolve },
|
|
||||||
);
|
|
||||||
const { data: elaprDocPrgrsList = [], isLoading: isElaprDocPrgrsLoading } =
|
const { data: elaprDocPrgrsList = [], isLoading: isElaprDocPrgrsLoading } =
|
||||||
useElaprDocPrgrsByDocNo(needsTeApprovalCheck ? docNo : null);
|
useElaprDocPrgrsByDocNo(needsTeApprovalCheck ? docNo : null);
|
||||||
|
|
||||||
const atrzSeComCdList = useMemo(
|
|
||||||
() => rvwLoginUserData?.content?.map((item) => item.atrzSeComCd) ?? [],
|
|
||||||
[rvwLoginUserData],
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasPendingApprovalRequest = hasCurrentUserPendingApprovalRequest(
|
const hasPendingApprovalRequest = hasCurrentUserPendingApprovalRequest(
|
||||||
elaprDocPrgrsList,
|
elaprDocPrgrsList,
|
||||||
userInfo?.userUnqId,
|
userInfo?.userUnqId,
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRef } from "react";
|
import { useMemo, useRef } from "react";
|
||||||
|
|
||||||
import { useUserStore } from "@/shared/lib";
|
import { useUserStore } from "@/shared/lib";
|
||||||
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
|
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
|
||||||
import { useFnrsCnsltnMng } from "@/entities/fnrs-cnsltn";
|
import { useFnrsCnsltnMng } from "@/entities/fnrs-cnsltn";
|
||||||
|
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
||||||
import { SectionCollapsible, SupplementDemandSection } from "@/widgets/pbadms-sprt";
|
import { SectionCollapsible, SupplementDemandSection } from "@/widgets/pbadms-sprt";
|
||||||
import { Skeleton } from "@common/components";
|
import { Skeleton } from "@common/components";
|
||||||
|
|
||||||
|
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
|
||||||
import { getEditableReviewStep } from "../lib/review-step-utils";
|
import { getEditableReviewStep } from "../lib/review-step-utils";
|
||||||
import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state";
|
import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state";
|
||||||
|
|
||||||
@@ -35,6 +37,17 @@ function FnrsViewPage({ id }: FnrsViewPageProps) {
|
|||||||
const { formState: checklistFormState, setFormState: setChecklistFormState } =
|
const { formState: checklistFormState, setFormState: setChecklistFormState } =
|
||||||
useChecklistFormState(data?.cklstResultList ?? EMPTY_CKLST_RESULT_LIST);
|
useChecklistFormState(data?.cklstResultList ?? EMPTY_CKLST_RESULT_LIST);
|
||||||
|
|
||||||
|
const prgrsSttsComCd = data?.detDto?.prgrsSttsComCd || "";
|
||||||
|
const needsAtrzResolve = needsRoleUser01AtrzResolve(userInfo?.dtlInstCd, prgrsSttsComCd);
|
||||||
|
const { data: rvwLoginUserData, isLoading: isRvwLoginUserLoading } = useRvwMnpwMngLoginUserList(
|
||||||
|
{},
|
||||||
|
{ enabled: needsAtrzResolve },
|
||||||
|
);
|
||||||
|
const atrzSeComCdList = useMemo(
|
||||||
|
() => rvwLoginUserData?.content?.map((item) => item.atrzSeComCd) ?? [],
|
||||||
|
[rvwLoginUserData],
|
||||||
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-6 flex flex-col gap-4 px-6">
|
<div className="mt-6 flex flex-col gap-4 px-6">
|
||||||
@@ -96,6 +109,8 @@ function FnrsViewPage({ id }: FnrsViewPageProps) {
|
|||||||
ref={techReviewOpinionRef}
|
ref={techReviewOpinionRef}
|
||||||
rvwList={rvwList}
|
rvwList={rvwList}
|
||||||
editableReviewStep={editableReviewStep}
|
editableReviewStep={editableReviewStep}
|
||||||
|
atrzSeComCdList={atrzSeComCdList}
|
||||||
|
isAtrzLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
||||||
/>
|
/>
|
||||||
<FloatingActionButtons
|
<FloatingActionButtons
|
||||||
data={data}
|
data={data}
|
||||||
@@ -103,6 +118,8 @@ function FnrsViewPage({ id }: FnrsViewPageProps) {
|
|||||||
fnrsCnsltnDocNo={detDto.fnrsCnsltnDocNo}
|
fnrsCnsltnDocNo={detDto.fnrsCnsltnDocNo}
|
||||||
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
|
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
|
||||||
dtlInstCd={userInfo?.dtlInstCd}
|
dtlInstCd={userInfo?.dtlInstCd}
|
||||||
|
atrzSeComCdList={atrzSeComCdList}
|
||||||
|
isRvwLoginUserLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
||||||
checklistFormState={checklistFormState}
|
checklistFormState={checklistFormState}
|
||||||
techReviewOpinionRef={techReviewOpinionRef}
|
techReviewOpinionRef={techReviewOpinionRef}
|
||||||
opnrNm={userInfo?.userName}
|
opnrNm={userInfo?.userName}
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
|||||||
return (
|
return (
|
||||||
<SectionCollapsible title="기본 정보">
|
<SectionCollapsible title="기본 정보">
|
||||||
<Table variant="secondary">
|
<Table variant="secondary">
|
||||||
|
<colgroup>
|
||||||
|
<col width="15%" />
|
||||||
|
<col width="35%" />
|
||||||
|
<col width="15%" />
|
||||||
|
<col width="35%" />
|
||||||
|
</colgroup>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>사업명</TableHead>
|
<TableHead>사업명</TableHead>
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
|||||||
interface TechReviewOpinionSectionProps {
|
interface TechReviewOpinionSectionProps {
|
||||||
rvwList: FnrsCnsltnRvwOpnnResponseDto[];
|
rvwList: FnrsCnsltnRvwOpnnResponseDto[];
|
||||||
editableReviewStep?: ReviewStepCode;
|
editableReviewStep?: ReviewStepCode;
|
||||||
|
atrzSeComCdList?: string[];
|
||||||
|
isAtrzLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TechReviewOpinionSectionHandle {
|
export interface TechReviewOpinionSectionHandle {
|
||||||
@@ -69,7 +71,13 @@ function hasFormContent(formData: ReviewOpinionFormData) {
|
|||||||
const TechReviewOpinionSection = forwardRef<
|
const TechReviewOpinionSection = forwardRef<
|
||||||
TechReviewOpinionSectionHandle,
|
TechReviewOpinionSectionHandle,
|
||||||
TechReviewOpinionSectionProps
|
TechReviewOpinionSectionProps
|
||||||
>(function TechReviewOpinionSection({ rvwList, editableReviewStep }, ref) {
|
>(function TechReviewOpinionSection({
|
||||||
|
rvwList,
|
||||||
|
editableReviewStep,
|
||||||
|
atrzSeComCdList = [],
|
||||||
|
isAtrzLoading = false,
|
||||||
|
}, ref) {
|
||||||
|
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||||
...EMPTY_FORM_DATA,
|
...EMPTY_FORM_DATA,
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
@@ -102,6 +110,9 @@ const TechReviewOpinionSection = forwardRef<
|
|||||||
if (!editableReviewStep || !hasFormContent(formData)) {
|
if (!editableReviewStep || !hasFormContent(formData)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
formData: {
|
formData: {
|
||||||
@@ -119,14 +130,15 @@ const TechReviewOpinionSection = forwardRef<
|
|||||||
setUploadedFile(null);
|
setUploadedFile(null);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[editableReviewStep, formData, rvwList],
|
[canWriteKecoOpinion, editableReviewStep, formData, rvwList],
|
||||||
);
|
);
|
||||||
|
|
||||||
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
||||||
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||||
|
|
||||||
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
||||||
const hasKecoSection = kecoRvwList.length > 0 || editableReviewStep === "KECO";
|
const hasKecoSection =
|
||||||
|
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteKecoOpinion);
|
||||||
|
|
||||||
if (!hasEnvSection && !hasKecoSection) {
|
if (!hasEnvSection && !hasKecoSection) {
|
||||||
return null;
|
return null;
|
||||||
@@ -170,6 +182,7 @@ const TechReviewOpinionSection = forwardRef<
|
|||||||
|
|
||||||
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||||
if (editableReviewStep !== reviewStep) return null;
|
if (editableReviewStep !== reviewStep) return null;
|
||||||
|
if (reviewStep === "KECO" && !canWriteKecoOpinion) return null;
|
||||||
|
|
||||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
type InstlSncnMngDetResponse,
|
type InstlSncnMngDetResponse,
|
||||||
instlSncnService,
|
instlSncnService,
|
||||||
} from "@/entities/instl-sncn";
|
} from "@/entities/instl-sncn";
|
||||||
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
|
||||||
import { SplmntDmndDialog, type SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt";
|
import { SplmntDmndDialog, type SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt";
|
||||||
import { Button } from "@common/components";
|
import { Button } from "@common/components";
|
||||||
|
|
||||||
@@ -52,6 +51,8 @@ interface FloatingActionButtonsProps {
|
|||||||
instlSncnDocNo: string;
|
instlSncnDocNo: string;
|
||||||
prgrsSttsComCd: string;
|
prgrsSttsComCd: string;
|
||||||
dtlInstCd: string | undefined;
|
dtlInstCd: string | undefined;
|
||||||
|
atrzSeComCdList: string[];
|
||||||
|
isRvwLoginUserLoading: boolean;
|
||||||
checklistFormState: ChecklistFormState;
|
checklistFormState: ChecklistFormState;
|
||||||
techReviewOpinionRef?: React.RefObject<TechReviewOpinionSectionHandle | null>;
|
techReviewOpinionRef?: React.RefObject<TechReviewOpinionSectionHandle | null>;
|
||||||
opnrNm?: string;
|
opnrNm?: string;
|
||||||
@@ -65,6 +66,8 @@ function FloatingActionButtons({
|
|||||||
instlSncnDocNo,
|
instlSncnDocNo,
|
||||||
prgrsSttsComCd,
|
prgrsSttsComCd,
|
||||||
dtlInstCd,
|
dtlInstCd,
|
||||||
|
atrzSeComCdList,
|
||||||
|
isRvwLoginUserLoading,
|
||||||
checklistFormState,
|
checklistFormState,
|
||||||
techReviewOpinionRef,
|
techReviewOpinionRef,
|
||||||
opnrNm,
|
opnrNm,
|
||||||
@@ -82,18 +85,9 @@ function FloatingActionButtons({
|
|||||||
|
|
||||||
const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd);
|
const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd);
|
||||||
const needsTeApprovalCheck = needsAtrzResolve && prgrsSttsComCd === "TE";
|
const needsTeApprovalCheck = needsAtrzResolve && prgrsSttsComCd === "TE";
|
||||||
const { data: rvwLoginUserData, isLoading: isRvwLoginUserLoading } = useRvwMnpwMngLoginUserList(
|
|
||||||
{},
|
|
||||||
{ enabled: needsAtrzResolve },
|
|
||||||
);
|
|
||||||
const { data: elaprDocPrgrsList = [], isLoading: isElaprDocPrgrsLoading } =
|
const { data: elaprDocPrgrsList = [], isLoading: isElaprDocPrgrsLoading } =
|
||||||
useElaprDocPrgrsByDocNo(needsTeApprovalCheck ? docNo : null);
|
useElaprDocPrgrsByDocNo(needsTeApprovalCheck ? docNo : null);
|
||||||
|
|
||||||
const atrzSeComCdList = useMemo(
|
|
||||||
() => rvwLoginUserData?.content?.map((item) => item.atrzSeComCd) ?? [],
|
|
||||||
[rvwLoginUserData],
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasPendingApprovalRequest = hasCurrentUserPendingApprovalRequest(
|
const hasPendingApprovalRequest = hasCurrentUserPendingApprovalRequest(
|
||||||
elaprDocPrgrsList,
|
elaprDocPrgrsList,
|
||||||
userInfo?.userUnqId,
|
userInfo?.userUnqId,
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRef } from "react";
|
import { useMemo, useRef } from "react";
|
||||||
|
|
||||||
import { useUserStore } from "@/shared/lib";
|
import { useUserStore } from "@/shared/lib";
|
||||||
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
|
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
|
||||||
import { useInstlSncnDetail } from "@/entities/instl-sncn";
|
import { useInstlSncnDetail } from "@/entities/instl-sncn";
|
||||||
|
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
||||||
import { SectionCollapsible, SupplementDemandSection } from "@/widgets/pbadms-sprt";
|
import { SectionCollapsible, SupplementDemandSection } from "@/widgets/pbadms-sprt";
|
||||||
import { Skeleton } from "@common/components";
|
import { Skeleton } from "@common/components";
|
||||||
|
|
||||||
|
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
|
||||||
import { getEditableReviewStep } from "../lib/review-step-utils";
|
import { getEditableReviewStep } from "../lib/review-step-utils";
|
||||||
import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state";
|
import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state";
|
||||||
|
|
||||||
@@ -34,6 +36,17 @@ function InstlViewPage({ id }: InstlViewPageProps) {
|
|||||||
const { formState: checklistFormState, setFormState: setChecklistFormState } =
|
const { formState: checklistFormState, setFormState: setChecklistFormState } =
|
||||||
useChecklistFormState(data?.cklstResultList ?? EMPTY_CKLST_RESULT_LIST);
|
useChecklistFormState(data?.cklstResultList ?? EMPTY_CKLST_RESULT_LIST);
|
||||||
|
|
||||||
|
const prgrsSttsComCd = data?.detDto?.prgrsSttsComCd || "";
|
||||||
|
const needsAtrzResolve = needsRoleUser01AtrzResolve(userInfo?.dtlInstCd, prgrsSttsComCd);
|
||||||
|
const { data: rvwLoginUserData, isLoading: isRvwLoginUserLoading } = useRvwMnpwMngLoginUserList(
|
||||||
|
{},
|
||||||
|
{ enabled: needsAtrzResolve },
|
||||||
|
);
|
||||||
|
const atrzSeComCdList = useMemo(
|
||||||
|
() => rvwLoginUserData?.content?.map((item) => item.atrzSeComCd) ?? [],
|
||||||
|
[rvwLoginUserData],
|
||||||
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-6 flex flex-col gap-4 px-6">
|
<div className="mt-6 flex flex-col gap-4 px-6">
|
||||||
@@ -94,6 +107,8 @@ function InstlViewPage({ id }: InstlViewPageProps) {
|
|||||||
ref={techReviewOpinionRef}
|
ref={techReviewOpinionRef}
|
||||||
rvwList={rvwList}
|
rvwList={rvwList}
|
||||||
editableReviewStep={editableReviewStep}
|
editableReviewStep={editableReviewStep}
|
||||||
|
atrzSeComCdList={atrzSeComCdList}
|
||||||
|
isAtrzLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
||||||
/>
|
/>
|
||||||
<SupplementDemandSection splmntDmndList={splmntDmndList} />
|
<SupplementDemandSection splmntDmndList={splmntDmndList} />
|
||||||
<FloatingActionButtons
|
<FloatingActionButtons
|
||||||
@@ -102,6 +117,8 @@ function InstlViewPage({ id }: InstlViewPageProps) {
|
|||||||
instlSncnDocNo={detDto.instlSncnDocNo}
|
instlSncnDocNo={detDto.instlSncnDocNo}
|
||||||
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
|
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
|
||||||
dtlInstCd={userInfo?.dtlInstCd}
|
dtlInstCd={userInfo?.dtlInstCd}
|
||||||
|
atrzSeComCdList={atrzSeComCdList}
|
||||||
|
isRvwLoginUserLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
||||||
checklistFormState={checklistFormState}
|
checklistFormState={checklistFormState}
|
||||||
techReviewOpinionRef={techReviewOpinionRef}
|
techReviewOpinionRef={techReviewOpinionRef}
|
||||||
opnrNm={userInfo?.userName}
|
opnrNm={userInfo?.userName}
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
|||||||
return (
|
return (
|
||||||
<SectionCollapsible title="기본 정보">
|
<SectionCollapsible title="기본 정보">
|
||||||
<Table variant="secondary">
|
<Table variant="secondary">
|
||||||
|
<colgroup>
|
||||||
|
<col width="15%" />
|
||||||
|
<col width="35%" />
|
||||||
|
<col width="15%" />
|
||||||
|
<col width="35%" />
|
||||||
|
</colgroup>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>사업명</TableHead>
|
<TableHead>사업명</TableHead>
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
|||||||
interface TechReviewOpinionSectionProps {
|
interface TechReviewOpinionSectionProps {
|
||||||
rvwList: InstlSncnRvwOpnnResponseDto[];
|
rvwList: InstlSncnRvwOpnnResponseDto[];
|
||||||
editableReviewStep?: ReviewStepCode;
|
editableReviewStep?: ReviewStepCode;
|
||||||
|
atrzSeComCdList?: string[];
|
||||||
|
isAtrzLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TechReviewOpinionSectionHandle {
|
export interface TechReviewOpinionSectionHandle {
|
||||||
@@ -65,7 +67,13 @@ function hasFormContent(formData: ReviewOpinionFormData) {
|
|||||||
const TechReviewOpinionSection = forwardRef<
|
const TechReviewOpinionSection = forwardRef<
|
||||||
TechReviewOpinionSectionHandle,
|
TechReviewOpinionSectionHandle,
|
||||||
TechReviewOpinionSectionProps
|
TechReviewOpinionSectionProps
|
||||||
>(function TechReviewOpinionSection({ rvwList, editableReviewStep }, ref) {
|
>(function TechReviewOpinionSection({
|
||||||
|
rvwList,
|
||||||
|
editableReviewStep,
|
||||||
|
atrzSeComCdList = [],
|
||||||
|
isAtrzLoading = false,
|
||||||
|
}, ref) {
|
||||||
|
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||||
...EMPTY_FORM_DATA,
|
...EMPTY_FORM_DATA,
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
@@ -98,6 +106,9 @@ const TechReviewOpinionSection = forwardRef<
|
|||||||
if (!editableReviewStep || !hasFormContent(formData)) {
|
if (!editableReviewStep || !hasFormContent(formData)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...formData,
|
...formData,
|
||||||
@@ -112,14 +123,15 @@ const TechReviewOpinionSection = forwardRef<
|
|||||||
setUploadedFile(null);
|
setUploadedFile(null);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[editableReviewStep, formData],
|
[canWriteKecoOpinion, editableReviewStep, formData],
|
||||||
);
|
);
|
||||||
|
|
||||||
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
||||||
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||||
|
|
||||||
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
||||||
const hasKecoSection = kecoRvwList.length > 0 || editableReviewStep === "KECO";
|
const hasKecoSection =
|
||||||
|
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteKecoOpinion);
|
||||||
|
|
||||||
if (!hasEnvSection && !hasKecoSection) {
|
if (!hasEnvSection && !hasKecoSection) {
|
||||||
return null;
|
return null;
|
||||||
@@ -163,6 +175,7 @@ const TechReviewOpinionSection = forwardRef<
|
|||||||
|
|
||||||
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||||
if (editableReviewStep !== reviewStep) return null;
|
if (editableReviewStep !== reviewStep) return null;
|
||||||
|
if (reviewStep === "KECO" && !canWriteKecoOpinion) return null;
|
||||||
|
|
||||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ export const ENDPOINT = {
|
|||||||
ADSTDSTR: "/as-service/api/v1/adstdstr",
|
ADSTDSTR: "/as-service/api/v1/adstdstr",
|
||||||
FNRS_CNSLTN_MNG: "/as-service/api/v1/fnrs-cnsltn-mng",
|
FNRS_CNSLTN_MNG: "/as-service/api/v1/fnrs-cnsltn-mng",
|
||||||
FNRS_SHAR_SE: "/as-service/api/v1/fnrs-shar-se-mng",
|
FNRS_SHAR_SE: "/as-service/api/v1/fnrs-shar-se-mng",
|
||||||
|
DESIGN_CNSTN: "/as-service/api/v1/design-cnstn",
|
||||||
|
DESIGN_CNSTN_BIZ_CST: "/as-service/api/v1/design-cnstn-biz-cst",
|
||||||
|
DESIGN_CNSTN_BIZ_SCL: "/as-service/api/v1/design-cnstn-biz-scl",
|
||||||
|
DESIGN_CNSTN_ESNTL_ATCH_FILE: "/as-service/api/v1/design-cnstn-esntl-atch-file",
|
||||||
|
DESIGN_CNSTN_RVW_OPNN: "/as-service/api/v1/design-cnstn-rvw-opnn",
|
||||||
},
|
},
|
||||||
USER_SERVICE: {
|
USER_SERVICE: {
|
||||||
LOGIN: "/user-service/login",
|
LOGIN: "/user-service/login",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getDefaultFileKey } from "@/shared/lib";
|
import { getDefaultFileKey } from "@/shared/lib";
|
||||||
|
|
||||||
export type FieldPrefix = "fnrsCnsltnMng" | "instlSncnMng";
|
export type FieldPrefix = "fnrsCnsltnMng" | "instlSncnMng" | "designCnstnMng";
|
||||||
|
|
||||||
export const ETC_FILE_KEY_PREFIX = "etc::";
|
export const ETC_FILE_KEY_PREFIX = "etc::";
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { buildChecklistTabs, syncCklstResultListFromTree } from "../lib/checklis
|
|||||||
|
|
||||||
import { SectionCollapsible } from "./section-collapsible";
|
import { SectionCollapsible } from "./section-collapsible";
|
||||||
|
|
||||||
type FieldPrefix = "fnrsCnsltnMng" | "instlSncnMng";
|
type FieldPrefix = "fnrsCnsltnMng" | "instlSncnMng" | "designCnstnMng";
|
||||||
|
|
||||||
interface CklstResultListItem {
|
interface CklstResultListItem {
|
||||||
cklstSn: number;
|
cklstSn: number;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Field, FieldError } from "@/shared/ui/field";
|
|||||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||||
import { Input, Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
import { Input, Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||||
|
|
||||||
type FieldPrefix = "fnrsCnsltnMng" | "instlSncnMng";
|
type FieldPrefix = "fnrsCnsltnMng" | "instlSncnMng" | "designCnstnMng";
|
||||||
|
|
||||||
interface ContactInfoFormSectionProps {
|
interface ContactInfoFormSectionProps {
|
||||||
fieldPrefix: FieldPrefix;
|
fieldPrefix: FieldPrefix;
|
||||||
|
|||||||
@@ -22,6 +22,12 @@
|
|||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
"../../packages/common/types/**/*.d.ts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules", ".next", "dist", "coverage", "storybook-static", "public"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"@common/assets": "workspace:*",
|
"@common/assets": "workspace:*",
|
||||||
"@common/auth": "workspace:*",
|
"@common/auth": "workspace:*",
|
||||||
"@common/components": "workspace:*",
|
"@common/components": "workspace:*",
|
||||||
"@common/config": "workspace:*",
|
"@common/network": "workspace:*",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
@@ -47,7 +47,6 @@
|
|||||||
"sonner": "^2.0.1",
|
"sonner": "^2.0.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tus-js-client": "^4.3.1",
|
"tus-js-client": "^4.3.1",
|
||||||
"uuid": "^14.0.1",
|
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.24.2",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,59 +5,42 @@ import {
|
|||||||
ENDPOINT,
|
ENDPOINT,
|
||||||
} from "@/shared/config";
|
} from "@/shared/config";
|
||||||
import {
|
import {
|
||||||
|
clearDeviceId,
|
||||||
|
getOrCreateDeviceId,
|
||||||
type AuthPayload,
|
type AuthPayload,
|
||||||
type AuthService,
|
type AuthService,
|
||||||
getRemainingMs,
|
|
||||||
} from "@common/auth";
|
} from "@common/auth";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
clearRefreshTimer,
|
clearRefreshTimer,
|
||||||
setToken,
|
setToken,
|
||||||
setupRefreshTimer,
|
|
||||||
tokenManager,
|
tokenManager,
|
||||||
} from "../lib/token-manager";
|
} from "../lib/token-manager";
|
||||||
import { clearDeviceId } from "../lib/device-id";
|
|
||||||
|
|
||||||
function resolveLoginError(message: string | undefined): string {
|
|
||||||
switch (message) {
|
|
||||||
case "Invalid Credentials":
|
|
||||||
return "아이디와 비밀번호가 일치하지 않습니다.";
|
|
||||||
case "User not found":
|
|
||||||
return "존재하지 않는 아이디입니다.";
|
|
||||||
case "User is not active":
|
|
||||||
return "활성화되지 않은 계정입니다.";
|
|
||||||
case "User is not verified":
|
|
||||||
return "이메일 인증이 필요합니다.";
|
|
||||||
default:
|
|
||||||
return message ?? "로그인에 실패했습니다.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function completeSession(accessToken: string, tokenId: string): void {
|
function completeSession(accessToken: string, tokenId: string): void {
|
||||||
setToken(CLAIM_NAME, accessToken);
|
setToken(CLAIM_NAME, accessToken);
|
||||||
setToken(AUTH_USER_ID, tokenId);
|
setToken(AUTH_USER_ID, tokenId);
|
||||||
|
|
||||||
const remainingMs = getRemainingMs(accessToken);
|
// const remainingMs = getRemainingMs(accessToken);
|
||||||
if (remainingMs > 0) {
|
// if (remainingMs > 0) {
|
||||||
setupRefreshTimer(silentRefresh, remainingMs);
|
// setupRefreshTimer(silentRefresh, remainingMs);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function login(payload: AuthPayload): Promise<void> {
|
async function login(payload: AuthPayload): Promise<void> {
|
||||||
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
|
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Device-Id": getOrCreateDeviceId(),
|
||||||
|
},
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
const accessToken = data[ACCESS_TOKEN];
|
||||||
throw new Error(resolveLoginError(data?.message));
|
const tokenId = data[AUTH_USER_ID];
|
||||||
}
|
|
||||||
|
|
||||||
const accessToken = data[ACCESS_TOKEN] as string | undefined;
|
|
||||||
const tokenId = data[AUTH_USER_ID] as string | undefined;
|
|
||||||
|
|
||||||
if (!accessToken || !tokenId) {
|
if (!accessToken || !tokenId) {
|
||||||
throw new Error("인증 토큰을 받지 못했습니다.");
|
throw new Error("인증 토큰을 받지 못했습니다.");
|
||||||
@@ -71,7 +54,12 @@ async function silentRefresh(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
||||||
{ method: "PUT" },
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"X-Device-Id": getOrCreateDeviceId(),
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -80,8 +68,8 @@ async function silentRefresh(): Promise<void> {
|
|||||||
throw new Error(data?.message ?? "토큰 갱신에 실패했습니다.");
|
throw new Error(data?.message ?? "토큰 갱신에 실패했습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessToken = data[ACCESS_TOKEN] as string | undefined;
|
const accessToken = data[ACCESS_TOKEN];
|
||||||
const tokenId = data[AUTH_USER_ID] as string | undefined;
|
const tokenId = data[AUTH_USER_ID];
|
||||||
|
|
||||||
if (!accessToken || !tokenId) {
|
if (!accessToken || !tokenId) {
|
||||||
throw new Error("갱신된 인증 토큰을 받지 못했습니다.");
|
throw new Error("갱신된 인증 토큰을 받지 못했습니다.");
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
|
|
||||||
const DEVICE_ID_KEY = "x-device-id";
|
|
||||||
|
|
||||||
export function getOrCreateDeviceId(): string {
|
|
||||||
if (typeof window === "undefined") return "";
|
|
||||||
|
|
||||||
const existing = localStorage.getItem(DEVICE_ID_KEY);
|
|
||||||
if (existing) return existing;
|
|
||||||
|
|
||||||
const id = uuidv4();
|
|
||||||
localStorage.setItem(DEVICE_ID_KEY, id);
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearDeviceId(): void {
|
|
||||||
if (typeof window === "undefined") return;
|
|
||||||
|
|
||||||
localStorage.removeItem(DEVICE_ID_KEY);
|
|
||||||
}
|
|
||||||
@@ -22,26 +22,23 @@
|
|||||||
"name": "next"
|
"name": "next"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"baseUrl": "./src",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"],
|
"@/*": ["./src/*"]
|
||||||
"@common/components": ["../../../packages/common/components/src/"],
|
|
||||||
"@common/config": ["../../../packages/common/config/dist/"],
|
|
||||||
"@common/network": ["../../../packages/common/network/"]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
".eslintrc.*",
|
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
"**/*.mts",
|
".next/types/**/*.ts",
|
||||||
"**/*.js",
|
"../../packages/common/types/**/*.d.ts"
|
||||||
"**/*.cjs",
|
|
||||||
"**/*.mjs",
|
|
||||||
"**/*.jsx",
|
|
||||||
"**/*.json",
|
|
||||||
".next/types/**/*.ts"
|
|
||||||
],
|
],
|
||||||
"exclude": ["**/node_modules", "**/.*/*"]
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
".next",
|
||||||
|
"dist",
|
||||||
|
"coverage",
|
||||||
|
"storybook-static",
|
||||||
|
"public"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,26 +22,17 @@
|
|||||||
"name": "next"
|
"name": "next"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"baseUrl": "./src",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"],
|
"@/*": ["./src/*"]
|
||||||
"@common/assets": ["../../../../packages/common/assets/dist/"],
|
|
||||||
"@common/components": ["../../../../packages/common/components/dist/"],
|
|
||||||
"@common/network": ["../../../../packages/common/network/"]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
".eslintrc.*",
|
"exclude": [
|
||||||
"next-env.d.ts",
|
"node_modules",
|
||||||
"**/*.ts",
|
".next",
|
||||||
"**/*.tsx",
|
"dist",
|
||||||
"**/*.mts",
|
"coverage",
|
||||||
"**/*.js",
|
"storybook-static",
|
||||||
"**/*.cjs",
|
"public"
|
||||||
"**/*.mjs",
|
]
|
||||||
"**/*.jsx",
|
|
||||||
"**/*.json",
|
|
||||||
".next/types/**/*.ts"
|
|
||||||
],
|
|
||||||
"exclude": ["**/node_modules", "**/.*/*"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import "@/app/styles/base.css";
|
import "@/app/styles/base.css";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "유역하수도 통합 정보플랫폼",
|
title: "하수도플랫폼 통합관리시스템",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AuthLayout({
|
export default function AuthLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
<body>{children}</body>
|
<body>{children}</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Metadata } from "next";
|
|||||||
import { App } from "@/app";
|
import { App } from "@/app";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "유역하수도 통합 정보플랫폼",
|
title: "하수도플랫폼 통합관리시스템",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
import { COOKIE_DOMAIN, REFRESH_TOKEN } from "@/shared/config";
|
||||||
|
|
||||||
|
export async function POST() {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
cookieStore.delete({ name: REFRESH_TOKEN, domain: COOKIE_DOMAIN });
|
||||||
|
return NextResponse.json({ message: "Logged out" }, { status: 200 });
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
import "@/app/styles/base.css";
|
||||||
|
import "@/app/styles/components.css";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "하수도플랫폼 통합관리시스템",
|
||||||
|
description: "페이지가 존재하지 않습니다. 관리자에게 문의하여 주십시오.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function GlobalNotFound() {
|
||||||
|
return (
|
||||||
|
<html lang="ko">
|
||||||
|
<body>
|
||||||
|
<main className="flex min-h-screen flex-col items-center justify-center bg-[#f0f4f8] px-4 py-16">
|
||||||
|
<div className="mb-16 text-center">
|
||||||
|
<h1 className="text-[3.2rem] font-bold leading-snug tracking-tight">
|
||||||
|
<span className="text-primary">하수도플랫폼 통합관리시스템</span>
|
||||||
|
에 오신 것을 환영합니다.
|
||||||
|
</h1>
|
||||||
|
<p className="mt-6 text-[2rem] text-[#666]">
|
||||||
|
오류가 발생하였습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="card card-border flex w-full max-w-[72rem] flex-col items-center !rounded-2xl !py-16">
|
||||||
|
<Image
|
||||||
|
src="/images/magnifying-glass-with-warning.png"
|
||||||
|
alt=""
|
||||||
|
width={280}
|
||||||
|
height={280}
|
||||||
|
className="size-[28rem] object-contain"
|
||||||
|
/>
|
||||||
|
<p className="mt-4 text-[2.4rem] font-bold text-[#444]">
|
||||||
|
페이지가 존재하지 않습니다.
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-[1.6rem] text-[#666]">
|
||||||
|
관리자에게 문의하여 주십시오
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ const nextConfig: NextConfig = {
|
|||||||
output: "standalone",
|
output: "standalone",
|
||||||
experimental: {
|
experimental: {
|
||||||
proxyTimeout: 600000,
|
proxyTimeout: 600000,
|
||||||
|
globalNotFound: true,
|
||||||
},
|
},
|
||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true,
|
ignoreDuringBuilds: true,
|
||||||
|
|||||||
@@ -45,7 +45,6 @@
|
|||||||
"sonner": "^2.0.1",
|
"sonner": "^2.0.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tus-js-client": "^4.3.1",
|
"tus-js-client": "^4.3.1",
|
||||||
"uuid": "^14.0.1",
|
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.24.2",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
|
|||||||
|
After Width: | Height: | Size: 483 KiB |
@@ -7,32 +7,16 @@ import {
|
|||||||
import {
|
import {
|
||||||
type AuthPayload,
|
type AuthPayload,
|
||||||
type AuthService,
|
type AuthService,
|
||||||
getRemainingMs,
|
clearDeviceId,
|
||||||
|
getOrCreateDeviceId,
|
||||||
} from "@common/auth";
|
} from "@common/auth";
|
||||||
|
|
||||||
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
|
|
||||||
import {
|
import {
|
||||||
clearRefreshTimer,
|
clearRefreshTimer,
|
||||||
setToken,
|
setToken,
|
||||||
setupRefreshTimer,
|
|
||||||
tokenManager,
|
tokenManager,
|
||||||
} from "../lib/token-manager";
|
} from "../lib/token-manager";
|
||||||
|
|
||||||
function resolveLoginError(message: string | undefined): string {
|
|
||||||
switch (message) {
|
|
||||||
case "Invalid Credentials":
|
|
||||||
return "아이디와 비밀번호가 일치하지 않습니다.";
|
|
||||||
case "User not found":
|
|
||||||
return "존재하지 않는 아이디입니다.";
|
|
||||||
case "User is not active":
|
|
||||||
return "활성화되지 않은 계정입니다.";
|
|
||||||
case "User is not verified":
|
|
||||||
return "이메일 인증이 필요합니다.";
|
|
||||||
default:
|
|
||||||
return message ?? "로그인에 실패했습니다.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function login(payload: AuthPayload): Promise<void> {
|
async function login(payload: AuthPayload): Promise<void> {
|
||||||
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
|
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -45,12 +29,8 @@ async function login(payload: AuthPayload): Promise<void> {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
const accessToken = data[ACCESS_TOKEN];
|
||||||
throw new Error(resolveLoginError(data?.message));
|
const tokenId = data[AUTH_USER_ID];
|
||||||
}
|
|
||||||
|
|
||||||
const accessToken = data[ACCESS_TOKEN] as string | undefined;
|
|
||||||
const tokenId = data[AUTH_USER_ID] as string | undefined;
|
|
||||||
|
|
||||||
if (!accessToken || !tokenId) {
|
if (!accessToken || !tokenId) {
|
||||||
throw new Error("인증 토큰을 받지 못했습니다.");
|
throw new Error("인증 토큰을 받지 못했습니다.");
|
||||||
@@ -59,10 +39,10 @@ async function login(payload: AuthPayload): Promise<void> {
|
|||||||
setToken(CLAIM_NAME, accessToken);
|
setToken(CLAIM_NAME, accessToken);
|
||||||
setToken(AUTH_USER_ID, tokenId);
|
setToken(AUTH_USER_ID, tokenId);
|
||||||
|
|
||||||
const remainingMs = getRemainingMs(accessToken);
|
// const remainingMs = getRemainingMs(accessToken);
|
||||||
if (remainingMs > 0) {
|
// if (remainingMs > 0) {
|
||||||
setupRefreshTimer(silentRefresh, remainingMs);
|
// setupRefreshTimer(silentRefresh, remainingMs);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function silentRefresh(): Promise<void> {
|
async function silentRefresh(): Promise<void> {
|
||||||
@@ -84,8 +64,8 @@ async function silentRefresh(): Promise<void> {
|
|||||||
throw new Error(data?.message ?? "토큰 갱신에 실패했습니다.");
|
throw new Error(data?.message ?? "토큰 갱신에 실패했습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessToken = data[ACCESS_TOKEN] as string | undefined;
|
const accessToken = data[ACCESS_TOKEN];
|
||||||
const tokenId = data[AUTH_USER_ID] as string | undefined;
|
const tokenId = data[AUTH_USER_ID];
|
||||||
|
|
||||||
if (!accessToken || !tokenId) {
|
if (!accessToken || !tokenId) {
|
||||||
throw new Error("갱신된 인증 토큰을 받지 못했습니다.");
|
throw new Error("갱신된 인증 토큰을 받지 못했습니다.");
|
||||||
@@ -94,10 +74,10 @@ async function silentRefresh(): Promise<void> {
|
|||||||
setToken(CLAIM_NAME, accessToken);
|
setToken(CLAIM_NAME, accessToken);
|
||||||
setToken(AUTH_USER_ID, tokenId);
|
setToken(AUTH_USER_ID, tokenId);
|
||||||
|
|
||||||
const remainingMs = getRemainingMs(accessToken);
|
// const remainingMs = getRemainingMs(accessToken);
|
||||||
if (remainingMs > 0) {
|
// if (remainingMs > 0) {
|
||||||
setupRefreshTimer(silentRefresh, remainingMs);
|
// setupRefreshTimer(silentRefresh, remainingMs);
|
||||||
}
|
// }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
`[silentRefresh] Failed at ${new Date().toISOString()}`,
|
`[silentRefresh] Failed at ${new Date().toISOString()}`,
|
||||||
@@ -112,6 +92,7 @@ async function silentRefresh(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function logout(): Promise<void> {
|
async function logout(): Promise<void> {
|
||||||
|
await fetch("/api/auth/logout", { method: "POST" });
|
||||||
tokenManager.clearAuthToken();
|
tokenManager.clearAuthToken();
|
||||||
clearDeviceId();
|
clearDeviceId();
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
|
|
||||||
const DEVICE_ID_KEY = "x-device-id";
|
|
||||||
|
|
||||||
export function getOrCreateDeviceId(): string {
|
|
||||||
if (typeof window === "undefined") return "";
|
|
||||||
|
|
||||||
const existing = localStorage.getItem(DEVICE_ID_KEY);
|
|
||||||
if (existing) return existing;
|
|
||||||
|
|
||||||
const id = uuidv4();
|
|
||||||
localStorage.setItem(DEVICE_ID_KEY, id);
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearDeviceId(): void {
|
|
||||||
if (typeof window === "undefined") return;
|
|
||||||
|
|
||||||
localStorage.removeItem(DEVICE_ID_KEY);
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { PageableList, PageableParams } from "@/shared/model";
|
import type { PageableList, PageableParams } from "@/shared/model";
|
||||||
|
|
||||||
export interface AuthorizationsParams extends PageableParams {
|
export interface AuthorizationsParams extends PageableParams {
|
||||||
keywordType?: string;
|
keywordType?: string;
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
authorizationName?: string;
|
authorizationName?: string;
|
||||||
|
urlPatternValue?: string;
|
||||||
httpMethodCode?: string;
|
httpMethodCode?: string;
|
||||||
useYn?: string;
|
useYn?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { columns } from "../model/authorizations-table-columns";
|
|||||||
|
|
||||||
const DEFAULT_PARAMS = {
|
const DEFAULT_PARAMS = {
|
||||||
authorizationName: "",
|
authorizationName: "",
|
||||||
|
urlPatternValue: "",
|
||||||
httpMethodCode: "",
|
httpMethodCode: "",
|
||||||
sort: "regDt,desc",
|
sort: "regDt,desc",
|
||||||
page: 0,
|
page: 0,
|
||||||
@@ -36,6 +37,9 @@ function ModulePage() {
|
|||||||
const [authorizationName, setAuthorizationName] = useState(
|
const [authorizationName, setAuthorizationName] = useState(
|
||||||
params.authorizationName,
|
params.authorizationName,
|
||||||
);
|
);
|
||||||
|
const [urlPatternValue, setUrlPatternValue] = useState(
|
||||||
|
params.urlPatternValue,
|
||||||
|
);
|
||||||
const [httpMethodCode, setHttpMethodCode] = useState(params.httpMethodCode);
|
const [httpMethodCode, setHttpMethodCode] = useState(params.httpMethodCode);
|
||||||
|
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
@@ -43,6 +47,7 @@ function ModulePage() {
|
|||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
AuthorizationsApi.getAuthorizationsList({
|
AuthorizationsApi.getAuthorizationsList({
|
||||||
authorizationName: params.authorizationName,
|
authorizationName: params.authorizationName,
|
||||||
|
urlPatternValue: params.urlPatternValue,
|
||||||
httpMethodCode: params.httpMethodCode,
|
httpMethodCode: params.httpMethodCode,
|
||||||
sort: params.sort,
|
sort: params.sort,
|
||||||
page: params.page,
|
page: params.page,
|
||||||
@@ -63,11 +68,12 @@ function ModulePage() {
|
|||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setParams({ authorizationName, httpMethodCode, page: 0 });
|
setParams({ authorizationName, urlPatternValue, httpMethodCode, page: 0 });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
setAuthorizationName("");
|
setAuthorizationName("");
|
||||||
|
setUrlPatternValue("");
|
||||||
setHttpMethodCode("");
|
setHttpMethodCode("");
|
||||||
resetParams();
|
resetParams();
|
||||||
};
|
};
|
||||||
@@ -113,12 +119,23 @@ function ModulePage() {
|
|||||||
API명
|
API명
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="keyword"
|
id="authorizationName"
|
||||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||||
value={authorizationName}
|
value={authorizationName}
|
||||||
onChange={(e) => setAuthorizationName(e.target.value)}
|
onChange={(e) => setAuthorizationName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Label htmlFor="urlPatternValue" className="text-label">
|
||||||
|
API URL
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="urlPatternValue"
|
||||||
|
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||||
|
value={urlPatternValue}
|
||||||
|
onChange={(e) => setUrlPatternValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button className="h-[3.2rem]" type="submit">
|
<Button className="h-[3.2rem]" type="submit">
|
||||||
조회
|
조회
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
import { ENDPOINT } from "@/shared/config";
|
||||||
|
import { useAlertStore, useMenuStore, useMutationWrapper } from "@/shared/lib";
|
||||||
|
import { AttachmentApi } from "@/entities/attachment";
|
||||||
|
import { BoardApi } from "@/entities/board";
|
||||||
|
import { PostApi } from "@/entities/post";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Label,
|
Label,
|
||||||
@@ -12,14 +19,11 @@ import {
|
|||||||
RadioGroupItem,
|
RadioGroupItem,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
} from "@common/components";
|
} from "@common/components";
|
||||||
import { AttachmentApi } from "@/entities/attachment";
|
|
||||||
import { PostApi } from "@/entities/post";
|
import { PostEvaluationApi } from "../api/post-evaluation-service";
|
||||||
import { BoardApi } from "@/entities/board";
|
|
||||||
import { ENDPOINT } from "@/shared/config";
|
|
||||||
import { useAlertStore, useMutationWrapper, useMenuStore } from "@/shared/lib";
|
|
||||||
import { PostAction } from "./post-action";
|
import { PostAction } from "./post-action";
|
||||||
import { PostComment } from "./post-comment";
|
import { PostComment } from "./post-comment";
|
||||||
import { PostEvaluationApi } from "../api/post-evaluation-service";
|
|
||||||
|
|
||||||
function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -255,7 +259,9 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</PostAction>
|
</PostAction>
|
||||||
<PostComment bbsId={parseInt(bbsId)} pstNo={parseInt(pstNo)} />
|
{bbsInfo?.cmntUseYn === "Y" && (
|
||||||
|
<PostComment bbsId={parseInt(bbsId)} pstNo={parseInt(pstNo)} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss";
|
||||||
import tailwindcssAnimate from "tailwindcss-animate";
|
import tailwindcssAnimate from "tailwindcss-animate";
|
||||||
|
|
||||||
import presetConfig from "../../tailwind.config.preset";
|
import presetConfig from "../../tailwind.config.preset";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
presets: [presetConfig],
|
presets: [presetConfig],
|
||||||
content: [
|
content: [
|
||||||
|
"./app/**/*.{js,ts,jsx,tsx}",
|
||||||
"./src/**/*.{js,ts,jsx,tsx}",
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
"../../packages/common/components/src/ui/**/*.{js,ts,jsx,tsx}",
|
"../../packages/common/components/src/ui/**/*.{js,ts,jsx,tsx}",
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -27,17 +27,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
".eslintrc.*",
|
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
"**/*.mts",
|
".next/types/**/*.ts",
|
||||||
"**/*.js",
|
"../../packages/common/types/**/*.d.ts"
|
||||||
"**/*.cjs",
|
|
||||||
"**/*.mjs",
|
|
||||||
"**/*.jsx",
|
|
||||||
"**/*.json",
|
|
||||||
".next/types/**/*.ts"
|
|
||||||
],
|
],
|
||||||
"exclude": ["**/node_modules", "**/.*/*"]
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
".next",
|
||||||
|
"dist",
|
||||||
|
"coverage",
|
||||||
|
"storybook-static",
|
||||||
|
"public"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11315
|
|||||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||||
SERVER_API_URL = http://dev.onasys.co.kr:8000
|
SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||||
CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||||
FO_SERVER_API_URL = http://con.mind-one.co.kr:11083/operation-service
|
FO_SERVER_API_URL = http://con.mind-one.co.kr:11083/operation-service/api/v1
|
||||||
WR_SERVER_API_URL = http://con.mind-one.co.kr:11083/reuse-service
|
WR_SERVER_API_URL = http://con.mind-one.co.kr:11083/reuse-service/api/v1
|
||||||
NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver
|
NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver
|
||||||
NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
|
NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
|
||||||
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
|
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
|
|||||||
#NEXT_PUBLIC_CM_LOGIN_PAGE_URL = https://devqueen.hasuon.or.kr/auth/login
|
#NEXT_PUBLIC_CM_LOGIN_PAGE_URL = https://devqueen.hasuon.or.kr/auth/login
|
||||||
SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
|
SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
|
||||||
CS_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
|
CS_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
|
||||||
FO_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/operation-service
|
FO_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/operation-service/api/v1
|
||||||
WR_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/reuse-service
|
WR_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/reuse-service/api/v1
|
||||||
NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver
|
NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver
|
||||||
NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl
|
NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl
|
||||||
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
|
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
|
||||||
|
|||||||
@@ -1,99 +1,133 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
// 공통 섹션 타이틀 컴포넌트
|
// 공통 섹션 타이틀 컴포넌트
|
||||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center gap-3'>
|
<div className="flex items-center gap-3">
|
||||||
<span className='h-5 w-[3px] bg-sky-500' />
|
<span className="h-5 w-[3px] bg-sky-500" />
|
||||||
<h3 className='text-[24px] font-bold text-neutral-900'>{children}</h3>
|
<h3 className="text-[24px] font-bold text-neutral-900">{children}</h3>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 구분선
|
// 구분선
|
||||||
function Hr({ className = '' }: { className?: string }) {
|
function Hr({ className = "" }: { className?: string }) {
|
||||||
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 출처 텍스트
|
// 출처 텍스트
|
||||||
function SourceText() {
|
function SourceText() {
|
||||||
return <p className='mt-7 text-center text-[13px] text-neutral-500'>자료: 환경부(2024), 「2023 하수도 통계」</p>;
|
return (
|
||||||
|
<p className="mt-7 text-center text-[13px] text-neutral-500">
|
||||||
|
자료: 환경부(2024), 「2023 하수도 통계」
|
||||||
|
</p>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tableStyles = {
|
const tableStyles = {
|
||||||
container: 'w-[1000px] mx-auto overflow-hidden',
|
container: "w-[1000px] mx-auto overflow-hidden",
|
||||||
table: 'w-full table-fixed border-collapse text-center text-[16px]',
|
table: "w-full table-fixed border-collapse text-center text-[16px]",
|
||||||
th: 'border border-[#858F98] py-3 font-bold text-neutral-800',
|
th: "border border-[#858F98] py-3 font-bold text-neutral-800",
|
||||||
td: 'border border-[#858F98] py-3 text-neutral-700 font-medium break-keep',
|
td: "border border-[#858F98] py-3 text-neutral-700 font-medium break-keep",
|
||||||
headerBg: 'bg-[#E3E7EA]',
|
headerBg: "bg-[#E3E7EA]",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RainStatusSection() {
|
export default function RainStatusSection() {
|
||||||
const title = '빗물이용시설(현황)';
|
const title = "빗물이용시설(현황)";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='bg-white'>
|
<div className="bg-white">
|
||||||
{/* 상단 타이틀 */}
|
{/* 상단 타이틀 */}
|
||||||
<div className='inline-block'>
|
<div className="inline-block">
|
||||||
<h2 className='relative inline-block px-2 text-[28px] font-extrabold tracking-tight leading-none text-slate-900'>
|
<h2 className="relative inline-block px-2 text-[28px] font-extrabold tracking-tight leading-none text-slate-900">
|
||||||
<span className='relative z-10'>{title}</span>
|
<span className="relative z-10">{title}</span>
|
||||||
<span className='absolute left-0 bottom-[-4px] z-0 h-3 w-full bg-gradient-to-r from-sky-300 to-green-300' />
|
<span className="absolute left-0 bottom-[-4px] z-0 h-3 w-full bg-gradient-to-r from-sky-300 to-green-300" />
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-10 pb-[120px]'>
|
<div className="mt-10 pb-[120px]">
|
||||||
{/* 1) 빗물이용시설의 건축물별 빗물이용량 현황 */}
|
{/* 1) 빗물이용시설의 건축물별 빗물이용량 현황 */}
|
||||||
<section id='status-rain-building' className='mt-10 scroll-mt-[120px]'>
|
<section id="status-rain-building" className="mt-10 scroll-mt-[120px]">
|
||||||
<SectionTitle>빗물이용시설의 건축물별 빗물이용량 현황</SectionTitle>
|
<SectionTitle>빗물이용시설의 건축물별 빗물이용량 현황</SectionTitle>
|
||||||
|
|
||||||
<div className='mt-10'>
|
<div className="mt-10">
|
||||||
<div className={tableStyles.container}>
|
<div className={tableStyles.container}>
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '160px' }}>
|
<th
|
||||||
|
rowSpan={2}
|
||||||
|
className={`${tableStyles.th} ${tableStyles.headerBg}`}
|
||||||
|
style={{ width: "160px" }}
|
||||||
|
>
|
||||||
구분
|
구분
|
||||||
</th>
|
</th>
|
||||||
<th colSpan={6} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
|
<th
|
||||||
|
colSpan={6}
|
||||||
|
className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}
|
||||||
|
>
|
||||||
빗물이용량 (천m³/년)
|
빗물이용량 (천m³/년)
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th className={`${tableStyles.th} bg-[#01ACE5] text-white`} style={{ width: '140px' }}>
|
<th
|
||||||
|
className={`${tableStyles.th} bg-[#01ACE5] text-white`}
|
||||||
|
style={{ width: "140px" }}
|
||||||
|
>
|
||||||
2018
|
2018
|
||||||
</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#17AFC7] text-white`} style={{ width: '140px' }}>
|
<th
|
||||||
|
className={`${tableStyles.th} bg-[#17AFC7] text-white`}
|
||||||
|
style={{ width: "140px" }}
|
||||||
|
>
|
||||||
2019
|
2019
|
||||||
</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#2CB2A9] text-white`} style={{ width: '140px' }}>
|
<th
|
||||||
|
className={`${tableStyles.th} bg-[#2CB2A9] text-white`}
|
||||||
|
style={{ width: "140px" }}
|
||||||
|
>
|
||||||
2020
|
2020
|
||||||
</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#42B58B] text-white`} style={{ width: '140px' }}>
|
<th
|
||||||
|
className={`${tableStyles.th} bg-[#42B58B] text-white`}
|
||||||
|
style={{ width: "140px" }}
|
||||||
|
>
|
||||||
2021
|
2021
|
||||||
</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#57B86D] text-white`} style={{ width: '140px' }}>
|
<th
|
||||||
|
className={`${tableStyles.th} bg-[#57B86D] text-white`}
|
||||||
|
style={{ width: "140px" }}
|
||||||
|
>
|
||||||
2022
|
2022
|
||||||
</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#81C784] text-white`} style={{ width: '140px' }}>
|
<th
|
||||||
|
className={`${tableStyles.th} bg-[#81C784] text-white`}
|
||||||
|
style={{ width: "140px" }}
|
||||||
|
>
|
||||||
2023
|
2023
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{[
|
{[
|
||||||
['골프장', '6,771.8', '6,417.4', '8,371.6', '10,215.4', '11,360', '10,191.6'],
|
["골프장", "6,771.8", "6,417.4", "8,371.6", "10,215.4", "11,360", "10,191.6"],
|
||||||
['공공시설', '364', '325.2', '201.9', '244.6', '209.9', '299.6'],
|
["공공시설", "364", "325.2", "201.9", "244.6", "209.9", "299.6"],
|
||||||
['공동주택', '291', '840.5', '804.2', '2,393.0', '2,386', '2,428.4'],
|
["공동주택", "291", "840.5", "804.2", "2,393.0", "2,386", "2,428.4"],
|
||||||
['대규모점포', '169.9', '214.4', '58.4', '427.2', '371', '293.3'],
|
["대규모점포", "169.9", "214.4", "58.4", "427.2", "371", "293.3"],
|
||||||
['체육시설', '52.6', '44.7', '48.6', '75.3', '71.8', '77.6'],
|
["체육시설", "52.6", "44.7", "48.6", "75.3", "71.8", "77.6"],
|
||||||
['학교', '33', '25.5', '22', '155.8', '165.4', '32.6'],
|
["학교", "33", "25.5", "22", "155.8", "165.4", "32.6"],
|
||||||
['기타', '311.6', '901.2', '344.5', '537.8', '564', '621.2'],
|
["기타", "311.6", "901.2", "344.5", "537.8", "564", "621.2"],
|
||||||
].map((row, idx) => (
|
].map((row, idx) => (
|
||||||
<tr key={idx} className='bg-white'>
|
<tr key={idx} className="bg-white">
|
||||||
{row.map((val, i) => (
|
{row.map((val, i) => (
|
||||||
<td key={i} className={`${tableStyles.td} ${i === 0 ? '!font-bold !text-neutral-900' : ''} ${[1, 3, 5].includes(i) ? 'bg-[#EAF5F5]' : ''}`}>
|
<td
|
||||||
|
key={i}
|
||||||
|
className={`${tableStyles.td} ${
|
||||||
|
i === 0 ? "!font-bold !text-neutral-900" : ""
|
||||||
|
} ${[1, 3, 5].includes(i) ? "bg-[#EAF5F5]" : ""}`}
|
||||||
|
>
|
||||||
{val}
|
{val}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
@@ -107,79 +141,129 @@ export default function RainStatusSection() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
{/* 2) 빗물이용시설의 용도별 분포 (2023년 기준) */}
|
{/* 2) 빗물이용시설의 용도별 분포 (2023년 기준) */}
|
||||||
<section id='status-rain-purpose-distribution' className='scroll-mt-[120px]'>
|
<section
|
||||||
|
id="status-rain-purpose-distribution"
|
||||||
|
className="scroll-mt-[120px]"
|
||||||
|
>
|
||||||
<SectionTitle>빗물이용시설의 용도별 분포 (2023년 기준)</SectionTitle>
|
<SectionTitle>빗물이용시설의 용도별 분포 (2023년 기준)</SectionTitle>
|
||||||
|
|
||||||
<div className='mt-8'>
|
<div className="mt-8">
|
||||||
<div className={tableStyles.container}>
|
<div className={tableStyles.container}>
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '160px' }}>
|
<th
|
||||||
|
rowSpan={2}
|
||||||
|
className={`${tableStyles.th} ${tableStyles.headerBg}`}
|
||||||
|
style={{ width: "160px" }}
|
||||||
|
>
|
||||||
용도
|
용도
|
||||||
</th>
|
</th>
|
||||||
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '84px' }}>
|
<th
|
||||||
|
rowSpan={2}
|
||||||
|
className={`${tableStyles.th} ${tableStyles.headerBg}`}
|
||||||
|
style={{ width: "84px" }}
|
||||||
|
>
|
||||||
개소수
|
개소수
|
||||||
<br />
|
<br />
|
||||||
(개소)
|
(개소)
|
||||||
</th>
|
</th>
|
||||||
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '84px' }}>
|
<th
|
||||||
저류조용량
|
rowSpan={2}
|
||||||
|
className={`${tableStyles.th} ${tableStyles.headerBg}`}
|
||||||
|
style={{ width: "84px" }}
|
||||||
|
>
|
||||||
|
저류용량
|
||||||
<br />
|
<br />
|
||||||
(천m³)
|
(천m³)
|
||||||
</th>
|
</th>
|
||||||
<th colSpan={8} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
|
<th
|
||||||
|
colSpan={8}
|
||||||
|
className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}
|
||||||
|
>
|
||||||
빗물이용량 (천m³/년)
|
빗물이용량 (천m³/년)
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th className={`${tableStyles.th} bg-[#01ACE5] py-2 text-[13px] text-white`} style={{ width: '84px' }}>
|
<th
|
||||||
|
className={`${tableStyles.th} bg-[#01ACE5] py-2 text-[13px] text-white`}
|
||||||
|
style={{ width: "84px" }}
|
||||||
|
>
|
||||||
총사용량
|
총사용량
|
||||||
</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#10AED0] py-2 text-[13px] text-white`} style={{ width: '84px' }}>
|
<th
|
||||||
|
className={`${tableStyles.th} bg-[#10AED0] py-2 text-[13px] text-white`}
|
||||||
|
style={{ width: "84px" }}
|
||||||
|
>
|
||||||
공공시설
|
공공시설
|
||||||
</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#20B0BA] py-2 text-[13px] text-white`} style={{ width: '84px' }}>
|
<th
|
||||||
|
className={`${tableStyles.th} bg-[#20B0BA] py-2 text-[13px] text-white`}
|
||||||
|
style={{ width: "84px" }}
|
||||||
|
>
|
||||||
공동주택
|
공동주택
|
||||||
</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#2FB2A5] py-2 text-[13px] text-white`} style={{ width: '84px' }}>
|
<th
|
||||||
|
className={`${tableStyles.th} bg-[#2FB2A5] py-2 text-[13px] text-white`}
|
||||||
|
style={{ width: "84px" }}
|
||||||
|
>
|
||||||
대규모점포
|
대규모점포
|
||||||
</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#3FB58F] py-2 text-[13px] text-white`} style={{ width: '84px' }}>
|
<th
|
||||||
|
className={`${tableStyles.th} bg-[#3FB58F] py-2 text-[13px] text-white`}
|
||||||
|
style={{ width: "84px" }}
|
||||||
|
>
|
||||||
체육시설
|
체육시설
|
||||||
</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#4EB77A] py-2 text-[13px] text-white`} style={{ width: '84px' }}>
|
<th
|
||||||
|
className={`${tableStyles.th} bg-[#4EB77A] py-2 text-[13px] text-white`}
|
||||||
|
style={{ width: "84px" }}
|
||||||
|
>
|
||||||
학교
|
학교
|
||||||
</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#5EB964] py-2 text-[13px] text-white`} style={{ width: '84px' }}>
|
<th
|
||||||
|
className={`${tableStyles.th} bg-[#5EB964] py-2 text-[13px] text-white`}
|
||||||
|
style={{ width: "84px" }}
|
||||||
|
>
|
||||||
골프장
|
골프장
|
||||||
</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#6DBB4F] py-2 text-[13px] text-white`} style={{ width: '84px' }}>
|
<th
|
||||||
|
className={`${tableStyles.th} bg-[#6DBB4F] py-2 text-[13px] text-white`}
|
||||||
|
style={{ width: "84px" }}
|
||||||
|
>
|
||||||
기타
|
기타
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{[
|
{[
|
||||||
['청소·환경정화용수', '195', '32.7', '108.8', '24.9', '5.8', '2.6', '4.4', '2.9', '-', '68.1'],
|
["청소·환경정화용수", "195", "32.7", "108.8", "24.9", "5.8", "2.6", "4.4", "2.9", "-", "68.1"],
|
||||||
['세척·살수용수', '12', '1.4', '37', '35', '-', '-', '1.9', '-', '-', '0.2'],
|
["세척·살수용수", "12", "1.4", "37", "35", "-", "-", "1.9", "-", "-", "0.2"],
|
||||||
['조경용수', '3,165', '7,440.8', '13,554.1', '168.1', '2,421.9', '290.7', '66.2', '29', '10,044', '534.3'],
|
["조경용수", "3,165", "7,440.8", "13,554.1", "168.1", "2,421.9", "290.7", "66.2", "29", "10,044", "534.3"],
|
||||||
['냉·난방수', '8', '1.6', '0.1', '-', '-', '-', '-', '0.1', '-', '-'],
|
["냉·난방수", "8", "1.6", "0.1", "-", "-", "-", "-", "0.1", "-", "-"],
|
||||||
['공업용수', '6', '14.9', '53.2', '52.8', '-', '-', '-', '-', '-', '0.4'],
|
["공업용수", "6", "14.9", "53.2", "52.8", "-", "-", "-", "-", "-", "0.4"],
|
||||||
['농업용수', '6.0', '0.5', '0.6', '-', '-', '-', '-', '-', '-', '0.6'],
|
["농업용수", "6.0", "0.5", "0.6", "-", "-", "-", "-", "-", "-", "0.6"],
|
||||||
['기타', '48', '306.1', '155.6', '2.8', '-', '-', '0.6', '0.5', '147.5', '4.2'],
|
["기타", "48", "306.1", "155.6", "2.8", "-", "-", "0.6", "0.5", "147.5", "4.2"],
|
||||||
['미기재', '103', '22.9', '34.8', '16.0', '0.6', '-', '4.6', '0.2', '-', '13.5'],
|
["미기재", "103", "22.9", "34.8", "16.0", "0.6", "-", "4.6", "0.2", "-", "13.5"],
|
||||||
['합계', '3,543', '7,820.8', '13,944.7', '299.6', '2,428.4', '293.3', '77.6', '32.6', '10,191.6', '621.2'],
|
["합계", "3,543", "7,820.8", "13,944.7", "299.6", "2,428.4", "293.3", "77.6", "32.6", "10,191.6", "621.2"],
|
||||||
].map((row, idx) => (
|
].map((row, idx) => (
|
||||||
<tr key={idx} className={idx === 8 ? 'bg-white font-bold' : 'bg-white'}>
|
<tr
|
||||||
|
key={idx}
|
||||||
|
className={idx === 8 ? "bg-white font-bold" : "bg-white"}
|
||||||
|
>
|
||||||
{row.map((val, i) => {
|
{row.map((val, i) => {
|
||||||
const isHighlightedCol = [1, 2].includes(i);
|
const isHighlightedCol = [1, 2].includes(i);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td key={i} className={`${tableStyles.td} ${i === 0 ? '!font-bold !text-neutral-900' : ''} ${isHighlightedCol ? 'bg-[#EAF5F5]' : ''}`}>
|
<td
|
||||||
|
key={i}
|
||||||
|
className={`${tableStyles.td} ${
|
||||||
|
i === 0 ? "!font-bold !text-neutral-900" : ""
|
||||||
|
} ${isHighlightedCol ? "bg-[#EAF5F5]" : ""}`}
|
||||||
|
>
|
||||||
{val}
|
{val}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
@@ -194,58 +278,91 @@ export default function RainStatusSection() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
{/* 3) 빗물이용시설의 용도별 빗물이용량 현황 */}
|
{/* 3) 빗물이용시설의 용도별 빗물이용량 현황 */}
|
||||||
<section id='status-rain-purpose-usage' className='scroll-mt-[120px]'>
|
<section
|
||||||
|
id="status-rain-purpose-usage"
|
||||||
|
className="scroll-mt-[120px]"
|
||||||
|
>
|
||||||
<SectionTitle>빗물이용시설의 용도별 빗물이용량 현황</SectionTitle>
|
<SectionTitle>빗물이용시설의 용도별 빗물이용량 현황</SectionTitle>
|
||||||
|
|
||||||
<div className='mt-8'>
|
<div className="mt-8">
|
||||||
<div className={tableStyles.container}>
|
<div className={tableStyles.container}>
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '160px' }}>
|
<th
|
||||||
|
rowSpan={2}
|
||||||
|
className={`${tableStyles.th} ${tableStyles.headerBg}`}
|
||||||
|
style={{ width: "160px" }}
|
||||||
|
>
|
||||||
용도
|
용도
|
||||||
</th>
|
</th>
|
||||||
<th colSpan={6} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
|
<th
|
||||||
|
colSpan={6}
|
||||||
|
className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}
|
||||||
|
>
|
||||||
빗물이용량 (천m³/년)
|
빗물이용량 (천m³/년)
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th className={`${tableStyles.th} bg-[#01ACE5] text-white`} style={{ width: '140px' }}>
|
<th
|
||||||
|
className={`${tableStyles.th} bg-[#01ACE5] text-white`}
|
||||||
|
style={{ width: "140px" }}
|
||||||
|
>
|
||||||
2018
|
2018
|
||||||
</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#14AFCD] text-white`} style={{ width: '140px' }}>
|
<th
|
||||||
|
className={`${tableStyles.th} bg-[#14AFCD] text-white`}
|
||||||
|
style={{ width: "140px" }}
|
||||||
|
>
|
||||||
2019
|
2019
|
||||||
</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#28B2B4] text-white`} style={{ width: '140px' }}>
|
<th
|
||||||
|
className={`${tableStyles.th} bg-[#28B2B4] text-white`}
|
||||||
|
style={{ width: "140px" }}
|
||||||
|
>
|
||||||
2020
|
2020
|
||||||
</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#3CB59A] text-white`} style={{ width: '140px' }}>
|
<th
|
||||||
|
className={`${tableStyles.th} bg-[#3CB59A] text-white`}
|
||||||
|
style={{ width: "140px" }}
|
||||||
|
>
|
||||||
2021
|
2021
|
||||||
</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#50B880] text-white`} style={{ width: '140px' }}>
|
<th
|
||||||
|
className={`${tableStyles.th} bg-[#50B880] text-white`}
|
||||||
|
style={{ width: "140px" }}
|
||||||
|
>
|
||||||
2022
|
2022
|
||||||
</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white`} style={{ width: '140px' }}>
|
<th
|
||||||
|
className={`${tableStyles.th} bg-[#6DBB4F] text-white`}
|
||||||
|
style={{ width: "140px" }}
|
||||||
|
>
|
||||||
2023
|
2023
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{[
|
{[
|
||||||
['청소·화장실용수', '126.5', '134.1', '96.5', '153.9', '140.5', '108.8'],
|
["청소·화장실용수", "126.5", "134.1", "96.5", "153.9", "140.5", "108.8"],
|
||||||
['세척·살수용수', '14.8', '14.6', '9.5', '49.8', '49.7', '37.0'],
|
["세척·살수용수", "14.8", "14.6", "9.5", "49.8", "49.7", "37.0"],
|
||||||
['조경용수', '7,629.8', '8,496.2', '9,415.4', '13,360.4', '14,466.3', '13,554.1'],
|
["조경용수", "7,629.8", "8,496.2", "9,415.4", "13,360.4", "14,466.3", "13,554.1"],
|
||||||
['친수용수', '1.0', '0.1', '0.1', '0.1', '0.1', '0.1'],
|
["친수용수", "1.0", "0.1", "0.1", "0.1", "0.1", "0.1"],
|
||||||
['공업용수', '152.0', '77.0', '77.0', '75.4', '0.4', '53.2'],
|
["공업용수", "152.0", "77.0", "77.0", "75.4", "0.4", "53.2"],
|
||||||
['농업용수', '4.4', '4.3', '4.3', '4.3', '0.6', '0.6'],
|
["농업용수", "4.4", "4.3", "4.3", "4.3", "0.6", "0.6"],
|
||||||
['기타', '37.7', '42.6', '230.7', '394.3', '463.2', '155.6'],
|
["기타", "37.7", "42.6", "230.7", "394.3", "463.2", "155.6"],
|
||||||
].map((row, idx) => (
|
].map((row, idx) => (
|
||||||
<tr key={idx} className='bg-white'>
|
<tr key={idx} className="bg-white">
|
||||||
{row.map((val, i) => (
|
{row.map((val, i) => (
|
||||||
<td key={i} className={`${tableStyles.td} ${i === 0 ? '!font-bold !text-neutral-900' : ''} ${[1, 3, 5].includes(i) ? 'bg-[#EAF5F5]' : ''}`}>
|
<td
|
||||||
|
key={i}
|
||||||
|
className={`${tableStyles.td} ${
|
||||||
|
i === 0 ? "!font-bold !text-neutral-900" : ""
|
||||||
|
} ${[1, 3, 5].includes(i) ? "bg-[#EAF5F5]" : ""}`}
|
||||||
|
>
|
||||||
{val}
|
{val}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
@@ -261,4 +378,4 @@ export default function RainStatusSection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,56 +1,60 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center gap-3'>
|
<div className="flex items-center gap-3">
|
||||||
<span className='h-5 w-[3px] bg-sky-500' />
|
<span className="h-5 w-[3px] bg-sky-500" />
|
||||||
<h3 className='text-[24px] font-bold text-neutral-900'>{children}</h3>
|
<h3 className="text-[24px] font-bold text-neutral-900">{children}</h3>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Hr({ className = '' }: { className?: string }) {
|
function Hr({ className = "" }: { className?: string }) {
|
||||||
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SourceText() {
|
function SourceText() {
|
||||||
return <p className='mt-7 text-center text-[13px] text-neutral-500'>자료: 환경부(2024), 「2023 하수도 통계」</p>;
|
return (
|
||||||
|
<p className="mt-7 text-center text-[13px] text-neutral-500">
|
||||||
|
자료: 환경부(2024), 「2023 하수도 통계」
|
||||||
|
</p>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tableStyles = {
|
const tableStyles = {
|
||||||
container: 'w-[1000px] mx-auto overflow-hidden',
|
container: "w-[1000px] mx-auto overflow-hidden",
|
||||||
table: 'w-full table-fixed border-collapse text-center text-[16px]',
|
table: "w-full table-fixed border-collapse text-center text-[16px]",
|
||||||
th: 'border border-[#858F98] py-3 font-bold text-neutral-800',
|
th: "border border-[#858F98] py-3 font-bold text-neutral-800",
|
||||||
td: 'border border-[#858F98] py-3 text-neutral-700 font-medium break-keep',
|
td: "border border-[#858F98] py-3 text-neutral-700 font-medium break-keep",
|
||||||
headerBg: 'bg-[#E3E7EA]',
|
headerBg: "bg-[#E3E7EA]",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SewageStatusSection() {
|
export default function SewageStatusSection() {
|
||||||
const title = '하수처리수 재이용시설(현황)';
|
const title = "하수처리수 재이용시설(현황)";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative bg-white'>
|
<div className="relative bg-white">
|
||||||
{/* 상단 타이틀 */}
|
{/* 상단 타이틀 */}
|
||||||
<div className='inline-block'>
|
<div className="inline-block">
|
||||||
<h2 className='relative inline-block px-2 text-[28px] font-extrabold tracking-tight leading-none text-slate-900'>
|
<h2 className="relative inline-block px-2 text-[28px] font-extrabold tracking-tight leading-none text-slate-900">
|
||||||
<span className='relative z-10'>{title}</span>
|
<span className="relative z-10">{title}</span>
|
||||||
<span className='absolute left-0 bottom-[-4px] z-0 h-3 w-full bg-gradient-to-r from-sky-300 to-green-300' />
|
<span className="absolute left-0 bottom-[-4px] z-0 h-3 w-full bg-gradient-to-r from-sky-300 to-green-300" />
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-10'>
|
<div className="mt-10">
|
||||||
{/* 1) 연도별 하수처리수 재이용 현황 */}
|
{/* 1) 연도별 하수처리수 재이용 현황 */}
|
||||||
<section id='status-treated-year' className='mt-10 scroll-mt-[120px]'>
|
<section id="status-treated-year" className="mt-10 scroll-mt-[120px]">
|
||||||
<SectionTitle>연도별 하수처리수 재이용 현황</SectionTitle>
|
<SectionTitle>연도별 하수처리수 재이용 현황</SectionTitle>
|
||||||
|
|
||||||
<div className='mt-10'>
|
<div className="mt-10">
|
||||||
<div className={tableStyles.container}>
|
<div className={tableStyles.container}>
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col style={{ width: '130px' }} />
|
<col style={{ width: "130px" }} />
|
||||||
<col style={{ width: '180px' }} />
|
<col style={{ width: "180px" }} />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
@@ -60,52 +64,166 @@ export default function SewageStatusSection() {
|
|||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
<th
|
||||||
|
colSpan={2}
|
||||||
|
className={`${tableStyles.th} ${tableStyles.headerBg}`}
|
||||||
|
>
|
||||||
구분
|
구분
|
||||||
</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#01ACE5] text-white`}>2018</th>
|
<th className={`${tableStyles.th} bg-[#01ACE5] text-white`}>
|
||||||
<th className={`${tableStyles.th} bg-[#14AFCD] text-white`}>2019</th>
|
2018
|
||||||
<th className={`${tableStyles.th} bg-[#28B2B4] text-white`}>2020</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#3CB59A] text-white`}>2021</th>
|
<th className={`${tableStyles.th} bg-[#14AFCD] text-white`}>
|
||||||
<th className={`${tableStyles.th} bg-[#50B880] text-white`}>2022</th>
|
2019
|
||||||
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white`}>2023</th>
|
</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#28B2B4] text-white`}>
|
||||||
|
2020
|
||||||
|
</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#3CB59A] text-white`}>
|
||||||
|
2021
|
||||||
|
</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#50B880] text-white`}>
|
||||||
|
2022
|
||||||
|
</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white`}>
|
||||||
|
2023
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
group: '전체',
|
group: "전체",
|
||||||
rows: [
|
rows: [
|
||||||
["개소수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>", '671', '681', '698', '704', '714', '717'],
|
[
|
||||||
["하수처리량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>", '7,163.7', '7,140.4', '7,375.6', '7,335.8', '7,378.9', '7,537.2'],
|
"개소수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>",
|
||||||
["재이용량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>", '1,112.7', '1,148.7', '1,148.7', '1,127.0', '1,136.8', '1,137.5'],
|
"671",
|
||||||
["재이용률 <span class='text-[13px] font-normal text-[#767676]'>(%)</span>", '17.4', '16.5', '15.5', '14.1', '14.3', '14.4'],
|
"681",
|
||||||
|
"698",
|
||||||
|
"704",
|
||||||
|
"714",
|
||||||
|
"717",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"하수처리량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>",
|
||||||
|
"7,163.7",
|
||||||
|
"7,140.4",
|
||||||
|
"7,375.6",
|
||||||
|
"7,335.8",
|
||||||
|
"7,378.9",
|
||||||
|
"7,537.2",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"재이용량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>",
|
||||||
|
"1,112.7",
|
||||||
|
"1,148.7",
|
||||||
|
"1,148.7",
|
||||||
|
"1,127.0",
|
||||||
|
"1,136.8",
|
||||||
|
"1,137.5",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"재이용률 <span class='text-[13px] font-normal text-[#767676]'>(%)</span>",
|
||||||
|
"17.4",
|
||||||
|
"16.5",
|
||||||
|
"15.5",
|
||||||
|
"14.1",
|
||||||
|
"14.3",
|
||||||
|
"14.4",
|
||||||
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: '법적 대상',
|
group: "법적 대상",
|
||||||
rows: [
|
rows: [
|
||||||
["개소수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>", '68', '82', '94', '97', '101', '118'],
|
[
|
||||||
["하수처리량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>", '761.2', '839.0', '935.3', '1,158.7', '1,220.6', '1,473.6'],
|
"개소수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>",
|
||||||
["재이용량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>", '162.2', '190.6', '218.5', '244.6', '247.2', '324.9'],
|
"68",
|
||||||
["재이용률 <span class='text-[13px] font-normal text-[#767676]'>(%)</span>", '24.7', '25.4', '27.8', '25.7', '24.2', '24.1'],
|
"82",
|
||||||
|
"94",
|
||||||
|
"97",
|
||||||
|
"101",
|
||||||
|
"118",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"하수처리량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>",
|
||||||
|
"761.2",
|
||||||
|
"839.0",
|
||||||
|
"935.3",
|
||||||
|
"1,158.7",
|
||||||
|
"1,220.6",
|
||||||
|
"1,473.6",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"재이용량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>",
|
||||||
|
"162.2",
|
||||||
|
"190.6",
|
||||||
|
"218.5",
|
||||||
|
"244.6",
|
||||||
|
"247.2",
|
||||||
|
"324.9",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"재이용률 <span class='text-[13px] font-normal text-[#767676]'>(%)</span>",
|
||||||
|
"24.7",
|
||||||
|
"25.4",
|
||||||
|
"27.8",
|
||||||
|
"25.7",
|
||||||
|
"24.2",
|
||||||
|
"24.1",
|
||||||
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: '법적 미대상',
|
group: "법적 미대상",
|
||||||
rows: [
|
rows: [
|
||||||
["개소수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>", '603', '599', '604', '607', '613', '599'],
|
[
|
||||||
["하수처리량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>", '6,402.5', '6,301.4', '6,440.3', '6,177.1', '6,158.3', '6,063.5'],
|
"개소수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>",
|
||||||
["재이용량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>", '950.6', '958.1', '926.2', '882.4', '889.5', '812.7'],
|
"603",
|
||||||
["재이용률 <span class='text-[13px] font-normal text-[#767676]'>(%)</span>", '16.5', '15.3', '13.6', '12.2', '12.6', '12.5'],
|
"599",
|
||||||
|
"604",
|
||||||
|
"607",
|
||||||
|
"613",
|
||||||
|
"599",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"하수처리량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>",
|
||||||
|
"6,402.5",
|
||||||
|
"6,301.4",
|
||||||
|
"6,440.3",
|
||||||
|
"6,177.1",
|
||||||
|
"6,158.3",
|
||||||
|
"6,063.5",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"재이용량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>",
|
||||||
|
"950.6",
|
||||||
|
"958.1",
|
||||||
|
"926.2",
|
||||||
|
"882.4",
|
||||||
|
"889.5",
|
||||||
|
"812.7",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"재이용률 <span class='text-[13px] font-normal text-[#767676]'>(%)</span>",
|
||||||
|
"16.5",
|
||||||
|
"15.3",
|
||||||
|
"13.6",
|
||||||
|
"12.2",
|
||||||
|
"12.6",
|
||||||
|
"12.5",
|
||||||
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
].map((section, sIdx) => (
|
].map((section, sIdx) => (
|
||||||
<React.Fragment key={sIdx}>
|
<React.Fragment key={sIdx}>
|
||||||
{section.rows.map((row, rIdx) => (
|
{section.rows.map((row, rIdx) => (
|
||||||
<tr key={`${sIdx}-${rIdx}`} className='bg-white'>
|
<tr key={`${sIdx}-${rIdx}`} className="bg-white">
|
||||||
{rIdx === 0 && (
|
{rIdx === 0 && (
|
||||||
<td rowSpan={section.rows.length} className={`${tableStyles.td} ${tableStyles.headerBg} !text-[18px] !font-bold !text-neutral-900`}>
|
<td
|
||||||
|
rowSpan={section.rows.length}
|
||||||
|
className={`${tableStyles.td} ${tableStyles.headerBg} !text-[18px] !font-bold !text-neutral-900`}
|
||||||
|
>
|
||||||
{section.group}
|
{section.group}
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
@@ -116,7 +234,9 @@ export default function SewageStatusSection() {
|
|||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
key={i}
|
key={i}
|
||||||
className={`${tableStyles.td} ${isFirstCol ? '!font-bold !text-neutral-900' : ''} ${isHighlightedCol ? 'bg-[#EAF5F5]' : ''}`}
|
className={`${tableStyles.td} ${
|
||||||
|
isFirstCol ? "!font-bold !text-neutral-900" : ""
|
||||||
|
} ${isHighlightedCol ? "bg-[#EAF5F5]" : ""}`}
|
||||||
dangerouslySetInnerHTML={{ __html: val }}
|
dangerouslySetInnerHTML={{ __html: val }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -133,17 +253,17 @@ export default function SewageStatusSection() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
{/* 2) 용도별 하수처리수 재이용 현황 */}
|
{/* 2) 용도별 하수처리수 재이용 현황 */}
|
||||||
<section id='status-treated-usage' className='scroll-mt-[120px]'>
|
<section id="status-treated-usage" className="scroll-mt-[120px]">
|
||||||
<SectionTitle>용도별 하수처리수 재이용 현황</SectionTitle>
|
<SectionTitle>용도별 하수처리수 재이용 현황</SectionTitle>
|
||||||
|
|
||||||
<div className='mt-8'>
|
<div className="mt-8">
|
||||||
<div className={tableStyles.container}>
|
<div className={tableStyles.container}>
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col style={{ width: '180px' }} />
|
<col style={{ width: "180px" }} />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
@@ -153,59 +273,88 @@ export default function SewageStatusSection() {
|
|||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className={`${tableStyles.th} ${tableStyles.headerBg}`}>구분</th>
|
<th className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
||||||
<th className={`${tableStyles.th} bg-[#01ACE5] text-white`}>2018</th>
|
구분
|
||||||
<th className={`${tableStyles.th} bg-[#14AFCD] text-white`}>2019</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#28B2B4] text-white`}>2020</th>
|
<th className={`${tableStyles.th} bg-[#01ACE5] text-white`}>
|
||||||
<th className={`${tableStyles.th} bg-[#3CB59A] text-white`}>2021</th>
|
2018
|
||||||
<th className={`${tableStyles.th} bg-[#50B880] text-white`}>2022</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white`}>2023</th>
|
<th className={`${tableStyles.th} bg-[#14AFCD] text-white`}>
|
||||||
|
2019
|
||||||
|
</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#28B2B4] text-white`}>
|
||||||
|
2020
|
||||||
|
</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#3CB59A] text-white`}>
|
||||||
|
2021
|
||||||
|
</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#50B880] text-white`}>
|
||||||
|
2022
|
||||||
|
</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white`}>
|
||||||
|
2023
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{[
|
{[
|
||||||
{ label: '총 재이용량', type: 'total', values: ['1,112.7', '1,148.7', '1,144.7', '1,127.0', '1,136.8', '1,137.5'] },
|
{ label: "총 재이용량", type: "total", values: ["1,112.7", "1,148.7", "1,144.7", "1,127.0", "1,136.8", "1,137.5"] },
|
||||||
{ label: '장내 재이용', type: 'category', values: ['520.6', '519.8', '513.9', '520.3', '502.2', '490.0'] },
|
{ label: "장내 재이용", type: "category", values: ["520.6", "519.8", "513.9", "520.3", "502.2", "490.0"] },
|
||||||
{ label: '세척수', type: 'red-item', values: ['177.9', '174.4', '169.4', '180.9', '196.8', '191.2'] },
|
{ label: "세척수", type: "red-item", values: ["177.9", "174.4", "169.4", "180.9", "196.8", "191.2"] },
|
||||||
{ label: '냉각수', type: 'red-item', values: ['45.5', '46.8', '50.4', '40.6', '38.3', '41.7'] },
|
{ label: "냉각수", type: "red-item", values: ["45.5", "46.8", "50.4", "40.6", "38.3", "41.7"] },
|
||||||
{ label: '청소수', type: 'red-item', values: ['17.6', '21.2', '21.2', '21.7', '20.8', '20.5'] },
|
{ label: "청소수", type: "red-item", values: ["17.6", "21.2", "21.2", "21.7", "20.8", "20.5"] },
|
||||||
{ label: '조경용수', type: 'red-item', values: ['0.1', '0.1', '-', '-', '-', '-'] },
|
{ label: "조경용수", type: "red-item", values: ["0.1", "0.1", "-", "-", "-", "-"] },
|
||||||
{ label: '회생용수', type: 'red-item', values: ['15.8', '16.7', '17.8', '15.7', '14.4', '14.0'] },
|
{ label: "회생용수", type: "red-item", values: ["15.8", "16.7", "17.8", "15.7", "14.4", "14.0"] },
|
||||||
{ label: '장내급수도', type: 'red-item', values: ['2.4', '-', '-', '-', '-', '-'] },
|
{ label: "장내급수도", type: "red-item", values: ["2.4", "-", "-", "-", "-", "-"] },
|
||||||
{ label: '장내기타용수', type: 'red-item', values: ['261.3', '260.6', '255.0', '261.1', '231.8', '222.6'] },
|
{ label: "장내기타용수", type: "red-item", values: ["261.3", "260.6", "255.0", "261.1", "231.8", "222.6"] },
|
||||||
{ label: '장외 재이용', type: 'category', values: ['592.1', '628.9', '630.8', '606.7', '634.6', '647.5'] },
|
{ label: "장외 재이용", type: "category", values: ["592.1", "628.9", "630.8", "606.7", "634.6", "647.5"] },
|
||||||
{ label: '청소·화장실용수', type: 'blue-item', values: ['0.6', '0.4', '0.4', '0.4', '0.4', '0.4'] },
|
{ label: "청소·화장실용수", type: "blue-item", values: ["0.6", "0.4", "0.4", "0.4", "0.4", "0.4"] },
|
||||||
{ label: '세척·살수용수', type: 'blue-item', values: ['5.6', '9.9', '2.9', '2.1', '3.0', '3.6'] },
|
{ label: "세척·살수용수", type: "blue-item", values: ["5.6", "9.9", "2.9", "2.1", "3.0", "3.6"] },
|
||||||
{ label: '조경용수', type: 'blue-item', values: ['10.3', '16.4', '5.9', '4.5', '8.9', '9.5'] },
|
{ label: "조경용수", type: "blue-item", values: ["10.3", "16.4", "5.9", "4.5", "8.9", "9.5"] },
|
||||||
{ label: '친수용수', type: 'blue-item', values: ['7.8', '3.1', '3.1', '3.0', '10.9', '10.5'] },
|
{ label: "친수용수", type: "blue-item", values: ["7.8", "3.1", "3.1", "3.0", "10.9", "10.5"] },
|
||||||
{ label: '하천유지용수', type: 'blue-item', values: ['480.5', '482.5', '496.2', '442.7', '471.7', '484.0'] },
|
{ label: "하천유지용수", type: "blue-item", values: ["480.5", "482.5", "496.2", "442.7", "471.7", "484.0"] },
|
||||||
{ label: '농업용수', type: 'blue-item', values: ['12.2', '11.9', '13.4', '10.7', '10.6', '11.4'] },
|
{ label: "농업용수", type: "blue-item", values: ["12.2", "11.9", "13.4", "10.7", "10.6", "11.4"] },
|
||||||
{ label: '지하수 충전', type: 'blue-item', values: ['0.0', '0.0', '0.0', '13.4', '0.0', '0.0'] },
|
{ label: "지하수 충전", type: "blue-item", values: ["0.0", "0.0", "0.0", "13.4", "0.0", "0.0"] },
|
||||||
{ label: '공업용수', type: 'blue-item', values: ['75.2', '104.6', '108.9', '129.9', '129.0', '128.1'] },
|
{ label: "공업용수", type: "blue-item", values: ["75.2", "104.6", "108.9", "129.9", "129.0", "128.1"] },
|
||||||
].map((row, idx) => {
|
].map((row, idx) => {
|
||||||
const isSubItem = row.type === 'red-item' || row.type === 'blue-item';
|
const isSubItem = row.type === "red-item" || row.type === "blue-item";
|
||||||
const isLastInGroup = idx === 8 || idx === 17;
|
const isLastInGroup = idx === 8 || idx === 17;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={idx} className='bg-white'>
|
<tr key={idx} className="bg-white">
|
||||||
<td className={`${tableStyles.td} relative`}>
|
<td className={`${tableStyles.td} relative`}>
|
||||||
{isSubItem ? (
|
{isSubItem ? (
|
||||||
<>
|
<>
|
||||||
<div className={`absolute left-[1px] w-[16px] bg-white z-10 top-[-2px] ${isLastInGroup ? 'bottom-[0px]' : 'bottom-[-2px]'}`} />
|
|
||||||
<div
|
<div
|
||||||
className={`absolute left-[16px] w-[4px] z-20 ${row.type === 'red-item' ? 'bg-[#FF8B8B]' : 'bg-[#4ECBFF]'} top-[-2px] ${
|
className={`absolute left-[1px] w-[16px] bg-white z-10 top-[-2px] ${
|
||||||
isLastInGroup ? 'bottom-[0px]' : 'bottom-[-2px]'
|
isLastInGroup ? "bottom-[0px]" : "bottom-[-2px]"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<span className='text-[#4f4f4f] font-bold block w-full text-center relative z-30 translate-x-[8px]'>{row.label}</span>
|
<div
|
||||||
|
className={`absolute left-[16px] w-[4px] z-20 ${
|
||||||
|
row.type === "red-item" ? "bg-[#FF8B8B]" : "bg-[#4ECBFF]"
|
||||||
|
} top-[-2px] ${
|
||||||
|
isLastInGroup ? "bottom-[0px]" : "bottom-[-2px]"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<span className="text-[#4f4f4f] font-bold block w-full text-center relative z-30 translate-x-[8px]">
|
||||||
|
{row.label}
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className='font-bold text-neutral-900 block w-full text-center'>{row.label}</span>
|
<span className="font-bold text-neutral-900 block w-full text-center">
|
||||||
|
{row.label}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{row.values.map((val, i) => (
|
{row.values.map((val, i) => (
|
||||||
<td key={i} className={`${tableStyles.td} ${[0, 2, 4].includes(i) ? 'bg-[#EAF5F5]' : ''}`}>
|
<td
|
||||||
|
key={i}
|
||||||
|
className={`${tableStyles.td} ${
|
||||||
|
[0, 2, 4].includes(i) ? "bg-[#EAF5F5]" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{val}
|
{val}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
@@ -220,18 +369,18 @@ export default function SewageStatusSection() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
{/* 3) 하수처리시설 설치 및 이용 현황 */}
|
{/* 3) 하수처리시설 설치 및 이용 현황 */}
|
||||||
<section id='status-treated-facility' className='scroll-mt-[120px]'>
|
<section id="status-treated-facility" className="scroll-mt-[120px]">
|
||||||
<SectionTitle>하수처리시설 설치 및 이용 현황</SectionTitle>
|
<SectionTitle>하수처리시설 설치 및 이용 현황</SectionTitle>
|
||||||
|
|
||||||
<div className='mt-8'>
|
<div className="mt-8">
|
||||||
<div className={tableStyles.container}>
|
<div className={tableStyles.container}>
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col style={{ width: '130px' }} />
|
<col style={{ width: "130px" }} />
|
||||||
<col style={{ width: '160px' }} />
|
<col style={{ width: "160px" }} />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
@@ -241,49 +390,139 @@ export default function SewageStatusSection() {
|
|||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
<th
|
||||||
|
colSpan={2}
|
||||||
|
className={`${tableStyles.th} ${tableStyles.headerBg}`}
|
||||||
|
>
|
||||||
구분
|
구분
|
||||||
</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#01ACE5] text-white`}>2018</th>
|
<th className={`${tableStyles.th} bg-[#01ACE5] text-white`}>
|
||||||
<th className={`${tableStyles.th} bg-[#14AFCD] text-white`}>2019</th>
|
2018
|
||||||
<th className={`${tableStyles.th} bg-[#28B2B4] text-white`}>2020</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#3CB59A] text-white`}>2021</th>
|
<th className={`${tableStyles.th} bg-[#14AFCD] text-white`}>
|
||||||
<th className={`${tableStyles.th} bg-[#50B880] text-white`}>2022</th>
|
2019
|
||||||
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white`}>2023</th>
|
</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#28B2B4] text-white`}>
|
||||||
|
2020
|
||||||
|
</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#3CB59A] text-white`}>
|
||||||
|
2021
|
||||||
|
</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#50B880] text-white`}>
|
||||||
|
2022
|
||||||
|
</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white`}>
|
||||||
|
2023
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
group: '전체',
|
group: "전체",
|
||||||
rows: [
|
rows: [
|
||||||
["시설수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>", '671', '681', '698', '704', '714', '717'],
|
[
|
||||||
["시설용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", '25,846', '26,067', '26,244', '26,492', '26,586', '26,766'],
|
"시설수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>",
|
||||||
["처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", '1,112.7', '1,148.7', '1,144.7', '1,127.0', '1,136.8', '1,137.5'],
|
"671",
|
||||||
|
"681",
|
||||||
|
"698",
|
||||||
|
"704",
|
||||||
|
"714",
|
||||||
|
"717",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"시설용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>",
|
||||||
|
"25,846",
|
||||||
|
"26,067",
|
||||||
|
"26,244",
|
||||||
|
"26,492",
|
||||||
|
"26,586",
|
||||||
|
"26,766",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>",
|
||||||
|
"1,112.7",
|
||||||
|
"1,148.7",
|
||||||
|
"1,144.7",
|
||||||
|
"1,127.0",
|
||||||
|
"1,136.8",
|
||||||
|
"1,137.5",
|
||||||
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: '법적 대상',
|
group: "법적 대상",
|
||||||
rows: [
|
rows: [
|
||||||
["시설수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>", '68', '82', '94', '97', '101', '118'],
|
[
|
||||||
["시설용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", '3,088.9', '3,359.2', '3,612.6', '4,412.2', '4,692.3', '5,503.8'],
|
"시설수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>",
|
||||||
["처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", '162.2', '190.6', '218.5', '244.6', '247.2', '324.9'],
|
"68",
|
||||||
|
"82",
|
||||||
|
"94",
|
||||||
|
"97",
|
||||||
|
"101",
|
||||||
|
"118",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"시설용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>",
|
||||||
|
"3,088.9",
|
||||||
|
"3,359.2",
|
||||||
|
"3,612.6",
|
||||||
|
"4,412.2",
|
||||||
|
"4,692.3",
|
||||||
|
"5,503.8",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>",
|
||||||
|
"162.2",
|
||||||
|
"190.6",
|
||||||
|
"218.5",
|
||||||
|
"244.6",
|
||||||
|
"247.2",
|
||||||
|
"324.9",
|
||||||
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: '법적 미대상',
|
group: "법적 미대상",
|
||||||
rows: [
|
rows: [
|
||||||
["시설수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>", '603', '599', '604', '607', '613', '599'],
|
[
|
||||||
["시설용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", '22,757', '22,708', '22,631', '22,080', '21,894', '21,262'],
|
"시설수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>",
|
||||||
["처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", '950.6', '958.1', '926.2', '882.4', '889.5', '812.7'],
|
"603",
|
||||||
|
"599",
|
||||||
|
"604",
|
||||||
|
"607",
|
||||||
|
"613",
|
||||||
|
"599",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"시설용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>",
|
||||||
|
"22,757",
|
||||||
|
"22,708",
|
||||||
|
"22,631",
|
||||||
|
"22,080",
|
||||||
|
"21,894",
|
||||||
|
"21,262",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>",
|
||||||
|
"950.6",
|
||||||
|
"958.1",
|
||||||
|
"926.2",
|
||||||
|
"882.4",
|
||||||
|
"889.5",
|
||||||
|
"812.7",
|
||||||
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
].map((section, sIdx) => (
|
].map((section, sIdx) => (
|
||||||
<React.Fragment key={sIdx}>
|
<React.Fragment key={sIdx}>
|
||||||
{section.rows.map((row, rIdx) => (
|
{section.rows.map((row, rIdx) => (
|
||||||
<tr key={`${sIdx}-${rIdx}`} className='bg-white'>
|
<tr key={`${sIdx}-${rIdx}`} className="bg-white">
|
||||||
{rIdx === 0 && (
|
{rIdx === 0 && (
|
||||||
<td rowSpan={section.rows.length} className={`${tableStyles.td} ${tableStyles.headerBg} !text-[18px] !font-bold !text-neutral-900`}>
|
<td
|
||||||
|
rowSpan={section.rows.length}
|
||||||
|
className={`${tableStyles.td} ${tableStyles.headerBg} !text-[18px] !font-bold !text-neutral-900`}
|
||||||
|
>
|
||||||
{section.group}
|
{section.group}
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
@@ -294,7 +533,9 @@ export default function SewageStatusSection() {
|
|||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
key={i}
|
key={i}
|
||||||
className={`${tableStyles.td} ${isFirstCol ? '!font-bold !text-neutral-900' : ''} ${isHighlightedCol ? 'bg-[#EAF5F5]' : ''}`}
|
className={`${tableStyles.td} ${
|
||||||
|
isFirstCol ? "!font-bold !text-neutral-900" : ""
|
||||||
|
} ${isHighlightedCol ? "bg-[#EAF5F5]" : ""}`}
|
||||||
dangerouslySetInnerHTML={{ __html: val }}
|
dangerouslySetInnerHTML={{ __html: val }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -311,19 +552,24 @@ export default function SewageStatusSection() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
{/* 4) 시도별 하수처리수 재이용 현황 */}
|
{/* 4) 시도별 하수처리수 재이용 현황 */}
|
||||||
<section id='status-treated-region' className='scroll-mt-[120px]'>
|
<section
|
||||||
|
id="status-treated-region"
|
||||||
|
className="scroll-mt-[120px]"
|
||||||
|
>
|
||||||
<SectionTitle>시도별 하수처리수 재이용 현황</SectionTitle>
|
<SectionTitle>시도별 하수처리수 재이용 현황</SectionTitle>
|
||||||
|
|
||||||
<div className='mt-8'>
|
<div className="mt-8">
|
||||||
<div className={tableStyles.container}>
|
<div className={tableStyles.container}>
|
||||||
<div className='mb-2 text-right text-[13px] text-neutral-600'>(단위: 백만m³/년)</div>
|
<div className="mb-2 text-right text-[13px] text-neutral-600">
|
||||||
|
(단위: 백만m³/일)
|
||||||
|
</div>
|
||||||
|
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col style={{ width: '160px' }} />
|
<col style={{ width: "160px" }} />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
@@ -337,71 +583,110 @@ export default function SewageStatusSection() {
|
|||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
<th
|
||||||
|
rowSpan={3}
|
||||||
|
className={`${tableStyles.th} ${tableStyles.headerBg}`}
|
||||||
|
>
|
||||||
시도
|
시도
|
||||||
</th>
|
</th>
|
||||||
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
<th
|
||||||
|
rowSpan={3}
|
||||||
|
className={`${tableStyles.th} ${tableStyles.headerBg}`}
|
||||||
|
>
|
||||||
연간
|
연간
|
||||||
<br />
|
<br />
|
||||||
하수
|
하수
|
||||||
<br />
|
<br />
|
||||||
처리량
|
처리량
|
||||||
</th>
|
</th>
|
||||||
<th colSpan={7} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
|
<th
|
||||||
|
colSpan={7}
|
||||||
|
className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}
|
||||||
|
>
|
||||||
재이용현황
|
재이용현황
|
||||||
</th>
|
</th>
|
||||||
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
<th
|
||||||
|
rowSpan={3}
|
||||||
|
className={`${tableStyles.th} ${tableStyles.headerBg}`}
|
||||||
|
>
|
||||||
재이용률
|
재이용률
|
||||||
<br />
|
<br />
|
||||||
(%)
|
(%)
|
||||||
</th>
|
</th>
|
||||||
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
<th
|
||||||
|
rowSpan={3}
|
||||||
|
className={`${tableStyles.th} ${tableStyles.headerBg}`}
|
||||||
|
>
|
||||||
비고
|
비고
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
<th
|
||||||
|
rowSpan={2}
|
||||||
|
className={`${tableStyles.th} ${tableStyles.headerBg}`}
|
||||||
|
>
|
||||||
계
|
계
|
||||||
</th>
|
</th>
|
||||||
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
<th
|
||||||
|
rowSpan={2}
|
||||||
|
className={`${tableStyles.th} ${tableStyles.headerBg}`}
|
||||||
|
>
|
||||||
장내
|
장내
|
||||||
</th>
|
</th>
|
||||||
<th colSpan={5} className={`${tableStyles.th} ${tableStyles.headerBg} py-1`}>
|
<th
|
||||||
|
colSpan={5}
|
||||||
|
className={`${tableStyles.th} ${tableStyles.headerBg} py-1`}
|
||||||
|
>
|
||||||
장외
|
장외
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th className={`${tableStyles.th} bg-[#01ACE5] text-white py-2 text-[16px]`}>소계</th>
|
<th className={`${tableStyles.th} bg-[#01ACE5] text-white py-2 text-[16px]`}>
|
||||||
<th className={`${tableStyles.th} bg-[#1FB1C2] text-white py-2 text-[13px]`}>하천유지용수</th>
|
소계
|
||||||
<th className={`${tableStyles.th} bg-[#3EB59F] text-white py-2 text-[16px]`}>공업용수</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#5DB97C] text-white py-2 text-[16px]`}>농업용수</th>
|
<th className={`${tableStyles.th} bg-[#1FB1C2] text-white py-2 text-[13px]`}>
|
||||||
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white py-2 text-[16px]`}>기타용수</th>
|
하천유지용수
|
||||||
|
</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#3EB59F] text-white py-2 text-[16px]`}>
|
||||||
|
공업용수
|
||||||
|
</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#5DB97C] text-white py-2 text-[16px]`}>
|
||||||
|
농업용수
|
||||||
|
</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white py-2 text-[16px]`}>
|
||||||
|
기타용수
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{[
|
{[
|
||||||
['합계', '7,537.2', '1,137.5', '490.0', '647.5', '484.0', '128.1', '11.4', '24.0', '15.1%', ''],
|
["합계", "7,537.2", "1,137.5", "490.0", "647.5", "484.0", "128.1", "11.4", "24.0", "15.1%", ""],
|
||||||
['서울특별시', '1,567.0', '114.9', '59.3', '55.6', '55.1', '-', '-', '0.5', '7.3%', ''],
|
["서울특별시", "1,567.0", "114.9", "59.3", "55.6", "55.1", "-", "-", "0.5", "7.3%", ""],
|
||||||
['부산광역시', '491.5', '146.2', '120.7', '25.5', '25.3', '-', '-', '0.2', '29.7%', ''],
|
["부산광역시", "491.5", "146.2", "120.7", "25.5", "25.3", "-", "-", "0.2", "29.7%", ""],
|
||||||
['대구광역시', '432.7', '102.4', '38.2', '64.2', '64.1', '-', '-', '0.1', '23.7%', ''],
|
["대구광역시", "432.7", "102.4", "38.2", "64.2", "64.1", "-", "-", "0.1", "23.7%", ""],
|
||||||
['인천광역시', '302.3', '57.7', '11.6', '46.1', '29.2', '12.1', '0.5', '4.3', '19.1%', ''],
|
["인천광역시", "302.3", "57.7", "11.6", "46.1", "29.2", "12.1", "0.5", "4.3", "19.1%", ""],
|
||||||
['광주광역시', '211.1', '28.7', '6.5', '22.3', '22.2', '-', '-', '0.1', '13.6%', ''],
|
["광주광역시", "211.1", "28.7", "6.5", "22.3", "22.2", "-", "-", "0.1", "13.6%", ""],
|
||||||
['대전광역시', '244.6', '11.9', '11.9', '0.68', '0.0', '-', '-', '0.1', '4.9%', ''],
|
["대전광역시", "244.6", "11.9", "11.9", "0.68", "0.0", "-", "-", "0.1", "4.9%", ""],
|
||||||
['울산광역시', '235.1', '36.1', '6.1', '29.9', '29.7', '0.2', '-', '-', '15.3%', ''],
|
["울산광역시", "235.1", "36.1", "6.1", "29.9", "29.7", "0.2", "-", "-", "15.3%", ""],
|
||||||
['세종특별자치시', '38.5', '2.5', '2.0', '0.5', '0.4', '-', '0.2', '-', '6.6%', ''],
|
["세종특별자치시", "38.5", "2.5", "2.0", "0.5", "0.4", "-", "0.2", "-", "6.6%", ""],
|
||||||
['경기도', '1,926.8', '313.3', '122.2', '191.0', '153.0', '33.3', '0.4', '4.3', '16.3%', ''],
|
["경기도", "1,926.8", "313.3", "122.2", "191.0", "153.0", "33.3", "0.4", "4.3", "16.3%", ""],
|
||||||
['강원도', '230.4', '19.6', '15.4', '4.2', '2.1', '-', '-', '2.1', '8.5%', ''],
|
["강원도", "230.4", "19.6", "15.4", "4.2", "2.1", "-", "-", "2.1", "8.5%", ""],
|
||||||
['충청북도', '190.9', '15.6', '6.3', '9.2', '7.4', '0.6', '-', '1.2', '8.2%', ''],
|
["충청북도", "190.9", "15.6", "6.3", "9.2", "7.4", "0.6", "-", "1.2", "8.2%", ""],
|
||||||
['충청남도', '241.5', '41.4', '9.8', '31.5', '17.7', '8.6', '4.5', '0.8', '17.1%', ''],
|
["충청남도", "241.5", "41.4", "9.8", "31.5", "17.7", "8.6", "4.5", "0.8", "17.1%", ""],
|
||||||
['전라북도', '292.4', '42.7', '7.5', '35.1', '33.5', '0.2', '1.0', '0.5', '14.6%', ''],
|
["전라북도", "292.4", "42.7", "7.5", "35.1", "33.5", "0.2", "1.0", "0.5", "14.6%", ""],
|
||||||
['전라남도', '254.0', '23.6', '12.3', '11.3', '8.7', '0.0', '1.0', '1.6', '9.3%', ''],
|
["전라남도", "254.0", "23.6", "12.3", "11.3", "8.7", "0.0", "1.0", "1.6", "9.3%", ""],
|
||||||
['경상북도', '378.4', '129.1', '33.7', '95.3', '20.8', '72.5', '0.8', '1.3', '34.1%', ''],
|
["경상북도", "378.4", "129.1", "33.7", "95.3", "20.8", "72.5", "0.8", "1.3", "34.1%", ""],
|
||||||
['경상남도', '411.3', '50.1', '25.0', '25.0', '14.7', '0.5', '3.0', '6.9', '12.2%', ''],
|
["경상남도", "411.3", "50.1", "25.0", "25.0", "14.7", "0.5", "3.0", "6.9", "12.2%", ""],
|
||||||
['제주특별자치도', '88.6', '1.7', '1.5', '0.12', '-', '-', '0.1', '-', '1.9%', ''],
|
["제주특별자치도", "88.6", "1.7", "1.5", "0.12", "-", "-", "0.1", "-", "1.9%", ""],
|
||||||
].map((row, idx) => (
|
].map((row, idx) => (
|
||||||
<tr key={idx} className='bg-white'>
|
<tr key={idx} className="bg-white">
|
||||||
{row.map((val, i) => (
|
{row.map((val, i) => (
|
||||||
<td key={i} className={`${tableStyles.td} ${i === 0 ? '!font-bold !text-neutral-900' : ''} ${[1, 3, 9].includes(i) ? 'bg-[#EAF5F5]' : ''}`}>
|
<td
|
||||||
|
key={i}
|
||||||
|
className={`${tableStyles.td} ${
|
||||||
|
i === 0 ? "!font-bold !text-neutral-900" : ""
|
||||||
|
} ${[1, 3, 9].includes(i) ? "bg-[#EAF5F5]" : ""}`}
|
||||||
|
>
|
||||||
{val}
|
{val}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
@@ -417,4 +702,4 @@ export default function SewageStatusSection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,92 +1,123 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import Image, { type StaticImageData } from 'next/image';
|
import Image, { type StaticImageData } from "next/image";
|
||||||
|
|
||||||
import lawPlan from '@/public/images/landing/intro/law_timeline.png';
|
import lawPlan from "@/public/images/landing/intro/law_timeline.png";
|
||||||
import planPerformance from '@/public/images/landing/intro/performance_left.png';
|
import planPerformance from "@/public/images/landing/intro/performance_left.png";
|
||||||
import domesticChange from '@/public/images/landing/intro/domestic_textbox.png';
|
import domesticChange from "@/public/images/landing/intro/domestic_textbox.png";
|
||||||
import policyHistory from '@/public/images/landing/intro/policy_timeline.png';
|
import policyHistory from "@/public/images/landing/intro/policy_timeline.png";
|
||||||
import lawRelation from '@/public/images/landing/intro/rel_note.png';
|
import lawRelation from "@/public/images/landing/intro/rel_note.png";
|
||||||
import domesticLinkage from '@/public/images/landing/intro/linkage.png';
|
import domesticLinkage from "@/public/images/landing/intro/linkage.png";
|
||||||
|
|
||||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center gap-3'>
|
<div className="flex items-center gap-3">
|
||||||
<span className='h-5 w-[3px] bg-sky-500' />
|
<span className="h-5 w-[3px] bg-sky-500" />
|
||||||
<h3 className='text-[24px] font-bold text-neutral-900'>{children}</h3>
|
<h3 className="text-[24px] font-bold text-neutral-900">{children}</h3>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Hr({ className = '' }: { className?: string }) {
|
function Hr({ className = "" }: { className?: string }) {
|
||||||
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ImgCard({ src, alt }: { src: StaticImageData; alt: string }) {
|
function ImgCard({ src, alt }: { src: StaticImageData; alt: string }) {
|
||||||
return (
|
return (
|
||||||
<div className='w-full overflow-x-auto'>
|
<div className="w-full overflow-x-auto">
|
||||||
<Image src={src} alt={alt} className='mx-auto block max-w-none' style={{ width: 'auto', height: 'auto' }} />
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
className="mx-auto block max-w-none"
|
||||||
|
style={{ width: "auto", height: "auto" }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SectionShell({ id, title, children, className = '' }: { id: string; title: string; children: React.ReactNode; className?: string }) {
|
function SectionShell({
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<section id={id} className={`scroll-mt-[120px] ${className}`}>
|
<section id={id} className={`scroll-mt-[120px] ${className}`}>
|
||||||
<SectionTitle>{title}</SectionTitle>
|
<SectionTitle>{title}</SectionTitle>
|
||||||
<div className='mt-8'>{children}</div>
|
<div className="mt-8">{children}</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LawIntroSection() {
|
export default function LawIntroSection() {
|
||||||
const title = '물 재이용 법';
|
const title = "물 재이용 법";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='bg-white'>
|
<div className="bg-white">
|
||||||
<div className='inline-block'>
|
<div className="inline-block">
|
||||||
<h2 className='relative inline-block px-2 text-[28px] font-extrabold leading-none tracking-tight text-slate-900'>
|
<h2 className="relative inline-block px-2 text-[28px] font-extrabold leading-none tracking-tight text-slate-900">
|
||||||
<span className='relative z-10'>{title}</span>
|
<span className="relative z-10">{title}</span>
|
||||||
<span className='absolute bottom-[-4px] left-0 z-0 h-3 w-full bg-gradient-to-r from-sky-300 to-green-300' />
|
<span className="absolute bottom-[-4px] left-0 z-0 h-3 w-full bg-gradient-to-r from-sky-300 to-green-300" />
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-10'>
|
<div className="mt-10">
|
||||||
<SectionShell id='law-plan' title='법률 제·개정 및 기본계획 수립' className='mt-10'>
|
<SectionShell
|
||||||
<ImgCard src={lawPlan} alt='법률 제·개정 및 기본계획 수립' />
|
id="law-plan"
|
||||||
|
title="법률 제·개정 및 기본계획 수립"
|
||||||
|
className="mt-10"
|
||||||
|
>
|
||||||
|
<ImgCard src={lawPlan} alt="법률 제·개정 및 기본계획 수립" />
|
||||||
</SectionShell>
|
</SectionShell>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
<SectionShell id='plan-performance' title='제2차 계획 대비 실적 현황'>
|
<SectionShell
|
||||||
<ImgCard src={planPerformance} alt='제2차 계획 대비 실적 현황' />
|
id="plan-performance"
|
||||||
|
title="제2차 계획 대비 실적 현황"
|
||||||
|
>
|
||||||
|
<ImgCard src={planPerformance} alt="제2차 계획 대비 실적 현황" />
|
||||||
</SectionShell>
|
</SectionShell>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
<SectionShell id='domestic-change' title='우리나라의 수자원 현황 및 물이용 여건'>
|
<SectionShell id="domestic-change" title="우리나라의 수자원 현황 및 물이용 여건">
|
||||||
<ImgCard src={domesticChange} alt='우리나라의 수자원 현황 및 물이용 여건' />
|
<ImgCard src={domesticChange} alt="우리나라의 수자원 현황 및 물이용 여건" />
|
||||||
</SectionShell>
|
</SectionShell>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
<SectionShell id='policy-history' title='물 재이용 정책 변화 추이'>
|
<SectionShell
|
||||||
<ImgCard src={policyHistory} alt='물 재이용 정책 변화 추이' />
|
id="policy-history"
|
||||||
|
title="물 재이용 정책 변화 추이"
|
||||||
|
>
|
||||||
|
<ImgCard src={policyHistory} alt="물 재이용 정책 변화 추이" />
|
||||||
</SectionShell>
|
</SectionShell>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
<SectionShell id='law-relation' title='타법령과의 관계 검토'>
|
<SectionShell
|
||||||
<ImgCard src={lawRelation} alt='타법령과의 관계 검토' />
|
id="law-relation"
|
||||||
|
title="타법령과의 관계 검토"
|
||||||
|
>
|
||||||
|
<ImgCard src={lawRelation} alt="타법령과의 관계 검토" />
|
||||||
</SectionShell>
|
</SectionShell>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
<SectionShell id='domestic-linkage' title='주요 국가계획의 정책방향과 연계한 물 재이용 정책 필요'>
|
<SectionShell
|
||||||
<ImgCard src={domesticLinkage} alt='주요 국가계획의 정책방향과 연계한 물 재이용 정책 필요' />
|
id="domestic-linkage"
|
||||||
|
title="주요 국가계획의 정책방향과 연계한 물 재이용 정책 필요"
|
||||||
|
>
|
||||||
|
<ImgCard src={domesticLinkage} alt="주요 국가계획의 정책방향과 연계한 물 재이용 정책 필요" />
|
||||||
</SectionShell>
|
</SectionShell>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
// apps/dm/app/(landing)/reuse/intro/law/page.tsx
|
// apps/dm/app/(landing)/reuse/intro/law/page.tsx
|
||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import Image, { type StaticImageData } from 'next/image';
|
import Image, { type StaticImageData } from "next/image";
|
||||||
import SubPageHeader from '../../_components/SubPageHeader';
|
import SubPageHeader from "../../_components/SubPageHeader";
|
||||||
import lawTimeline from '@/public/images/landing/intro/law_timeline.png';
|
import lawTimeline from "@/public/images/landing/intro/law_timeline.png";
|
||||||
import performanceLeft from '@/public/images/landing/intro/performance_left.png';
|
import performanceLeft from "@/public/images/landing/intro/performance_left.png";
|
||||||
import domesticTextbox from '@/public/images/landing/intro/domestic_textbox.png';
|
import domesticTextbox from "@/public/images/landing/intro/domestic_textbox.png";
|
||||||
import policyTimeline from '@/public/images/landing/intro/policy_timeline.png';
|
import policyTimeline from "@/public/images/landing/intro/policy_timeline.png";
|
||||||
import relNote from '@/public/images/landing/intro/rel_note.png';
|
import relNote from "@/public/images/landing/intro/rel_note.png";
|
||||||
import linkage from '@/public/images/landing/intro/linkage.png';
|
import linkage from "@/public/images/landing/intro/linkage.png";
|
||||||
|
|
||||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center gap-3'>
|
<div className="flex items-center gap-3">
|
||||||
<span className='h-5 w-[3px] bg-sky-500' />
|
<span className="h-5 w-[3px] bg-sky-500" />
|
||||||
<h3 className='text-[24px] font-bold text-neutral-900'>{children}</h3>
|
<h3 className="text-[24px] font-bold text-neutral-900">{children}</h3>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@ type HrProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function Hr({ className = '' }: HrProps) {
|
function Hr({ className = "" }: HrProps) {
|
||||||
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,87 +34,95 @@ type ImgCardProps = {
|
|||||||
|
|
||||||
function ImgCard({ src, alt }: ImgCardProps) {
|
function ImgCard({ src, alt }: ImgCardProps) {
|
||||||
return (
|
return (
|
||||||
<div className='w-full overflow-x-auto'>
|
<div className="w-full overflow-x-auto">
|
||||||
<Image src={src} alt={alt} className='block mx-auto max-w-none' style={{ width: 'auto', height: 'auto' }} />
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
className="block mx-auto max-w-none"
|
||||||
|
style={{ width: "auto", height: "auto" }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WaterReuseLawPage() {
|
export default function WaterReuseLawPage() {
|
||||||
return (
|
return (
|
||||||
<div className='bg-white'>
|
<div className="bg-white">
|
||||||
<SubPageHeader
|
<SubPageHeader
|
||||||
title='물 재이용 법'
|
title="물 재이용 법"
|
||||||
crumbs={[{ label: 'Home', href: '/reuse' }, { label: '물 재이용 소개', href: '/reuse/intro/law' }, { label: '물 재이용 법' }]}
|
crumbs={[
|
||||||
|
{ label: "Home", href: "/reuse" },
|
||||||
|
{ label: "물 재이용 소개", href: "/reuse/intro/law" },
|
||||||
|
{ label: "물 재이용 법" },
|
||||||
|
]}
|
||||||
hideFooterLine
|
hideFooterLine
|
||||||
hideGuideText
|
hideGuideText
|
||||||
wrapClassName='!pb-0'
|
wrapClassName="!pb-0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Hr className='my-7' />
|
<Hr className="my-7" />
|
||||||
|
|
||||||
{/* 1) 법제도/계획 수립 타임라인 */}
|
{/* 1) 법제도/계획 수립 타임라인 */}
|
||||||
<section className='mt-6'>
|
<section className="mt-6">
|
||||||
<SectionTitle>법제도 · 계획 및 기본계획 수립</SectionTitle>
|
<SectionTitle>법제도 · 계획 및 기본계획 수립</SectionTitle>
|
||||||
<div className='mt-8'>
|
<div className="mt-8">
|
||||||
<ImgCard src={lawTimeline} alt='법제도 타임라인' />
|
<ImgCard src={lawTimeline} alt="법제도 타임라인" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
{/* 2) 제2차 계획 대비 실적 현황 (2컬럼) */}
|
{/* 2) 제2차 계획 대비 실적 현황 (2컬럼) */}
|
||||||
<section>
|
<section>
|
||||||
<SectionTitle>제 2차 계획 대비 실적 현황</SectionTitle>
|
<SectionTitle>제 2차 계획 대비 실적 현황</SectionTitle>
|
||||||
<div className='mt-8'>
|
<div className="mt-8">
|
||||||
<ImgCard src={performanceLeft} alt='실적 현황 요약' />
|
<ImgCard src={performanceLeft} alt="실적 현황 요약" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
{/* 3) 국내 관련 현황 */}
|
{/* 3) 국내 관련 현황 */}
|
||||||
<section>
|
<section>
|
||||||
<SectionTitle>국내 관련 현황</SectionTitle>
|
<SectionTitle>국내 관련 현황</SectionTitle>
|
||||||
<div className='mt-8'>
|
<div className="mt-8">
|
||||||
<ImgCard src={domesticTextbox} alt='국내 관련 현황 설명' />
|
<ImgCard src={domesticTextbox} alt="국내 관련 현황 설명" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
{/* 4) 정책 변천 추이 */}
|
{/* 4) 정책 변천 추이 */}
|
||||||
<section>
|
<section>
|
||||||
<SectionTitle>물 재이용 정책 변천 추이</SectionTitle>
|
<SectionTitle>물 재이용 정책 변천 추이</SectionTitle>
|
||||||
<div className='mt-8'>
|
<div className="mt-8">
|
||||||
<ImgCard src={policyTimeline} alt='정책 변천 타임라인' />
|
<ImgCard src={policyTimeline} alt="정책 변천 타임라인" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
{/* 5) 타 법령과의 관계도 */}
|
{/* 5) 타 법령과의 관계도 */}
|
||||||
<section>
|
<section>
|
||||||
<SectionTitle>타법령과의 관계도</SectionTitle>
|
<SectionTitle>타법령과의 관계도</SectionTitle>
|
||||||
<div className='mt-8'>
|
<div className="mt-8">
|
||||||
<ImgCard src={relNote} alt='관계도 설명' />
|
<ImgCard src={relNote} alt="관계도 설명" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
{/* 6) 국내 연계 현황 */}
|
{/* 6) 국내 연계 현황 */}
|
||||||
<section className='pb-[200px]'>
|
<section className="pb-[200px]">
|
||||||
<SectionTitle>국내 연계 현황</SectionTitle>
|
<SectionTitle>국내 연계 현황</SectionTitle>
|
||||||
<div className='mt-8'>
|
<div className="mt-8">
|
||||||
<div className='mx-[20px] mb-8'>
|
<div className="mx-[20px] mb-8">
|
||||||
<p className='text-[24px] font-semibold mb-2'>주요 국가계획의 정책방향과 연계한 물 재이용 정책 필요</p>
|
<p className="text-[24px] font-semibold mb-2">주요 국가계획의 정책방향과 연계한 물 재이용 정책 필요</p>
|
||||||
<p className='text-[#4F4F4F]'>
|
<p className="text-[#4F4F4F]">국가 상위계획의 정책 방향이 기후위기 대응 등 수자원의 공급 안전성을 위한 물 재이용 활성화로 정책 실효성 제고 필요<br />
|
||||||
국가 상위계획의 정책 방향이 기후위기 대응 등 수자원의 공급 안전성을 위한 물 재이용 활성화로 정책 실효성 제고 필요
|
• 물관리 계획(국가수도기본계획, 국가 및 유역하수도종합계획, 하천유역수자원관리계획 등)과의 연계성 확보 → 통합적인 관점에서 물이용 체계 구축 필요
|
||||||
<br />• 물관리 계획(국가수도기본계획, 국가 및 유역하수도종합계획, 하천유역수자원관리계획 등)과의 연계성 확보 → 통합적인 관점에서 물이용 체계 구축 필요
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ImgCard src={linkage} alt='국내 연계 현황' />
|
<ImgCard src={linkage} alt="국내 연계 현황" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -185,9 +185,7 @@ export default function DemandStatusPage() {
|
|||||||
const userData = JSON.parse(user) as SessionUser;
|
const userData = JSON.parse(user) as SessionUser;
|
||||||
const dmdInfoId = userData.dmdInfoId?.trim();
|
const dmdInfoId = userData.dmdInfoId?.trim();
|
||||||
if (dmdInfoId) {
|
if (dmdInfoId) {
|
||||||
void getSdmInfoStatusList(dmdInfoId).then((active) => {
|
getSdmInfoStatusList(dmdInfoId);
|
||||||
if (!active) window.location.href = "/";
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error parsing user data:", error);
|
console.error("Error parsing user data:", error);
|
||||||
@@ -343,9 +341,7 @@ export default function DemandStatusPage() {
|
|||||||
if (res) {
|
if (res) {
|
||||||
const user = JSON.parse(sessionStorage.getItem("user") || "{}");
|
const user = JSON.parse(sessionStorage.getItem("user") || "{}");
|
||||||
if (user?.dmdInfoId) {
|
if (user?.dmdInfoId) {
|
||||||
void getSdmInfoStatusList(user.dmdInfoId).then((active) => {
|
getSdmInfoStatusList(user.dmdInfoId);
|
||||||
if (!active) window.location.href = "/";
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
setIsEditMode(false);
|
setIsEditMode(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
export default function LawIntroSection() {
|
export default function LawIntroSection() {
|
||||||
const contentsHtml = `
|
const contentsHtml = `
|
||||||
@@ -84,5 +84,10 @@ export default function LawIntroSection() {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return <div className='bg-white' dangerouslySetInnerHTML={{ __html: contentsHtml }} />;
|
return (
|
||||||
|
<div
|
||||||
|
className="bg-white"
|
||||||
|
dangerouslySetInnerHTML={{ __html: contentsHtml }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
// DM 물 재이용 소개(물 재이용 법) 페이지
|
// DM 물 재이용 소개(물 재이용 법) 페이지
|
||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import Image, { type StaticImageData } from 'next/image';
|
import Image, { type StaticImageData } from "next/image";
|
||||||
import SubPageHeader from '@/components/SubPageHeader';
|
import SubPageHeader from "@/components/SubPageHeader";
|
||||||
// import BusinessSubNav from "@/components/BusinessSubNav";
|
// import BusinessSubNav from "@/components/BusinessSubNav";
|
||||||
|
|
||||||
import lawTimeline from '@/public/images/landing/intro/law_timeline.png';
|
import lawTimeline from "@/public/images/landing/intro/law_timeline.png";
|
||||||
import performanceLeft from '@/public/images/landing/intro/performance_left.png';
|
import performanceLeft from "@/public/images/landing/intro/performance_left.png";
|
||||||
import domesticTextbox from '@/public/images/landing/intro/domestic_textbox.png';
|
import domesticTextbox from "@/public/images/landing/intro/domestic_textbox.png";
|
||||||
import policyTimeline from '@/public/images/landing/intro/policy_timeline.png';
|
import policyTimeline from "@/public/images/landing/intro/policy_timeline.png";
|
||||||
import relNote from '@/public/images/landing/intro/rel_note.png';
|
import relNote from "@/public/images/landing/intro/rel_note.png";
|
||||||
import linkage from '@/public/images/landing/intro/linkage.png';
|
import linkage from "@/public/images/landing/intro/linkage.png";
|
||||||
|
|
||||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center gap-3'>
|
<div className="flex items-center gap-3">
|
||||||
<span className='h-5 w-[3px] bg-sky-500' />
|
<span className="h-5 w-[3px] bg-sky-500" />
|
||||||
<h3 className='text-[24px] font-bold text-neutral-900'>{children}</h3>
|
<h3 className="text-[24px] font-bold text-neutral-900">{children}</h3>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ type HrProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function Hr({ className = '' }: HrProps) {
|
function Hr({ className = "" }: HrProps) {
|
||||||
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,8 +36,14 @@ type ImgCardProps = {
|
|||||||
|
|
||||||
function ImgCard({ src, alt }: ImgCardProps) {
|
function ImgCard({ src, alt }: ImgCardProps) {
|
||||||
return (
|
return (
|
||||||
<div className='w-full overflow-x-auto'>
|
<div className="w-full overflow-x-auto">
|
||||||
<Image src={src} alt={alt} className='mx-auto block max-w-none' style={{ width: 'auto', height: 'auto' }} priority={false} />
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
className="mx-auto block max-w-none"
|
||||||
|
style={{ width: "auto", height: "auto" }}
|
||||||
|
priority={false}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -49,73 +55,109 @@ type SectionShellProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function SectionShell({ id, title, children, className = '' }: SectionShellProps) {
|
function SectionShell({ id, title, children, className = "" }: SectionShellProps) {
|
||||||
return (
|
return (
|
||||||
<section id={id} className={['scroll-mt-[140px]', className].join(' ')}>
|
<section
|
||||||
|
id={id}
|
||||||
|
className={[
|
||||||
|
"scroll-mt-[140px]",
|
||||||
|
className,
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
<SectionTitle>{title}</SectionTitle>
|
<SectionTitle>{title}</SectionTitle>
|
||||||
<div className='mt-8'>{children}</div>
|
<div className="mt-8">{children}</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WaterReuseLawPage() {
|
export default function WaterReuseLawPage() {
|
||||||
return (
|
return (
|
||||||
<div className='relative bg-white'>
|
<div className="relative bg-white">
|
||||||
{/* <BusinessSubNav /> */}
|
{/* <BusinessSubNav /> */}
|
||||||
|
|
||||||
<SubPageHeader
|
<SubPageHeader
|
||||||
title='물 재이용 법'
|
title="물 재이용 법"
|
||||||
crumbs={[{ label: 'Home', href: '/main' }, { label: '물 재이용 소개', href: '/intro/law' }, { label: '물 재이용 법' }]}
|
crumbs={[
|
||||||
|
{ label: "Home", href: "/main" },
|
||||||
|
{ label: "물 재이용 소개", href: "/intro/law" },
|
||||||
|
{ label: "물 재이용 법" },
|
||||||
|
]}
|
||||||
hideFooterLine
|
hideFooterLine
|
||||||
hideGuideText
|
hideGuideText
|
||||||
wrapClassName='!pb-0'
|
wrapClassName="!pb-0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='pr-0'>
|
<div className="pr-0">
|
||||||
<Hr className='my-7' />
|
<Hr className="my-7" />
|
||||||
|
|
||||||
<SectionShell id='law-plan' title='법률 제·개정 및 기본계획 수립' className='mt-6'>
|
<SectionShell
|
||||||
<ImgCard src={lawTimeline} alt='법률 제·개정 및 기본계획 수립' />
|
id="law-plan"
|
||||||
|
title="법률 제·개정 및 기본계획 수립"
|
||||||
|
className="mt-6"
|
||||||
|
>
|
||||||
|
<ImgCard src={lawTimeline} alt="법률 제·개정 및 기본계획 수립" />
|
||||||
</SectionShell>
|
</SectionShell>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
<SectionShell id='plan-performance' title='제2차 계획 대비 실적 현황'>
|
<SectionShell
|
||||||
<ImgCard src={performanceLeft} alt='제2차 계획 대비 실적 현황' />
|
id="plan-performance"
|
||||||
|
title="제2차 계획 대비 실적 현황"
|
||||||
|
>
|
||||||
|
<ImgCard src={performanceLeft} alt="제2차 계획 대비 실적 현황" />
|
||||||
</SectionShell>
|
</SectionShell>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
<SectionShell id='domestic-change' title='국내 여건 변화'>
|
<SectionShell
|
||||||
<ImgCard src={domesticTextbox} alt='국내 여건 변화' />
|
id="domestic-change"
|
||||||
|
title="국내 여건 변화"
|
||||||
|
>
|
||||||
|
<ImgCard src={domesticTextbox} alt="국내 여건 변화" />
|
||||||
</SectionShell>
|
</SectionShell>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
<SectionShell id='policy-history' title='물 재이용 정책 변화 추이'>
|
<SectionShell
|
||||||
<ImgCard src={policyTimeline} alt='물 재이용 정책 변화 추이' />
|
id="policy-history"
|
||||||
|
title="물 재이용 정책 변화 추이"
|
||||||
|
>
|
||||||
|
<ImgCard src={policyTimeline} alt="물 재이용 정책 변화 추이" />
|
||||||
</SectionShell>
|
</SectionShell>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
<SectionShell id='law-relation' title='타법령과의 관계 검토'>
|
<SectionShell
|
||||||
<ImgCard src={relNote} alt='타법령과의 관계 검토' />
|
id="law-relation"
|
||||||
|
title="타법령과의 관계 검토"
|
||||||
|
>
|
||||||
|
<ImgCard src={relNote} alt="타법령과의 관계 검토" />
|
||||||
</SectionShell>
|
</SectionShell>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
<SectionShell id='domestic-linkage' title='국내 연계 현황' className='pb-[250px]'>
|
<SectionShell
|
||||||
<div className='mx-[20px] mb-8'>
|
id="domestic-linkage"
|
||||||
<p className='mb-2 text-[24px] font-semibold'>주요 국가계획의 정책방향과 연계한 물 재이용 정책 필요</p>
|
title="국내 연계 현황"
|
||||||
<p className='text-[#4F4F4F]'>
|
className="pb-[250px]"
|
||||||
국가 상위계획의 정책 방향이 기후위기 대응 등 수자원의 공급 안전성을 위한 물 재이용 활성화로 정책 실효성 제고 필요
|
>
|
||||||
<br />• 물관리 계획(국가수도기본계획, 국가 및 유역하수도종합계획, 하천유역수자원관리계획 등)과의 연계성 확보 → 통합적인 관점에서 물이용 체계 구축 필요
|
<div className="mx-[20px] mb-8">
|
||||||
|
<p className="mb-2 text-[24px] font-semibold">
|
||||||
|
주요 국가계획의 정책방향과 연계한 물 재이용 정책 필요
|
||||||
|
</p>
|
||||||
|
<p className="text-[#4F4F4F]">
|
||||||
|
국가 상위계획의 정책 방향이 기후위기 대응 등 수자원의 공급 안전성을 위한
|
||||||
|
물 재이용 활성화로 정책 실효성 제고 필요
|
||||||
|
<br />
|
||||||
|
• 물관리 계획(국가수도기본계획, 국가 및 유역하수도종합계획,
|
||||||
|
하천유역수자원관리계획 등)과의 연계성 확보 → 통합적인 관점에서
|
||||||
|
물이용 체계 구축 필요
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ImgCard src={linkage} alt='국내 연계 현황' />
|
<ImgCard src={linkage} alt="국내 연계 현황" />
|
||||||
</SectionShell>
|
</SectionShell>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,30 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from "react";
|
||||||
// import SubPageHeader from "@/components/SubPageHeader";
|
// import SubPageHeader from "@/components/SubPageHeader";
|
||||||
import Image from 'next/image';
|
import Image from "next/image";
|
||||||
import { useDmStore } from '@/app/store';
|
import { useDmStore } from "@/app/store";
|
||||||
import { formatUsage } from '@/app/common/transValueFormat';
|
import { formatUsage } from "@/app/common/transValueFormat";
|
||||||
import type { DmStatusBildTpYearsType, DmStatusUsgBildTpType, DmStatusUsgYearsType, TableHeaderType } from '@/app/store/dm-status/type';
|
import type {
|
||||||
|
DmStatusBildTpYearsType,
|
||||||
|
DmStatusUsgBildTpType,
|
||||||
|
DmStatusUsgYearsType,
|
||||||
|
TableHeaderType,
|
||||||
|
} from "@/app/store/dm-status/type";
|
||||||
|
|
||||||
|
|
||||||
// 공통 섹션 타이틀 컴포넌트
|
// 공통 섹션 타이틀 컴포넌트
|
||||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center gap-3'>
|
<div className="flex items-center gap-3">
|
||||||
<span className='h-5 w-[3px] bg-sky-500' />
|
<span className="h-5 w-[3px] bg-sky-500" />
|
||||||
<h3 className='text-[24px] font-bold text-neutral-900'>{children}</h3>
|
<h3 className="text-[24px] font-bold text-neutral-900">{children}</h3>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 구분선
|
// 구분선
|
||||||
function Hr({ className = '' }: { className?: string }) {
|
function Hr({ className = "" }: { className?: string }) {
|
||||||
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,49 +32,62 @@ function Hr({ className = '' }: { className?: string }) {
|
|||||||
function RainIntroSummary() {
|
function RainIntroSummary() {
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
icon: '/images/landing/icon_chart.png',
|
icon: "/images/landing/icon_chart.png",
|
||||||
title: '지속적인 이용량 증가',
|
title: "지속적인 이용량 증가",
|
||||||
desc: '2018년 약 797만m³이었던 빗물 이용량은 5년 사이 약 75% 증가하여 수자원 절약에 기여하고 있습니다.',
|
desc: "2018년 약 797만m³이었던 빗물 이용량은 5년 사이 약 75% 증가하여 수자원 절약에 기여하고 있습니다.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: '/images/landing/icon_hand.png',
|
icon: "/images/landing/icon_hand.png",
|
||||||
title: '조경용수의 핵심 자원',
|
title: "조경용수의 핵심 자원",
|
||||||
desc: '재이용되는 빗물의 약 97%가 조경용수로 활용되고 있으며 이는 공원, 아파트 단지 등의 녹지 관리에 중요한 역할을 합니다.',
|
desc: "재이용되는 빗물의 약 97%가 조경용수로 활용되고 있으며 이는 공원, 아파트 단지 등의 녹지 관리에 중요한 역할을 합니다.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: '/images/landing/icon_building.png',
|
icon: "/images/landing/icon_building.png",
|
||||||
title: '민간·공공의 협력',
|
title: "민간·공공의 협력",
|
||||||
desc: '골프장과 같은 대규모 스포츠 시설은 물론, 최근에는 공동주택·아파트 등 내 이용량이 급증하며 일상생활 속 빗물 활용이 확산되는 추세입니다.',
|
desc: "골프장과 같은 대규모 스포츠 시설은 물론, 최근에는 공동주택·아파트 등 내 이용량이 급증하며 일상생활 속 빗물 활용이 확산되는 추세입니다.",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mb-[70px] px-[160px]'>
|
<div className="mb-[70px] px-[160px]">
|
||||||
<div className='grid grid-cols-[1fr_400px] items-center gap-[60px]'>
|
<div className="grid grid-cols-[1fr_400px] items-center gap-[60px]">
|
||||||
{/* 좌측 */}
|
{/* 좌측 */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className='text-[40px] font-extrabold leading-[1.35] tracking-[-0.04em] text-neutral-900'>
|
<h3 className="text-[40px] font-extrabold leading-[1.35] tracking-[-0.04em] text-neutral-900">
|
||||||
기후위기 시대의 수자원 확보,
|
기후위기 시대의 수자원 확보,
|
||||||
<br />
|
<br />
|
||||||
<span className='inline-block bg-gradient-to-b from-[#6DBB4F] to-[#01ACE5] bg-clip-text text-transparent'>빗물 재이용</span>이 앞장섭니다.
|
<span className="inline-block bg-gradient-to-b from-[#6DBB4F] to-[#01ACE5] bg-clip-text text-transparent">
|
||||||
|
빗물 재이용
|
||||||
|
</span>
|
||||||
|
이 앞장섭니다.
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p className='mt-5 text-[16px] leading-[1.8] tracking-[-0.04em] text-neutral-600'>
|
<p className="mt-5 text-[16px] leading-[1.8] tracking-[-0.04em] text-neutral-600">
|
||||||
2023년 기준 전국 3,500여 개 시설에서
|
2023년 기준 전국 3,500여 개 시설에서
|
||||||
<br />
|
<br />
|
||||||
연간 약 1,394만m³의 빗물을 소중한 자원으로 다시 활용하고 있습니다.
|
연간 약 1,394만m³의 빗물을 소중한 자원으로 다시 활용하고 있습니다.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className='mt-10 space-y-8'>
|
<div className="mt-10 space-y-8">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<div key={item.title} className='flex gap-3 items-center'>
|
<div key={item.title} className="flex gap-3 items-center">
|
||||||
<div className='flex h-12 w-12 shrink-0 items-center justify-center'>
|
<div className="flex h-12 w-12 shrink-0 items-center justify-center">
|
||||||
<Image src={item.icon} alt='' width={48} height={48} className='object-contain' />
|
<Image
|
||||||
|
src={item.icon}
|
||||||
|
alt=""
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
|
className="object-contain"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<strong className='block text-[18px] font-extrabold text-neutral-900'>{item.title}</strong>
|
<strong className="block text-[18px] font-extrabold text-neutral-900">
|
||||||
<p className='mt-2 text-[16px] leading-[1.7] text-neutral-600 tracking-tighter'>{item.desc}</p>
|
{item.title}
|
||||||
|
</strong>
|
||||||
|
<p className="mt-2 text-[16px] leading-[1.7] text-neutral-600 tracking-tighter">
|
||||||
|
{item.desc}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -76,8 +95,15 @@ function RainIntroSummary() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 우측 이미지 */}
|
{/* 우측 이미지 */}
|
||||||
<div className='flex items-center justify-center'>
|
<div className="flex items-center justify-center">
|
||||||
<Image src='/images/landing/rain_status_img.png' alt='빗물 재이용 개념도' width={400} height={343} className='h-auto w-full max-w-[400px] object-contain' priority />
|
<Image
|
||||||
|
src="/images/landing/rain_status_img.png"
|
||||||
|
alt="빗물 재이용 개념도"
|
||||||
|
width={400}
|
||||||
|
height={343}
|
||||||
|
className="h-auto w-full max-w-[400px] object-contain"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,15 +111,31 @@ function RainIntroSummary() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tableStyles = {
|
const tableStyles = {
|
||||||
container: 'w-[1000px] mx-auto overflow-hidden',
|
container: "w-[1000px] mx-auto overflow-hidden",
|
||||||
table: 'w-full table-fixed border-collapse text-center text-[16px]',
|
table: "w-full table-fixed border-collapse text-center text-[16px]",
|
||||||
th: 'border border-[#858F98] py-3 font-bold text-neutral-800',
|
th: "border border-[#858F98] py-3 font-bold text-neutral-800",
|
||||||
td: 'border border-[#858F98] py-3 text-neutral-700 font-medium break-keep',
|
td: "border border-[#858F98] py-3 text-neutral-700 font-medium break-keep",
|
||||||
headerBg: 'bg-[#E3E7EA]',
|
headerBg: "bg-[#E3E7EA]",
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerColors6 = ['bg-[#01ACE5]', 'bg-[#17AFC7]', 'bg-[#2CB2A9]', 'bg-[#42B58B]', 'bg-[#57B86D]', 'bg-[#81C784]'];
|
const headerColors6 = [
|
||||||
const headerColors8 = ['bg-[#01ACE5]', 'bg-[#10AED0]', 'bg-[#20B0BA]', 'bg-[#2FB2A5]', 'bg-[#3FB58F]', 'bg-[#4EB77A]', 'bg-[#5EB964]', 'bg-[#6DBB4F]'];
|
"bg-[#01ACE5]",
|
||||||
|
"bg-[#17AFC7]",
|
||||||
|
"bg-[#2CB2A9]",
|
||||||
|
"bg-[#42B58B]",
|
||||||
|
"bg-[#57B86D]",
|
||||||
|
"bg-[#81C784]",
|
||||||
|
];
|
||||||
|
const headerColors8 = [
|
||||||
|
"bg-[#01ACE5]",
|
||||||
|
"bg-[#10AED0]",
|
||||||
|
"bg-[#20B0BA]",
|
||||||
|
"bg-[#2FB2A5]",
|
||||||
|
"bg-[#3FB58F]",
|
||||||
|
"bg-[#4EB77A]",
|
||||||
|
"bg-[#5EB964]",
|
||||||
|
"bg-[#6DBB4F]",
|
||||||
|
];
|
||||||
export default function RainStatusSection() {
|
export default function RainStatusSection() {
|
||||||
const getRainyByBildTpYears = useDmStore((state) => state.getRainyByBildTpYears);
|
const getRainyByBildTpYears = useDmStore((state) => state.getRainyByBildTpYears);
|
||||||
const getRainyByUsgBildTp = useDmStore((state) => state.getRainyByUsgBildTp);
|
const getRainyByUsgBildTp = useDmStore((state) => state.getRainyByUsgBildTp);
|
||||||
@@ -106,11 +148,19 @@ export default function RainStatusSection() {
|
|||||||
const rainyByUsgBildTp = useDmStore((state) => state.rainy.rainyByUsgBildTp);
|
const rainyByUsgBildTp = useDmStore((state) => state.rainy.rainyByUsgBildTp);
|
||||||
const rainyByUsgYearsHeader = useDmStore((state) => state.rainy.rainyByUsgYearsHeader);
|
const rainyByUsgYearsHeader = useDmStore((state) => state.rainy.rainyByUsgYearsHeader);
|
||||||
const rainyByUsgYears = useDmStore((state) => state.rainy.rainyByUsgYears);
|
const rainyByUsgYears = useDmStore((state) => state.rainy.rainyByUsgYears);
|
||||||
const partBildTpHeader = useMemo(() => rainyByUsgBildTpHeader.slice(3).map((header) => header.title), [rainyByUsgBildTpHeader]);
|
const partBildTpHeader = useMemo(
|
||||||
|
() => rainyByUsgBildTpHeader.slice(3).map((header) => header.title),
|
||||||
|
[rainyByUsgBildTpHeader]
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
// 출처 텍스트
|
// 출처 텍스트
|
||||||
function SourceText() {
|
function SourceText() {
|
||||||
return <p className='mt-7 text-center text-[13px] text-neutral-500'>자료: 하수도정보시스템 {selectYear} 하수도 통계</p>;
|
return (
|
||||||
|
<p className="mt-7 text-center text-[13px] text-neutral-500">
|
||||||
|
자료: 하수도정보시스템 {selectYear} 하수도 통계
|
||||||
|
</p>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -121,22 +171,32 @@ export default function RainStatusSection() {
|
|||||||
|
|
||||||
const renderPartHeaders = (headers: string[], headerColors: string[]) => {
|
const renderPartHeaders = (headers: string[], headerColors: string[]) => {
|
||||||
return headers.map((header, index) => (
|
return headers.map((header, index) => (
|
||||||
<th key={header} className={`${tableStyles.th} ${headerColors[index] ?? 'bg-[#81C784]'} text-white`} style={{ width: '140px' }}>
|
<th
|
||||||
|
key={header}
|
||||||
|
className={`${tableStyles.th} ${headerColors[index] ?? "bg-[#81C784]"} text-white`}
|
||||||
|
style={{ width: "140px" }}
|
||||||
|
>
|
||||||
{header}
|
{header}
|
||||||
</th>
|
</th>
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderBuildingUsageCells = (row: DmStatusBildTpYearsType | DmStatusUsgYearsType | DmStatusUsgYearsType, headers: TableHeaderType[]) => {
|
const renderBuildingUsageCells = (
|
||||||
|
row: DmStatusBildTpYearsType | DmStatusUsgYearsType | DmStatusUsgYearsType,
|
||||||
|
headers: TableHeaderType[]
|
||||||
|
) => {
|
||||||
const rowData = row as Record<string, string | number>;
|
const rowData = row as Record<string, string | number>;
|
||||||
const firstHeaderId = headers[0]?.id;
|
const firstHeaderId = headers[0]?.id;
|
||||||
const rowKey = firstHeaderId ? String(rowData[firstHeaderId] ?? '') : '';
|
const rowKey = firstHeaderId ? String(rowData[firstHeaderId] ?? "") : "";
|
||||||
|
|
||||||
return headers.map((header, index) => {
|
return headers.map((header, index) => {
|
||||||
const value = rowData[header.id];
|
const value = rowData[header.id];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td key={`${rowKey}-${header.id}`} className={`${tableStyles.td} ${index === 0 ? '!font-bold !text-neutral-900' : ''} ${[1, 3, 5].includes(index) ? 'bg-[#EAF5F5]' : ''}`}>
|
<td
|
||||||
|
key={`${rowKey}-${header.id}`}
|
||||||
|
className={`${tableStyles.td} ${index === 0 ? "!font-bold !text-neutral-900" : ""} ${[1, 3, 5].includes(index) ? "bg-[#EAF5F5]" : ""}`}
|
||||||
|
>
|
||||||
{index === 0 ? value : formatUsage(value as string | number, 1000, 1)}
|
{index === 0 ? value : formatUsage(value as string | number, 1000, 1)}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
@@ -146,99 +206,102 @@ export default function RainStatusSection() {
|
|||||||
const renderRainyByUsgBildTpCells = (row: DmStatusUsgBildTpType) => {
|
const renderRainyByUsgBildTpCells = (row: DmStatusUsgBildTpType) => {
|
||||||
return rainyByUsgBildTpHeader.map((header, index) => {
|
return rainyByUsgBildTpHeader.map((header, index) => {
|
||||||
const value = row[header.id as keyof DmStatusUsgBildTpType];
|
const value = row[header.id as keyof DmStatusUsgBildTpType];
|
||||||
const displayValue = index === 0 ? value : header.id === 'CNT' ? Number(value).toLocaleString('ko-KR') : formatUsage(value as string | number, 1000, 1);
|
const displayValue =
|
||||||
|
index === 0
|
||||||
|
? value
|
||||||
|
: header.id === "CNT"
|
||||||
|
? Number(value).toLocaleString("ko-KR")
|
||||||
|
: formatUsage(value as string | number, 1000, 1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td key={`${row.USG_NM}-${header.id}`} className={`${tableStyles.td} ${index === 0 ? '!font-bold !text-neutral-900' : ''} ${[1, 2].includes(index) ? 'bg-[#EAF5F5]' : ''}`}>
|
<td
|
||||||
{displayValue == 0.0 ? '-' : displayValue}
|
key={`${row.USG_NM}-${header.id}`}
|
||||||
|
className={`${tableStyles.td} ${index === 0 ? "!font-bold !text-neutral-900" : ""} ${[1, 2].includes(index) ? "bg-[#EAF5F5]" : ""}`}
|
||||||
|
>
|
||||||
|
{displayValue == 0.0 ? "-" : displayValue}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const title = '빗물이용시설(현황)';
|
const title = "빗물이용시설(현황)";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='bg-white'>
|
<div className="bg-white">
|
||||||
{/* 상단 타이틀 */}
|
{/* 상단 타이틀 */}
|
||||||
<div className='inline-block'>
|
<div className="inline-block">
|
||||||
<h2 className='relative inline-block px-2 text-[28px] font-extrabold tracking-tight leading-none text-slate-900'>
|
<h2 className="relative inline-block px-2 text-[28px] font-extrabold tracking-tight leading-none text-slate-900">
|
||||||
<span className='relative z-10'>{title}</span>
|
<span className="relative z-10">{title}</span>
|
||||||
<span className='absolute left-0 bottom-[-4px] z-0 h-3 w-full bg-gradient-to-r from-sky-300 to-green-300' />
|
<span className="absolute left-0 bottom-[-4px] z-0 h-3 w-full bg-gradient-to-r from-sky-300 to-green-300" />
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-[70px] pb-[120px]'>
|
<div className="mt-[70px] pb-[120px]">
|
||||||
{/* 1) 빗물이용시설의 건축물별 빗물이용량 현황 */}
|
{/* 1) 빗물이용시설의 건축물별 빗물이용량 현황 */}
|
||||||
<section>
|
<section>
|
||||||
{/* 인트로 영역 */}
|
{/* 인트로 영역 */}
|
||||||
<RainIntroSummary />
|
<RainIntroSummary />
|
||||||
|
|
||||||
<div id='status-rain-building' className='scroll-mt-[120px]'>
|
<div id="status-rain-building" className="scroll-mt-[120px]">
|
||||||
<SectionTitle>빗물이용시설의 건축물별 빗물이용량 현황</SectionTitle>
|
<SectionTitle>빗물이용시설의 건축물별 빗물이용량 현황</SectionTitle>
|
||||||
|
|
||||||
<div className='mt-10'>
|
<div className="mt-10">
|
||||||
<div className={tableStyles.container}>
|
<div className={tableStyles.container}>
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '160px' }}>
|
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: "160px" }}>
|
||||||
구분
|
구분
|
||||||
</th>
|
</th>
|
||||||
<th colSpan={rainyYears.length || 6} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
|
<th colSpan={rainyYears.length || 6} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
|
||||||
빗물이용량 (천m³/년)
|
빗물이용량 (천m³/년)
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>{renderPartHeaders(rainyYears, headerColors6)}</tr>
|
<tr>
|
||||||
</thead>
|
{renderPartHeaders(rainyYears, headerColors6)}
|
||||||
<tbody>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{rainyByBildTpYears.map((row, idx) => (
|
{rainyByBildTpYears.map((row, idx) => (
|
||||||
<tr key={idx} className='bg-white'>
|
<tr key={idx} className="bg-white">
|
||||||
{renderBuildingUsageCells(row, rainyByBildTpYearsHeader)}
|
{renderBuildingUsageCells(row, rainyByBildTpYearsHeader)}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SourceText />
|
<SourceText />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
{/* 2) 빗물이용시설의 용도별 분포 (xxxx년 기준) */}
|
{/* 2) 빗물이용시설의 용도별 분포 (xxxx년 기준) */}
|
||||||
<section id='status-rain-purpose-distribution' className='scroll-mt-[120px]'>
|
<section
|
||||||
|
id="status-rain-purpose-distribution"
|
||||||
|
className="scroll-mt-[120px]"
|
||||||
|
>
|
||||||
<SectionTitle>빗물이용시설의 용도별 분포 ({selectYear}년 기준)</SectionTitle>
|
<SectionTitle>빗물이용시설의 용도별 분포 ({selectYear}년 기준)</SectionTitle>
|
||||||
|
|
||||||
<div className='mt-8'>
|
<div className="mt-8">
|
||||||
<div className={tableStyles.container}>
|
<div className={tableStyles.container}>
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '160px' }}>
|
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: "160px" }}>용도</th>
|
||||||
용도
|
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: "84px" }}>개소수<br />(개소)</th>
|
||||||
</th>
|
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: "84px" }}>저류용량<br />(천m³)</th>
|
||||||
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '84px' }}>
|
<th colSpan={8} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>빗물이용량 (천m³/년)</th>
|
||||||
개소수
|
</tr>
|
||||||
<br />
|
<tr>
|
||||||
(개소)
|
{renderPartHeaders(partBildTpHeader, headerColors8)}
|
||||||
</th>
|
|
||||||
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '84px' }}>
|
|
||||||
저류조용량
|
|
||||||
<br />
|
|
||||||
(천m³)
|
|
||||||
</th>
|
|
||||||
<th colSpan={8} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
|
|
||||||
빗물이용량 (천m³/년)
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>{renderPartHeaders(partBildTpHeader, headerColors8)}</tr>
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{rainyByUsgBildTp.map((row, idx) => (
|
{rainyByUsgBildTp.map((row, idx) => (
|
||||||
<tr key={`${row.USG_NM}-${idx}`} className='bg-white'>
|
<tr key={`${row.USG_NM}-${idx}`} className="bg-white">
|
||||||
{renderRainyByUsgBildTpCells(row)}
|
{renderRainyByUsgBildTpCells(row)}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@@ -250,29 +313,30 @@ export default function RainStatusSection() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
{/* 3) 빗물이용시설의 용도별 빗물이용량 현황 */}
|
{/* 3) 빗물이용시설의 용도별 빗물이용량 현황 */}
|
||||||
<section id='status-rain-purpose-usage' className='scroll-mt-[120px]'>
|
<section
|
||||||
|
id="status-rain-purpose-usage"
|
||||||
|
className="scroll-mt-[120px]"
|
||||||
|
>
|
||||||
<SectionTitle>빗물이용시설의 용도별 빗물이용량 현황</SectionTitle>
|
<SectionTitle>빗물이용시설의 용도별 빗물이용량 현황</SectionTitle>
|
||||||
|
|
||||||
<div className='mt-8'>
|
<div className="mt-8">
|
||||||
<div className={tableStyles.container}>
|
<div className={tableStyles.container}>
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '160px' }}>
|
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: "160px" }}>용도</th>
|
||||||
용도
|
<th colSpan={6} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>빗물이용량 (천m³/년)</th>
|
||||||
</th>
|
</tr>
|
||||||
<th colSpan={6} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
|
<tr>
|
||||||
빗물이용량 (천m³/년)
|
{renderPartHeaders(rainyYears, headerColors6)}
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>{renderPartHeaders(rainyYears, headerColors6)}</tr>
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{rainyByUsgYears.map((row, idx) => (
|
{rainyByUsgYears.map((row, idx) => (
|
||||||
<tr key={idx} className='bg-white'>
|
<tr key={idx} className="bg-white">
|
||||||
{renderBuildingUsageCells(row, rainyByUsgYearsHeader)}
|
{renderBuildingUsageCells(row, rainyByUsgYearsHeader)}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@@ -286,4 +350,4 @@ export default function RainStatusSection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,97 +1,116 @@
|
|||||||
/**
|
/**
|
||||||
* 하수처리수
|
* 하수처리수
|
||||||
*/
|
*/
|
||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import Image from 'next/image';
|
import Image from "next/image";
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from "react";
|
||||||
import { useDmStore } from '@/app/store';
|
import { useDmStore } from "@/app/store";
|
||||||
import { formatUsage } from '@/app/common/transValueFormat';
|
import { formatUsage } from "@/app/common/transValueFormat";
|
||||||
import type { DmStatusLawYearsType, DmStatusBildTpYearsType, DmStatusUsgYearsType, DmStatusSidoUsgType, TableHeaderType } from '@/app/store/dm-status/type';
|
import type {
|
||||||
import { Col2Name } from '@/app/store/dm-status/type';
|
DmStatusLawYearsType,
|
||||||
|
DmStatusBildTpYearsType,
|
||||||
|
DmStatusUsgYearsType,
|
||||||
|
DmStatusSidoUsgType,
|
||||||
|
TableHeaderType,
|
||||||
|
} from "@/app/store/dm-status/type";
|
||||||
|
import { Col2Name } from "@/app/store/dm-status/type";
|
||||||
|
|
||||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center gap-3'>
|
<div className="flex items-center gap-3">
|
||||||
<span className='h-5 w-[3px] bg-sky-500' />
|
<span className="h-5 w-[3px] bg-sky-500" />
|
||||||
<h3 className='text-[24px] font-bold text-neutral-900'>{children}</h3>
|
<h3 className="text-[24px] font-bold text-neutral-900">{children}</h3>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Hr({ className = '' }: { className?: string }) {
|
function Hr({ className = "" }: { className?: string }) {
|
||||||
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SewageIntroSummary() {
|
function SewageIntroSummary() {
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
icon: '/images/landing/icon_cycle_water.png',
|
icon: "/images/landing/icon_cycle_water.png",
|
||||||
title: '안정적인 재이용률 유지',
|
title: "안정적인 재이용률 유지",
|
||||||
desc: (
|
desc: (
|
||||||
<>
|
<>
|
||||||
연간 약 1,136백만m³의 하수처리수가 재이용되고 있으며,
|
연간 약 1,136백만m³의 하수처리수가 재이용되고 있으며,
|
||||||
<br />
|
<br/>
|
||||||
약 15%에 달하는 높은 재이용률을 통해
|
약 15%에 달하는 높은 재이용률을 통해
|
||||||
<br />
|
<br/>
|
||||||
수자원 선순환 체계를 구축하고 있습니다.
|
수자원 선순환 체계를 구축하고 있습니다.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: '/images/landing/icon_water_flow.png',
|
icon: "/images/landing/icon_water_flow.png",
|
||||||
title: '메마른 하천을 살리는 물',
|
title: "메마른 하천을 살리는 물",
|
||||||
desc: (
|
desc: (
|
||||||
<>
|
<>
|
||||||
재이용되는 물의 가장 많은 부분이 하천유지용수로 사용되어
|
재이용되는 물의 가장 많은 부분이 하천유지용수로 사용되어
|
||||||
<br />
|
<br/>
|
||||||
도심 속 하천의 생태계를 보호하고 쾌적한 수변 환경을 조성합니다.
|
도심 속 하천의 생태계를 보호하고 쾌적한 수변 환경을 조성합니다.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: '/images/landing/icon_location.png',
|
icon: "/images/landing/icon_location.png",
|
||||||
title: '지역별 균형 있는 활용',
|
title: "지역별 균형 있는 활용",
|
||||||
desc: (
|
desc: (
|
||||||
<>
|
<>
|
||||||
전국적으로 고르게 시설이 확충되고 있으며, 특히 경기 지역을 중심으로
|
전국적으로 고르게 시설이 확충되고 있으며, 특히 경기 지역을 중심으로
|
||||||
<br />
|
<br/>
|
||||||
대규모 공업용수 공급망을 구축하여
|
대규모 공업용수 공급망을 구축하여
|
||||||
<br />
|
<br/>
|
||||||
지역 경제와 환경 보호를 동시에 실현하고 있습니다.
|
지역 경제와 환경 보호를 동시에 실현하고 있습니다.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mb-[70px] px-[160px]'>
|
<div className="mb-[70px] px-[160px]">
|
||||||
<div className='grid grid-cols-[1fr_400px] items-center gap-0'>
|
<div className="grid grid-cols-[1fr_400px] items-center gap-0">
|
||||||
{/* 좌측 */}
|
{/* 좌측 */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className='text-[40px] font-extrabold leading-[1.35] tracking-[-0.14em] text-neutral-900'>
|
<h3 className="text-[40px] font-extrabold leading-[1.35] tracking-[-0.14em] text-neutral-900">
|
||||||
버려지는 하수에서 찾는 새로운 가치,
|
버려지는 하수에서 찾는 새로운 가치,
|
||||||
<br />
|
<br />
|
||||||
<span className='inline-block bg-gradient-to-b from-[#6DBB4F] to-[#01ACE5] bg-clip-text text-transparent'>하수처리수 재이용</span>이 도시를 숨쉬게 합니다.
|
<span className="inline-block bg-gradient-to-b from-[#6DBB4F] to-[#01ACE5] bg-clip-text text-transparent">
|
||||||
|
하수처리수 재이용
|
||||||
|
</span>
|
||||||
|
이 도시를 숨쉬게 합니다.
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p className='mt-5 text-[16px] leading-[1.8] text-neutral-600'>
|
<p className="mt-5 text-[16px] leading-[1.8] text-neutral-600">
|
||||||
하수처리장에서 정화된 물은 하천의 수량을 조절하고
|
하수처리장에서 정화된 물은 하천의 수량을 조절하고
|
||||||
<br />
|
<br />
|
||||||
산업 현장에 용수를 공급하는 소중한 자원입니다.
|
산업 현장에 용수를 공급하는 소중한 자원입니다.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className='mt-10 space-y-8'>
|
<div className="mt-10 space-y-8">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<div key={item.title} className='flex gap-4 items-center'>
|
<div key={item.title} className="flex gap-4 items-center">
|
||||||
<div className='mt-1 flex h-12 w-12 shrink-0 items-center justify-center'>
|
<div className="mt-1 flex h-12 w-12 shrink-0 items-center justify-center">
|
||||||
<Image src={item.icon} alt='' width={48} height={48} className='object-contain' />
|
<Image
|
||||||
|
src={item.icon}
|
||||||
|
alt=""
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
|
className="object-contain"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<strong className='block text-[18px] font-extrabold text-neutral-900'>{item.title}</strong>
|
<strong className="block text-[18px] font-extrabold text-neutral-900">
|
||||||
<p className='mt-2 text-[16px] leading-[1.7] text-neutral-600'>{item.desc}</p>
|
{item.title}
|
||||||
|
</strong>
|
||||||
|
<p className="mt-2 text-[16px] leading-[1.7] text-neutral-600">
|
||||||
|
{item.desc}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -99,8 +118,15 @@ function SewageIntroSummary() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 우측 이미지 */}
|
{/* 우측 이미지 */}
|
||||||
<div className='flex items-center justify-center'>
|
<div className="flex items-center justify-center">
|
||||||
<Image src='/images/landing/sewage_status_img.png' alt='하수처리수 재이용 개념도' width={420} height={360} className='h-auto w-full max-w-[420px] object-contain' priority />
|
<Image
|
||||||
|
src="/images/landing/sewage_status_img.png"
|
||||||
|
alt="하수처리수 재이용 개념도"
|
||||||
|
width={420}
|
||||||
|
height={360}
|
||||||
|
className="h-auto w-full max-w-[420px] object-contain"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,24 +134,31 @@ function SewageIntroSummary() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tableStyles = {
|
const tableStyles = {
|
||||||
container: 'w-[1000px] mx-auto overflow-hidden',
|
container: "w-[1000px] mx-auto overflow-hidden",
|
||||||
table: 'w-full table-fixed border-collapse text-center text-[16px]',
|
table: "w-full table-fixed border-collapse text-center text-[16px]",
|
||||||
th: 'border border-[#858F98] py-3 font-bold text-neutral-800',
|
th: "border border-[#858F98] py-3 font-bold text-neutral-800",
|
||||||
td: 'border border-[#858F98] py-3 text-neutral-700 font-medium break-keep',
|
td: "border border-[#858F98] py-3 text-neutral-700 font-medium break-keep",
|
||||||
headerBg: 'bg-[#E3E7EA]',
|
headerBg: "bg-[#E3E7EA]",
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerColors6 = ['bg-[#01ACE5]', 'bg-[#17AFC7]', 'bg-[#2CB2A9]', 'bg-[#42B58B]', 'bg-[#57B86D]', 'bg-[#81C784]'];
|
const headerColors6 = [
|
||||||
|
"bg-[#01ACE5]",
|
||||||
|
"bg-[#17AFC7]",
|
||||||
|
"bg-[#2CB2A9]",
|
||||||
|
"bg-[#42B58B]",
|
||||||
|
"bg-[#57B86D]",
|
||||||
|
"bg-[#81C784]",
|
||||||
|
];
|
||||||
|
|
||||||
export default function SewageStatusSection() {
|
export default function SewageStatusSection() {
|
||||||
const getPbnstByLawYears = useDmStore((state) => state.getPbnstByLawYears);
|
const getPbnstByLawYears = useDmStore((state) => state.getPbnstByLawYears);
|
||||||
const getPbnstByUsgYears = useDmStore((state) => state.getPbnstByUsgYears);
|
const getPbnstByUsgYears = useDmStore((state) => state.getPbnstByUsgYears);
|
||||||
const getPbnstByCpcYears = useDmStore((state) => state.getPbnstByCpcYears);
|
const getPbnstByCpcYears = useDmStore((state) => state.getPbnstByCpcYears);
|
||||||
const getPbnstBySido = useDmStore((state) => state.getPbnstBySido);
|
const getPbnstBySido = useDmStore((state) => state.getPbnstBySido);
|
||||||
const pbnstYears = useDmStore((state) => state.pbnst.pbnstYears);
|
const pbnstYears = useDmStore((state) => state.pbnst.pbnstYears);
|
||||||
const selectYear = pbnstYears.slice(-1)[0];
|
const selectYear = pbnstYears.slice(-1)[0];
|
||||||
const pbnstByLawYearsHeader = useDmStore((state) => state.pbnst.pbnstByLawYearsHeader);
|
const pbnstByLawYearsHeader = useDmStore((state) => state.pbnst.pbnstByLawYearsHeader);
|
||||||
const pbnstByLawYears = useDmStore((state) => state.pbnst.pbnstByLawYears);
|
const pbnstByLawYears = useDmStore((state) => state.pbnst.pbnstByLawYears);
|
||||||
const pbnstByUsgYearsHeader = useDmStore((state) => state.pbnst.pbnstByUsgYearsHeader);
|
const pbnstByUsgYearsHeader = useDmStore((state) => state.pbnst.pbnstByUsgYearsHeader);
|
||||||
const pbnstByUsgYears = useDmStore((state) => state.pbnst.pbnstByUsgYears);
|
const pbnstByUsgYears = useDmStore((state) => state.pbnst.pbnstByUsgYears);
|
||||||
const pbnstByCpcYearsHeader = useDmStore((state) => state.pbnst.pbnstByCpcYearsHeader);
|
const pbnstByCpcYearsHeader = useDmStore((state) => state.pbnst.pbnstByCpcYearsHeader);
|
||||||
@@ -135,11 +168,15 @@ export default function SewageStatusSection() {
|
|||||||
|
|
||||||
// 출처 텍스트
|
// 출처 텍스트
|
||||||
function SourceText() {
|
function SourceText() {
|
||||||
return <p className='mt-7 text-center text-[13px] text-neutral-500'>자료: 하수도정보시스템 {selectYear} 하수도 통계</p>;
|
return (
|
||||||
|
<p className="mt-7 text-center text-[13px] text-neutral-500">
|
||||||
|
자료: 하수도정보시스템 {selectYear} 하수도 통계
|
||||||
|
</p>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void getPbnstByLawYears();
|
void getPbnstByLawYears();
|
||||||
void getPbnstByUsgYears();
|
void getPbnstByUsgYears();
|
||||||
void getPbnstByCpcYears();
|
void getPbnstByCpcYears();
|
||||||
void getPbnstBySido();
|
void getPbnstBySido();
|
||||||
@@ -147,7 +184,10 @@ export default function SewageStatusSection() {
|
|||||||
|
|
||||||
const renderPartHeaders = (headers: string[], headerColors: string[]) => {
|
const renderPartHeaders = (headers: string[], headerColors: string[]) => {
|
||||||
return headers.map((header, index) => (
|
return headers.map((header, index) => (
|
||||||
<th key={header} className={`${tableStyles.th} ${headerColors[index] ?? 'bg-[#81C784]'} text-white`}>
|
<th
|
||||||
|
key={header}
|
||||||
|
className={`${tableStyles.th} ${headerColors[index] ?? "bg-[#81C784]"} text-white`}
|
||||||
|
>
|
||||||
{header}
|
{header}
|
||||||
</th>
|
</th>
|
||||||
));
|
));
|
||||||
@@ -158,10 +198,10 @@ export default function SewageStatusSection() {
|
|||||||
row: DmStatusLawYearsType,
|
row: DmStatusLawYearsType,
|
||||||
data: DmStatusLawYearsType[],
|
data: DmStatusLawYearsType[],
|
||||||
headers: TableHeaderType[],
|
headers: TableHeaderType[],
|
||||||
unitSize: number = 1 // 단위 변경 1000 인 경우, 100,000 -> 100 으로 변경
|
unitSize: number = 1, // 단위 변경 1000 인 경우, 100,000 -> 100 으로 변경
|
||||||
) => {
|
) => {
|
||||||
const rowData = row as Record<string, string | number>;
|
const rowData = row as Record<string, string | number>;
|
||||||
const currentCol1 = String(rowData['COL1'] ?? '');
|
const currentCol1 = String(rowData["COL1"] ?? "");
|
||||||
|
|
||||||
// 이전 값
|
// 이전 값
|
||||||
const prevCol1 = rowIdx > 0 ? data[rowIdx - 1]?.COL1 : null;
|
const prevCol1 = rowIdx > 0 ? data[rowIdx - 1]?.COL1 : null;
|
||||||
@@ -180,38 +220,56 @@ export default function SewageStatusSection() {
|
|||||||
const value = rowData[header.id];
|
const value = rowData[header.id];
|
||||||
|
|
||||||
// COL1 처리 (rowspan 핵심)
|
// COL1 처리 (rowspan 핵심)
|
||||||
if (header.id === 'COL1') {
|
if (header.id === "COL1") {
|
||||||
// 이전과 같으면 렌더링 안함
|
// 이전과 같으면 렌더링 안함
|
||||||
if (currentCol1 === prevCol1) return null;
|
if (currentCol1 === prevCol1) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td key={`${rowIdx}-${header.id}`} rowSpan={getRowSpan()} className={`${tableStyles.td} !font-bold !text-neutral-900`}>
|
<td
|
||||||
|
key={`${rowIdx}-${header.id}`}
|
||||||
|
rowSpan={getRowSpan()}
|
||||||
|
className={`${tableStyles.td} !font-bold !text-neutral-900`}
|
||||||
|
>
|
||||||
{value}
|
{value}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
} else if (header.id === 'COL2') {
|
} else if (header.id === "COL2") {
|
||||||
return <td key={`${rowIdx}-${header.id}`} className={`${tableStyles.td} !font-bold !text-neutral-900`} dangerouslySetInnerHTML={{ __html: Col2Name[value as string] ?? '' }} />;
|
return (
|
||||||
|
<td
|
||||||
|
key={`${rowIdx}-${header.id}`}
|
||||||
|
className={`${tableStyles.td} !font-bold !text-neutral-900`}
|
||||||
|
dangerouslySetInnerHTML={{ __html: Col2Name[value as string] ?? "" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td key={`${rowIdx}-${header.id}`} className={`${tableStyles.td} ${[0, 2, 4].includes(index) ? 'bg-[#EAF5F5]' : ''}`}>
|
<td
|
||||||
{String(rowData['COL2']).indexOf('CNT') > -1
|
key={`${rowIdx}-${header.id}`}
|
||||||
? Number(value).toLocaleString('ko-KR')
|
className={`${tableStyles.td} ${[0, 2, 4].includes(index) ? "bg-[#EAF5F5]" : ""}`}
|
||||||
: String(rowData['COL2']).indexOf('REUSERT') > -1
|
>
|
||||||
|
{(String(rowData["COL2"]).indexOf("CNT") > -1)
|
||||||
|
? Number(value).toLocaleString("ko-KR")
|
||||||
|
: ((String(rowData["COL2"]).indexOf("REUSERT") > -1)
|
||||||
? formatUsage(value as string | number, 1, 1)
|
? formatUsage(value as string | number, 1, 1)
|
||||||
: formatUsage(value as string | number, unitSize, 1)}
|
: formatUsage(value as string | number, unitSize, 1))
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderPbnstUsgCells = (rowIdx: number, row: DmStatusBildTpYearsType | DmStatusUsgYearsType, headers: TableHeaderType[]) => {
|
const renderPbnstUsgCells = (
|
||||||
|
rowIdx: number,
|
||||||
|
row: DmStatusBildTpYearsType | DmStatusUsgYearsType,
|
||||||
|
headers: TableHeaderType[]
|
||||||
|
) => {
|
||||||
const rowData = row as Record<string, string | number>;
|
const rowData = row as Record<string, string | number>;
|
||||||
const isLastInGroup = rowIdx === 8 || rowIdx === 17;
|
const isLastInGroup = rowIdx === 8 || rowIdx === 17;
|
||||||
|
|
||||||
const getType = (colNm: string) => {
|
const getType = (colNm:string) => {
|
||||||
const last3 = colNm.slice(-3); // 끝 3자리
|
const last3 = colNm.slice(-3); // 끝 3자리
|
||||||
const prefix = last3[0]; // S 또는 D
|
const prefix = last3[0]; // S 또는 D
|
||||||
|
|
||||||
if (prefix === 'S') {
|
if (prefix === 'S') {
|
||||||
const num = parseInt(last3.slice(1), 10); // 숫자
|
const num = parseInt(last3.slice(1), 10); // 숫자
|
||||||
@@ -224,117 +282,149 @@ export default function SewageStatusSection() {
|
|||||||
|
|
||||||
return headers.map((header, index) => {
|
return headers.map((header, index) => {
|
||||||
const value = rowData[header.id];
|
const value = rowData[header.id];
|
||||||
const colNm = String(rowData['COL_NM'] ?? '');
|
const colNm = String(rowData["COL_NM"] ?? "");
|
||||||
const type = getType(colNm);
|
const type = getType(colNm);
|
||||||
const isSubItem = type === 'red-item' || type === 'blue-item';
|
const isSubItem = type === "red-item" || type === "blue-item";
|
||||||
|
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
// console.log(header.id, value);
|
// console.log(header.id, value);
|
||||||
return (
|
return (
|
||||||
<td key={`${rowIdx}-${header.id}`} className={`${tableStyles.td} relative`}>
|
<td key={`${rowIdx}-${header.id}`} className={`${tableStyles.td} relative`}>
|
||||||
{isSubItem ? (
|
{isSubItem ? (
|
||||||
<>
|
<>
|
||||||
<div className={`absolute left-[1px] w-[16px] bg-white z-10 top-[-2px] ${isLastInGroup ? 'bottom-[0px]' : 'bottom-[-2px]'}`} />
|
<div
|
||||||
<div className={`absolute left-[16px] w-[4px] z-20 ${type === 'red-item' ? 'bg-[#FF8B8B]' : 'bg-[#4ECBFF]'} top-[-2px] ${isLastInGroup ? 'bottom-[0px]' : 'bottom-[-2px]'}`} />
|
className={`absolute left-[1px] w-[16px] bg-white z-10 top-[-2px] ${
|
||||||
<span className='text-[#4f4f4f] font-bold block w-full text-center relative z-30 translate-x-[8px]'>{value}</span>
|
isLastInGroup ? "bottom-[0px]" : "bottom-[-2px]"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`absolute left-[16px] w-[4px] z-20 ${
|
||||||
|
type === "red-item" ? "bg-[#FF8B8B]" : "bg-[#4ECBFF]"
|
||||||
|
} top-[-2px] ${
|
||||||
|
isLastInGroup ? "bottom-[0px]" : "bottom-[-2px]"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<span className="text-[#4f4f4f] font-bold block w-full text-center relative z-30 translate-x-[8px]">
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className='font-bold text-neutral-900 block w-full text-center'>{value}</span>
|
<span className="font-bold text-neutral-900 block w-full text-center">
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td key={`${rowIdx}-${header.id}`} className={`${tableStyles.td} ${index === 0 ? '!font-bold !text-neutral-900' : ''} ${[0, 2, 4].includes(index) ? 'bg-[#EAF5F5]' : ''}`}>
|
<td
|
||||||
|
key={`${rowIdx}-${header.id}`}
|
||||||
|
className={`${tableStyles.td} ${index === 0 ? "!font-bold !text-neutral-900" : ""} ${[0, 2, 4].includes(index) ? "bg-[#EAF5F5]" : ""}`}
|
||||||
|
>
|
||||||
{formatUsage(value as string | number, 1000, 1)}
|
{formatUsage(value as string | number, 1000, 1)}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderSidoUsgCells = (rowIdx: number, row: DmStatusSidoUsgType, headers: TableHeaderType[]) => {
|
const renderSidoUsgCells = (
|
||||||
|
rowIdx: number,
|
||||||
|
row: DmStatusSidoUsgType,
|
||||||
|
headers: TableHeaderType[]
|
||||||
|
) => {
|
||||||
const rowData = row as Record<string, string | number>;
|
const rowData = row as Record<string, string | number>;
|
||||||
|
|
||||||
return headers.map((header, index) => {
|
return headers.map((header, index) => {
|
||||||
let value = rowData[header.id];
|
let value = rowData[header.id];
|
||||||
value = index === 0 || index === 9 ? value : formatUsage(value as string | number, 1000, 1);
|
value = (index === 0||index ===9) ? value : formatUsage(value as string | number, 1000, 1);
|
||||||
value = value === '0.0' ? '-' : value;
|
value = value === "0.0"? "-" : value;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td key={`${rowIdx}-${header.id}`} className={`${tableStyles.td} ${index === 0 ? '!font-bold !text-neutral-900' : ''} ${[1, 9].includes(index) ? 'bg-[#EAF5F5]' : ''}`}>
|
<td
|
||||||
|
key={`${rowIdx}-${header.id}`}
|
||||||
|
className={`${tableStyles.td} ${index === 0 ? "!font-bold !text-neutral-900" : ""} ${[1, 9].includes(index) ? "bg-[#EAF5F5]" : ""}`}
|
||||||
|
>
|
||||||
{value}
|
{value}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const title = '하수처리수 재이용시설(현황)';
|
const title = "하수처리수 재이용시설(현황)";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='bg-white'>
|
<div className="bg-white">
|
||||||
{/* 상단 타이틀 */}
|
{/* 상단 타이틀 */}
|
||||||
<div className='inline-block'>
|
<div className="inline-block">
|
||||||
<h2 className='relative inline-block px-2 text-[28px] font-extrabold tracking-tight leading-none text-slate-900'>
|
<h2 className="relative inline-block px-2 text-[28px] font-extrabold tracking-tight leading-none text-slate-900">
|
||||||
<span className='relative z-10'>{title}</span>
|
<span className="relative z-10">{title}</span>
|
||||||
<span className='absolute left-0 bottom-[-4px] z-0 h-3 w-full bg-gradient-to-r from-sky-300 to-green-300' />
|
<span className="absolute left-0 bottom-[-4px] z-0 h-3 w-full bg-gradient-to-r from-sky-300 to-green-300" />
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-[70px] pb-[120px]'>
|
<div className="mt-[70px] pb-[120px]">
|
||||||
{/* 1) 연도별 하수처리수 재이용 현황 */}
|
{/* 1) 연도별 하수처리수 재이용 현황 */}
|
||||||
<section>
|
<section>
|
||||||
{/* 인트로 */}
|
{/* 인트로 */}
|
||||||
<SewageIntroSummary />
|
<SewageIntroSummary />
|
||||||
|
|
||||||
<div id='status-treated-year' className='scroll-mt-[120px]'>
|
<div id="status-treated-year" className="scroll-mt-[120px]">
|
||||||
<SectionTitle>연도별 하수처리수 재이용 현황</SectionTitle>
|
<SectionTitle>연도별 하수처리수 재이용 현황</SectionTitle>
|
||||||
<div className='mt-10'>
|
<div className="mt-10">
|
||||||
<div className={tableStyles.container}>
|
<div className={tableStyles.container}>
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col style={{ width: '130px' }} />
|
<col style={{ width: "130px" }} />
|
||||||
<col style={{ width: '180px' }} />
|
<col style={{ width: "180px" }} />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
<th
|
||||||
구분
|
colSpan={2}
|
||||||
</th>
|
className={`${tableStyles.th} ${tableStyles.headerBg}`}
|
||||||
{renderPartHeaders(pbnstYears, headerColors6)}
|
>
|
||||||
</tr>
|
구분
|
||||||
</thead>
|
</th>
|
||||||
<tbody>
|
{renderPartHeaders(pbnstYears, headerColors6)}
|
||||||
{pbnstByLawYears.map((row, idx, arr) => (
|
</tr>
|
||||||
<tr key={idx}>{renderCells(idx, row, arr, pbnstByLawYearsHeader, 1000)}</tr>
|
</thead>
|
||||||
))}
|
<tbody>
|
||||||
</tbody>
|
{pbnstByLawYears.map((row, idx, arr) => (
|
||||||
</table>
|
<tr key={idx}>
|
||||||
</div>
|
{renderCells(idx, row, arr, pbnstByLawYearsHeader, 1000)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SourceText />
|
<SourceText />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
{/* 2) 용도별 하수처리수 재이용 현황 */}
|
{/* 2) 용도별 하수처리수 재이용 현황 */}
|
||||||
<section id='status-treated-usage' className='scroll-mt-[120px]'>
|
<section id="status-treated-usage" className="scroll-mt-[120px]">
|
||||||
<SectionTitle>용도별 하수처리수 재이용 현황</SectionTitle>
|
<SectionTitle>용도별 하수처리수 재이용 현황</SectionTitle>
|
||||||
|
|
||||||
<div className='mt-8'>
|
<div className="mt-8">
|
||||||
<div className={tableStyles.container}>
|
<div className={tableStyles.container}>
|
||||||
<div className='mb-2 text-right text-[13px] text-neutral-600'>(단위: 백만m³/년)</div>
|
<div className="mb-2 text-right text-[13px] text-neutral-600">
|
||||||
|
(단위: 백만m³/일)
|
||||||
|
</div>
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col style={{ width: '180px' }} />
|
<col style={{ width: "180px" }} />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
@@ -344,15 +434,17 @@ export default function SewageStatusSection() {
|
|||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className={`${tableStyles.th} ${tableStyles.headerBg}`}>구분</th>
|
<th className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
||||||
|
구분
|
||||||
|
</th>
|
||||||
{renderPartHeaders(pbnstYears, headerColors6)}
|
{renderPartHeaders(pbnstYears, headerColors6)}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{pbnstByUsgYears.map((row, idx) => (
|
{pbnstByUsgYears.map((row, idx) => (
|
||||||
<tr key={idx} className='bg-white'>
|
<tr key={idx} className="bg-white">
|
||||||
{renderPbnstUsgCells(idx, row, pbnstByUsgYearsHeader)}
|
{renderPbnstUsgCells(idx, row, pbnstByUsgYearsHeader)}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -362,18 +454,18 @@ export default function SewageStatusSection() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
{/* 3) 하수처리시설 설치 및 이용 현황 */}
|
{/* 3) 하수처리시설 설치 및 이용 현황 */}
|
||||||
<section id='status-treated-facility' className='scroll-mt-[120px]'>
|
<section id="status-treated-facility" className="scroll-mt-[120px]">
|
||||||
<SectionTitle>하수처리시설 설치 및 이용 현황</SectionTitle>
|
<SectionTitle>하수처리시설 설치 및 이용 현황</SectionTitle>
|
||||||
|
|
||||||
<div className='mt-8'>
|
<div className="mt-8">
|
||||||
<div className={tableStyles.container}>
|
<div className={tableStyles.container}>
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col style={{ width: '130px' }} />
|
<col style={{ width: "130px" }} />
|
||||||
<col style={{ width: '160px' }} />
|
<col style={{ width: "160px" }} />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
@@ -383,7 +475,10 @@ export default function SewageStatusSection() {
|
|||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
<th
|
||||||
|
colSpan={2}
|
||||||
|
className={`${tableStyles.th} ${tableStyles.headerBg}`}
|
||||||
|
>
|
||||||
구분
|
구분
|
||||||
</th>
|
</th>
|
||||||
{renderPartHeaders(pbnstYears, headerColors6)}
|
{renderPartHeaders(pbnstYears, headerColors6)}
|
||||||
@@ -391,7 +486,9 @@ export default function SewageStatusSection() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{pbnstByCpcYears.map((row, idx, arr) => (
|
{pbnstByCpcYears.map((row, idx, arr) => (
|
||||||
<tr key={idx}>{renderCells(idx, row, arr, pbnstByCpcYearsHeader, 10000)}</tr>
|
<tr key={idx}>
|
||||||
|
{renderCells(idx, row, arr, pbnstByCpcYearsHeader, 10000)}
|
||||||
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -401,19 +498,24 @@ export default function SewageStatusSection() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
{/* 4) 시도별 하수처리수 재이용 현황 */}
|
{/* 4) 시도별 하수처리수 재이용 현황 */}
|
||||||
<section id='status-treated-region' className='scroll-mt-[120px]'>
|
<section
|
||||||
|
id="status-treated-region"
|
||||||
|
className="scroll-mt-[120px]"
|
||||||
|
>
|
||||||
<SectionTitle>시도별 하수처리수 재이용 현황</SectionTitle>
|
<SectionTitle>시도별 하수처리수 재이용 현황</SectionTitle>
|
||||||
|
|
||||||
<div className='mt-8'>
|
<div className="mt-8">
|
||||||
<div className={tableStyles.container}>
|
<div className={tableStyles.container}>
|
||||||
<div className='mb-2 text-right text-[13px] text-neutral-600'>(단위: 백만m³/년)</div>
|
<div className="mb-2 text-right text-[13px] text-neutral-600">
|
||||||
|
(단위: 백만m³/일)
|
||||||
|
</div>
|
||||||
|
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col style={{ width: '160px' }} />
|
<col style={{ width: "160px" }} />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
@@ -427,53 +529,54 @@ export default function SewageStatusSection() {
|
|||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}> 시도 </th>
|
||||||
{' '}
|
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`} >
|
||||||
시도{' '}
|
연간<br />하수<br />처리량
|
||||||
</th>
|
</th>
|
||||||
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
<th colSpan={7} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`} >
|
||||||
연간
|
|
||||||
<br />
|
|
||||||
하수
|
|
||||||
<br />
|
|
||||||
처리량
|
|
||||||
</th>
|
|
||||||
<th colSpan={7} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
|
|
||||||
재이용현황
|
재이용현황
|
||||||
</th>
|
</th>
|
||||||
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`} >
|
||||||
재이용률
|
재이용률<br />(%)
|
||||||
<br />
|
|
||||||
(%)
|
|
||||||
</th>
|
</th>
|
||||||
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`} >
|
||||||
비고
|
비고
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} >
|
||||||
계
|
계
|
||||||
</th>
|
</th>
|
||||||
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} >
|
||||||
장내
|
장내
|
||||||
</th>
|
</th>
|
||||||
<th colSpan={5} className={`${tableStyles.th} ${tableStyles.headerBg} py-1`}>
|
<th colSpan={5} className={`${tableStyles.th} ${tableStyles.headerBg} py-1`} >
|
||||||
장외
|
장외
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th className={`${tableStyles.th} bg-[#01ACE5] text-white py-2 text-[16px]`}>소계</th>
|
<th className={`${tableStyles.th} bg-[#01ACE5] text-white py-2 text-[16px]`}>
|
||||||
<th className={`${tableStyles.th} bg-[#1FB1C2] text-white py-2 text-[13px]`}>하천유지용수</th>
|
소계
|
||||||
<th className={`${tableStyles.th} bg-[#3EB59F] text-white py-2 text-[16px]`}>공업용수</th>
|
</th>
|
||||||
<th className={`${tableStyles.th} bg-[#5DB97C] text-white py-2 text-[16px]`}>농업용수</th>
|
<th className={`${tableStyles.th} bg-[#1FB1C2] text-white py-2 text-[13px]`}>
|
||||||
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white py-2 text-[16px]`}>기타용수</th>
|
하천유지용수
|
||||||
|
</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#3EB59F] text-white py-2 text-[16px]`}>
|
||||||
|
공업용수
|
||||||
|
</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#5DB97C] text-white py-2 text-[16px]`}>
|
||||||
|
농업용수
|
||||||
|
</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white py-2 text-[16px]`}>
|
||||||
|
기타용수
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{pbnstBySido.map((row, idx) => (
|
{pbnstBySido.map((row, idx) => (
|
||||||
<tr key={idx} className='bg-white'>
|
<tr key={idx} className="bg-white">
|
||||||
{renderSidoUsgCells(idx, row, pbnstBySidoHeader)}
|
{renderSidoUsgCells(idx, row, pbnstBySidoHeader)}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,36 +1,60 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from "react";
|
||||||
import SubPageHeader from '@/components/SubPageHeader';
|
import SubPageHeader from "@/components/SubPageHeader";
|
||||||
import { useDmStore } from '@/app/store';
|
import { useDmStore } from "@/app/store";
|
||||||
import { formatUsage } from '@/app/common/transValueFormat';
|
import { formatUsage } from "@/app/common/transValueFormat";
|
||||||
import type { DmStatusBildTpYearsType, DmStatusUsgBildTpType, DmStatusUsgYearsType, TableHeaderType } from '@/app/store/dm-status/type';
|
import type {
|
||||||
|
DmStatusBildTpYearsType,
|
||||||
|
DmStatusUsgBildTpType,
|
||||||
|
DmStatusUsgYearsType,
|
||||||
|
TableHeaderType,
|
||||||
|
} from "@/app/store/dm-status/type";
|
||||||
|
|
||||||
// 공통 섹션 타이틀 컴포넌트
|
// 공통 섹션 타이틀 컴포넌트
|
||||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center gap-3'>
|
<div className="flex items-center gap-3">
|
||||||
<span className='h-5 w-[3px] bg-sky-500' />
|
<span className="h-5 w-[3px] bg-sky-500" />
|
||||||
<h3 className='text-[24px] font-bold text-neutral-900'>{children}</h3>
|
<h3 className="text-[24px] font-bold text-neutral-900">{children}</h3>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 구분선
|
// 구분선
|
||||||
function Hr({ className = '' }: { className?: string }) {
|
function Hr({ className = "" }: { className?: string }) {
|
||||||
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const tableStyles = {
|
const tableStyles = {
|
||||||
container: 'w-[1000px] mx-auto overflow-hidden',
|
container: "w-[1000px] mx-auto overflow-hidden",
|
||||||
table: 'w-full table-fixed border-collapse text-center text-[16px]',
|
table: "w-full table-fixed border-collapse text-center text-[16px]",
|
||||||
th: 'border border-[#858F98] py-3 font-bold text-neutral-800',
|
th: "border border-[#858F98] py-3 font-bold text-neutral-800",
|
||||||
td: 'border border-[#858F98] py-3 text-neutral-700 font-medium break-keep',
|
td: "border border-[#858F98] py-3 text-neutral-700 font-medium break-keep",
|
||||||
headerBg: 'bg-[#E3E7EA]',
|
headerBg: "bg-[#E3E7EA]",
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerColors6 = ['bg-[#01ACE5]', 'bg-[#17AFC7]', 'bg-[#2CB2A9]', 'bg-[#42B58B]', 'bg-[#57B86D]', 'bg-[#81C784]'];
|
const headerColors6 = [
|
||||||
const headerColors8 = ['bg-[#01ACE5]', 'bg-[#10AED0]', 'bg-[#20B0BA]', 'bg-[#2FB2A5]', 'bg-[#3FB58F]', 'bg-[#4EB77A]', 'bg-[#5EB964]', 'bg-[#6DBB4F]'];
|
"bg-[#01ACE5]",
|
||||||
|
"bg-[#17AFC7]",
|
||||||
|
"bg-[#2CB2A9]",
|
||||||
|
"bg-[#42B58B]",
|
||||||
|
"bg-[#57B86D]",
|
||||||
|
"bg-[#81C784]",
|
||||||
|
];
|
||||||
|
const headerColors8 = [
|
||||||
|
"bg-[#01ACE5]",
|
||||||
|
"bg-[#10AED0]",
|
||||||
|
"bg-[#20B0BA]",
|
||||||
|
"bg-[#2FB2A5]",
|
||||||
|
"bg-[#3FB58F]",
|
||||||
|
"bg-[#4EB77A]",
|
||||||
|
"bg-[#5EB964]",
|
||||||
|
"bg-[#6DBB4F]",
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
export default function RainwaterStatusPage() {
|
export default function RainwaterStatusPage() {
|
||||||
const getRainyByBildTpYears = useDmStore((state) => state.getRainyByBildTpYears);
|
const getRainyByBildTpYears = useDmStore((state) => state.getRainyByBildTpYears);
|
||||||
@@ -44,11 +68,19 @@ export default function RainwaterStatusPage() {
|
|||||||
const rainyByUsgBildTp = useDmStore((state) => state.rainy.rainyByUsgBildTp);
|
const rainyByUsgBildTp = useDmStore((state) => state.rainy.rainyByUsgBildTp);
|
||||||
const rainyByUsgYearsHeader = useDmStore((state) => state.rainy.rainyByUsgYearsHeader);
|
const rainyByUsgYearsHeader = useDmStore((state) => state.rainy.rainyByUsgYearsHeader);
|
||||||
const rainyByUsgYears = useDmStore((state) => state.rainy.rainyByUsgYears);
|
const rainyByUsgYears = useDmStore((state) => state.rainy.rainyByUsgYears);
|
||||||
const partBildTpHeader = useMemo(() => rainyByUsgBildTpHeader.slice(3).map((header) => header.title), [rainyByUsgBildTpHeader]);
|
const partBildTpHeader = useMemo(
|
||||||
|
() => rainyByUsgBildTpHeader.slice(3).map((header) => header.title),
|
||||||
|
[rainyByUsgBildTpHeader]
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
// 출처 텍스트
|
// 출처 텍스트
|
||||||
function SourceText() {
|
function SourceText() {
|
||||||
return <p className='mt-7 text-center text-[13px] text-neutral-500'>자료: 환경부(2024), 「{selectYear} 하수도 통계」</p>;
|
return (
|
||||||
|
<p className="mt-7 text-center text-[13px] text-neutral-500">
|
||||||
|
자료: 환경부(2024), 「{selectYear} 하수도 통계」
|
||||||
|
</p>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -59,22 +91,32 @@ export default function RainwaterStatusPage() {
|
|||||||
|
|
||||||
const renderPartHeaders = (headers: string[], headerColors: string[]) => {
|
const renderPartHeaders = (headers: string[], headerColors: string[]) => {
|
||||||
return headers.map((header, index) => (
|
return headers.map((header, index) => (
|
||||||
<th key={header} className={`${tableStyles.th} ${headerColors[index] ?? 'bg-[#81C784]'} text-white`} style={{ width: '140px' }}>
|
<th
|
||||||
|
key={header}
|
||||||
|
className={`${tableStyles.th} ${headerColors[index] ?? "bg-[#81C784]"} text-white`}
|
||||||
|
style={{ width: "140px" }}
|
||||||
|
>
|
||||||
{header}
|
{header}
|
||||||
</th>
|
</th>
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderBuildingUsageCells = (row: DmStatusBildTpYearsType | DmStatusUsgYearsType | DmStatusUsgYearsType, headers: TableHeaderType[]) => {
|
const renderBuildingUsageCells = (
|
||||||
|
row: DmStatusBildTpYearsType | DmStatusUsgYearsType | DmStatusUsgYearsType,
|
||||||
|
headers: TableHeaderType[]
|
||||||
|
) => {
|
||||||
const rowData = row as Record<string, string | number>;
|
const rowData = row as Record<string, string | number>;
|
||||||
const firstHeaderId = headers[0]?.id;
|
const firstHeaderId = headers[0]?.id;
|
||||||
const rowKey = firstHeaderId ? String(rowData[firstHeaderId] ?? '') : '';
|
const rowKey = firstHeaderId ? String(rowData[firstHeaderId] ?? "") : "";
|
||||||
|
|
||||||
return headers.map((header, index) => {
|
return headers.map((header, index) => {
|
||||||
const value = rowData[header.id];
|
const value = rowData[header.id];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td key={`${rowKey}-${header.id}`} className={`${tableStyles.td} ${index === 0 ? '!font-bold !text-neutral-900' : ''} ${[1, 3, 5].includes(index) ? 'bg-[#EAF5F5]' : ''}`}>
|
<td
|
||||||
|
key={`${rowKey}-${header.id}`}
|
||||||
|
className={`${tableStyles.td} ${index === 0 ? "!font-bold !text-neutral-900" : ""} ${[1, 3, 5].includes(index) ? "bg-[#EAF5F5]" : ""}`}
|
||||||
|
>
|
||||||
{index === 0 ? value : formatUsage(value as string | number, 1000, 1)}
|
{index === 0 ? value : formatUsage(value as string | number, 1000, 1)}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
@@ -84,49 +126,63 @@ export default function RainwaterStatusPage() {
|
|||||||
const renderRainyByUsgBildTpCells = (row: DmStatusUsgBildTpType) => {
|
const renderRainyByUsgBildTpCells = (row: DmStatusUsgBildTpType) => {
|
||||||
return rainyByUsgBildTpHeader.map((header, index) => {
|
return rainyByUsgBildTpHeader.map((header, index) => {
|
||||||
const value = row[header.id as keyof DmStatusUsgBildTpType];
|
const value = row[header.id as keyof DmStatusUsgBildTpType];
|
||||||
const displayValue = index === 0 ? value : header.id === 'CNT' ? Number(value).toLocaleString('ko-KR') : formatUsage(value as string | number, 1000, 1);
|
const displayValue =
|
||||||
|
index === 0
|
||||||
|
? value
|
||||||
|
: header.id === "CNT"
|
||||||
|
? Number(value).toLocaleString("ko-KR")
|
||||||
|
: formatUsage(value as string | number, 1000, 1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td key={`${row.USG_NM}-${header.id}`} className={`${tableStyles.td} ${index === 0 ? '!font-bold !text-neutral-900' : ''} ${[1, 2].includes(index) ? 'bg-[#EAF5F5]' : ''}`}>
|
<td
|
||||||
{displayValue == 0.0 ? '-' : displayValue}
|
key={`${row.USG_NM}-${header.id}`}
|
||||||
|
className={`${tableStyles.td} ${index === 0 ? "!font-bold !text-neutral-900" : ""} ${[1, 2].includes(index) ? "bg-[#EAF5F5]" : ""}`}
|
||||||
|
>
|
||||||
|
{displayValue == 0.0 ? "-" : displayValue}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='bg-white'>
|
<div className="bg-white">
|
||||||
<SubPageHeader
|
<SubPageHeader
|
||||||
title='빗물이용시설(현황)'
|
title="빗물이용시설(현황)"
|
||||||
crumbs={[{ label: 'Home', href: '/reuse' }, { label: '물 재이용 현황', href: '/reuse/cases/rain' }, { label: '빗물이용시설(현황)' }]}
|
crumbs={[
|
||||||
|
{ label: "Home", href: "/reuse" },
|
||||||
|
{ label: "물 재이용 현황", href: "/reuse/cases/rain" },
|
||||||
|
{ label: "빗물이용시설(현황)" },
|
||||||
|
]}
|
||||||
hideFooterLine
|
hideFooterLine
|
||||||
hideGuideText
|
hideGuideText
|
||||||
wrapClassName='!pb-0'
|
wrapClassName="!pb-0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='pr-[100px]'>
|
<div className="pr-[100px]">
|
||||||
<Hr className='my-7' />
|
<Hr className="my-7" />
|
||||||
|
|
||||||
{/* 1) 빗물이용시설의 건축물별 빗물이용량 현황 */}
|
{/* 1) 빗물이용시설의 건축물별 빗물이용량 현황 */}
|
||||||
<section id='status-rain-building' className='mt-10 scroll-mt-[120px]'>
|
<section id="status-rain-building" className="mt-10 scroll-mt-[120px]">
|
||||||
<SectionTitle>빗물이용시설의 건축물별 빗물이용량 현황</SectionTitle>
|
<SectionTitle>빗물이용시설의 건축물별 빗물이용량 현황</SectionTitle>
|
||||||
<div className='mt-10'>
|
<div className="mt-10">
|
||||||
<div className={tableStyles.container}>
|
<div className={tableStyles.container}>
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '160px' }}>
|
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: "160px" }}>
|
||||||
구분
|
구분
|
||||||
</th>
|
</th>
|
||||||
<th colSpan={rainyYears.length || 6} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
|
<th colSpan={rainyYears.length || 6} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
|
||||||
빗물이용량 (천m³/년)
|
빗물이용량 (천m³/년)
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>{renderPartHeaders(rainyYears, headerColors6)}</tr>
|
<tr>
|
||||||
|
{renderPartHeaders(rainyYears, headerColors6)}
|
||||||
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{rainyByBildTpYears.map((row, idx) => (
|
{rainyByBildTpYears.map((row, idx) => (
|
||||||
<tr key={idx} className='bg-white'>
|
<tr key={idx} className="bg-white">
|
||||||
{renderBuildingUsageCells(row, rainyByBildTpYearsHeader)}
|
{renderBuildingUsageCells(row, rainyByBildTpYearsHeader)}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@@ -137,38 +193,28 @@ export default function RainwaterStatusPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
{/* 2) 빗물이용시설의 용도별 분포 (2023년 기준) */}
|
{/* 2) 빗물이용시설의 용도별 분포 (2023년 기준) */}
|
||||||
<section id='status-rain-purpose-distribution' className='scroll-mt-[120px]'>
|
<section id="status-rain-purpose-distribution" className="scroll-mt-[120px]">
|
||||||
<SectionTitle>빗물이용시설의 용도별 분포 ({selectYear}년 기준)</SectionTitle>
|
<SectionTitle>빗물이용시설의 용도별 분포 ({selectYear}년 기준)</SectionTitle>
|
||||||
<div className='mt-8'>
|
<div className="mt-8">
|
||||||
<div className={tableStyles.container}>
|
<div className={tableStyles.container}>
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '160px' }}>
|
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: "160px" }}>용도</th>
|
||||||
용도
|
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: "84px" }}>개소수<br />(개소)</th>
|
||||||
</th>
|
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: "84px" }}>저류용량<br />(천m³)</th>
|
||||||
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '84px' }}>
|
<th colSpan={8} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>빗물이용량 (천m³/년)</th>
|
||||||
개소수
|
</tr>
|
||||||
<br />
|
<tr>
|
||||||
(개소)
|
{renderPartHeaders(partBildTpHeader, headerColors8)}
|
||||||
</th>
|
|
||||||
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '84px' }}>
|
|
||||||
저류조용량
|
|
||||||
<br />
|
|
||||||
(천m³)
|
|
||||||
</th>
|
|
||||||
<th colSpan={8} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
|
|
||||||
빗물이용량 (천m³/년)
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>{renderPartHeaders(partBildTpHeader, headerColors8)}</tr>
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{rainyByUsgBildTp.map((row, idx) => (
|
{rainyByUsgBildTp.map((row, idx) => (
|
||||||
<tr key={`${row.USG_NM}-${idx}`} className='bg-white'>
|
<tr key={`${row.USG_NM}-${idx}`} className="bg-white">
|
||||||
{renderRainyByUsgBildTpCells(row)}
|
{renderRainyByUsgBildTpCells(row)}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@@ -179,28 +225,26 @@ export default function RainwaterStatusPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
{/* 3) 빗물이용시설의 용도별 빗물이용량 현황 */}
|
{/* 3) 빗물이용시설의 용도별 빗물이용량 현황 */}
|
||||||
<section id='status-rain-purpose-usage' className='pb-[200px] scroll-mt-[120px]'>
|
<section id="status-rain-purpose-usage" className="pb-[200px] scroll-mt-[120px]">
|
||||||
<SectionTitle>빗물이용시설의 용도별 빗물이용량 현황</SectionTitle>
|
<SectionTitle>빗물이용시설의 용도별 빗물이용량 현황</SectionTitle>
|
||||||
<div className='mt-8'>
|
<div className="mt-8">
|
||||||
<div className={tableStyles.container}>
|
<div className={tableStyles.container}>
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '160px' }}>
|
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: "160px" }}>용도</th>
|
||||||
용도
|
<th colSpan={6} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>빗물이용량 (천m³/년)</th>
|
||||||
</th>
|
</tr>
|
||||||
<th colSpan={6} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
|
<tr>
|
||||||
빗물이용량 (천m³/년)
|
{renderPartHeaders(rainyYears, headerColors6)}
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>{renderPartHeaders(rainyYears, headerColors6)}</tr>
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{rainyByUsgYears.map((row, idx) => (
|
{rainyByUsgYears.map((row, idx) => (
|
||||||
<tr key={idx} className='bg-white'>
|
<tr key={idx} className="bg-white">
|
||||||
{renderBuildingUsageCells(row, rainyByUsgYearsHeader)}
|
{renderBuildingUsageCells(row, rainyByUsgYearsHeader)}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,26 +1,30 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import SubPageHeader from '@/components/SubPageHeader';
|
import SubPageHeader from "@/components/SubPageHeader";
|
||||||
|
|
||||||
// 공통 섹션 타이틀 컴포넌트
|
// 공통 섹션 타이틀 컴포넌트
|
||||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center gap-3'>
|
<div className="flex items-center gap-3">
|
||||||
<span className='h-5 w-[3px] bg-sky-500' />
|
<span className="h-5 w-[3px] bg-sky-500" />
|
||||||
<h3 className='text-[24px] font-bold text-neutral-900'>{children}</h3>
|
<h3 className="text-[24px] font-bold text-neutral-900">{children}</h3>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 구분선
|
// 구분선
|
||||||
function Hr({ className = '' }: { className?: string }) {
|
function Hr({ className = "" }: { className?: string }) {
|
||||||
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 출처 텍스트
|
// 출처 텍스트
|
||||||
function SourceText() {
|
function SourceText() {
|
||||||
return <p className='mt-7 text-center text-[13px] text-neutral-500'>자료: 환경부(2024), 「2023 하수도 통계」</p>;
|
return (
|
||||||
|
<p className="mt-7 text-center text-[13px] text-neutral-500">
|
||||||
|
자료: 환경부(2024), 「2023 하수도 통계」
|
||||||
|
</p>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,11 +33,11 @@ function SourceText() {
|
|||||||
* - 폰트 사이즈: 16px로 수정
|
* - 폰트 사이즈: 16px로 수정
|
||||||
*/
|
*/
|
||||||
const tableStyles = {
|
const tableStyles = {
|
||||||
container: 'w-[1000px] mx-auto overflow-hidden',
|
container: "w-[1000px] mx-auto overflow-hidden",
|
||||||
table: 'w-full table-fixed border-collapse text-center text-[16px]',
|
table: "w-full table-fixed border-collapse text-center text-[16px]",
|
||||||
th: 'border border-[#858F98] py-3 font-bold text-neutral-800',
|
th: "border border-[#858F98] py-3 font-bold text-neutral-800",
|
||||||
td: 'border border-[#858F98] py-3 text-neutral-700 font-medium break-keep',
|
td: "border border-[#858F98] py-3 text-neutral-700 font-medium break-keep",
|
||||||
headerBg: 'bg-[#E3E7EA]',
|
headerBg: "bg-[#E3E7EA]",
|
||||||
};
|
};
|
||||||
|
|
||||||
// export default function TreatedWaterPage() {
|
// export default function TreatedWaterPage() {
|
||||||
@@ -47,382 +51,368 @@ const tableStyles = {
|
|||||||
|
|
||||||
export default function TreatedStatusPage() {
|
export default function TreatedStatusPage() {
|
||||||
return (
|
return (
|
||||||
<div className='relative bg-white'>
|
<div className="relative bg-white">
|
||||||
{/* <BusinessSubNav /> */}
|
{/* <BusinessSubNav /> */}
|
||||||
|
|
||||||
<SubPageHeader
|
<SubPageHeader
|
||||||
title='하수처리수 재이용시설(현황)'
|
title="하수처리수 재이용시설(현황)"
|
||||||
crumbs={[{ label: 'Home', href: '/main' }, { label: '물 재이용 현황', href: '/status/rain' }, { label: '하수처리수 재이용시설(현황)' }]}
|
crumbs={[
|
||||||
|
{ label: "Home", href: "/main" },
|
||||||
|
{ label: "물 재이용 현황", href: "/status/rain" },
|
||||||
|
{ label: "하수처리수 재이용시설(현황)" },
|
||||||
|
]}
|
||||||
hideFooterLine
|
hideFooterLine
|
||||||
hideGuideText
|
hideGuideText
|
||||||
wrapClassName='!pb-0'
|
wrapClassName="!pb-0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='pr-0'>
|
<div className="pr-0">
|
||||||
<Hr className='my-7' />
|
<Hr className="my-7" />
|
||||||
|
|
||||||
{/* 1) 연도별 하수처리수 재이용 현황 */}
|
{/* 1) 연도별 하수처리수 재이용 현황 */}
|
||||||
<section className='mt-10'>
|
<section className="mt-10">
|
||||||
<SectionTitle>연도별 하수처리수 재이용 현황</SectionTitle>
|
<SectionTitle>연도별 하수처리수 재이용 현황</SectionTitle>
|
||||||
<div className='mt-10'>
|
<div className="mt-10">
|
||||||
<div className={tableStyles.container}>
|
<div className={tableStyles.container}>
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col style={{ width: '130px' }} />
|
<col style={{ width: '130px' }} />
|
||||||
<col style={{ width: '180px' }} />
|
<col style={{ width: '180px' }} />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
<th colSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>구분</th>
|
||||||
구분
|
<th className={`${tableStyles.th} bg-[#01ACE5] text-white`}>2018</th>
|
||||||
</th>
|
<th className={`${tableStyles.th} bg-[#14AFCD] text-white`}>2019</th>
|
||||||
<th className={`${tableStyles.th} bg-[#01ACE5] text-white`}>2018</th>
|
<th className={`${tableStyles.th} bg-[#28B2B4] text-white`}>2020</th>
|
||||||
<th className={`${tableStyles.th} bg-[#14AFCD] text-white`}>2019</th>
|
<th className={`${tableStyles.th} bg-[#3CB59A] text-white`}>2021</th>
|
||||||
<th className={`${tableStyles.th} bg-[#28B2B4] text-white`}>2020</th>
|
<th className={`${tableStyles.th} bg-[#50B880] text-white`}>2022</th>
|
||||||
<th className={`${tableStyles.th} bg-[#3CB59A] text-white`}>2021</th>
|
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white`}>2023</th>
|
||||||
<th className={`${tableStyles.th} bg-[#50B880] text-white`}>2022</th>
|
</tr>
|
||||||
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white`}>2023</th>
|
</thead>
|
||||||
</tr>
|
<tbody>
|
||||||
</thead>
|
{[
|
||||||
<tbody>
|
{
|
||||||
{[
|
group: "전체",
|
||||||
{
|
rows: [
|
||||||
group: '전체',
|
["개소수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>", "671", "681", "698", "704", "714", "717"],
|
||||||
rows: [
|
["하수처리량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>", "7,163.7", "7,140.4", "7,375.6", "7,335.8", "7,378.9", "7,537.2"],
|
||||||
["개소수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>", '671', '681', '698', '704', '714', '717'],
|
["재이용량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>", "1,112.7", "1,148.7", "1,148.7", "1,127.0", "1,136.8", "1,137.5"],
|
||||||
["하수처리량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>", '7,163.7', '7,140.4', '7,375.6', '7,335.8', '7,378.9', '7,537.2'],
|
["재이용률 <span class='text-[13px] font-normal text-[#767676]'>(%)</span>", "17.4", "16.5", "15.5", "14.1", "14.3", "14.4"],
|
||||||
["재이용량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>", '1,112.7', '1,148.7', '1,148.7', '1,127.0', '1,136.8', '1,137.5'],
|
]
|
||||||
["재이용률 <span class='text-[13px] font-normal text-[#767676]'>(%)</span>", '17.4', '16.5', '15.5', '14.1', '14.3', '14.4'],
|
},
|
||||||
],
|
{
|
||||||
},
|
group: "법적 대상",
|
||||||
{
|
rows: [
|
||||||
group: '법적 대상',
|
["개소수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>", "68", "82", "94", "97", "101", "118"],
|
||||||
rows: [
|
["하수처리량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>", "761.2", "839.0", "935.3", "1,158.7", "1,220.6", "1,473.6"],
|
||||||
["개소수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>", '68', '82', '94', '97', '101', '118'],
|
["재이용량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>", "162.2", "190.6", "218.5", "244.6", "247.2", "324.9"],
|
||||||
["하수처리량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>", '761.2', '839.0', '935.3', '1,158.7', '1,220.6', '1,473.6'],
|
["재이용률 <span class='text-[13px] font-normal text-[#767676]'>(%)</span>", "24.7", "25.4", "27.8", "25.7", "24.2", "24.1"],
|
||||||
["재이용량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>", '162.2', '190.6', '218.5', '244.6', '247.2', '324.9'],
|
]
|
||||||
["재이용률 <span class='text-[13px] font-normal text-[#767676]'>(%)</span>", '24.7', '25.4', '27.8', '25.7', '24.2', '24.1'],
|
},
|
||||||
],
|
{
|
||||||
},
|
group: "법적 미대상",
|
||||||
{
|
rows: [
|
||||||
group: '법적 미대상',
|
["개소수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>", "603", "599", "604", "607", "613", "599"],
|
||||||
rows: [
|
["하수처리량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>", "6,402.5", "6,301.4", "6,440.3", "6,177.1", "6,158.3", "6,063.5"],
|
||||||
["개소수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>", '603', '599', '604', '607', '613', '599'],
|
["재이용량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>", "950.6", "958.1", "926.2", "882.4", "889.5", "812.7"],
|
||||||
["하수처리량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>", '6,402.5', '6,301.4', '6,440.3', '6,177.1', '6,158.3', '6,063.5'],
|
["재이용률 <span class='text-[13px] font-normal text-[#767676]'>(%)</span>", "16.5", "15.3", "13.6", "12.2", "12.6", "12.5"],
|
||||||
["재이용량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>", '950.6', '958.1', '926.2', '882.4', '889.5', '812.7'],
|
]
|
||||||
["재이용률 <span class='text-[13px] font-normal text-[#767676]'>(%)</span>", '16.5', '15.3', '13.6', '12.2', '12.6', '12.5'],
|
}
|
||||||
],
|
].map((section, sIdx) => (
|
||||||
},
|
<React.Fragment key={sIdx}>
|
||||||
].map((section, sIdx) => (
|
{section.rows.map((row, rIdx) => (
|
||||||
<React.Fragment key={sIdx}>
|
<tr key={`${sIdx}-${rIdx}`} className="bg-white">
|
||||||
{section.rows.map((row, rIdx) => (
|
{rIdx === 0 && (
|
||||||
<tr key={`${sIdx}-${rIdx}`} className='bg-white'>
|
<td
|
||||||
{rIdx === 0 && (
|
rowSpan={section.rows.length}
|
||||||
<td rowSpan={section.rows.length} className={`${tableStyles.td} ${tableStyles.headerBg} !text-[18px] !font-bold !text-neutral-900`}>
|
className={`${tableStyles.td} ${tableStyles.headerBg} !text-[18px] !font-bold !text-neutral-900`}
|
||||||
{section.group}
|
>
|
||||||
</td>
|
{section.group}
|
||||||
)}
|
|
||||||
{row.map((val, i) => {
|
|
||||||
const isFirstCol = i === 0;
|
|
||||||
const isHighlightedCol = [1, 3, 5].includes(i);
|
|
||||||
return (
|
|
||||||
<td
|
|
||||||
key={i}
|
|
||||||
className={`${tableStyles.td} ${isFirstCol ? '!font-bold !text-neutral-900' : ''} ${isHighlightedCol ? 'bg-[#EAF5F5]' : ''}`}
|
|
||||||
dangerouslySetInnerHTML={{ __html: val }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<SourceText />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<Hr className='my-11' />
|
|
||||||
|
|
||||||
{/* 2) 용도별 하수처리수 재이용 현황 */}
|
|
||||||
<section>
|
|
||||||
<SectionTitle>용도별 하수처리수 재이용 현황</SectionTitle>
|
|
||||||
<div className='mt-8'>
|
|
||||||
<div className={tableStyles.container}>
|
|
||||||
<table className={tableStyles.table}>
|
|
||||||
<colgroup>
|
|
||||||
<col style={{ width: '180px' }} />
|
|
||||||
<col />
|
|
||||||
<col />
|
|
||||||
<col />
|
|
||||||
<col />
|
|
||||||
<col />
|
|
||||||
<col />
|
|
||||||
</colgroup>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className={`${tableStyles.th} ${tableStyles.headerBg}`}>구분</th>
|
|
||||||
<th className={`${tableStyles.th} bg-[#01ACE5] text-white`}>2018</th>
|
|
||||||
<th className={`${tableStyles.th} bg-[#14AFCD] text-white`}>2019</th>
|
|
||||||
<th className={`${tableStyles.th} bg-[#28B2B4] text-white`}>2020</th>
|
|
||||||
<th className={`${tableStyles.th} bg-[#3CB59A] text-white`}>2021</th>
|
|
||||||
<th className={`${tableStyles.th} bg-[#50B880] text-white`}>2022</th>
|
|
||||||
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white`}>2023</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{[
|
|
||||||
{ label: '총 재이용량', type: 'total', values: ['1,112.7', '1,148.7', '1,144.7', '1,127.0', '1,136.8', '1,137.5'] },
|
|
||||||
{ label: '장내 재이용', type: 'category', values: ['520.6', '519.8', '513.9', '520.3', '502.2', '490.0'] },
|
|
||||||
{ label: '세척수', type: 'red-item', values: ['177.9', '174.4', '169.4', '180.9', '196.8', '191.2'] },
|
|
||||||
{ label: '냉각수', type: 'red-item', values: ['45.5', '46.8', '50.4', '40.6', '38.3', '41.7'] },
|
|
||||||
{ label: '청소수', type: 'red-item', values: ['17.6', '21.2', '21.2', '21.7', '20.8', '20.5'] },
|
|
||||||
{ label: '조경용수', type: 'red-item', values: ['0.1', '0.1', '-', '-', '-', '-'] },
|
|
||||||
{ label: '회생용수', type: 'red-item', values: ['15.8', '16.7', '17.8', '15.7', '14.4', '14.0'] },
|
|
||||||
{ label: '장내급수도', type: 'red-item', values: ['2.4', '-', '-', '-', '-', '-'] },
|
|
||||||
{ label: '장내기타용수', type: 'red-item', values: ['261.3', '260.6', '255.0', '261.1', '231.8', '222.6'] },
|
|
||||||
{ label: '장외 재이용', type: 'category', values: ['592.1', '628.9', '630.8', '606.7', '634.6', '647.5'] },
|
|
||||||
{ label: '청소·화장실용수', type: 'blue-item', values: ['0.6', '0.4', '0.4', '0.4', '0.4', '0.4'] },
|
|
||||||
{ label: '세척·살수용수', type: 'blue-item', values: ['5.6', '9.9', '2.9', '2.1', '3.0', '3.6'] },
|
|
||||||
{ label: '조경용수', type: 'blue-item', values: ['10.3', '16.4', '5.9', '4.5', '8.9', '9.5'] },
|
|
||||||
{ label: '친수용수', type: 'blue-item', values: ['7.8', '3.1', '3.1', '3.0', '10.9', '10.5'] },
|
|
||||||
{ label: '하천유지용수', type: 'blue-item', values: ['480.5', '482.5', '496.2', '442.7', '471.7', '484.0'] },
|
|
||||||
{ label: '농업용수', type: 'blue-item', values: ['12.2', '11.9', '13.4', '10.7', '10.6', '11.4'] },
|
|
||||||
{ label: '지하수 충전', type: 'blue-item', values: ['0.0', '0.0', '0.0', '13.4', '0.0', '0.0'] },
|
|
||||||
{ label: '공업용수', type: 'blue-item', values: ['75.2', '104.6', '108.9', '129.9', '129.0', '128.1'] },
|
|
||||||
].map((row, idx) => {
|
|
||||||
const isSubItem = row.type === 'red-item' || row.type === 'blue-item';
|
|
||||||
const isLastInGroup = idx === 8 || idx === 17;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr key={idx} className='bg-white'>
|
|
||||||
<td className={`${tableStyles.td} relative`}>
|
|
||||||
{isSubItem ? (
|
|
||||||
<>
|
|
||||||
<div className={`absolute left-[1px] w-[16px] bg-white z-10 top-[-2px] ${isLastInGroup ? 'bottom-[0px]' : 'bottom-[-2px]'}`} />
|
|
||||||
<div
|
|
||||||
className={`absolute left-[16px] w-[4px] z-20 ${row.type === 'red-item' ? 'bg-[#FF8B8B]' : 'bg-[#4ECBFF]'} top-[-2px] ${isLastInGroup ? 'bottom-[0px]' : 'bottom-[-2px]'}`}
|
|
||||||
/>
|
|
||||||
<span className='text-[#4f4f4f] font-bold block w-full text-center relative z-30 translate-x-[8px]'>{row.label}</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span className='font-bold text-neutral-900 block w-full text-center'>{row.label}</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
{row.values.map((val, i) => (
|
|
||||||
<td key={i} className={`${tableStyles.td} ${[0, 2, 4].includes(i) ? 'bg-[#EAF5F5]' : ''}`}>
|
|
||||||
{val}
|
|
||||||
</td>
|
</td>
|
||||||
))}
|
)}
|
||||||
|
{row.map((val, i) => {
|
||||||
|
const isFirstCol = i === 0;
|
||||||
|
const isHighlightedCol = [1, 3, 5].includes(i);
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
key={i}
|
||||||
|
className={`${tableStyles.td} ${isFirstCol ? "!font-bold !text-neutral-900" : ""} ${isHighlightedCol ? "bg-[#EAF5F5]" : ""}`}
|
||||||
|
dangerouslySetInnerHTML={{ __html: val }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
);
|
))}
|
||||||
})}
|
</React.Fragment>
|
||||||
</tbody>
|
))}
|
||||||
</table>
|
</tbody>
|
||||||
</div>
|
</table>
|
||||||
<SourceText />
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
<SourceText />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<Hr className='my-11' />
|
<Hr className="my-11" />
|
||||||
|
|
||||||
{/* 3) 하수처리시설 설치 및 이용 현황 */}
|
{/* 2) 용도별 하수처리수 재이용 현황 */}
|
||||||
<section>
|
<section>
|
||||||
<SectionTitle>하수처리시설 설치 및 이용 현황</SectionTitle>
|
<SectionTitle>용도별 하수처리수 재이용 현황</SectionTitle>
|
||||||
<div className='mt-8'>
|
<div className="mt-8">
|
||||||
<div className={tableStyles.container}>
|
<div className={tableStyles.container}>
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col style={{ width: '130px' }} />
|
<col style={{ width: '180px' }} />
|
||||||
<col style={{ width: '160px' }} />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
</colgroup>
|
||||||
</colgroup>
|
<thead>
|
||||||
<thead>
|
<tr>
|
||||||
<tr>
|
<th className={`${tableStyles.th} ${tableStyles.headerBg}`}>구분</th>
|
||||||
<th colSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
<th className={`${tableStyles.th} bg-[#01ACE5] text-white`}>2018</th>
|
||||||
구분
|
<th className={`${tableStyles.th} bg-[#14AFCD] text-white`}>2019</th>
|
||||||
</th>
|
<th className={`${tableStyles.th} bg-[#28B2B4] text-white`}>2020</th>
|
||||||
<th className={`${tableStyles.th} bg-[#01ACE5] text-white`}>2018</th>
|
<th className={`${tableStyles.th} bg-[#3CB59A] text-white`}>2021</th>
|
||||||
<th className={`${tableStyles.th} bg-[#14AFCD] text-white`}>2019</th>
|
<th className={`${tableStyles.th} bg-[#50B880] text-white`}>2022</th>
|
||||||
<th className={`${tableStyles.th} bg-[#28B2B4] text-white`}>2020</th>
|
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white`}>2023</th>
|
||||||
<th className={`${tableStyles.th} bg-[#3CB59A] text-white`}>2021</th>
|
</tr>
|
||||||
<th className={`${tableStyles.th} bg-[#50B880] text-white`}>2022</th>
|
</thead>
|
||||||
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white`}>2023</th>
|
<tbody>
|
||||||
</tr>
|
{[
|
||||||
</thead>
|
{ label: "총 재이용량", type: "total", values: ["1,112.7", "1,148.7", "1,144.7", "1,127.0", "1,136.8", "1,137.5"] },
|
||||||
<tbody>
|
{ label: "장내 재이용", type: "category", values: ["520.6", "519.8", "513.9", "520.3", "502.2", "490.0"] },
|
||||||
{[
|
{ label: "세척수", type: "red-item", values: ["177.9", "174.4", "169.4", "180.9", "196.8", "191.2"] },
|
||||||
{
|
{ label: "냉각수", type: "red-item", values: ["45.5", "46.8", "50.4", "40.6", "38.3", "41.7"] },
|
||||||
group: '전체',
|
{ label: "청소수", type: "red-item", values: ["17.6", "21.2", "21.2", "21.7", "20.8", "20.5"] },
|
||||||
rows: [
|
{ label: "조경용수", type: "red-item", values: ["0.1", "0.1", "-", "-", "-", "-"] },
|
||||||
["시설수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>", '671', '681', '698', '704', '714', '717'],
|
{ label: "회생용수", type: "red-item", values: ["15.8", "16.7", "17.8", "15.7", "14.4", "14.0"] },
|
||||||
["시설용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", '25,846', '26,067', '26,244', '26,492', '26,586', '26,766'],
|
{ label: "장내급수도", type: "red-item", values: ["2.4", "-", "-", "-", "-", "-"] },
|
||||||
["처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", '1,112.7', '1,148.7', '1,144.7', '1,127.0', '1,136.8', '1,137.5'],
|
{ label: "장내기타용수", type: "red-item", values: ["261.3", "260.6", "255.0", "261.1", "231.8", "222.6"] },
|
||||||
],
|
{ label: "장외 재이용", type: "category", values: ["592.1", "628.9", "630.8", "606.7", "634.6", "647.5"] },
|
||||||
},
|
{ label: "청소·화장실용수", type: "blue-item", values: ["0.6", "0.4", "0.4", "0.4", "0.4", "0.4"] },
|
||||||
{
|
{ label: "세척·살수용수", type: "blue-item", values: ["5.6", "9.9", "2.9", "2.1", "3.0", "3.6"] },
|
||||||
group: '법적 대상',
|
{ label: "조경용수", type: "blue-item", values: ["10.3", "16.4", "5.9", "4.5", "8.9", "9.5"] },
|
||||||
rows: [
|
{ label: "친수용수", type: "blue-item", values: ["7.8", "3.1", "3.1", "3.0", "10.9", "10.5"] },
|
||||||
["시설수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>", '68', '82', '94', '97', '101', '118'],
|
{ label: "하천유지용수", type: "blue-item", values: ["480.5", "482.5", "496.2", "442.7", "471.7", "484.0"] },
|
||||||
["시설용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", '3,088.9', '3,359.2', '3,612.6', '4,412.2', '4,692.3', '5,503.8'],
|
{ label: "농업용수", type: "blue-item", values: ["12.2", "11.9", "13.4", "10.7", "10.6", "11.4"] },
|
||||||
["처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", '162.2', '190.6', '218.5', '244.6', '247.2', '324.9'],
|
{ label: "지하수 충전", type: "blue-item", values: ["0.0", "0.0", "0.0", "13.4", "0.0", "0.0"] },
|
||||||
],
|
{ label: "공업용수", type: "blue-item", values: ["75.2", "104.6", "108.9", "129.9", "129.0", "128.1"] },
|
||||||
},
|
].map((row, idx) => {
|
||||||
{
|
const isSubItem = row.type === "red-item" || row.type === "blue-item";
|
||||||
group: '법적 미대상',
|
const isLastInGroup = idx === 8 || idx === 17;
|
||||||
rows: [
|
|
||||||
["시설수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>", '603', '599', '604', '607', '613', '599'],
|
|
||||||
["시설용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", '22,757', '22,708', '22,631', '22,080', '21,894', '21,262'],
|
|
||||||
["처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", '950.6', '958.1', '926.2', '882.4', '889.5', '812.7'],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
].map((section, sIdx) => (
|
|
||||||
<React.Fragment key={sIdx}>
|
|
||||||
{section.rows.map((row, rIdx) => (
|
|
||||||
<tr key={`${sIdx}-${rIdx}`} className='bg-white'>
|
|
||||||
{rIdx === 0 && (
|
|
||||||
<td rowSpan={section.rows.length} className={`${tableStyles.td} ${tableStyles.headerBg} !text-[18px] !font-bold !text-neutral-900`}>
|
|
||||||
{section.group}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
{row.map((val, i) => {
|
|
||||||
const isFirstCol = i === 0;
|
|
||||||
const isHighlightedCol = [1, 3, 5].includes(i);
|
|
||||||
return (
|
|
||||||
<td
|
|
||||||
key={i}
|
|
||||||
className={`${tableStyles.td} ${isFirstCol ? '!font-bold !text-neutral-900' : ''} ${isHighlightedCol ? 'bg-[#EAF5F5]' : ''}`}
|
|
||||||
dangerouslySetInnerHTML={{ __html: val }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<SourceText />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<Hr className='my-11' />
|
return (
|
||||||
|
<tr key={idx} className="bg-white">
|
||||||
{/* 4) 시도별 하수처리수 재이용 현황 */}
|
<td className={`${tableStyles.td} relative`}>
|
||||||
<section className='pb-[200px]'>
|
{isSubItem ? (
|
||||||
<SectionTitle>시도별 하수처리수 재이용 현황</SectionTitle>
|
<>
|
||||||
<div className='mt-8'>
|
<div className={`absolute left-[1px] w-[16px] bg-white z-10 top-[-2px] ${isLastInGroup ? 'bottom-[0px]' : 'bottom-[-2px]'}`} />
|
||||||
<div className={tableStyles.container}>
|
<div className={`absolute left-[16px] w-[4px] z-20 ${row.type === 'red-item' ? 'bg-[#FF8B8B]' : 'bg-[#4ECBFF]'} top-[-2px] ${isLastInGroup ? 'bottom-[0px]' : 'bottom-[-2px]'}`} />
|
||||||
<div className='text-right text-[13px] mb-2 text-neutral-600'>(단위: 백만m³/년)</div>
|
<span className="text-[#4f4f4f] font-bold block w-full text-center relative z-30 translate-x-[8px]">{row.label}</span>
|
||||||
<table className={tableStyles.table}>
|
</>
|
||||||
<colgroup>
|
) : (
|
||||||
<col style={{ width: '160px' }} />
|
<span className="font-bold text-neutral-900 block w-full text-center">{row.label}</span>
|
||||||
<col />
|
)}
|
||||||
<col />
|
</td>
|
||||||
<col />
|
{row.values.map((val, i) => (
|
||||||
<col />
|
<td key={i} className={`${tableStyles.td} ${[0, 2, 4].includes(i) ? "bg-[#EAF5F5]" : ""}`}>
|
||||||
<col />
|
|
||||||
<col />
|
|
||||||
<col />
|
|
||||||
<col />
|
|
||||||
<col />
|
|
||||||
<col />
|
|
||||||
</colgroup>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
|
||||||
시도
|
|
||||||
</th>
|
|
||||||
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
|
||||||
연간
|
|
||||||
<br />
|
|
||||||
하수
|
|
||||||
<br />
|
|
||||||
처리량
|
|
||||||
</th>
|
|
||||||
<th colSpan={7} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
|
|
||||||
재이용현황
|
|
||||||
</th>
|
|
||||||
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
|
||||||
재이용률
|
|
||||||
<br />
|
|
||||||
(%)
|
|
||||||
</th>
|
|
||||||
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
|
||||||
비고
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
|
||||||
계
|
|
||||||
</th>
|
|
||||||
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
|
|
||||||
장내
|
|
||||||
</th>
|
|
||||||
<th colSpan={5} className={`${tableStyles.th} ${tableStyles.headerBg} py-1`}>
|
|
||||||
장외
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
{/* 💡 하천유지용수는 14px, 나머지 하위 헤더는 16px로 적용 */}
|
|
||||||
<th className={`${tableStyles.th} bg-[#01ACE5] text-white py-2 text-[16px]`}>소계</th>
|
|
||||||
<th className={`${tableStyles.th} bg-[#1FB1C2] text-white py-2 text-[13px]`}>하천유지용수</th>
|
|
||||||
<th className={`${tableStyles.th} bg-[#3EB59F] text-white py-2 text-[16px]`}>공업용수</th>
|
|
||||||
<th className={`${tableStyles.th} bg-[#5DB97C] text-white py-2 text-[16px]`}>농업용수</th>
|
|
||||||
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white py-2 text-[16px]`}>기타용수</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{[
|
|
||||||
['합계', '7,537.2', '1,137.5', '490.0', '647.5', '484.0', '128.1', '11.4', '24.0', '15.1%', ''],
|
|
||||||
['서울특별시', '1,567.0', '114.9', '59.3', '55.6', '55.1', '-', '-', '0.5', '7.3%', ''],
|
|
||||||
['부산광역시', '491.5', '146.2', '120.7', '25.5', '25.3', '-', '-', '0.2', '29.7%', ''],
|
|
||||||
['대구광역시', '432.7', '102.4', '38.2', '64.2', '64.1', '-', '-', '0.1', '23.7%', ''],
|
|
||||||
['인천광역시', '302.3', '57.7', '11.6', '46.1', '29.2', '12.1', '0.5', '4.3', '19.1%', ''],
|
|
||||||
['광주광역시', '211.1', '28.7', '6.5', '22.3', '22.2', '-', '-', '0.1', '13.6%', ''],
|
|
||||||
['대전광역시', '244.6', '11.9', '11.9', '0.68', '0.0', '-', '-', '0.1', '4.9%', ''],
|
|
||||||
['울산광역시', '235.1', '36.1', '6.1', '29.9', '29.7', '0.2', '-', '-', '15.3%', ''],
|
|
||||||
['세종특별자치시', '38.5', '2.5', '2.0', '0.5', '0.4', '-', '0.2', '-', '6.6%', ''],
|
|
||||||
['경기도', '1,926.8', '313.3', '122.2', '191.0', '153.0', '33.3', '0.4', '4.3', '16.3%', ''],
|
|
||||||
['강원도', '230.4', '19.6', '15.4', '4.2', '2.1', '-', '-', '2.1', '8.5%', ''],
|
|
||||||
['충청북도', '190.9', '15.6', '6.3', '9.2', '7.4', '0.6', '-', '1.2', '8.2%', ''],
|
|
||||||
['충청남도', '241.5', '41.4', '9.8', '31.5', '17.7', '8.6', '4.5', '0.8', '17.1%', ''],
|
|
||||||
['전라북도', '292.4', '42.7', '7.5', '35.1', '33.5', '0.2', '1.0', '0.5', '14.6%', ''],
|
|
||||||
['전라남도', '254.0', '23.6', '12.3', '11.3', '8.7', '0.0', '1.0', '1.6', '9.3%', ''],
|
|
||||||
['경상북도', '378.4', '129.1', '33.7', '95.3', '20.8', '72.5', '0.8', '1.3', '34.1%', ''],
|
|
||||||
['경상남도', '411.3', '50.1', '25.0', '25.0', '14.7', '0.5', '3.0', '6.9', '12.2%', ''],
|
|
||||||
['제주특별자치도', '88.6', '1.7', '1.5', '0.12', '-', '-', '0.1', '-', '1.9%', ''],
|
|
||||||
].map((row, idx) => (
|
|
||||||
<tr key={idx} className='bg-white'>
|
|
||||||
{/* 💡 연간하수처리량(index 1), 장내(index 3), 재이용률(index 9) 컬러 하이라이트 적용 */}
|
|
||||||
{row.map((val, i) => (
|
|
||||||
<td key={i} className={`${tableStyles.td} ${i === 0 ? '!font-bold !text-neutral-900' : ''} ${[1, 3, 9].includes(i) ? 'bg-[#EAF5F5]' : ''}`}>
|
|
||||||
{val}
|
{val}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
);
|
||||||
</tbody>
|
})}
|
||||||
</table>
|
</tbody>
|
||||||
</div>
|
</table>
|
||||||
<SourceText />
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
<SourceText />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Hr className="my-11" />
|
||||||
|
|
||||||
|
{/* 3) 하수처리시설 설치 및 이용 현황 */}
|
||||||
|
<section>
|
||||||
|
<SectionTitle>하수처리시설 설치 및 이용 현황</SectionTitle>
|
||||||
|
<div className="mt-8">
|
||||||
|
<div className={tableStyles.container}>
|
||||||
|
<table className={tableStyles.table}>
|
||||||
|
<colgroup>
|
||||||
|
<col style={{ width: '130px' }} />
|
||||||
|
<col style={{ width: '160px' }} />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>구분</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#01ACE5] text-white`}>2018</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#14AFCD] text-white`}>2019</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#28B2B4] text-white`}>2020</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#3CB59A] text-white`}>2021</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#50B880] text-white`}>2022</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white`}>2023</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
group: "전체",
|
||||||
|
rows: [
|
||||||
|
["시설수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>", "671", "681", "698", "704", "714", "717"],
|
||||||
|
["시설용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", "25,846", "26,067", "26,244", "26,492", "26,586", "26,766"],
|
||||||
|
["처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", "1,112.7", "1,148.7", "1,144.7", "1,127.0", "1,136.8", "1,137.5"],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "법적 대상",
|
||||||
|
rows: [
|
||||||
|
["시설수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>", "68", "82", "94", "97", "101", "118"],
|
||||||
|
["시설용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", "3,088.9", "3,359.2", "3,612.6", "4,412.2", "4,692.3", "5,503.8"],
|
||||||
|
["처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", "162.2", "190.6", "218.5", "244.6", "247.2", "324.9"],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "법적 미대상",
|
||||||
|
rows: [
|
||||||
|
["시설수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>", "603", "599", "604", "607", "613", "599"],
|
||||||
|
["시설용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", "22,757", "22,708", "22,631", "22,080", "21,894", "21,262"],
|
||||||
|
["처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", "950.6", "958.1", "926.2", "882.4", "889.5", "812.7"],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
].map((section, sIdx) => (
|
||||||
|
<React.Fragment key={sIdx}>
|
||||||
|
{section.rows.map((row, rIdx) => (
|
||||||
|
<tr key={`${sIdx}-${rIdx}`} className="bg-white">
|
||||||
|
{rIdx === 0 && (
|
||||||
|
<td
|
||||||
|
rowSpan={section.rows.length}
|
||||||
|
className={`${tableStyles.td} ${tableStyles.headerBg} !text-[18px] !font-bold !text-neutral-900`}
|
||||||
|
>
|
||||||
|
{section.group}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
{row.map((val, i) => {
|
||||||
|
const isFirstCol = i === 0;
|
||||||
|
const isHighlightedCol = [1, 3, 5].includes(i);
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
key={i}
|
||||||
|
className={`${tableStyles.td} ${isFirstCol ? "!font-bold !text-neutral-900" : ""} ${isHighlightedCol ? "bg-[#EAF5F5]" : ""}`}
|
||||||
|
dangerouslySetInnerHTML={{ __html: val }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<SourceText />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Hr className="my-11" />
|
||||||
|
|
||||||
|
{/* 4) 시도별 하수처리수 재이용 현황 */}
|
||||||
|
<section className="pb-[200px]">
|
||||||
|
<SectionTitle>시도별 하수처리수 재이용 현황</SectionTitle>
|
||||||
|
<div className="mt-8">
|
||||||
|
<div className={tableStyles.container}>
|
||||||
|
<div className="text-right text-[13px] mb-2 text-neutral-600">(단위: 백만m³/일)</div>
|
||||||
|
<table className={tableStyles.table}>
|
||||||
|
<colgroup>
|
||||||
|
<col style={{ width: '160px' }} />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>시도</th>
|
||||||
|
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>연간<br/>하수<br/>처리량</th>
|
||||||
|
<th colSpan={7} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>재이용현황</th>
|
||||||
|
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>재이용률<br/>(%)</th>
|
||||||
|
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>비고</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>계</th>
|
||||||
|
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>장내</th>
|
||||||
|
<th colSpan={5} className={`${tableStyles.th} ${tableStyles.headerBg} py-1`}>장외</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
{/* 💡 하천유지용수는 14px, 나머지 하위 헤더는 16px로 적용 */}
|
||||||
|
<th className={`${tableStyles.th} bg-[#01ACE5] text-white py-2 text-[16px]`}>소계</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#1FB1C2] text-white py-2 text-[13px]`}>하천유지용수</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#3EB59F] text-white py-2 text-[16px]`}>공업용수</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#5DB97C] text-white py-2 text-[16px]`}>농업용수</th>
|
||||||
|
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white py-2 text-[16px]`}>기타용수</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
["합계", "7,537.2", "1,137.5", "490.0", "647.5", "484.0", "128.1", "11.4", "24.0", "15.1%", ""],
|
||||||
|
["서울특별시", "1,567.0", "114.9", "59.3", "55.6", "55.1", "-", "-", "0.5", "7.3%", ""],
|
||||||
|
["부산광역시", "491.5", "146.2", "120.7", "25.5", "25.3", "-", "-", "0.2", "29.7%", ""],
|
||||||
|
["대구광역시", "432.7", "102.4", "38.2", "64.2", "64.1", "-", "-", "0.1", "23.7%", ""],
|
||||||
|
["인천광역시", "302.3", "57.7", "11.6", "46.1", "29.2", "12.1", "0.5", "4.3", "19.1%", ""],
|
||||||
|
["광주광역시", "211.1", "28.7", "6.5", "22.3", "22.2", "-", "-", "0.1", "13.6%", ""],
|
||||||
|
["대전광역시", "244.6", "11.9", "11.9", "0.68", "0.0", "-", "-", "0.1", "4.9%", ""],
|
||||||
|
["울산광역시", "235.1", "36.1", "6.1", "29.9", "29.7", "0.2", "-", "-", "15.3%", ""],
|
||||||
|
["세종특별자치시", "38.5", "2.5", "2.0", "0.5", "0.4", "-", "0.2", "-", "6.6%", ""],
|
||||||
|
["경기도", "1,926.8", "313.3", "122.2", "191.0", "153.0", "33.3", "0.4", "4.3", "16.3%", ""],
|
||||||
|
["강원도", "230.4", "19.6", "15.4", "4.2", "2.1", "-", "-", "2.1", "8.5%", ""],
|
||||||
|
["충청북도", "190.9", "15.6", "6.3", "9.2", "7.4", "0.6", "-", "1.2", "8.2%", ""],
|
||||||
|
["충청남도", "241.5", "41.4", "9.8", "31.5", "17.7", "8.6", "4.5", "0.8", "17.1%", ""],
|
||||||
|
["전라북도", "292.4", "42.7", "7.5", "35.1", "33.5", "0.2", "1.0", "0.5", "14.6%", ""],
|
||||||
|
["전라남도", "254.0", "23.6", "12.3", "11.3", "8.7", "0.0", "1.0", "1.6", "9.3%", ""],
|
||||||
|
["경상북도", "378.4", "129.1", "33.7", "95.3", "20.8", "72.5", "0.8", "1.3", "34.1%", ""],
|
||||||
|
["경상남도", "411.3", "50.1", "25.0", "25.0", "14.7", "0.5", "3.0", "6.9", "12.2%", ""],
|
||||||
|
["제주특별자치도", "88.6", "1.7", "1.5", "0.12", "-", "-", "0.1", "-", "1.9%", ""],
|
||||||
|
].map((row, idx) => (
|
||||||
|
<tr key={idx} className="bg-white">
|
||||||
|
{/* 💡 연간하수처리량(index 1), 장내(index 3), 재이용률(index 9) 컬러 하이라이트 적용 */}
|
||||||
|
{row.map((val, i) => (
|
||||||
|
<td
|
||||||
|
key={i}
|
||||||
|
className={`${tableStyles.td} ${i === 0 ? "!font-bold !text-neutral-900" : ""} ${[1, 3, 9].includes(i) ? "bg-[#EAF5F5]" : ""}`}
|
||||||
|
>
|
||||||
|
{val}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<SourceText />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -211,32 +211,18 @@ const selectMiddleRegionData: SelectDataType[] = [
|
|||||||
* STORE - API
|
* STORE - API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** API 선택 목록의 빈 값과 중복 값을 제거하고 전체 항목을 한 번만 추가한다. */
|
|
||||||
function normalizeSelectData<T extends SelectDataType>(items: T[], allItem: T): T[] {
|
|
||||||
const uniqueItems = new Map<string, T>();
|
|
||||||
|
|
||||||
items.forEach((item) => {
|
|
||||||
const val = String(item.val ?? '').trim();
|
|
||||||
|
|
||||||
if (!val || val === '0' || uniqueItems.has(val)) return;
|
|
||||||
uniqueItems.set(val, { ...item, val });
|
|
||||||
});
|
|
||||||
|
|
||||||
return [allItem, ...uniqueItems.values()];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 시도 API 호출 함수 추가
|
// 시도 API 호출 함수 추가
|
||||||
const fetchSidoData = async (): Promise<SelectDataType[]> => {
|
const fetchSidoData = async (): Promise<SelectDataType[]> => {
|
||||||
try {
|
try {
|
||||||
const res = await getFetch(fetchType.FO, '/addr/city');
|
const res = await getFetch(fetchType.FO, '/addr/city');
|
||||||
const data = res.data;
|
const data = res.data;
|
||||||
selectSidoData = normalizeSelectData(
|
selectSidoData = [
|
||||||
data.map((item: sidoDataType) => ({
|
{ text: "전체", val: '0'},
|
||||||
text: item.sido || '',
|
...data.map((item: sidoDataType) => ({
|
||||||
val: String(item.adstDstrCd) || ''
|
text: item.sido || '',
|
||||||
})),
|
val: String(item.adstDstrCd) || ''
|
||||||
{ text: "전체", val: '0' }
|
}))
|
||||||
);
|
];
|
||||||
return selectSidoData;
|
return selectSidoData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('시도 데이터를 가져오는데 실패했습니다:', error);
|
console.error('시도 데이터를 가져오는데 실패했습니다:', error);
|
||||||
@@ -249,13 +235,14 @@ const fetchSigunData = async (): Promise<SelectDataType[]> => {
|
|||||||
try {
|
try {
|
||||||
const res = await getFetch(fetchType.FO, '/addr/sgg');
|
const res = await getFetch(fetchType.FO, '/addr/sgg');
|
||||||
const data = res.data;
|
const data = res.data;
|
||||||
selectSigunData = normalizeSelectData(
|
selectSigunData = [
|
||||||
data.map((item: sigunDataType) => ({
|
{ text: "전체", val: '0' },
|
||||||
text: item.gugun || '',
|
...data
|
||||||
val: String(item.adstDstrCd) || ''
|
.map((item: sigunDataType) => ({
|
||||||
})),
|
text: item.gugun || '',
|
||||||
{ text: "전체", val: '0' }
|
val: String(item.adstDstrCd) || ''
|
||||||
);
|
}))
|
||||||
|
];
|
||||||
return selectSigunData;
|
return selectSigunData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('시군구 데이터를 가져오는데 실패했습니다:', error);
|
console.error('시군구 데이터를 가져오는데 실패했습니다:', error);
|
||||||
@@ -268,13 +255,13 @@ const fetchRegionData = async (): Promise<SelectDataType[]> => {
|
|||||||
try {
|
try {
|
||||||
const res = await getFetch(fetchType.FO, '/addr/sarea');
|
const res = await getFetch(fetchType.FO, '/addr/sarea');
|
||||||
const data = res.data;
|
const data = res.data;
|
||||||
return normalizeSelectData(
|
return [
|
||||||
data.map((item: regionDataType) => ({
|
{ text: "전체", val: '0'},
|
||||||
|
...data.map((item: regionDataType) => ({
|
||||||
text: item.rvbsn || '',
|
text: item.rvbsn || '',
|
||||||
val: String(item.rvbsnCd) || ''
|
val: String(item.rvbsnCd) || ''
|
||||||
})),
|
}))
|
||||||
{ text: "전체", val: '0' }
|
];
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('권역 데이터를 가져오는데 실패했습니다:', error);
|
console.error('권역 데이터를 가져오는데 실패했습니다:', error);
|
||||||
return selectRegionData;
|
return selectRegionData;
|
||||||
@@ -286,13 +273,14 @@ const fetchMiddleRegionData = async (): Promise<SelectDataType[]> => {
|
|||||||
try {
|
try {
|
||||||
const res = await getFetch(fetchType.FO, '/addr/midArea');
|
const res = await getFetch(fetchType.FO, '/addr/midArea');
|
||||||
const data = res.data;
|
const data = res.data;
|
||||||
return normalizeSelectData(
|
return [
|
||||||
data.map((item: middleRegionDataType) => ({
|
{ text: "전체", val: '0' },
|
||||||
|
...data
|
||||||
|
.map((item: middleRegionDataType) => ({
|
||||||
text: item.mdmbsn || '',
|
text: item.mdmbsn || '',
|
||||||
val: String(item.rvbsnCd) || ''
|
val: String(item.rvbsnCd) || ''
|
||||||
})),
|
}))
|
||||||
{ text: "전체", val: '0' }
|
];
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('중권역 데이터를 가져오는데 실패했습니다:', error);
|
console.error('중권역 데이터를 가져오는데 실패했습니다:', error);
|
||||||
return selectMiddleRegionData;
|
return selectMiddleRegionData;
|
||||||
@@ -304,13 +292,14 @@ const fetchUnitWaterShedData = async (): Promise<SelectDataType[]> => {
|
|||||||
try {
|
try {
|
||||||
const res = await getFetch(fetchType.FO, '/addr/largeArea');
|
const res = await getFetch(fetchType.FO, '/addr/largeArea');
|
||||||
const data = res.data;
|
const data = res.data;
|
||||||
return normalizeSelectData(
|
return [
|
||||||
data.map((item: unitWaterShedDataType) => ({
|
{ text: "전체", val: '0' },
|
||||||
|
...data
|
||||||
|
.map((item: unitWaterShedDataType) => ({
|
||||||
text: item.lgbsn || '',
|
text: item.lgbsn || '',
|
||||||
val: String(item.rvbsnCd) || ''
|
val: String(item.rvbsnCd) || ''
|
||||||
})),
|
}))
|
||||||
{ text: "전체", val: '0' }
|
];
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('단위유역 데이터를 가져오는데 실패했습니다:', error);
|
console.error('단위유역 데이터를 가져오는데 실패했습니다:', error);
|
||||||
return selectUnitWaterShedData;
|
return selectUnitWaterShedData;
|
||||||
@@ -322,14 +311,15 @@ const fetchEnvPartData = async (): Promise<envPartSelectDataType[]> => {
|
|||||||
try {
|
try {
|
||||||
const res = await getFetch(fetchType.FO, '/addr/govArea');
|
const res = await getFetch(fetchType.FO, '/addr/govArea');
|
||||||
const data = res.data;
|
const data = res.data;
|
||||||
selectEnvPartData = normalizeSelectData(
|
selectEnvPartData = [
|
||||||
data.map((item: envPartDataType) => ({
|
{ text: "전체", val: '0', adstDliStrCd: '' },
|
||||||
|
...data
|
||||||
|
.map((item: envPartDataType) => ({
|
||||||
text: item.lclgvrnmntNm || '',
|
text: item.lclgvrnmntNm || '',
|
||||||
val: String(item.lclgvrnmntCd) || '',
|
val: String(item.lclgvrnmntCd) || '',
|
||||||
adstDliStrCd: item.adstDliStrCd || ''
|
adstDliStrCd: item.adstDliStrCd || ''
|
||||||
})),
|
}))
|
||||||
{ text: "전체", val: '0', adstDliStrCd: '' }
|
];
|
||||||
);
|
|
||||||
return selectEnvPartData;
|
return selectEnvPartData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('환경청 데이터를 가져오는데 실패했습니다:', error);
|
console.error('환경청 데이터를 가져오는데 실패했습니다:', error);
|
||||||
|
|||||||
@@ -134,23 +134,22 @@ export type SupplyCandidate = {
|
|||||||
|
|
||||||
|
|
||||||
export type DemandMatchings = {
|
export type DemandMatchings = {
|
||||||
splyFcltFk: string | ''; // 공급시설 외래키
|
splyFcltFk: string | '';
|
||||||
splyFcltFlg: string | ''; // 공급시설 진행상태
|
splyFcltFlg: string | '';
|
||||||
pbnstRuseFcltBizFk: number | ''; // 공공하수 재이용시설 사업 외래키
|
pbnstRuseFcltBizFk: number | '';
|
||||||
splyBgngDay: string | ''; // 공급시작일
|
splyBgngDay: string | '';
|
||||||
splyEndDay: string | ''; // 공급종료일
|
splyEndDay: string | '';
|
||||||
splyCpct: number | ''; // 계약 공급량
|
splyCpct: number | '';
|
||||||
splyFmYn: string | ''; // 공급 유량계 유무
|
splyFmYn: string | '';
|
||||||
useUsgCd: string | ''; // 사용용도 코드
|
useUsgCd: string | '';
|
||||||
dmdInfoSn: number | ''; // 수요정보 일련번호
|
dmdInfoSn: number | '';
|
||||||
ruseUtztnFcltNm?: string | ''; // 재이용시설명
|
splyCndtList: string | '';
|
||||||
splyCndtList: string | ''; // 공급 후보지 목록
|
mtchnCmptnYmd: string | '';
|
||||||
mtchnCmptnYmd: string | ''; // 매칭 완료일자
|
rvwCmptnYmd: string | '';
|
||||||
rvwCmptnYmd: string | ''; // 검토 완료일자
|
regDt: string | '';
|
||||||
regDt: string | ''; // 등록일시
|
rgtr: string | '';
|
||||||
rgtr: string | ''; // 등록자
|
mdfcnDt: string | '';
|
||||||
mdfcnDt: string | ''; // 수정일시
|
mdfr: string | '';
|
||||||
mdfr: string | ''; // 수정자
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 유사사례
|
// 유사사례
|
||||||
@@ -504,14 +503,6 @@ const postSdmData = async (data: DemandCompType) => {
|
|||||||
// 수요정보 등록
|
// 수요정보 등록
|
||||||
const postSdmInfoData = async (data: SdmInfoFormData): Promise<boolean> => {
|
const postSdmInfoData = async (data: SdmInfoFormData): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
const activeDemand = data.dmdInfoId
|
|
||||||
? await getSdmInfoStatusList(data.dmdInfoId)
|
|
||||||
: null;
|
|
||||||
if (!activeDemand?.dmdInfoId) {
|
|
||||||
sessionStorage.removeItem("user");
|
|
||||||
alert('사용 중지되었거나 등록되지 않은 수요처입니다. 다시 로그인해 주세요.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
await postFetch("/reuseFc/request/demandInfo", data);
|
await postFetch("/reuseFc/request/demandInfo", data);
|
||||||
alert('수요 정보 등록에 성공하였습니다.');
|
alert('수요 정보 등록에 성공하였습니다.');
|
||||||
return true;
|
return true;
|
||||||
@@ -525,14 +516,6 @@ const postSdmInfoData = async (data: SdmInfoFormData): Promise<boolean> => {
|
|||||||
// 수요정보 수정
|
// 수요정보 수정
|
||||||
const fetchUpdateSdmInfoData = async (data: SdmInfoFormData): Promise<boolean> => {
|
const fetchUpdateSdmInfoData = async (data: SdmInfoFormData): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
const activeDemand = data.dmdInfoId
|
|
||||||
? await getSdmInfoStatusList(data.dmdInfoId)
|
|
||||||
: null;
|
|
||||||
if (!activeDemand?.dmdInfoId) {
|
|
||||||
sessionStorage.removeItem("user");
|
|
||||||
alert('사용 중지되었거나 등록되지 않은 수요처입니다. 다시 로그인해 주세요.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
await postFetch("/reuseFc/request/updateDemandInfo", data);
|
await postFetch("/reuseFc/request/updateDemandInfo", data);
|
||||||
alert('수요 정보 수정에 성공하였습니다.');
|
alert('수요 정보 수정에 성공하였습니다.');
|
||||||
return true;
|
return true;
|
||||||
@@ -601,19 +584,14 @@ const getDemandCompList = async (searchText: string, pageCnt: number) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type DemandStatusResponse = DemandCompType & {
|
const getSdmInfoStatusList = async (dmdInfoId: string) => {
|
||||||
demandInfo?: DemandCompBizType[];
|
|
||||||
demandMatchings?: DemandMatchings[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSdmInfoStatusList = async (dmdInfoId: string): Promise<DemandStatusResponse | null> => {
|
|
||||||
try {
|
try {
|
||||||
const url = '/reuseFc/request/' + dmdInfoId + '?infoListYn=Y';
|
const url = '/reuseFc/request/' + dmdInfoId + '?infoListYn=Y';
|
||||||
const res = await getFetch(fetchType.WR, url);
|
const res = await getFetch(fetchType.WR, url);
|
||||||
return res?.data ?? null;
|
return res?.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error + ' 수요정보 신청현황을 불러오지 못했습니다.');
|
console.error(error + ' 수요정보 신청현황을 불러오지 못했습니다.');
|
||||||
return null;
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,7 +620,7 @@ export type SdmActions = {
|
|||||||
updateSdmInfoData: (data: SdmInfoFormData) => Promise<boolean>;
|
updateSdmInfoData: (data: SdmInfoFormData) => Promise<boolean>;
|
||||||
getDemandCompList: (searchText: string, pageCnt: number) => Promise<void>;
|
getDemandCompList: (searchText: string, pageCnt: number) => Promise<void>;
|
||||||
actionLogin: (data: DemandCompType) => Promise<boolean>;
|
actionLogin: (data: DemandCompType) => Promise<boolean>;
|
||||||
getSdmInfoStatusList: (dmdInfoId: string) => Promise<boolean>;
|
getSdmInfoStatusList: (dmdInfoId: string) => Promise<void>;
|
||||||
getSimilarCaseList: (part: string, value: number | string) => Promise<void>;
|
getSimilarCaseList: (part: string, value: number | string) => Promise<void>;
|
||||||
actionLogout: () => Promise<void>;
|
actionLogout: () => Promise<void>;
|
||||||
|
|
||||||
@@ -758,25 +736,19 @@ export const SdmReducer = (set: any): SdmActions => {
|
|||||||
getSdmInfoStatusList: async (dmdInfoId: string) => {
|
getSdmInfoStatusList: async (dmdInfoId: string) => {
|
||||||
const res = await getSdmInfoStatusList(dmdInfoId);
|
const res = await getSdmInfoStatusList(dmdInfoId);
|
||||||
|
|
||||||
if (!res?.dmdInfoId) {
|
if (res.length === 0) {
|
||||||
sessionStorage.removeItem("user");
|
|
||||||
set((state: SdmType) => ({
|
set((state: SdmType) => ({
|
||||||
Sdm: {
|
Sdm: {
|
||||||
...state.Sdm,
|
...state.Sdm,
|
||||||
sdmInfoStatusList: [],
|
sdmInfoStatusList: [],
|
||||||
successLogin: false,
|
|
||||||
loginUser: null,
|
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
toast.error("사용 중지되었거나 등록되지 않은 수요처입니다.");
|
return;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
//const sdmInfoStatusList : DemandCompBizType[] = [];
|
//const sdmInfoStatusList : DemandCompBizType[] = [];
|
||||||
|
|
||||||
const demandInfo = Array.isArray(res.demandInfo) ? res.demandInfo : [];
|
const sdmInfoStatusList: DemandCompBizType[] = res.demandInfo.map((el: DemandCompBizType, index: number) => {
|
||||||
const demandMatchings = Array.isArray(res.demandMatchings) ? res.demandMatchings : [];
|
const matchingInfo = res.demandMatchings.find((matching: DemandMatchings) => matching.dmdInfoSn === el.dmdInfoSn);
|
||||||
const sdmInfoStatusList: DemandCompBizType[] = demandInfo.map((el: DemandCompBizType, index: number) => {
|
|
||||||
const matchingInfo = demandMatchings.find((matching: DemandMatchings) => matching.dmdInfoSn === el.dmdInfoSn);
|
|
||||||
const status = matchingInfo?.splyFcltFlg?.trim() === 'Sub' ? '검토' : matchingInfo?.splyFcltFlg?.trim() === 'matched' ? '매칭완료' : '신청';
|
const status = matchingInfo?.splyFcltFlg?.trim() === 'Sub' ? '검토' : matchingInfo?.splyFcltFlg?.trim() === 'matched' ? '매칭완료' : '신청';
|
||||||
const ruseUtztnFcltNm = matchingInfo?.ruseUtztnFcltNm ? matchingInfo.ruseUtztnFcltNm : '';
|
const ruseUtztnFcltNm = matchingInfo?.ruseUtztnFcltNm ? matchingInfo.ruseUtztnFcltNm : '';
|
||||||
|
|
||||||
@@ -809,7 +781,6 @@ export const SdmReducer = (set: any): SdmActions => {
|
|||||||
sdmInfoStatusList: sdmInfoStatusList,
|
sdmInfoStatusList: sdmInfoStatusList,
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
actionLogin: async (data: DemandCompType): Promise<boolean> => {
|
actionLogin: async (data: DemandCompType): Promise<boolean> => {
|
||||||
|
|||||||
@@ -20,24 +20,9 @@ type FieldKey = "bizNo" | "workerNo" | "pw";
|
|||||||
const REGISTER_HREF = "/apply/dmdco";
|
const REGISTER_HREF = "/apply/dmdco";
|
||||||
|
|
||||||
function isBizNoValid(v: string) {
|
function isBizNoValid(v: string) {
|
||||||
return /^\d{10}$/.test(v.replace(/\D/g, ""));
|
return /^\d{10}$/.test(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getBusinessNumberDigits = (value: string) =>
|
|
||||||
value.replace(/\D/g, "").slice(0, 10);
|
|
||||||
|
|
||||||
const formatBusinessNumber = (value: string) => {
|
|
||||||
const normalized = value.replace(/[^\d-]/g, "").slice(0, 12);
|
|
||||||
const digits = getBusinessNumberDigits(normalized);
|
|
||||||
|
|
||||||
if (normalized.endsWith("-") && digits.length === 3) return `${digits}-`;
|
|
||||||
if (normalized.endsWith("-") && digits.length === 5)
|
|
||||||
return `${digits.slice(0, 3)}-${digits.slice(3)}-`;
|
|
||||||
if (digits.length <= 3) return digits;
|
|
||||||
if (digits.length <= 5) return `${digits.slice(0, 3)}-${digits.slice(3)}`;
|
|
||||||
return `${digits.slice(0, 3)}-${digits.slice(3, 5)}-${digits.slice(5)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
function inputCls(opts: { hasError?: boolean; disabled?: boolean }) {
|
function inputCls(opts: { hasError?: boolean; disabled?: boolean }) {
|
||||||
const base =
|
const base =
|
||||||
"h-[48px] w-full rounded-[4px] border px-5 text-[18px] outline-none";
|
"h-[48px] w-full rounded-[4px] border px-5 text-[18px] outline-none";
|
||||||
@@ -117,7 +102,7 @@ export default function LoginModal({
|
|||||||
const validate = () => {
|
const validate = () => {
|
||||||
const next: Partial<Record<FieldKey, string>> = {};
|
const next: Partial<Record<FieldKey, string>> = {};
|
||||||
|
|
||||||
const b = getBusinessNumberDigits(bizNo);
|
const b = bizNo.trim();
|
||||||
const w = workerNo.trim();
|
const w = workerNo.trim();
|
||||||
const p = pw.trim();
|
const p = pw.trim();
|
||||||
|
|
||||||
@@ -137,6 +122,23 @@ export default function LoginModal({
|
|||||||
return next;
|
return next;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatBusinessNumber = (value: string) => {
|
||||||
|
// 숫자만 추출
|
||||||
|
const digitsOnly = value.replace(/\D/g, "");
|
||||||
|
|
||||||
|
// 10자리까지만 처리
|
||||||
|
const limitedDigits = digitsOnly.slice(0, 10);
|
||||||
|
|
||||||
|
// 3-2-5 형식으로 포맷팅
|
||||||
|
if (limitedDigits.length <= 3) {
|
||||||
|
return limitedDigits;
|
||||||
|
} else if (limitedDigits.length <= 5) {
|
||||||
|
return `${limitedDigits.slice(0, 3)}-${limitedDigits.slice(3)}`;
|
||||||
|
} else {
|
||||||
|
return `${limitedDigits.slice(0, 3)}-${limitedDigits.slice(3, 5)}-${limitedDigits.slice(5)}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const markTouched = (key: FieldKey) =>
|
const markTouched = (key: FieldKey) =>
|
||||||
setTouched((p) => ({ ...p, [key]: true }));
|
setTouched((p) => ({ ...p, [key]: true }));
|
||||||
|
|
||||||
@@ -152,9 +154,8 @@ export default function LoginModal({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
const businessNumber = getBusinessNumberDigits(bizNo);
|
|
||||||
const param = {
|
const param = {
|
||||||
dmdInfoId: businessNumber + (noWorker ? "0000" : workerNo), // 종사업자번호 해당없음 "0000"으로 채움
|
dmdInfoId: bizNo + (noWorker ? "0000" : workerNo), // 종사업자번호 해당없음 "0000"으로 채움
|
||||||
dmdAplyPswd: sha512.hex(pw),
|
dmdAplyPswd: sha512.hex(pw),
|
||||||
};
|
};
|
||||||
await actionLogin(param as DemandCompType);
|
await actionLogin(param as DemandCompType);
|
||||||
@@ -226,9 +227,9 @@ export default function LoginModal({
|
|||||||
사업자 등록번호
|
사업자 등록번호
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
value={bizNo}
|
value={formatBusinessNumber(bizNo)}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setBizNo(formatBusinessNumber(e.target.value));
|
setBizNo(e.target.value.replace(/\D/g, "").slice(0, 10));
|
||||||
setErrors((p) => ({ ...p, bizNo: undefined }));
|
setErrors((p) => ({ ...p, bizNo: undefined }));
|
||||||
}}
|
}}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 317 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 284 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 60 KiB |