Compare commits
89
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ffc02e887 | ||
|
|
cbacb27bb1 | ||
|
|
5331e10c96 | ||
|
|
de8971bea6 | ||
|
|
a7fd6a8e16 | ||
|
|
8039ab8281 | ||
|
|
794691f05e | ||
|
|
e58b84a01b | ||
|
|
3d51f003c0 | ||
|
|
b5a99e5f59 | ||
|
|
afca685fd4 | ||
|
|
261941452a | ||
|
|
8576ef83b5 | ||
|
|
e80b8953a0 | ||
|
|
e8b309460e | ||
|
|
37ac927c45 | ||
|
|
bf9a34cbe8 | ||
|
|
18a11ab5fa | ||
|
|
214ff1a435 | ||
|
|
d7cc579335 | ||
|
|
9b53c1ae08 | ||
|
|
64ed53cf4d | ||
|
|
043fd3d4a8 | ||
|
|
6dd09971f1 | ||
|
|
23424b5eee | ||
|
|
3f190dfff6 | ||
|
|
6cc02da7a4 | ||
|
|
c87ccf8788 | ||
|
|
8e757ab9db | ||
|
|
de7e66afb4 | ||
|
|
c981b83284 | ||
|
|
927bb7cb3a | ||
|
|
725976b5c0 | ||
|
|
cdbe6a855f | ||
|
|
a1c96e9e98 | ||
|
|
1488805242 | ||
|
|
5b12f81b8f | ||
|
|
df24e7c2a8 | ||
|
|
3b5fee047b | ||
|
|
181730bcde | ||
|
|
678af2f06e | ||
|
|
82d67bb4c6 | ||
|
|
85ef1805e5 | ||
|
|
26f64988ef | ||
|
|
29e39f98af | ||
|
|
5192f3e3c0 | ||
|
|
83de317308 | ||
|
|
84b1fd07ae | ||
|
|
a88f8ab33e | ||
|
|
c67b5844b6 | ||
|
|
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);
|
|
||||||
}
|
|
||||||
+11
-14
@@ -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", "**/.*/*"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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()}`,
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
+10
-9
@@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,7 @@
|
|||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.24.2",
|
||||||
"axios": "^1.8.1",
|
"axios": "^1.8.1",
|
||||||
"proj4": "^2.9.0",
|
"proj4": "^2.9.0"
|
||||||
"uuid": "^14.0.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
|||||||
@@ -1,47 +1,82 @@
|
|||||||
import {
|
import {
|
||||||
ACCESS_TOKEN,
|
ACCESS_TOKEN,
|
||||||
AUTH_USER_ID,
|
AUTH_USER_ID,
|
||||||
CLAIM_NAME,
|
CLAIM_NAME,
|
||||||
ENDPOINT,
|
ENDPOINT,
|
||||||
} from "@/src/shared/config";
|
} from "@/src/shared/config";
|
||||||
import { getCookie } from "@/src/shared/lib";
|
import { getCookie } from "@/src/shared/lib";
|
||||||
import {
|
import {
|
||||||
type AuthPayload,
|
clearDeviceId,
|
||||||
type AuthService,
|
getOrCreateDeviceId,
|
||||||
getRemainingMs,
|
type AuthPayload,
|
||||||
|
type AuthService,
|
||||||
|
getRemainingMs,
|
||||||
} from "@common/auth";
|
} from "@common/auth";
|
||||||
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
|
|
||||||
import {
|
import {
|
||||||
clearRefreshTimer,
|
clearRefreshTimer,
|
||||||
setToken,
|
setToken,
|
||||||
setupRefreshTimer,
|
setupRefreshTimer,
|
||||||
tokenManager,
|
tokenManager,
|
||||||
} from "../lib/token-manager";
|
} from "../lib/token-manager";
|
||||||
|
|
||||||
import { clearTabStorePersist } from "@/src/store/tab-store";
|
import { clearTabStorePersist } from "@/src/store/tab-store";
|
||||||
|
|
||||||
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",
|
||||||
|
"X-Device-Id": getOrCreateDeviceId(),
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const accessToken = data[ACCESS_TOKEN];
|
||||||
|
const tokenId = data[AUTH_USER_ID];
|
||||||
|
|
||||||
|
if (!accessToken || !tokenId) {
|
||||||
|
throw new Error("인증 토큰을 받지 못했습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevTokenId = getCookie(AUTH_USER_ID);
|
||||||
|
if (prevTokenId !== undefined && prevTokenId !== tokenId) {
|
||||||
|
clearTabStorePersist();
|
||||||
|
}
|
||||||
|
|
||||||
|
setToken(CLAIM_NAME, accessToken);
|
||||||
|
setToken(AUTH_USER_ID, tokenId);
|
||||||
|
|
||||||
|
const remainingMs = getRemainingMs(accessToken);
|
||||||
|
if (remainingMs > 0) {
|
||||||
|
setupRefreshTimer(silentRefresh, remainingMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function silentRefresh(): Promise<void> {
|
||||||
|
console.log(`[silentRefresh] Called at ${new Date().toISOString()}`);
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"X-Device-Id": getOrCreateDeviceId(),
|
||||||
"X-Device-Id": getOrCreateDeviceId(),
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload),
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data?.message ?? "토큰 갱신에 실패했습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
const accessToken = data[ACCESS_TOKEN];
|
const accessToken = data[ACCESS_TOKEN];
|
||||||
const tokenId = data[AUTH_USER_ID];
|
const tokenId = data[AUTH_USER_ID];
|
||||||
|
|
||||||
if (!accessToken || !tokenId) {
|
if (!accessToken || !tokenId) {
|
||||||
throw new Error("인증 토큰을 받지 못했습니다.");
|
throw new Error("갱신된 인증 토큰을 받지 못했습니다.");
|
||||||
}
|
|
||||||
|
|
||||||
const prevTokenId = getCookie(AUTH_USER_ID);
|
|
||||||
if (prevTokenId !== undefined && prevTokenId !== tokenId) {
|
|
||||||
clearTabStorePersist();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setToken(CLAIM_NAME, accessToken);
|
setToken(CLAIM_NAME, accessToken);
|
||||||
@@ -49,67 +84,32 @@ async function login(payload: AuthPayload): Promise<void> {
|
|||||||
|
|
||||||
const remainingMs = getRemainingMs(accessToken);
|
const remainingMs = getRemainingMs(accessToken);
|
||||||
if (remainingMs > 0) {
|
if (remainingMs > 0) {
|
||||||
setupRefreshTimer(silentRefresh, remainingMs);
|
setupRefreshTimer(silentRefresh, remainingMs);
|
||||||
}
|
}
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
async function silentRefresh(): Promise<void> {
|
`[silentRefresh] Failed at ${new Date().toISOString()}`,
|
||||||
console.log(`[silentRefresh] Called at ${new Date().toISOString()}`);
|
error,
|
||||||
try {
|
);
|
||||||
const response = await fetch(
|
tokenManager.clearAuthToken();
|
||||||
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
if (typeof window !== "undefined") {
|
||||||
{
|
window.dispatchEvent(new CustomEvent("auth:logout"));
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"X-Device-Id": getOrCreateDeviceId(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data?.message ?? "토큰 갱신에 실패했습니다.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessToken = data[ACCESS_TOKEN];
|
|
||||||
const tokenId = data[AUTH_USER_ID];
|
|
||||||
|
|
||||||
if (!accessToken || !tokenId) {
|
|
||||||
throw new Error("갱신된 인증 토큰을 받지 못했습니다.");
|
|
||||||
}
|
|
||||||
|
|
||||||
setToken(CLAIM_NAME, accessToken);
|
|
||||||
setToken(AUTH_USER_ID, tokenId);
|
|
||||||
|
|
||||||
const remainingMs = getRemainingMs(accessToken);
|
|
||||||
if (remainingMs > 0) {
|
|
||||||
setupRefreshTimer(silentRefresh, remainingMs);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`[silentRefresh] Failed at ${new Date().toISOString()}`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
tokenManager.clearAuthToken();
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
window.dispatchEvent(new CustomEvent("auth:logout"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logout(): Promise<void> {
|
export async function logout(): Promise<void> {
|
||||||
tokenManager.clearAuthToken();
|
tokenManager.clearAuthToken();
|
||||||
clearDeviceId();
|
clearDeviceId();
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
window.dispatchEvent(new CustomEvent("auth:logout"));
|
window.dispatchEvent(new CustomEvent("auth:logout"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authService: AuthService = {
|
export const authService: AuthService = {
|
||||||
login,
|
login,
|
||||||
silentRefresh,
|
silentRefresh,
|
||||||
declineSessionExtend: clearRefreshTimer,
|
declineSessionExtend: clearRefreshTimer,
|
||||||
logout,
|
logout,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -37,7 +37,6 @@
|
|||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"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",
|
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "^0.18.5",
|
||||||
"xlsx-populate": "^1.21.0",
|
"xlsx-populate": "^1.21.0",
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.24.2",
|
||||||
|
|||||||
@@ -1,47 +1,83 @@
|
|||||||
import {
|
import {
|
||||||
ACCESS_TOKEN,
|
ACCESS_TOKEN,
|
||||||
AUTH_USER_ID,
|
AUTH_USER_ID,
|
||||||
CLAIM_NAME,
|
CLAIM_NAME,
|
||||||
ENDPOINT,
|
ENDPOINT,
|
||||||
} from "@/shared/config";
|
} from "@/shared/config";
|
||||||
import { getCookie } from "@/shared/lib";
|
import { getCookie } from "@/shared/lib";
|
||||||
import {
|
import {
|
||||||
type AuthPayload,
|
clearDeviceId,
|
||||||
type AuthService,
|
getOrCreateDeviceId,
|
||||||
getRemainingMs,
|
type AuthPayload,
|
||||||
|
type AuthService,
|
||||||
|
getRemainingMs,
|
||||||
} from "@common/auth";
|
} from "@common/auth";
|
||||||
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
|
|
||||||
import {
|
import {
|
||||||
clearRefreshTimer,
|
clearRefreshTimer,
|
||||||
setToken,
|
setToken,
|
||||||
setupRefreshTimer,
|
setupRefreshTimer,
|
||||||
tokenManager,
|
tokenManager,
|
||||||
} from "../lib/token-manager";
|
} from "../lib/token-manager";
|
||||||
|
|
||||||
import { clearTabStorePersist } from "../../../store/tab-store";
|
import { clearTabStorePersist } from "../../../store/tab-store";
|
||||||
|
|
||||||
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",
|
||||||
|
"X-Device-Id": getOrCreateDeviceId(),
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const accessToken = data[ACCESS_TOKEN];
|
||||||
|
const tokenId = data[AUTH_USER_ID];
|
||||||
|
|
||||||
|
if (!accessToken || !tokenId) {
|
||||||
|
throw new Error("인증 토큰을 받지 못했습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevTokenId = getCookie(AUTH_USER_ID);
|
||||||
|
if (prevTokenId !== undefined && prevTokenId !== tokenId) {
|
||||||
|
clearTabStorePersist();
|
||||||
|
}
|
||||||
|
|
||||||
|
setToken(CLAIM_NAME, accessToken);
|
||||||
|
setToken(AUTH_USER_ID, tokenId);
|
||||||
|
|
||||||
|
const remainingMs = getRemainingMs(accessToken);
|
||||||
|
if (remainingMs > 0) {
|
||||||
|
setupRefreshTimer(silentRefresh, remainingMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function silentRefresh(): Promise<void> {
|
||||||
|
console.log(`[silentRefresh] Called at ${new Date().toISOString()}`);
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"X-Device-Id": getOrCreateDeviceId(),
|
||||||
"X-Device-Id": getOrCreateDeviceId(),
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload),
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data?.message ?? "토큰 갱신에 실패했습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
const accessToken = data[ACCESS_TOKEN];
|
const accessToken = data[ACCESS_TOKEN];
|
||||||
const tokenId = data[AUTH_USER_ID];
|
const tokenId = data[AUTH_USER_ID];
|
||||||
|
|
||||||
if (!accessToken || !tokenId) {
|
if (!accessToken || !tokenId) {
|
||||||
throw new Error("인증 토큰을 받지 못했습니다.");
|
throw new Error("갱신된 인증 토큰을 받지 못했습니다.");
|
||||||
}
|
|
||||||
|
|
||||||
const prevTokenId = getCookie(AUTH_USER_ID);
|
|
||||||
if (prevTokenId !== undefined && prevTokenId !== tokenId) {
|
|
||||||
clearTabStorePersist();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setToken(CLAIM_NAME, accessToken);
|
setToken(CLAIM_NAME, accessToken);
|
||||||
@@ -49,67 +85,32 @@ async function login(payload: AuthPayload): Promise<void> {
|
|||||||
|
|
||||||
const remainingMs = getRemainingMs(accessToken);
|
const remainingMs = getRemainingMs(accessToken);
|
||||||
if (remainingMs > 0) {
|
if (remainingMs > 0) {
|
||||||
setupRefreshTimer(silentRefresh, remainingMs);
|
setupRefreshTimer(silentRefresh, remainingMs);
|
||||||
}
|
}
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
async function silentRefresh(): Promise<void> {
|
`[silentRefresh] Failed at ${new Date().toISOString()}`,
|
||||||
console.log(`[silentRefresh] Called at ${new Date().toISOString()}`);
|
error,
|
||||||
try {
|
);
|
||||||
const response = await fetch(
|
tokenManager.clearAuthToken();
|
||||||
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
if (typeof window !== "undefined") {
|
||||||
{
|
window.dispatchEvent(new CustomEvent("auth:logout"));
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"X-Device-Id": getOrCreateDeviceId(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data?.message ?? "토큰 갱신에 실패했습니다.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessToken = data[ACCESS_TOKEN];
|
|
||||||
const tokenId = data[AUTH_USER_ID];
|
|
||||||
|
|
||||||
if (!accessToken || !tokenId) {
|
|
||||||
throw new Error("갱신된 인증 토큰을 받지 못했습니다.");
|
|
||||||
}
|
|
||||||
|
|
||||||
setToken(CLAIM_NAME, accessToken);
|
|
||||||
setToken(AUTH_USER_ID, tokenId);
|
|
||||||
|
|
||||||
const remainingMs = getRemainingMs(accessToken);
|
|
||||||
if (remainingMs > 0) {
|
|
||||||
setupRefreshTimer(silentRefresh, remainingMs);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`[silentRefresh] Failed at ${new Date().toISOString()}`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
tokenManager.clearAuthToken();
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
window.dispatchEvent(new CustomEvent("auth:logout"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logout(): Promise<void> {
|
export async function logout(): Promise<void> {
|
||||||
tokenManager.clearAuthToken();
|
tokenManager.clearAuthToken();
|
||||||
clearDeviceId();
|
clearDeviceId();
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
window.dispatchEvent(new CustomEvent("auth:logout"));
|
window.dispatchEvent(new CustomEvent("auth:logout"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authService: AuthService = {
|
export const authService: AuthService = {
|
||||||
login,
|
login,
|
||||||
silentRefresh,
|
silentRefresh,
|
||||||
declineSessionExtend: clearRefreshTimer,
|
declineSessionExtend: clearRefreshTimer,
|
||||||
logout,
|
logout,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,7 +1,7 @@
|
|||||||
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({
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "@rsone/components";
|
} from "@rsone/components";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { ENDPOINT } from "@/shared/config";
|
import { ENDPOINT, CLAIM_NAME } from "@/shared/config";
|
||||||
import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib";
|
import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib";
|
||||||
import { isBbsNoticeMenu } from "@/shared/lib/bbs-notice-menu";
|
import { isBbsNoticeMenu } from "@/shared/lib/bbs-notice-menu";
|
||||||
import {
|
import {
|
||||||
@@ -20,8 +20,10 @@ import { useStatisticsTabUrlSync } from "@/shared/lib/use-statistics-tab-url-syn
|
|||||||
import { isVerificationSelectionReportMenu } from "@/shared/lib/verification-selection-report";
|
import { isVerificationSelectionReportMenu } from "@/shared/lib/verification-selection-report";
|
||||||
import { StatisticsPageLayout } from "@/shared/ui/statistics-page-layout";
|
import { StatisticsPageLayout } from "@/shared/ui/statistics-page-layout";
|
||||||
import { authService } from "@/entities/auth";
|
import { authService } from "@/entities/auth";
|
||||||
|
import { getToken } from "@/entities/auth/lib/token-manager";
|
||||||
import type { MenuItem } from "@/entities/menu";
|
import type { MenuItem } from "@/entities/menu";
|
||||||
import { MenuApi } from "@/entities/menu";
|
import { MenuApi } from "@/entities/menu";
|
||||||
|
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||||
import { BaseLayout } from "@common/components";
|
import { BaseLayout } from "@common/components";
|
||||||
import { getConfig } from "@common/config";
|
import { getConfig } from "@common/config";
|
||||||
|
|
||||||
@@ -93,6 +95,13 @@ function WithBaseLayoutInner({ children }: { children: React.ReactNode }) {
|
|||||||
enabled: isLoggedIn,
|
enabled: isLoggedIn,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!getToken(CLAIM_NAME)) return;
|
||||||
|
void statisticsApi.refreshAllCommonCodes().catch(() => {
|
||||||
|
// 캐시 동기화 실패는 화면 진입을 막지 않음
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (menuListData) {
|
if (menuListData) {
|
||||||
setMenuList(menuListData);
|
setMenuList(menuListData);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { Metadata } from "next";
|
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,47 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
import "@/app/styles/base.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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,9 @@ const subDomain =
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
|
experimental: {
|
||||||
|
globalNotFound: true,
|
||||||
|
},
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
"chartjs-plugin-zoom": "^2.2.0",
|
"chartjs-plugin-zoom": "^2.2.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"decimal.js": "^10.6.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lucide-react": "^0.474.0",
|
"lucide-react": "^0.474.0",
|
||||||
@@ -57,7 +58,6 @@
|
|||||||
"swr": "^2.3.7",
|
"swr": "^2.3.7",
|
||||||
"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",
|
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "^0.18.5",
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.24.2",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 483 KiB |
@@ -1,12 +1,14 @@
|
|||||||
|
import { cookies } from "next/headers";
|
||||||
|
import Script from "next/script";
|
||||||
|
|
||||||
|
import "@fontsource-variable/noto-sans-kr";
|
||||||
|
|
||||||
|
import { AppProvider } from "./providers/app-provider";
|
||||||
|
|
||||||
import "./styles/base.css";
|
import "./styles/base.css";
|
||||||
import "./styles/components.css";
|
import "./styles/components.css";
|
||||||
import "@fontsource-variable/noto-sans-kr";
|
|
||||||
import "realgrid/dist/realgrid-style.css";
|
import "realgrid/dist/realgrid-style.css";
|
||||||
import "../components/css/DataGrid.css";
|
import "../components/css/DataGrid.css";
|
||||||
|
|
||||||
import { cookies } from "next/headers";
|
|
||||||
import { AppProvider } from "./providers/app-provider";
|
|
||||||
import Script from "next/script";
|
|
||||||
const App = async ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
const App = async ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||||
const theme = (await cookies()).get("site-theme-key");
|
const theme = (await cookies()).get("site-theme-key");
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { DndProvider } from "react-dnd";
|
import { DndProvider } from "react-dnd";
|
||||||
import { MultiBackend, getBackendOptions } from "@minoru/react-dnd-treeview";
|
|
||||||
|
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
|
||||||
|
import { getBackendOptions,MultiBackend } from "@minoru/react-dnd-treeview";
|
||||||
|
import { AlertDialog as RsoneAlertDialog } from "@rsone/components";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||||
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
|
|
||||||
|
import { PromptProvider, StoreProvider } from "@/shared/providers";
|
||||||
|
import { AlertDialog } from "@/shared/ui/alert-dialog";
|
||||||
|
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||||
|
import { GlobalProgressOverlay } from "@/shared/ui/global-progress-overlay";
|
||||||
|
import { PromptDialog } from "@/shared/ui/prompt-dialog";
|
||||||
|
import { SelectPromptDialog } from "@/shared/ui/select-prompt-dialog";
|
||||||
|
import { Toaster } from "@/shared/ui/sonner";
|
||||||
import { AuthWrapper } from "@/entities/auth";
|
import { AuthWrapper } from "@/entities/auth";
|
||||||
import { MenuWrapper } from "@/entities/menu";
|
import { MenuWrapper } from "@/entities/menu";
|
||||||
import { Toaster } from "@/shared/ui/sonner";
|
|
||||||
import { AlertDialog } from "@/shared/ui/alert-dialog";
|
|
||||||
import { PromptDialog } from "@/shared/ui/prompt-dialog";
|
|
||||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
|
||||||
import { SelectPromptDialog } from "@/shared/ui/select-prompt-dialog";
|
|
||||||
import { GlobalProgressOverlay } from "@/shared/ui/global-progress-overlay";
|
|
||||||
import { PromptProvider, StoreProvider } from "@/shared/providers";
|
|
||||||
import { AlertDialog as RsoneAlertDialog } from "@rsone/components";
|
|
||||||
|
|
||||||
const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Button } from "@common/components";
|
|
||||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||||
|
|
||||||
|
import { Button } from "@common/components";
|
||||||
|
|
||||||
interface LastYearDataButtonProps {
|
interface LastYearDataButtonProps {
|
||||||
getLastYearDataFunc: () => void;
|
getLastYearDataFunc: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Checkbox, buttonVariants, cn } from "@common/components";
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
|
import { buttonVariants, Checkbox, cn } from "@common/components";
|
||||||
|
|
||||||
interface NotApplicableCheckButtonProp {
|
interface NotApplicableCheckButtonProp {
|
||||||
isCheck: boolean;
|
isCheck: boolean;
|
||||||
setIsCheck: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsCheck: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ import { Button } from "@common/components";
|
|||||||
|
|
||||||
interface SubmitButtonProps {
|
interface SubmitButtonProps {
|
||||||
submitFunc: () => void;
|
submitFunc: () => void;
|
||||||
|
/** 버튼 문구 (기본: 제출) */
|
||||||
|
text?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SubmitButton = ({ submitFunc }: SubmitButtonProps) => {
|
const SubmitButton = ({ submitFunc, text = "제출" }: SubmitButtonProps) => {
|
||||||
return (
|
return (
|
||||||
<Button className="h-[3.2rem] w-[96px]" onClick={submitFunc}>
|
<Button className="h-[3.2rem] min-w-[96px] px-4" onClick={submitFunc}>
|
||||||
<i className="diveicon di-upload" />
|
<i className="diveicon di-upload" />
|
||||||
<span>제출</span>
|
<span>{text}</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,22 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
|
|
||||||
|
/* RealGrid 기본은 white-space: nowrap → max-width만 주면 잘림. 너비 초과 시에만 줄바꿈 */
|
||||||
|
.rg-tooltip {
|
||||||
|
max-width: 60rem;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RealGrid Excel export — applyDynamicStyles가 class background-color를 읽을 수 있도록 */
|
||||||
|
.rg-cell-rowtype-biz-sum {
|
||||||
|
background-color: #ffe9cf !important;
|
||||||
|
}
|
||||||
|
.rg-cell-rowtype-summary {
|
||||||
|
background-color: #feee85 !important;
|
||||||
|
}
|
||||||
|
.rg-cell-rowtype-other {
|
||||||
|
background-color: #fff7bf !important;
|
||||||
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell {
|
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell {
|
||||||
@apply !bg-white;
|
@apply !bg-white;
|
||||||
@@ -8,6 +25,15 @@
|
|||||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly {
|
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly {
|
||||||
@apply !bg-[#f0f0f0] !text-[#666];
|
@apply !bg-[#f0f0f0] !text-[#666];
|
||||||
}
|
}
|
||||||
|
/* 고정 컬럼 — readonly와 동일한 화면 스타일 (엑셀 export 시 exportStyleName에서 제외) */
|
||||||
|
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly,
|
||||||
|
.rg-grid .rg-body .rg-data-cell.rg-cell-readonly {
|
||||||
|
@apply !bg-[#f0f0f0] !text-[#666];
|
||||||
|
}
|
||||||
|
.rg-grid .rg-fixed-body .rg-table .rg-data-row td.rg-cell-readonly,
|
||||||
|
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-readonly {
|
||||||
|
@apply !bg-[#f0f0f0] !text-[#666];
|
||||||
|
}
|
||||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly.rg-cell-rowtype-curr {
|
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly.rg-cell-rowtype-curr {
|
||||||
@apply !bg-white !text-[#333];
|
@apply !bg-white !text-[#333];
|
||||||
}
|
}
|
||||||
@@ -17,6 +43,15 @@
|
|||||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error-imr {
|
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error-imr {
|
||||||
@apply !bg-[#fff7bf] !text-[#b45309];
|
@apply !bg-[#fff7bf] !text-[#b45309];
|
||||||
}
|
}
|
||||||
|
/* 총계 footer 검증 오류 (commonInfo.verificationResult) */
|
||||||
|
.rg-grid .rg-footer .rg-table .rg-footer-cell.rg-cell-verify-error,
|
||||||
|
.rg-grid .rg-footers .rg-table .rg-footer-cell.rg-cell-verify-error {
|
||||||
|
@apply !bg-[#ffe6e6] !text-[#c00];
|
||||||
|
}
|
||||||
|
.rg-grid .rg-footer .rg-table .rg-footer-cell.rg-cell-verify-error-imr,
|
||||||
|
.rg-grid .rg-footers .rg-table .rg-footer-cell.rg-cell-verify-error-imr {
|
||||||
|
@apply !bg-[#fff7bf] !text-[#b45309];
|
||||||
|
}
|
||||||
/* 검증결과 내역: 처리여부(prcsYn 0·4 미처리 등) */
|
/* 검증결과 내역: 처리여부(prcsYn 0·4 미처리 등) */
|
||||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-bad {
|
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-bad {
|
||||||
@apply !font-semibold !text-[#dc2626];
|
@apply !font-semibold !text-[#dc2626];
|
||||||
@@ -399,6 +434,13 @@
|
|||||||
background-color: #ffe6e6 !important;
|
background-color: #ffe6e6 !important;
|
||||||
color: #c00 !important;
|
color: #c00 !important;
|
||||||
}
|
}
|
||||||
|
.rg-grid .rg-footer .rg-footer-cell.rg-cell-verify-error.rg-cell-verify-error,
|
||||||
|
.rg-grid .rg-footers .rg-footer-cell.rg-cell-verify-error.rg-cell-verify-error,
|
||||||
|
.rg-grid .rg-footer .rg-table .rg-footer-cell.rg-cell-verify-error.rg-cell-verify-error,
|
||||||
|
.rg-grid .rg-footers .rg-table .rg-footer-cell.rg-cell-verify-error.rg-cell-verify-error {
|
||||||
|
background-color: #ffe6e6 !important;
|
||||||
|
color: #c00 !important;
|
||||||
|
}
|
||||||
.rg-grid .rg-body .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
|
.rg-grid .rg-body .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
|
||||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
|
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
|
||||||
.rg-grid .rg-fixed-body .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
|
.rg-grid .rg-fixed-body .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
|
||||||
@@ -406,6 +448,13 @@
|
|||||||
background-color: #fff7bf !important;
|
background-color: #fff7bf !important;
|
||||||
color: #b45309 !important;
|
color: #b45309 !important;
|
||||||
}
|
}
|
||||||
|
.rg-grid .rg-footer .rg-footer-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
|
||||||
|
.rg-grid .rg-footers .rg-footer-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
|
||||||
|
.rg-grid .rg-footer .rg-table .rg-footer-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
|
||||||
|
.rg-grid .rg-footers .rg-table .rg-footer-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr {
|
||||||
|
background-color: #fff7bf !important;
|
||||||
|
color: #b45309 !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* dark theme에서도 검증 오류 색을 유지하고 싶다면 동일 색 유지(가독성 우선) */
|
/* dark theme에서도 검증 오류 색을 유지하고 싶다면 동일 색 유지(가독성 우선) */
|
||||||
[data-theme="black"] .rg-grid .rg-body .rg-data-cell.rg-cell-verify-error.rg-cell-verify-error,
|
[data-theme="black"] .rg-grid .rg-body .rg-data-cell.rg-cell-verify-error.rg-cell-verify-error,
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { useRef, useEffect, useLayoutEffect } from "react"
|
import { useEffect, useLayoutEffect,useRef } from "react"
|
||||||
import { ValueType } from "realgrid"
|
|
||||||
|
|
||||||
|
import { ValueType } from "realgrid"
|
||||||
import RealGrid, { GridFitStyle } from "realgrid"
|
import RealGrid, { GridFitStyle } from "realgrid"
|
||||||
import { RealGridReact, RGDataColumn } from "realgrid-react"
|
import { RealGridReact, RGDataColumn } from "realgrid-react"
|
||||||
import type { DataGridProps, RealGridColumnInfo, GridViewWithSetColumns, FooterItemWithCallback, CellStyleCallback, CellStyleCallbackResult, CellStyleCallbackParams } from "../type/DataGrid.type"
|
|
||||||
|
import type { CellStyleCallback, CellStyleCallbackParams,CellStyleCallbackResult, DataGridProps, FooterItemWithCallback, GridViewWithSetColumns, RealGridColumnInfo } from "../type/DataGrid.type"
|
||||||
|
|
||||||
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"
|
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"
|
||||||
|
import { wrapWithFormatTruncate } from "@/components/util/grid/decimalNumber"
|
||||||
|
|
||||||
function mergeCellStyleCallbackResult(
|
function mergeCellStyleCallbackResult(
|
||||||
baseStyleName: string | undefined,
|
baseStyleName: string | undefined,
|
||||||
@@ -30,7 +33,7 @@ declare module "realgrid" {
|
|||||||
setOptions(options: {
|
setOptions(options: {
|
||||||
footer?: { visible?: boolean };
|
footer?: { visible?: boolean };
|
||||||
summaryMode?: 'aggregate' | 'statistical' | 'none';
|
summaryMode?: 'aggregate' | 'statistical' | 'none';
|
||||||
checkBar?: { visible?: boolean };
|
checkBar?: { visible?: boolean; checkableOnly?: boolean };
|
||||||
}): void;
|
}): void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,10 +50,11 @@ type GridViewWithCheckApi = GridViewWithSetColumns & {
|
|||||||
checkItem?: (itemIndex: number, checked: boolean, exclusive?: boolean) => void;
|
checkItem?: (itemIndex: number, checked: boolean, exclusive?: boolean) => void;
|
||||||
checkRow?: (dataRow: number, checked: boolean, exclusive?: boolean) => void;
|
checkRow?: (dataRow: number, checked: boolean, exclusive?: boolean) => void;
|
||||||
checkAll?: (checked: boolean) => void;
|
checkAll?: (checked: boolean) => void;
|
||||||
|
setCheckable?: (itemIndex: number, checkable: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** columnInfo → RealGrid DataField 배열 (computed → valueCallback 포함, setFields용) */
|
/** columnInfo → RealGrid DataField 배열 (computed → valueCallback 포함, setFields용) */
|
||||||
function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueType; computed?: import("../type/DataGrid.type").ComputedFieldConfig }[]) {
|
function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueType; dataFormat?: string; computed?: import("../type/DataGrid.type").ComputedFieldConfig }[]) {
|
||||||
return columns.map((col) => {
|
return columns.map((col) => {
|
||||||
const field: { fieldName: string; dataType: string; valueCallback?: (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number } = {
|
const field: { fieldName: string; dataType: string; valueCallback?: (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number } = {
|
||||||
fieldName: col.fieldName,
|
fieldName: col.fieldName,
|
||||||
@@ -66,7 +70,7 @@ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueT
|
|||||||
const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0;
|
const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0;
|
||||||
const hasSumPerTenThousand = col.computed.type === "sumPerTenThousand" && "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorField" in col.computed && Boolean(col.computed.denominatorField);
|
const hasSumPerTenThousand = col.computed.type === "sumPerTenThousand" && "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorField" in col.computed && Boolean(col.computed.denominatorField);
|
||||||
if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups || hasSumPerTenThousand) {
|
if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups || hasSumPerTenThousand) {
|
||||||
field.valueCallback = getComputedValueCallback(col.computed) as (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number;
|
field.valueCallback = wrapWithFormatTruncate(getComputedValueCallback(col.computed), col.dataFormat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return field;
|
return field;
|
||||||
@@ -136,6 +140,7 @@ const CompactDataGrid = ({
|
|||||||
onRowClick,
|
onRowClick,
|
||||||
onItemChecked,
|
onItemChecked,
|
||||||
onItemAllChecked,
|
onItemAllChecked,
|
||||||
|
checkableCallback,
|
||||||
compactFullWidth = false,
|
compactFullWidth = false,
|
||||||
compactGridHeightClass = "h-[55vh]",
|
compactGridHeightClass = "h-[55vh]",
|
||||||
headerHeight = 60,
|
headerHeight = 60,
|
||||||
@@ -145,9 +150,11 @@ const CompactDataGrid = ({
|
|||||||
const columnsRef = useRef(columns);
|
const columnsRef = useRef(columns);
|
||||||
const rowDataRef = useRef<Record<string, string | number>[]>(rowData);
|
const rowDataRef = useRef<Record<string, string | number>[]>(rowData);
|
||||||
const cellStyleCallbackRef = useRef<CellStyleCallback | undefined>(cellStyleCallback);
|
const cellStyleCallbackRef = useRef<CellStyleCallback | undefined>(cellStyleCallback);
|
||||||
|
const checkableCallbackRef = useRef<DataGridProps["checkableCallback"]>(checkableCallback);
|
||||||
columnsRef.current = columns;
|
columnsRef.current = columns;
|
||||||
rowDataRef.current = rowData;
|
rowDataRef.current = rowData;
|
||||||
cellStyleCallbackRef.current = cellStyleCallback;
|
cellStyleCallbackRef.current = cellStyleCallback;
|
||||||
|
checkableCallbackRef.current = checkableCallback;
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!cellStyleCallback || !gridRef.current?.gridView) return;
|
if (!cellStyleCallback || !gridRef.current?.gridView) return;
|
||||||
@@ -176,8 +183,10 @@ const CompactDataGrid = ({
|
|||||||
try {
|
try {
|
||||||
checkGrid.checkAll?.(false);
|
checkGrid.checkAll?.(false);
|
||||||
rowData.forEach((row, itemIndex) => {
|
rowData.forEach((row, itemIndex) => {
|
||||||
|
const checkable = checkableCallbackRef.current?.(row, itemIndex) ?? true;
|
||||||
|
checkGrid.setCheckable?.(itemIndex, checkable);
|
||||||
const checked = checkByRowFlag((row as Record<string, unknown>).__checked);
|
const checked = checkByRowFlag((row as Record<string, unknown>).__checked);
|
||||||
if (!checked) return;
|
if (!checked || !checkable) return;
|
||||||
if (checkGrid.checkItem) {
|
if (checkGrid.checkItem) {
|
||||||
checkGrid.checkItem(itemIndex, true, false);
|
checkGrid.checkItem(itemIndex, true, false);
|
||||||
return;
|
return;
|
||||||
@@ -274,7 +283,7 @@ const CompactDataGrid = ({
|
|||||||
|
|
||||||
// 헤더 체크박스 표시 여부 설정
|
// 헤더 체크박스 표시 여부 설정
|
||||||
gridView.setOptions({
|
gridView.setOptions({
|
||||||
checkBar: { visible: showHeaderCheckbox },
|
checkBar: { visible: showHeaderCheckbox, checkableOnly: true },
|
||||||
rowIndicator: { visible: false },
|
rowIndicator: { visible: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -306,6 +315,7 @@ const CompactDataGrid = ({
|
|||||||
if (fixedColumns !== undefined && fixedColumns > 0) {
|
if (fixedColumns !== undefined && fixedColumns > 0) {
|
||||||
gridView.setFixedOptions({
|
gridView.setFixedOptions({
|
||||||
colCount: fixedColumns,
|
colCount: fixedColumns,
|
||||||
|
resizable: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,6 +506,7 @@ const CompactDataGrid = ({
|
|||||||
compactFullWidth,
|
compactFullWidth,
|
||||||
compactGridHeightClass,
|
compactGridHeightClass,
|
||||||
headerHeight,
|
headerHeight,
|
||||||
|
checkableCallback,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 행 클릭 이벤트
|
// 행 클릭 이벤트
|
||||||
@@ -524,6 +535,17 @@ const CompactDataGrid = ({
|
|||||||
|
|
||||||
if (onItemChecked) {
|
if (onItemChecked) {
|
||||||
gridView.onItemChecked = (_grid, itemIndex, checked) => {
|
gridView.onItemChecked = (_grid, itemIndex, checked) => {
|
||||||
|
const row = rowDataRef.current[itemIndex];
|
||||||
|
if (
|
||||||
|
row &&
|
||||||
|
checkableCallbackRef.current &&
|
||||||
|
!checkableCallbackRef.current(row, itemIndex)
|
||||||
|
) {
|
||||||
|
if (checked) {
|
||||||
|
(_grid as GridViewWithCheckApi).checkItem?.(itemIndex, false, false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
onItemChecked(itemIndex, checked);
|
onItemChecked(itemIndex, checked);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,6 @@ import { type Dispatch, type MutableRefObject, type SetStateAction, useCallback,
|
|||||||
|
|
||||||
import { RealGridReact } from "realgrid-react";
|
import { RealGridReact } from "realgrid-react";
|
||||||
|
|
||||||
import { getUserInfo } from "../../../../../packages/common/components/src/api";
|
|
||||||
import { useAlertStore } from "@/shared/lib";
|
import { useAlertStore } from "@/shared/lib";
|
||||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||||
import type {
|
import type {
|
||||||
@@ -29,6 +28,8 @@ import {
|
|||||||
Textarea,
|
Textarea,
|
||||||
} from "@common/components";
|
} from "@common/components";
|
||||||
|
|
||||||
|
import { getUserInfo } from "../../../../../packages/common/components/src/api";
|
||||||
|
|
||||||
import DataGrid from "@/components/module/DataGrid";
|
import DataGrid from "@/components/module/DataGrid";
|
||||||
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
|
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
|
||||||
import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type";
|
import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type";
|
||||||
@@ -56,8 +57,8 @@ type ExceptionHistoryTabKey = "NV" | "IMR" | "HB";
|
|||||||
|
|
||||||
const exceptionHistoryTabs: { value: ExceptionHistoryTabKey; label: string }[] = [
|
const exceptionHistoryTabs: { value: ExceptionHistoryTabKey; label: string }[] = [
|
||||||
{ value: "NV", label: "일반검증 내역" },
|
{ value: "NV", label: "일반검증 내역" },
|
||||||
{ value: "IMR", label: "I-MR 검증내역" },
|
{ value: "IMR", label: "시계열 검증내역" },
|
||||||
{ value: "HB", label: "HB 검증내역" },
|
{ value: "HB", label: "모집단 검증내역" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const TITLE = "예외처리 내역 조회";
|
const TITLE = "예외처리 내역 조회";
|
||||||
@@ -274,7 +275,7 @@ export function ExceptionProcessingHistoryDialog({
|
|||||||
setImrPageCount(response?.data?.pageInfo?.totalPages ?? 1);
|
setImrPageCount(response?.data?.pageInfo?.totalPages ?? 1);
|
||||||
setImrPageIndex(pageIndex);
|
setImrPageIndex(pageIndex);
|
||||||
} catch {
|
} catch {
|
||||||
setErrorMessage("I-MR 검증내역 조회 중 오류가 발생했습니다.");
|
setErrorMessage("시계열 검증내역 조회 중 오류가 발생했습니다.");
|
||||||
} finally {
|
} finally {
|
||||||
setImrLoading(false);
|
setImrLoading(false);
|
||||||
}
|
}
|
||||||
@@ -293,7 +294,7 @@ export function ExceptionProcessingHistoryDialog({
|
|||||||
setHbPageCount(response?.data?.pageInfo?.totalPages ?? 1);
|
setHbPageCount(response?.data?.pageInfo?.totalPages ?? 1);
|
||||||
setHbPageIndex(pageIndex);
|
setHbPageIndex(pageIndex);
|
||||||
} catch {
|
} catch {
|
||||||
setErrorMessage("HB 검증내역 조회 중 오류가 발생했습니다.");
|
setErrorMessage("모집단 검증내역 조회 중 오류가 발생했습니다.");
|
||||||
} finally {
|
} finally {
|
||||||
setHbLoading(false);
|
setHbLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle } from "@common/components";
|
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle } from "@common/components";
|
||||||
|
|
||||||
import SidoSggFacilityNameSearchForm from "./SidoSggFacilityNameSearchForm";
|
|
||||||
import type { SearchFormValues } from "../type/SearchForm.type";
|
import type { SearchFormValues } from "../type/SearchForm.type";
|
||||||
|
|
||||||
|
import SidoSggFacilityNameSearchForm from "./SidoSggFacilityNameSearchForm";
|
||||||
|
|
||||||
type GridTopActionButtonProps = {
|
type GridTopActionButtonProps = {
|
||||||
text: string;
|
text: string;
|
||||||
onClick?: (helpers: { openDialog: (title?: string) => void; closeDialog: () => void }) => void;
|
onClick?: (helpers: { openDialog: (title?: string) => void; closeDialog: () => void }) => void;
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import { useEffect, useState } from "react";
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||||
|
import {
|
||||||
|
fetchSearchCondition,
|
||||||
|
fetchSearchConditionInput,
|
||||||
|
SEARCH_CONDITION_SWR_KEY,
|
||||||
|
} from "@/shared/lib/use-search-condition-sido-list";
|
||||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||||
import type {
|
import type {
|
||||||
ApiResponse,
|
ApiResponse,
|
||||||
@@ -23,28 +28,43 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@common/components";
|
} from "@common/components";
|
||||||
|
|
||||||
|
import { doList, doNameList } from "@/components/util/column/input/DoList";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
defaultSearchFormProps,
|
defaultSearchFormProps,
|
||||||
type SearchFormClsfCd,
|
type SearchFormClsfCd,
|
||||||
type SearchFormProps,
|
type SearchFormProps,
|
||||||
type SearchFormValues,
|
type SearchFormValues,
|
||||||
} from "../type/SearchForm.type";
|
} from "../type/SearchForm.type";
|
||||||
import { doList } from "../util/column/input/DoList";
|
|
||||||
|
|
||||||
const getSidoListFetcher = async (
|
function isDoListBsnm(bsnmCd: string | undefined): boolean {
|
||||||
key: [string, string],
|
return Boolean(bsnmCd && doList.includes(bsnmCd));
|
||||||
): Promise<ApiResponse<SearchCondition>> => {
|
}
|
||||||
return statisticsApi.getSidoList({ instSeCd: key[1] });
|
|
||||||
};
|
function isDoSido(sido?: { bsnmCd?: string; sido?: string }): boolean {
|
||||||
|
if (!sido) return false;
|
||||||
|
if (isDoListBsnm(sido.bsnmCd)) return true;
|
||||||
|
return Boolean(sido.sido && doNameList.includes(sido.sido));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAllGugunValue(gugun: string | undefined): boolean {
|
||||||
|
const value = (gugun ?? "").trim();
|
||||||
|
return !value || value === "ALL" || value === "전체";
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstConcreteSgg(list: Sgg[] | undefined): Sgg | undefined {
|
||||||
|
return (list ?? []).find((sgg) => !isAllGugunValue(sgg.gugun));
|
||||||
|
}
|
||||||
|
|
||||||
const getSggListFetcher = async (
|
const getSggListFetcher = async (
|
||||||
key: [string, string, string, string],
|
key: [string, string, string, "Y" | "N"],
|
||||||
): Promise<ApiResponse<Sgg[]>> => {
|
): Promise<ApiResponse<Sgg[]>> => {
|
||||||
const gugunSkipYn = doList.includes(key[3]) ? "N" : "Y";
|
const [kind, sidoName, instSeCd, gugunSkipYn] = key;
|
||||||
return statisticsApi.getSggList(key[1], {
|
const params = { instSeCd, gugunSkipYn };
|
||||||
instSeCd: key[2],
|
if (kind === "sgg-all") {
|
||||||
gugunSkipYn,
|
return statisticsApi.getSggAllList(sidoName, params);
|
||||||
});
|
}
|
||||||
|
return statisticsApi.getSggList(sidoName, params);
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildSearchFormValues(
|
function buildSearchFormValues(
|
||||||
@@ -59,9 +79,15 @@ function buildSearchFormValues(
|
|||||||
: "ALL";
|
: "ALL";
|
||||||
|
|
||||||
if (d?.sido) {
|
if (d?.sido) {
|
||||||
|
const sidoItem = sidoList?.data?.sidoList?.find(
|
||||||
|
(item) => item.sido === d.sido,
|
||||||
|
);
|
||||||
|
const gugun =
|
||||||
|
d.gugun ||
|
||||||
|
(sidoItem && !isDoSido(sidoItem) ? "ALL" : "");
|
||||||
return {
|
return {
|
||||||
sido: d.sido,
|
sido: d.sido,
|
||||||
gugun: d.gugun || "",
|
gugun,
|
||||||
year: d.year || defaultYear.toString(),
|
year: d.year || defaultYear.toString(),
|
||||||
order: d.order || "",
|
order: d.order || "",
|
||||||
fctName: d.fctName || "",
|
fctName: d.fctName || "",
|
||||||
@@ -69,6 +95,22 @@ function buildSearchFormValues(
|
|||||||
isDetail: false,
|
isDetail: false,
|
||||||
isSummary: d.isSummary || "",
|
isSummary: d.isSummary || "",
|
||||||
clsfCd: initialClsfCd,
|
clsfCd: initialClsfCd,
|
||||||
|
capacityGbn: d.capacityGbn || "ALL",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.defaultRegionAll) {
|
||||||
|
return {
|
||||||
|
sido: "ALL",
|
||||||
|
gugun: "ALL",
|
||||||
|
year: d?.year || defaultYear.toString(),
|
||||||
|
order: d?.order || "",
|
||||||
|
fctName: d?.fctName || "",
|
||||||
|
bsnmCd: "",
|
||||||
|
isDetail: false,
|
||||||
|
isSummary: d?.isSummary || "",
|
||||||
|
clsfCd: initialClsfCd,
|
||||||
|
capacityGbn: d?.capacityGbn || "ALL",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +119,8 @@ function buildSearchFormValues(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
sido: useFirstSido ? first.sido : "",
|
sido: useFirstSido ? first.sido : "",
|
||||||
gugun: "",
|
gugun:
|
||||||
|
useFirstSido && first && !isDoSido(first) ? "ALL" : "",
|
||||||
year: d?.year || defaultYear.toString(),
|
year: d?.year || defaultYear.toString(),
|
||||||
order: d?.order || "",
|
order: d?.order || "",
|
||||||
fctName: d?.fctName || "",
|
fctName: d?.fctName || "",
|
||||||
@@ -85,6 +128,7 @@ function buildSearchFormValues(
|
|||||||
isDetail: false,
|
isDetail: false,
|
||||||
isSummary: d?.isSummary || "",
|
isSummary: d?.isSummary || "",
|
||||||
clsfCd: initialClsfCd,
|
clsfCd: initialClsfCd,
|
||||||
|
capacityGbn: d?.capacityGbn || "ALL",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,10 +144,12 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
|||||||
|
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
const defaultYear = currentYear - 1;
|
const defaultYear = currentYear - 1;
|
||||||
|
const isInput = Boolean(props.isInput);
|
||||||
|
const sidoListKind = isInput ? "input" : "default";
|
||||||
|
|
||||||
const { data: sidoList } = useSWR<ApiResponse<SearchCondition>>(
|
const { data: sidoList } = useSWR<ApiResponse<SearchCondition>>(
|
||||||
["sido-list", instSeCd],
|
SEARCH_CONDITION_SWR_KEY(instSeCd, sidoListKind),
|
||||||
getSidoListFetcher,
|
isInput ? fetchSearchConditionInput : fetchSearchCondition,
|
||||||
{
|
{
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
revalidateOnReconnect: false,
|
revalidateOnReconnect: false,
|
||||||
@@ -130,6 +176,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
|||||||
|
|
||||||
// 최초 진입만 시도 API가 늦게 오는 경우
|
// 최초 진입만 시도 API가 늦게 오는 경우
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (props.defaultRegionAll) return;
|
||||||
if (props.defaultValues?.sido) return;
|
if (props.defaultValues?.sido) return;
|
||||||
const first = sidoList?.data?.sidoList?.[0];
|
const first = sidoList?.data?.sidoList?.[0];
|
||||||
if (!first || !mergedProps.sido) return;
|
if (!first || !mergedProps.sido) return;
|
||||||
@@ -137,17 +184,33 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
|||||||
prev.sido
|
prev.sido
|
||||||
? prev
|
? prev
|
||||||
: {
|
: {
|
||||||
...prev,
|
...prev,
|
||||||
sido: first.sido,
|
sido: first.sido,
|
||||||
gugun: "",
|
gugun: isDoSido(first) ? "" : "ALL",
|
||||||
bsnmCd: first.bsnmCd,
|
bsnmCd: first.bsnmCd,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}, [sidoList, props.defaultValues?.sido, mergedProps.sido]);
|
}, [
|
||||||
|
sidoList,
|
||||||
|
props.defaultValues?.sido,
|
||||||
|
props.defaultRegionAll,
|
||||||
|
mergedProps.sido,
|
||||||
|
]);
|
||||||
|
|
||||||
const sggKey: [string, string, string, string] | null =
|
const selectedSido = sidoList?.data?.sidoList?.find(
|
||||||
values.sido && values.sido !== "ALL"
|
(item) => item.sido === values.sido,
|
||||||
? ["sgg-list", values.sido, instSeCd, values.bsnmCd || ""]
|
);
|
||||||
|
const gugunSkipYn = selectedSido?.gugunSkipYn ?? "Y";
|
||||||
|
const useSggAllList =
|
||||||
|
gugunSkipYn === "N" && !isDoListBsnm(selectedSido?.bsnmCd);
|
||||||
|
const sggKey: [string, string, string, "Y" | "N"] | null =
|
||||||
|
values.sido && values.sido !== "ALL" && sidoList?.data
|
||||||
|
? [
|
||||||
|
useSggAllList ? "sgg-all" : "sgg-list",
|
||||||
|
values.sido,
|
||||||
|
instSeCd,
|
||||||
|
gugunSkipYn,
|
||||||
|
]
|
||||||
: null;
|
: null;
|
||||||
const { data: sggList } = useSWR<ApiResponse<Sgg[]>>(
|
const { data: sggList } = useSWR<ApiResponse<Sgg[]>>(
|
||||||
sggKey,
|
sggKey,
|
||||||
@@ -161,39 +224,60 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
|||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (data.status !== "success") {
|
if (data.status !== "success") {
|
||||||
alert(`시군구 목록을 불러오는데 실패했습니다.
|
alert(`시군구 목록을 불러오는데 실패했습니다.
|
||||||
새로고침 후 다시 시도해주세요.`);
|
새로고침 후 다시 시도해주세요.`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
alert(`시군구 목록을 불러오는데 실패했습니다.
|
alert(`시군구 목록을 불러오는데 실패했습니다.
|
||||||
새로고침 후 다시 시도해주세요.`);
|
새로고침 후 다시 시도해주세요.`);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const sggOptions = sggList?.data ?? [];
|
const sggOptions = sggList?.data ?? [];
|
||||||
|
const isDoSelected = isDoSido(selectedSido);
|
||||||
|
const firstConcreteSggOption = firstConcreteSgg(sggOptions);
|
||||||
|
// 도: 빈 값일 때만 첫 시군구로 보이게. ALL은 사용자가 고른 전체 유지
|
||||||
|
const gugunSelectValue =
|
||||||
|
isDoSelected && !values.gugun
|
||||||
|
? (firstConcreteSggOption?.gugun ?? undefined)
|
||||||
|
: values.gugun || undefined;
|
||||||
|
|
||||||
// 시도 설정 후 시군구 목록이 오면 첫 번째 항목 자동 선택
|
// 도: 초기/시도 변경(빈 값)만 첫 시군구. 전체(ALL)는 유지. 그 외: 전체
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mergedProps.gugun) return;
|
if (!mergedProps.gugun) return;
|
||||||
if (props.defaultValues?.gugun) return;
|
if (props.defaultRegionAll) return;
|
||||||
if (!values.sido || values.sido === "ALL") return;
|
if (!values.sido || values.sido === "ALL") return;
|
||||||
|
if (!selectedSido) return;
|
||||||
|
|
||||||
const firstSgg = sggList?.data?.[0];
|
if (!isDoSelected) {
|
||||||
if (!firstSgg) return;
|
setValues((prev) => {
|
||||||
|
if (prev.gugun) return prev;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
gugun: "ALL",
|
||||||
|
bsnmCd: selectedSido.bsnmCd || prev.bsnmCd,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firstConcreteSggOption) return;
|
||||||
|
|
||||||
setValues((prev) => {
|
setValues((prev) => {
|
||||||
if (prev.gugun) return prev;
|
if (prev.gugun) return prev;
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
gugun: firstSgg.gugun,
|
gugun: firstConcreteSggOption.gugun,
|
||||||
bsnmCd: firstSgg.bsnmCd,
|
bsnmCd: firstConcreteSggOption.bsnmCd,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
sggList,
|
firstConcreteSggOption,
|
||||||
values.sido,
|
isDoSelected,
|
||||||
mergedProps.gugun,
|
mergedProps.gugun,
|
||||||
props.defaultValues?.gugun,
|
props.defaultRegionAll,
|
||||||
|
selectedSido,
|
||||||
|
values.sido,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
@@ -207,12 +291,17 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSidoChange = (value: string) => {
|
const handleSidoChange = (value: string) => {
|
||||||
const selectedBsnm =
|
const selected =
|
||||||
sidoList?.data?.sidoList?.find((v) => v.sido === value)?.bsnmCd || "";
|
value === "ALL"
|
||||||
|
? undefined
|
||||||
|
: sidoList?.data?.sidoList?.find((v) => v.sido === value);
|
||||||
|
const selectedBsnm = selected?.bsnmCd || "";
|
||||||
|
const gugunAll =
|
||||||
|
props.defaultRegionAll || value === "ALL" || !isDoSido(selected);
|
||||||
setValues((prev) => ({
|
setValues((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
sido: value,
|
sido: value,
|
||||||
gugun: "",
|
gugun: gugunAll ? "ALL" : "",
|
||||||
bsnmCd: selectedBsnm,
|
bsnmCd: selectedBsnm,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
@@ -222,7 +311,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
|||||||
const selectedBsnm =
|
const selectedBsnm =
|
||||||
value === "ALL"
|
value === "ALL"
|
||||||
? sidoList?.data?.sidoList?.find((v) => v.sido === values.sido)
|
? sidoList?.data?.sidoList?.find((v) => v.sido === values.sido)
|
||||||
?.bsnmCd || ""
|
?.bsnmCd || ""
|
||||||
: listForBsnm.find((v) => v.gugun === value)?.bsnmCd || "";
|
: listForBsnm.find((v) => v.gugun === value)?.bsnmCd || "";
|
||||||
|
|
||||||
setValues((prev) => ({
|
setValues((prev) => ({
|
||||||
@@ -272,7 +361,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Label className="text-label">시군구</Label>
|
<Label className="text-label">시군구</Label>
|
||||||
<Select
|
<Select
|
||||||
value={values.gugun}
|
value={gugunSelectValue}
|
||||||
onValueChange={handleSggChange}
|
onValueChange={handleSggChange}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[12rem]">
|
<SelectTrigger className="w-[12rem]">
|
||||||
@@ -362,9 +451,36 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{mergedProps.capacityGbn && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Label className="text-label">구분</Label>
|
||||||
|
<Select
|
||||||
|
value={values.capacityGbn}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setValues({ ...values, capacityGbn: value })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[10rem]">
|
||||||
|
<SelectValue placeholder="전체"></SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="ALL">전체</SelectItem>
|
||||||
|
<SelectItem value="500톤이상">
|
||||||
|
500톤이상
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="500톤미만">
|
||||||
|
500톤미만
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{mergedProps.isDetail && (
|
{mergedProps.isDetail && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Label className="text-label cursor-pointer" htmlFor="isDetail">
|
<Label
|
||||||
|
className="cursor-pointer text-label"
|
||||||
|
htmlFor="isDetail"
|
||||||
|
>
|
||||||
세부 데이터 포함
|
세부 데이터 포함
|
||||||
</Label>
|
</Label>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -381,7 +497,11 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
|||||||
)}
|
)}
|
||||||
{mergedProps.isSummary && (
|
{mergedProps.isSummary && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Label className="text-label cursor-pointer">
|
<Label
|
||||||
|
className="cursor-pointer text-label"
|
||||||
|
title="시·군·구 및 읍·면·동 변경에 따라 인구 및 관로 등 공표된 통계수치와 일부 상이할 수 있으니 유의 바랍니다."
|
||||||
|
>
|
||||||
|
<i className="diveicon di-exclamation-circle mr-1" />
|
||||||
요약표 포함
|
요약표 포함
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
@@ -394,11 +514,17 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
|||||||
<SelectValue placeholder="선택 안함"></SelectValue>
|
<SelectValue placeholder="선택 안함"></SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="ALL">선택 안함</SelectItem>
|
<SelectItem value="ALL">
|
||||||
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => (
|
선택 안함
|
||||||
<SelectItem value={i.toString()} key={i.toString()}>{`${i}개년`}</SelectItem>
|
</SelectItem>
|
||||||
))
|
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(
|
||||||
}
|
(i) => (
|
||||||
|
<SelectItem
|
||||||
|
value={i.toString()}
|
||||||
|
key={i.toString()}
|
||||||
|
>{`${i}개년`}</SelectItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -433,7 +559,9 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button className="h-[3.2rem] w-[96px]" type="submit">
|
<Button className="h-[3.2rem] w-[96px]" type="submit">
|
||||||
{props.searchButtonText ? props.searchButtonText : '조회'}
|
{props.searchButtonText
|
||||||
|
? props.searchButtonText
|
||||||
|
: "조회"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,41 +1,42 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import useSWR from "swr";
|
|
||||||
import { ValueType } from "realgrid";
|
|
||||||
|
|
||||||
import {
|
import { ValueType } from "realgrid";
|
||||||
Button,
|
import useSWR from "swr";
|
||||||
Input,
|
|
||||||
Label,
|
|
||||||
Select,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectValue,
|
|
||||||
LoadingWithAnimation,
|
|
||||||
} from "@common/components";
|
|
||||||
import CompactDataGrid from "./CompactDataGrid";
|
|
||||||
import SaveButton from "../button/SaveButton";
|
|
||||||
|
|
||||||
import { useAlertStore } from "@/shared/lib";
|
import { useAlertStore } from "@/shared/lib";
|
||||||
|
import { statisticsInputSewPipeApiService } from "@/entities/statistics/api/input/statistics-sewage-pipe-input-service";
|
||||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||||
|
import { isGugunRequiredSido } from "@/entities/statistics";
|
||||||
|
import type {
|
||||||
|
SeweragePipeFacilitiesListRequest,
|
||||||
|
SeweragePipeFacilityAdminAreaListRequest,
|
||||||
|
SeweragePipeFactConnectionSaveItem,
|
||||||
|
SeweragePipeFactConnectionSaveRequest,
|
||||||
|
} from "@/entities/statistics/model/input/statistics-sewage-pipe-input-interface";
|
||||||
import type {
|
import type {
|
||||||
ApiResponse,
|
ApiResponse,
|
||||||
SearchCondition,
|
SearchCondition,
|
||||||
Sgg,
|
Sgg,
|
||||||
} from "@/entities/statistics/model/statistics-interface";
|
} from "@/entities/statistics/model/statistics-interface";
|
||||||
import type { ColumnInfo } from "../type/DataGrid.type";
|
import {
|
||||||
import type {
|
Button,
|
||||||
SeweragePipeFacilitiesListRequest,
|
Input,
|
||||||
SeweragePipeFacilityAdminAreaListRequest,
|
Label,
|
||||||
SeweragePipeFactConnectionSaveRequest,
|
LoadingWithAnimation,
|
||||||
SeweragePipeFactConnectionSaveItem,
|
Select,
|
||||||
} from "@/entities/statistics/model/input/statistics-sewage-pipe-input-interface";
|
SelectContent,
|
||||||
import { statisticsInputSewPipeApiService } from "@/entities/statistics/api/input/statistics-sewage-pipe-input-service";
|
SelectItem,
|
||||||
import type { SearchFormValues } from "../type/SearchForm.type";
|
SelectTrigger,
|
||||||
import { doNameList } from "../util/column/input/DoList";
|
SelectValue,
|
||||||
|
} from "@common/components";
|
||||||
|
|
||||||
import { getUserInfo } from "../../../../../packages/common/components/src/api";
|
import { getUserInfo } from "../../../../../packages/common/components/src/api";
|
||||||
|
import type { ColumnInfo } from "../type/DataGrid.type";
|
||||||
|
import type { SearchFormValues } from "../type/SearchForm.type";
|
||||||
|
|
||||||
|
import CompactDataGrid from "./CompactDataGrid";
|
||||||
|
|
||||||
/** 다이얼로그 좌측: 처리시설 정보 (순번 없음) */
|
/** 다이얼로그 좌측: 처리시설 정보 (순번 없음) */
|
||||||
const treatmentFacilityColumnInfo: ColumnInfo = {
|
const treatmentFacilityColumnInfo: ColumnInfo = {
|
||||||
@@ -114,9 +115,9 @@ const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getSggAllListFetcher = async (
|
const getSggAllListFetcher = async (
|
||||||
key: [string, string],
|
key: [string, string, "Y" | "N"],
|
||||||
): Promise<ApiResponse<Sgg[]>> => {
|
): Promise<ApiResponse<Sgg[]>> => {
|
||||||
return statisticsApi.getSggList(key[1]);
|
return statisticsApi.getSggList(key[1], { gugunSkipYn: key[2] });
|
||||||
};
|
};
|
||||||
|
|
||||||
type SidoSggFacilityNameSearchFormProps = {
|
type SidoSggFacilityNameSearchFormProps = {
|
||||||
@@ -182,9 +183,16 @@ const SidoSggFacilityNameSearchForm = ({
|
|||||||
Record<string, string | number> | null
|
Record<string, string | number> | null
|
||||||
>(null);
|
>(null);
|
||||||
const didAutoFetchRef = useRef(false);
|
const didAutoFetchRef = useRef(false);
|
||||||
|
const isSavingConnectionRef = useRef(false);
|
||||||
|
|
||||||
const sggKey: [string, string] | null =
|
const selectedSido = sidoList?.data?.sidoList?.find(
|
||||||
sido && sido !== "ALL" ? ["sgg-all", sido] : null;
|
(item) => item.sido === sido,
|
||||||
|
);
|
||||||
|
const gugunSkipYn = selectedSido?.gugunSkipYn ?? "Y";
|
||||||
|
const sggKey: [string, string, "Y" | "N"] | null =
|
||||||
|
sido && sido !== "ALL" && sidoList?.data
|
||||||
|
? ["sgg-all", sido, gugunSkipYn]
|
||||||
|
: null;
|
||||||
const { data: sggList } = useSWR<ApiResponse<Sgg[]>>(
|
const { data: sggList } = useSWR<ApiResponse<Sgg[]>>(
|
||||||
sggKey,
|
sggKey,
|
||||||
getSggAllListFetcher,
|
getSggAllListFetcher,
|
||||||
@@ -297,7 +305,11 @@ const SidoSggFacilityNameSearchForm = ({
|
|||||||
swtrmtFcltCd,
|
swtrmtFcltCd,
|
||||||
sewerBzmnCd: initialSearchValues?.bsnmCd || '',
|
sewerBzmnCd: initialSearchValues?.bsnmCd || '',
|
||||||
ctpvNm: initialSidoValue || '',
|
ctpvNm: initialSidoValue || '',
|
||||||
...(doNameList.includes(initialSidoValue) ? { sggNm: initialGugunValue } : {}),
|
...(isGugunRequiredSido(sidoList?.data?.sidoList, {
|
||||||
|
sido: initialSidoValue,
|
||||||
|
})
|
||||||
|
? { sggNm: initialGugunValue }
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -329,119 +341,213 @@ const SidoSggFacilityNameSearchForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveAdminRegions = async () => {
|
const canEditFactConnection = async (): Promise<boolean> => {
|
||||||
const statusCodeResponse = await statisticsApi.CheckStatusCode({
|
const statusCodeResponse = await statisticsApi.CheckStatusCode({
|
||||||
sewerBzmnCd: initialSearchValues?.bsnmCd?.trim() ?? "",
|
sewerBzmnCd: initialSearchValues?.bsnmCd?.trim() ?? "",
|
||||||
statsCd: 'ST00020002',
|
statsCd: "ST00020002",
|
||||||
statsYr: statsYr?.trim() ?? ''
|
statsYr: statsYr?.trim() ?? "",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (statusCodeResponse.status === 'success') {
|
if (statusCodeResponse.status === "success") {
|
||||||
if (!['0', '1', '4'].includes(statusCodeResponse.data)) {
|
if (!["0", "1", "4"].includes(statusCodeResponse.data)) {
|
||||||
const codeName = statusCodeResponse.data === '2' ? '제출' : (statusCodeResponse.data === '3' ? '승인' : '검증중');
|
const codeName =
|
||||||
alert(`시설별 설치 현황 데이터가 ${codeName} 상태일 때는 처리 시설 정보를 변경할 수 없습니다.`);
|
statusCodeResponse.data === "2"
|
||||||
return;
|
? "제출"
|
||||||
|
: statusCodeResponse.data === "3"
|
||||||
|
? "승인"
|
||||||
|
: "검증중";
|
||||||
|
alert(
|
||||||
|
`시설별 설치 현황 데이터가 ${codeName} 상태일 때는 처리 시설 정보를 변경할 수 없습니다.`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
return true;
|
||||||
alert('오류가 발생했습니다.\n관리자에게 문의하세요')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
alert("오류가 발생했습니다.\n관리자에게 문의하세요");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const persistFactConnection = async (
|
||||||
|
saveList: SeweragePipeFactConnectionSaveItem[],
|
||||||
|
deleteList: string[],
|
||||||
|
): Promise<boolean> => {
|
||||||
if (!statsYr?.trim()) {
|
if (!statsYr?.trim()) {
|
||||||
alert("상단에서 통계연도로 조회한 뒤 이용해 주세요.");
|
alert("상단에서 통계연도로 조회한 뒤 이용해 주세요.");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
if (!selectedFacilityRow) {
|
if (!selectedFacilityRow) {
|
||||||
alert("처리시설을 먼저 선택해주세요.");
|
alert("처리시설을 먼저 선택해주세요.");
|
||||||
return;
|
return false;
|
||||||
|
}
|
||||||
|
if (saveList.length === 0 && deleteList.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(await canEditFactConnection())) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasChanged = adminRegionRows.some(
|
try {
|
||||||
(row) =>
|
setIsLoading(true);
|
||||||
(row.__checked === "1" && row.isConnection !== "1") ||
|
const userInfo = await getUserInfo();
|
||||||
(row.__checked !== "1" && row.isConnection === "1"),
|
const request: SeweragePipeFactConnectionSaveRequest = {
|
||||||
);
|
ctpvNm: String(selectedFacilityRow.ctpvNm ?? ""),
|
||||||
|
sggNm: String(selectedFacilityRow.sggNm ?? ""),
|
||||||
|
fcltSn: String(selectedFacilityRow.fcltSn ?? ""),
|
||||||
|
statsYr: statsYr.trim(),
|
||||||
|
swtrmtFcltCd: String(selectedFacilityRow.fcltCd ?? ""),
|
||||||
|
swtrmtFcltNm: String(selectedFacilityRow.fcltNm ?? ""),
|
||||||
|
prcsFcltCpct: Number(selectedFacilityRow.fcltCpct ?? 0),
|
||||||
|
sewerBzmnCd: initialSearchValues?.bsnmCd?.trim() ?? "",
|
||||||
|
userId: userInfo.userUnqId,
|
||||||
|
saveList,
|
||||||
|
deleteList,
|
||||||
|
};
|
||||||
|
|
||||||
const saveList: SeweragePipeFactConnectionSaveItem[] = adminRegionRows
|
const response =
|
||||||
.filter((row) => row.__checked === "1")
|
await statisticsInputSewPipeApiService.saveFactConnection(
|
||||||
.map((row) => ({
|
request,
|
||||||
admdstCd: String(row.admdstCd ?? ""),
|
|
||||||
admdstNm: String(row.emdNm ?? ""),
|
|
||||||
}))
|
|
||||||
.filter((row) => row.admdstCd && row.admdstNm);
|
|
||||||
|
|
||||||
const deleteList = adminRegionRows
|
|
||||||
.filter((row) => row.__checked !== "1" && row.isConnection === "1")
|
|
||||||
.map((row) => String(row.admdstCd ?? ""))
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
if (!hasChanged) {
|
|
||||||
alert("변경된 연결 정보가 없습니다.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const run = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
const userInfo = await getUserInfo();
|
|
||||||
const request: SeweragePipeFactConnectionSaveRequest = {
|
|
||||||
ctpvNm: String(selectedFacilityRow.ctpvNm ?? ""),
|
|
||||||
sggNm: String(selectedFacilityRow.sggNm ?? ""),
|
|
||||||
fcltSn: String(selectedFacilityRow.fcltSn ?? ""),
|
|
||||||
statsYr: statsYr.trim(),
|
|
||||||
swtrmtFcltCd: String(selectedFacilityRow.fcltCd ?? ""),
|
|
||||||
swtrmtFcltNm: String(selectedFacilityRow.fcltNm ?? ""),
|
|
||||||
prcsFcltCpct: Number(selectedFacilityRow.fcltCpct ?? 0),
|
|
||||||
sewerBzmnCd: initialSearchValues?.bsnmCd?.trim() ?? "",
|
|
||||||
userId: userInfo.userUnqId,
|
|
||||||
saveList,
|
|
||||||
deleteList,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response =
|
|
||||||
await statisticsInputSewPipeApiService.saveFactConnection(
|
|
||||||
request,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.status !== "success") {
|
|
||||||
alert(response.message ?? "연결관계 저장에 실패했습니다.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setAdminRegionRows((prev) =>
|
|
||||||
prev.map((row) => ({
|
|
||||||
...row,
|
|
||||||
isConnection: row.__checked === "1" ? "1" : "0",
|
|
||||||
})),
|
|
||||||
);
|
);
|
||||||
await onSaveSuccess?.();
|
|
||||||
alert("저장되었습니다.");
|
|
||||||
} catch {
|
|
||||||
alert("연결관계 저장 중 오류가 발생했습니다.");
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void run();
|
if (response.status !== "success") {
|
||||||
|
alert(response.message ?? "연결관계 저장에 실패했습니다.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await onSaveSuccess?.();
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
alert("연결관계 저장 중 오류가 발생했습니다.");
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAdminRegionItemChecked = (
|
const syncConnectionFlags = (
|
||||||
|
rows: Record<string, string | number>[],
|
||||||
|
): Record<string, string | number>[] =>
|
||||||
|
rows.map((row) => ({
|
||||||
|
...row,
|
||||||
|
isConnection: row.__checked === "1" ? "1" : "0",
|
||||||
|
}));
|
||||||
|
|
||||||
|
const revertCheckedToConnection = (
|
||||||
|
rows: Record<string, string | number>[],
|
||||||
|
): Record<string, string | number>[] =>
|
||||||
|
rows.map((row) => ({
|
||||||
|
...row,
|
||||||
|
__checked: row.isConnection === "1" ? "1" : "0",
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handleAdminRegionItemChecked = async (
|
||||||
itemIndex: number,
|
itemIndex: number,
|
||||||
checked: boolean,
|
checked: boolean,
|
||||||
) => {
|
) => {
|
||||||
|
if (isSavingConnectionRef.current) {
|
||||||
|
// RealGrid가 이미 토글했을 수 있어 __checked 기준으로 재동기화
|
||||||
|
setAdminRegionRows((prev) => prev.map((row) => ({ ...row })));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = adminRegionRows[itemIndex];
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
const nextChecked = checked ? "1" : "0";
|
||||||
|
const prevChecked = target.isConnection === "1" ? "1" : "0";
|
||||||
|
|
||||||
setAdminRegionRows((prev) =>
|
setAdminRegionRows((prev) =>
|
||||||
prev.map((row, idx) =>
|
prev.map((row, idx) =>
|
||||||
idx === itemIndex
|
idx === itemIndex
|
||||||
? { ...row, __checked: checked ? "1" : "0" }
|
? { ...row, __checked: nextChecked }
|
||||||
: row,
|
: row,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 서버 상태와 동일하면 API 호출 없이 UI만 반영
|
||||||
|
if (nextChecked === prevChecked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveList: SeweragePipeFactConnectionSaveItem[] =
|
||||||
|
checked
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
admdstCd: String(target.admdstCd ?? ""),
|
||||||
|
admdstNm: String(target.emdNm ?? ""),
|
||||||
|
},
|
||||||
|
].filter((row) => row.admdstCd && row.admdstNm)
|
||||||
|
: [];
|
||||||
|
const deleteList =
|
||||||
|
!checked && String(target.admdstCd ?? "")
|
||||||
|
? [String(target.admdstCd)]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (saveList.length === 0 && deleteList.length === 0) {
|
||||||
|
setAdminRegionRows((prev) => revertCheckedToConnection(prev));
|
||||||
|
alert("행정구역 코드가 없어 저장할 수 없습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSavingConnectionRef.current = true;
|
||||||
|
const ok = await persistFactConnection(saveList, deleteList);
|
||||||
|
isSavingConnectionRef.current = false;
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
setAdminRegionRows((prev) => revertCheckedToConnection(prev));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAdminRegionRows((prev) => syncConnectionFlags(prev));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAdminRegionItemAllChecked = (checked: boolean) => {
|
const handleAdminRegionItemAllChecked = async (checked: boolean) => {
|
||||||
|
if (isSavingConnectionRef.current) {
|
||||||
|
setAdminRegionRows((prev) => prev.map((row) => ({ ...row })));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshot = adminRegionRows;
|
||||||
|
if (snapshot.length === 0) return;
|
||||||
|
|
||||||
setAdminRegionRows((prev) =>
|
setAdminRegionRows((prev) =>
|
||||||
prev.map((row) => ({ ...row, __checked: checked ? "1" : "0" })),
|
prev.map((row) => ({
|
||||||
|
...row,
|
||||||
|
__checked: checked ? "1" : "0",
|
||||||
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const saveList: SeweragePipeFactConnectionSaveItem[] = checked
|
||||||
|
? snapshot
|
||||||
|
.filter((row) => row.isConnection !== "1")
|
||||||
|
.map((row) => ({
|
||||||
|
admdstCd: String(row.admdstCd ?? ""),
|
||||||
|
admdstNm: String(row.emdNm ?? ""),
|
||||||
|
}))
|
||||||
|
.filter((row) => row.admdstCd && row.admdstNm)
|
||||||
|
: [];
|
||||||
|
const deleteList = !checked
|
||||||
|
? snapshot
|
||||||
|
.filter((row) => row.isConnection === "1")
|
||||||
|
.map((row) => String(row.admdstCd ?? ""))
|
||||||
|
.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (saveList.length === 0 && deleteList.length === 0) {
|
||||||
|
setAdminRegionRows((prev) => syncConnectionFlags(prev));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSavingConnectionRef.current = true;
|
||||||
|
const ok = await persistFactConnection(saveList, deleteList);
|
||||||
|
isSavingConnectionRef.current = false;
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
setAdminRegionRows((prev) => revertCheckedToConnection(prev));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAdminRegionRows((prev) => syncConnectionFlags(prev));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -575,8 +681,10 @@ const SidoSggFacilityNameSearchForm = ({
|
|||||||
조회
|
조회
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto">
|
<div>
|
||||||
<SaveButton saveFunc={handleSaveAdminRegions} />
|
<span className="text-lg text-gray-500 float-end">
|
||||||
|
* 체크 박스를 선택 / 해제 시 반영됩니다.
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const SplitDataGrid = ({
|
|||||||
leftCellStyleCallback,
|
leftCellStyleCallback,
|
||||||
rightOnItemChecked,
|
rightOnItemChecked,
|
||||||
rightOnItemAllChecked,
|
rightOnItemAllChecked,
|
||||||
|
rightCheckableCallback,
|
||||||
enableSort = false,
|
enableSort = false,
|
||||||
}: SplitDataGridProps) => {
|
}: SplitDataGridProps) => {
|
||||||
return (
|
return (
|
||||||
@@ -39,6 +40,7 @@ const SplitDataGrid = ({
|
|||||||
setRowData={setRightRowData}
|
setRowData={setRightRowData}
|
||||||
onItemChecked={rightOnItemChecked}
|
onItemChecked={rightOnItemChecked}
|
||||||
onItemAllChecked={rightOnItemAllChecked}
|
onItemAllChecked={rightOnItemAllChecked}
|
||||||
|
checkableCallback={rightCheckableCallback}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,32 +18,48 @@ function mergeCellStyleCallbackResult(
|
|||||||
baseStyleName: string | undefined,
|
baseStyleName: string | undefined,
|
||||||
extra: CellStyleCallbackResult | undefined,
|
extra: CellStyleCallbackResult | undefined,
|
||||||
): CellStyleCallbackResult {
|
): CellStyleCallbackResult {
|
||||||
|
let styleName: string | undefined;
|
||||||
|
let extraObj: Extract<CellStyleCallbackResult, object> | undefined;
|
||||||
|
|
||||||
if (!extra) {
|
if (!extra) {
|
||||||
return baseStyleName ? { styleName: baseStyleName } : {};
|
styleName = baseStyleName || undefined;
|
||||||
|
} else if (typeof extra === "string") {
|
||||||
|
styleName = [extra, baseStyleName].filter(Boolean).join(" ") || undefined;
|
||||||
|
} else {
|
||||||
|
extraObj = extra;
|
||||||
|
styleName =
|
||||||
|
[extra.styleName, baseStyleName].filter(Boolean).join(" ") ||
|
||||||
|
undefined;
|
||||||
}
|
}
|
||||||
if (typeof extra === "string") {
|
|
||||||
const styleName = [extra, baseStyleName].filter(Boolean).join(" ");
|
if (!styleName && !extraObj) return {};
|
||||||
return styleName ? { styleName } : {};
|
|
||||||
}
|
const result: CellStyleCallbackResult = {
|
||||||
const styleName = [extra.styleName, baseStyleName].filter(Boolean).join(" ");
|
...extraObj,
|
||||||
return {
|
|
||||||
...extra,
|
|
||||||
...(styleName ? { styleName } : {}),
|
...(styleName ? { styleName } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
import SplitDataGrid from "./SplitDataGrid";
|
import SplitDataGrid from "./SplitDataGrid";
|
||||||
|
|
||||||
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"
|
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"
|
||||||
|
import { wrapWithFormatTruncate } from "@/components/util/grid/decimalNumber"
|
||||||
import {
|
import {
|
||||||
buildExcelExportFileBaseName,
|
buildExcelExportFileBaseName,
|
||||||
|
buildFixedColumnNameSet,
|
||||||
getExcelImportColumnFieldNames,
|
getExcelImportColumnFieldNames,
|
||||||
getNonEditableColumnFieldNames,
|
getNonEditableColumnFieldNames,
|
||||||
hasAdmdstCdColumn,
|
hasAdmdstCdColumn,
|
||||||
isExcelImportAdmdstCdInvalid,
|
isExcelImportAdmdstCdInvalid,
|
||||||
mergeExcelImportWithExistingRows,
|
mergeExcelImportWithExistingRows,
|
||||||
prepareGridColumnsForExcelExport,
|
prepareGridColumnsForExcelExport,
|
||||||
|
releaseFixedOptionsForExcelExport,
|
||||||
|
resolveColumnStyleName,
|
||||||
|
restoreFixedOptionsAfterExcelExport,
|
||||||
restoreGridColumnsAfterExcelExport,
|
restoreGridColumnsAfterExcelExport,
|
||||||
shouldPrepareColumnsForExcelExport,
|
shouldPrepareColumnsForExcelExport,
|
||||||
|
disableAlternateRowStyle,
|
||||||
} from "@/components/util/gridExcelExport"
|
} from "@/components/util/gridExcelExport"
|
||||||
import { useTabStore } from "@/store/tab-store"
|
import { useTabStore } from "@/store/tab-store"
|
||||||
|
|
||||||
@@ -88,7 +104,7 @@ function safeGridViewRefresh(gv: { refresh?: () => void } | null | undefined) {
|
|||||||
try {
|
try {
|
||||||
gv.refresh();
|
gv.refresh();
|
||||||
} catch {
|
} catch {
|
||||||
/* noop */
|
console.error('refresh error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +253,7 @@ function sortRowsGroupPreserving(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** columnInfo → RealGrid DataField 배열 (computed → valueCallback 포함, setFields용) */
|
/** columnInfo → RealGrid DataField 배열 (computed → valueCallback 포함, setFields용) */
|
||||||
function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueType; computed?: import("../type/DataGrid.type").ComputedFieldConfig }[]) {
|
function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueType; dataFormat?: string; computed?: import("../type/DataGrid.type").ComputedFieldConfig }[]) {
|
||||||
return columns.map((col) => {
|
return columns.map((col) => {
|
||||||
const field: { fieldName: string; dataType: string; valueCallback?: (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number } = {
|
const field: { fieldName: string; dataType: string; valueCallback?: (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number } = {
|
||||||
fieldName: col.fieldName,
|
fieldName: col.fieldName,
|
||||||
@@ -253,7 +269,7 @@ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueT
|
|||||||
const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0;
|
const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0;
|
||||||
const hasSumPerTenThousand = col.computed.type === "sumPerTenThousand" && "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorField" in col.computed && Boolean(col.computed.denominatorField);
|
const hasSumPerTenThousand = col.computed.type === "sumPerTenThousand" && "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorField" in col.computed && Boolean(col.computed.denominatorField);
|
||||||
if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups || hasSumPerTenThousand) {
|
if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups || hasSumPerTenThousand) {
|
||||||
field.valueCallback = getComputedValueCallback(col.computed) as (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number;
|
field.valueCallback = wrapWithFormatTruncate(getComputedValueCallback(col.computed), col.dataFormat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return field;
|
return field;
|
||||||
@@ -282,7 +298,7 @@ function createNumberComparer(dp: RealGridDataProviderComparer): (field: string
|
|||||||
const TabDataGrid = ({
|
const TabDataGrid = ({
|
||||||
columnInfo,
|
columnInfo,
|
||||||
isExcelImport = true,
|
isExcelImport = true,
|
||||||
excelImportSkipNonEditable = true,
|
excelImportSkipNonEditable = false,
|
||||||
excelImportSewerBzmnCd,
|
excelImportSewerBzmnCd,
|
||||||
excelExportSearchContext,
|
excelExportSearchContext,
|
||||||
isExcelExport = true,
|
isExcelExport = true,
|
||||||
@@ -313,17 +329,23 @@ const TabDataGrid = ({
|
|||||||
const cellStyleCallbackRef = useRef<CellStyleCallback | undefined>(undefined);
|
const cellStyleCallbackRef = useRef<CellStyleCallback | undefined>(undefined);
|
||||||
cellStyleCallbackRef.current = cellStyleCallback;
|
cellStyleCallbackRef.current = cellStyleCallback;
|
||||||
|
|
||||||
const attachCellStyleCallbacksForTab = useCallback((tabKey: string, columns: ColumnConfig[], attempt = 0) => {
|
const attachCellStyleCallbacksForTab = useCallback((tabKey: string, columns: ColumnConfig[], columnLayout: ColumnLayoutItem[] | undefined, fixedCols: number | undefined, attempt = 0) => {
|
||||||
if (!cellStyleCallbackRef.current) return;
|
|
||||||
const el = gridRefs.current.get(tabKey);
|
const el = gridRefs.current.get(tabKey);
|
||||||
const gv = el?.gridView;
|
const gv = el?.gridView;
|
||||||
if (!gv) return;
|
if (!gv) return;
|
||||||
|
|
||||||
|
const fixedColumnNameSet = buildFixedColumnNameSet(columns, columnLayout, fixedCols);
|
||||||
|
const hasUserCellStyleCallback = Boolean(cellStyleCallbackRef.current);
|
||||||
|
if (fixedColumnNameSet.size === 0 && !hasUserCellStyleCallback) return;
|
||||||
|
|
||||||
let attachedCount = 0;
|
let attachedCount = 0;
|
||||||
for (const column of columns) {
|
for (const column of columns) {
|
||||||
|
const isFixedColumn = fixedColumnNameSet.has(column.name);
|
||||||
|
if (!isFixedColumn && !hasUserCellStyleCallback) continue;
|
||||||
|
|
||||||
const gridCol = safeColumnByName(gv, column.name);
|
const gridCol = safeColumnByName(gv, column.name);
|
||||||
if (!gridCol) continue;
|
if (!gridCol) continue;
|
||||||
const baseStyleName = column.styleName;
|
const baseStyleName = resolveColumnStyleName(column, fixedColumnNameSet);
|
||||||
gridCol.styleCallback = (grid, dataCell) => {
|
gridCol.styleCallback = (grid, dataCell) => {
|
||||||
const dc = dataCell as {
|
const dc = dataCell as {
|
||||||
itemIndex?: number;
|
itemIndex?: number;
|
||||||
@@ -338,19 +360,21 @@ const TabDataGrid = ({
|
|||||||
return mergeCellStyleCallbackResult(baseStyleName, undefined);
|
return mergeCellStyleCallbackResult(baseStyleName, undefined);
|
||||||
}
|
}
|
||||||
const row = data[itemIndex] as Record<string, string | number>;
|
const row = data[itemIndex] as Record<string, string | number>;
|
||||||
const extra = cellStyleCallbackRef.current?.({
|
const extra = hasUserCellStyleCallback
|
||||||
rowIndex: itemIndex,
|
? cellStyleCallbackRef.current?.({
|
||||||
fieldName,
|
rowIndex: itemIndex,
|
||||||
row,
|
fieldName,
|
||||||
grid: grid as CellStyleCallbackParams["grid"],
|
row,
|
||||||
});
|
grid: grid as CellStyleCallbackParams["grid"],
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
return mergeCellStyleCallbackResult(baseStyleName, extra);
|
return mergeCellStyleCallbackResult(baseStyleName, extra);
|
||||||
};
|
};
|
||||||
attachedCount += 1;
|
attachedCount += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachedCount === 0 && attempt < 20) {
|
if (attachedCount === 0 && attempt < 20) {
|
||||||
setTimeout(() => attachCellStyleCallbacksForTab(tabKey, columns, attempt + 1), 100);
|
setTimeout(() => attachCellStyleCallbacksForTab(tabKey, columns, columnLayout, fixedCols, attempt + 1), 100);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,6 +483,14 @@ const TabDataGrid = ({
|
|||||||
const excelExportVisibilitySnapshot = useExcelColumnVisibilityPrep
|
const excelExportVisibilitySnapshot = useExcelColumnVisibilityPrep
|
||||||
? prepareGridColumnsForExcelExport(gridView, columns)
|
? prepareGridColumnsForExcelExport(gridView, columns)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const savedFixedColCount = columnInfoItem.fixedColumns
|
||||||
|
? releaseFixedOptionsForExcelExport(
|
||||||
|
gridView,
|
||||||
|
columnInfoItem.fixedColumns,
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
disableAlternateRowStyle(gridView, true);
|
||||||
|
|
||||||
const exportBaseName = buildExcelExportFileBaseName(
|
const exportBaseName = buildExcelExportFileBaseName(
|
||||||
menuName,
|
menuName,
|
||||||
@@ -473,6 +505,12 @@ const TabDataGrid = ({
|
|||||||
footer: 'hidden',
|
footer: 'hidden',
|
||||||
allColumns: !useExcelColumnVisibilityPrep,
|
allColumns: !useExcelColumnVisibilityPrep,
|
||||||
done: () => {
|
done: () => {
|
||||||
|
if (savedFixedColCount !== undefined) {
|
||||||
|
restoreFixedOptionsAfterExcelExport(
|
||||||
|
gridView,
|
||||||
|
savedFixedColCount,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (excelExportVisibilitySnapshot) {
|
if (excelExportVisibilitySnapshot) {
|
||||||
restoreGridColumnsAfterExcelExport(gridView, excelExportVisibilitySnapshot);
|
restoreGridColumnsAfterExcelExport(gridView, excelExportVisibilitySnapshot);
|
||||||
}
|
}
|
||||||
@@ -968,6 +1006,11 @@ const TabDataGrid = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const { columns, columnLayout } = info;
|
const { columns, columnLayout } = info;
|
||||||
|
const fixedColumnNameSet = buildFixedColumnNameSet(
|
||||||
|
columns,
|
||||||
|
columnLayout,
|
||||||
|
info.fixedColumns,
|
||||||
|
);
|
||||||
const currentRowData = rowDataMap.get(tabKey) || [];
|
const currentRowData = rowDataMap.get(tabKey) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1074,6 +1117,7 @@ const TabDataGrid = ({
|
|||||||
gridView.setOptions({
|
gridView.setOptions({
|
||||||
summaryMode: "aggregate"
|
summaryMode: "aggregate"
|
||||||
});
|
});
|
||||||
|
disableAlternateRowStyle(gridView);
|
||||||
|
|
||||||
// footerUserSpans가 있는 경우 setColumnLayout으로 설정
|
// footerUserSpans가 있는 경우 setColumnLayout으로 설정
|
||||||
const checkFooterUserSpans = (layout: unknown[]): boolean => {
|
const checkFooterUserSpans = (layout: unknown[]): boolean => {
|
||||||
@@ -1189,9 +1233,27 @@ const TabDataGrid = ({
|
|||||||
if (currentInfo.fixedColumns !== undefined && currentInfo.fixedColumns > 0) {
|
if (currentInfo.fixedColumns !== undefined && currentInfo.fixedColumns > 0) {
|
||||||
gridView.setFixedOptions({
|
gridView.setFixedOptions({
|
||||||
colCount: currentInfo.fixedColumns,
|
colCount: currentInfo.fixedColumns,
|
||||||
|
resizable: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tabFixedSet = buildFixedColumnNameSet(
|
||||||
|
columns,
|
||||||
|
currentInfo.columnLayout,
|
||||||
|
currentInfo.fixedColumns,
|
||||||
|
);
|
||||||
|
columns.forEach((col) => {
|
||||||
|
const styleName = resolveColumnStyleName(
|
||||||
|
col,
|
||||||
|
tabFixedSet,
|
||||||
|
);
|
||||||
|
gridView.setColumnProperty?.(
|
||||||
|
col.name,
|
||||||
|
"styleName",
|
||||||
|
styleName,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// 각 탭별 ref 초기화
|
// 각 탭별 ref 초기화
|
||||||
if (!sortDirRefs.current.has(tabKey)) {
|
if (!sortDirRefs.current.has(tabKey)) {
|
||||||
sortDirRefs.current.set(tabKey, { current: {} });
|
sortDirRefs.current.set(tabKey, { current: {} });
|
||||||
@@ -1247,9 +1309,17 @@ const TabDataGrid = ({
|
|||||||
const onSortingHandler = createOnSortingHandler(columns, setRowData, sortDirRef, originalRowsRef, findGridElement);
|
const onSortingHandler = createOnSortingHandler(columns, setRowData, sortDirRef, originalRowsRef, findGridElement);
|
||||||
(gridView as { onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean }).onSorting = enableSort ? onSortingHandler : () => false;
|
(gridView as { onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean }).onSorting = enableSort ? onSortingHandler : () => false;
|
||||||
|
|
||||||
if (cellStyleCallbackRef.current) {
|
if (
|
||||||
|
cellStyleCallbackRef.current ||
|
||||||
|
tabFixedSet.size > 0
|
||||||
|
) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
attachCellStyleCallbacksForTab(tabKey, columns);
|
attachCellStyleCallbacksForTab(
|
||||||
|
tabKey,
|
||||||
|
columns,
|
||||||
|
currentInfo.columnLayout,
|
||||||
|
currentInfo.fixedColumns,
|
||||||
|
);
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
}, 150);
|
}, 150);
|
||||||
@@ -1262,7 +1332,16 @@ const TabDataGrid = ({
|
|||||||
layout={columnLayout}
|
layout={columnLayout}
|
||||||
rows={currentRowData}
|
rows={currentRowData}
|
||||||
>
|
>
|
||||||
{columns.map((column) => (
|
{columns.map((column) => {
|
||||||
|
const resolvedStyleName =
|
||||||
|
resolveColumnStyleName(
|
||||||
|
column,
|
||||||
|
fixedColumnNameSet,
|
||||||
|
) ||
|
||||||
|
(column.editable === false
|
||||||
|
? "rg-cell-readonly"
|
||||||
|
: undefined);
|
||||||
|
return (
|
||||||
<RGDataColumn
|
<RGDataColumn
|
||||||
key={column.name}
|
key={column.name}
|
||||||
name={column.name}
|
name={column.name}
|
||||||
@@ -1271,11 +1350,9 @@ const TabDataGrid = ({
|
|||||||
{...(column.valueType === ValueType.NUMBER && { numberFormat: column.dataFormat ? column.dataFormat : '#,##0.00' })}
|
{...(column.valueType === ValueType.NUMBER && { numberFormat: column.dataFormat ? column.dataFormat : '#,##0.00' })}
|
||||||
header={column.header}
|
header={column.header}
|
||||||
editable={column.editable}
|
editable={column.editable}
|
||||||
{...(column.styleName != null
|
{...(resolvedStyleName != null
|
||||||
? { styleName: column.styleName }
|
? { styleName: resolvedStyleName }
|
||||||
: column.editable === false
|
: {})}
|
||||||
? { styleName: 'rg-cell-readonly' as const }
|
|
||||||
: {})}
|
|
||||||
{...(column.visible === false && { visible: false })}
|
{...(column.visible === false && { visible: false })}
|
||||||
footer={column.footer && !Array.isArray(column.footer) ? {
|
footer={column.footer && !Array.isArray(column.footer) ? {
|
||||||
text: (column.footer as { text?: string }).text,
|
text: (column.footer as { text?: string }).text,
|
||||||
@@ -1285,7 +1362,8 @@ const TabDataGrid = ({
|
|||||||
mergeRule={column.mergeCells ? { criteria: column.mergeGroupField ? `values['${column.mergeGroupField}'] + value` : 'value' } : undefined}
|
mergeRule={column.mergeCells ? { criteria: column.mergeGroupField ? `values['${column.mergeGroupField}'] + value` : 'value' } : undefined}
|
||||||
sortable={enableSort && !column.sortdisable}
|
sortable={enableSort && !column.sortdisable}
|
||||||
/>
|
/>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</RealGridReact>
|
</RealGridReact>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import type { RealGridReact } from "realgrid-react";
|
import type { RealGridReact } from "realgrid-react";
|
||||||
|
|
||||||
|
import { useAlertStore } from "@/shared/lib";
|
||||||
|
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||||
|
import type { VerificationResultListRequest, VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -21,17 +26,17 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
LoadingWithAnimation,
|
LoadingWithAnimation,
|
||||||
} from "@common/components";
|
} from "@common/components";
|
||||||
|
|
||||||
|
import { getUserInfo } from "../../../../../packages/common/components/src/api";
|
||||||
|
|
||||||
import DataGrid from "@/components/module/DataGrid";
|
import DataGrid from "@/components/module/DataGrid";
|
||||||
|
import { cellContentTooltip } from "@/components/module/verificationResultCellTooltip";
|
||||||
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
|
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
|
||||||
import type {
|
import type {
|
||||||
CellEditedCallbackParams,
|
CellEditedCallbackParams,
|
||||||
CellStyleCallbackParams,
|
CellStyleCallbackParams,
|
||||||
ColumnInfo,
|
ColumnInfo,
|
||||||
} from "@/components/type/DataGrid.type";
|
} from "@/components/type/DataGrid.type";
|
||||||
import { getUserInfo } from "../../../../../packages/common/components/src/api";
|
|
||||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
|
||||||
import type { VerificationResultListRequest, VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface";
|
|
||||||
import { useAlertStore } from "@/shared/lib";
|
|
||||||
|
|
||||||
type VerificationResultGridRow = Record<string, string | number>;
|
type VerificationResultGridRow = Record<string, string | number>;
|
||||||
|
|
||||||
@@ -41,8 +46,8 @@ const VERIFICATION_METHOD_CODES: VerificationMethodCd[] = ["NV", "IMR", "HB"];
|
|||||||
|
|
||||||
const VRFC_MTHD_NAME_MAP: Record<VerificationMethodCd, string> = {
|
const VRFC_MTHD_NAME_MAP: Record<VerificationMethodCd, string> = {
|
||||||
NV: "일반",
|
NV: "일반",
|
||||||
IMR: "I-MR",
|
IMR: "시계열",
|
||||||
HB: "HB",
|
HB: "모집단",
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VerificationExceptionSubmitItem = {
|
export type VerificationExceptionSubmitItem = {
|
||||||
@@ -63,7 +68,7 @@ function setRowCheckByItemIndex(
|
|||||||
try {
|
try {
|
||||||
gv?.checkItem?.(itemIndex, checked, false);
|
gv?.checkItem?.(itemIndex, checked, false);
|
||||||
} catch {
|
} catch {
|
||||||
/* RealGrid 체크 API 미지원 등 */
|
console.error('RealGrid 체크 API error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +98,7 @@ const verificationSubmitResultColumnInfo: ColumnInfo = {
|
|||||||
{ name: "vrfcMthdCd", fieldName: "vrfcMthdCd", header: { text: "검증유형코드" }, editable: false, visible: false },
|
{ name: "vrfcMthdCd", fieldName: "vrfcMthdCd", header: { text: "검증유형코드" }, editable: false, visible: false },
|
||||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||||
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역" }, editable: false },
|
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역" }, editable: false },
|
||||||
|
{ name: "fcltNm", fieldName: "fcltNm", header: { text: "시설명" }, editable: false },
|
||||||
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
|
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
|
||||||
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
|
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
|
||||||
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
|
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
|
||||||
@@ -111,6 +117,7 @@ const verificationSubmitResultColumnInfo: ColumnInfo = {
|
|||||||
"vrfcMthdNm",
|
"vrfcMthdNm",
|
||||||
"ctpvNm",
|
"ctpvNm",
|
||||||
"admdstNm",
|
"admdstNm",
|
||||||
|
"fcltNm",
|
||||||
"vrfcTrgtNm",
|
"vrfcTrgtNm",
|
||||||
"rsnExpln",
|
"rsnExpln",
|
||||||
"nowVlCn",
|
"nowVlCn",
|
||||||
@@ -125,7 +132,7 @@ const verificationSubmitResultColumnInfo: ColumnInfo = {
|
|||||||
export interface VerificationResultDialogQueryContext {
|
export interface VerificationResultDialogQueryContext {
|
||||||
sewerBzmnCd: string;
|
sewerBzmnCd: string;
|
||||||
ctpvNm: string;
|
ctpvNm: string;
|
||||||
sggNm: string;
|
sggNm?: string;
|
||||||
statsYr: string;
|
statsYr: string;
|
||||||
statsCd: string;
|
statsCd: string;
|
||||||
}
|
}
|
||||||
@@ -201,6 +208,7 @@ export function VerificationResultDialog({
|
|||||||
vrfcMthdNm: VRFC_MTHD_NAME_MAP[vrfcMthdCd] ?? item.vrfcMthdNm,
|
vrfcMthdNm: VRFC_MTHD_NAME_MAP[vrfcMthdCd] ?? item.vrfcMthdNm,
|
||||||
ctpvNm: item.ctpvNm,
|
ctpvNm: item.ctpvNm,
|
||||||
admdstNm: item.admdstNm ?? item.sggNm ?? "-",
|
admdstNm: item.admdstNm ?? item.sggNm ?? "-",
|
||||||
|
fcltNm: item.fcltNm ?? "-",
|
||||||
vrfcTrgtNm: item.vrfcTrgtNm,
|
vrfcTrgtNm: item.vrfcTrgtNm,
|
||||||
rsnExpln: item.rsnExpln,
|
rsnExpln: item.rsnExpln,
|
||||||
nowVlCn: item.nowVlCn ?? "-",
|
nowVlCn: item.nowVlCn ?? "-",
|
||||||
@@ -311,15 +319,24 @@ export function VerificationResultDialog({
|
|||||||
}, [rows, loading]);
|
}, [rows, loading]);
|
||||||
|
|
||||||
const handleExceptionSubmitClick = useCallback(async () => {
|
const handleExceptionSubmitClick = useCallback(async () => {
|
||||||
|
if (checkedIndices.size === 0) {
|
||||||
|
alert("작성된 예외 제출 항목이 없습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const gridRows = getGridDataRef.current?.() ?? [];
|
const gridRows = getGridDataRef.current?.() ?? [];
|
||||||
const payload: VerificationExceptionSubmitItem[] = [];
|
const payload: VerificationExceptionSubmitItem[] = [];
|
||||||
checkedIndices.forEach((idx) => {
|
for (const idx of checkedIndices) {
|
||||||
const prcsOpnnExpln = String(gridRows[idx]?.prcsOpnnExpln ?? "").trim();
|
const prcsOpnnExpln = String(gridRows[idx]?.prcsOpnnExpln ?? "").trim();
|
||||||
if (!prcsOpnnExpln) return;
|
if (!prcsOpnnExpln) {
|
||||||
|
alert("예외 제출 사유를 작성해주세요.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const vrfcSn = Number(rows[idx]?.vrfcSn);
|
const vrfcSn = Number(rows[idx]?.vrfcSn);
|
||||||
if (!Number.isFinite(vrfcSn)) return;
|
if (!Number.isFinite(vrfcSn)) continue;
|
||||||
payload.push({ vrfcSn, prcsOpnnExpln });
|
payload.push({ vrfcSn, prcsOpnnExpln });
|
||||||
});
|
}
|
||||||
|
|
||||||
if (payload.length === 0) {
|
if (payload.length === 0) {
|
||||||
alert("작성된 예외 제출 항목이 없습니다.");
|
alert("작성된 예외 제출 항목이 없습니다.");
|
||||||
return;
|
return;
|
||||||
@@ -404,6 +421,7 @@ export function VerificationResultDialog({
|
|||||||
getGridDataRef={getGridDataRef}
|
getGridDataRef={getGridDataRef}
|
||||||
onCellEdited={onPrcsOpnnExplnEdited}
|
onCellEdited={onPrcsOpnnExplnEdited}
|
||||||
cellStyleCallback={verificationMthdCellStyle}
|
cellStyleCallback={verificationMthdCellStyle}
|
||||||
|
cellTooltipCallback={cellContentTooltip}
|
||||||
onItemChecked={(itemIndex, checked) => {
|
onItemChecked={(itemIndex, checked) => {
|
||||||
setCheckedIndices((prev) => {
|
setCheckedIndices((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import type { RealGridReact } from "realgrid-react";
|
import type { RealGridReact } from "realgrid-react";
|
||||||
|
|
||||||
import { getUserInfo } from "../../../../../packages/common/components/src/api";
|
import { useAlertStore } from "@/shared/lib";
|
||||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||||
import type { VerificationResultListRequest, VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface";
|
import type { VerificationResultListRequest, VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface";
|
||||||
import {
|
import {
|
||||||
@@ -30,18 +31,20 @@ import {
|
|||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from "@common/components";
|
} from "@common/components";
|
||||||
|
|
||||||
|
import { getUserInfo } from "../../../../../packages/common/components/src/api";
|
||||||
|
|
||||||
import DataGrid from "@/components/module/DataGrid";
|
import DataGrid from "@/components/module/DataGrid";
|
||||||
|
import { cellContentTooltip } from "@/components/module/verificationResultCellTooltip";
|
||||||
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
|
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
|
||||||
import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type";
|
import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type";
|
||||||
import { useAlertStore } from "@/shared/lib";
|
|
||||||
|
|
||||||
type VerificationResultGridRow = Record<string, string | number>;
|
type VerificationResultGridRow = Record<string, string | number>;
|
||||||
type VerificationHistoryTabKey = "NV" | "IMR" | "HB";
|
type VerificationHistoryTabKey = "NV" | "IMR" | "HB";
|
||||||
|
|
||||||
const verificationHistoryTabs: { value: VerificationHistoryTabKey; label: string }[] = [
|
const verificationHistoryTabs: { value: VerificationHistoryTabKey; label: string }[] = [
|
||||||
{ value: "NV", label: "일반검증 내역" },
|
{ value: "NV", label: "일반검증 내역" },
|
||||||
{ value: "IMR", label: "I-MR 검증내역" },
|
{ value: "IMR", label: "시계열 검증내역" },
|
||||||
{ value: "HB", label: "HB 검증내역" },
|
{ value: "HB", label: "모집단 검증내역" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const PAGE_SIZE = 30;
|
const PAGE_SIZE = 30;
|
||||||
@@ -88,7 +91,7 @@ function uncheckGridRow(gridRef: MutableRefObject<RealGridReact | null>, itemInd
|
|||||||
try {
|
try {
|
||||||
(gridRef.current?.gridView as GridViewWithCheckApi | undefined)?.checkItem?.(itemIndex, false, false);
|
(gridRef.current?.gridView as GridViewWithCheckApi | undefined)?.checkItem?.(itemIndex, false, false);
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
console.error('RealGrid 체크 API error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +141,7 @@ const verificationResultColumnInfo: ColumnInfo = {
|
|||||||
columns: [
|
columns: [
|
||||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||||
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역" }, editable: false },
|
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역" }, editable: false },
|
||||||
|
{ name: "fcltNm", fieldName: "fcltNm", header: { text: "시설명" }, editable: false },
|
||||||
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
|
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
|
||||||
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
|
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
|
||||||
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
|
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
|
||||||
@@ -154,6 +158,7 @@ const verificationResultColumnInfo: ColumnInfo = {
|
|||||||
columnLayout: [
|
columnLayout: [
|
||||||
"ctpvNm",
|
"ctpvNm",
|
||||||
"admdstNm",
|
"admdstNm",
|
||||||
|
"fcltNm",
|
||||||
"vrfcTrgtNm",
|
"vrfcTrgtNm",
|
||||||
"rsnExpln",
|
"rsnExpln",
|
||||||
"nowVlCn",
|
"nowVlCn",
|
||||||
@@ -175,7 +180,7 @@ export interface VerificationResultHistoryDialogProps {
|
|||||||
queryContext?: {
|
queryContext?: {
|
||||||
sewerBzmnCd: string;
|
sewerBzmnCd: string;
|
||||||
ctpvNm: string;
|
ctpvNm: string;
|
||||||
sggNm: string;
|
sggNm?: string;
|
||||||
statsYr: string;
|
statsYr: string;
|
||||||
statsCd: string;
|
statsCd: string;
|
||||||
} | null;
|
} | null;
|
||||||
@@ -375,8 +380,9 @@ export function VerificationResultHistoryDialog({
|
|||||||
return {
|
return {
|
||||||
vrfcSn: item.vrfcSn,
|
vrfcSn: item.vrfcSn,
|
||||||
__checked: isChecked ? 1 : 0,
|
__checked: isChecked ? 1 : 0,
|
||||||
ctpvNm: item.ctpvNm,
|
ctpvNm: item.ctpvNm ?? "-",
|
||||||
admdstNm: item.admdstNm ?? item.sggNm ?? "-",
|
admdstNm: item.admdstNm ?? item.sggNm ?? "-",
|
||||||
|
fcltNm: item.fcltNm ?? "-",
|
||||||
vrfcTrgtNm: item.vrfcTrgtNm,
|
vrfcTrgtNm: item.vrfcTrgtNm,
|
||||||
rsnExpln: item.rsnExpln,
|
rsnExpln: item.rsnExpln,
|
||||||
nowVlCn: item.nowVlCn ?? "-",
|
nowVlCn: item.nowVlCn ?? "-",
|
||||||
@@ -500,6 +506,7 @@ export function VerificationResultHistoryDialog({
|
|||||||
enableSort={false}
|
enableSort={false}
|
||||||
getGridRef={gridRef}
|
getGridRef={gridRef}
|
||||||
cellStyleCallback={verificationPrcsYnNmCellStyle}
|
cellStyleCallback={verificationPrcsYnNmCellStyle}
|
||||||
|
cellTooltipCallback={cellContentTooltip}
|
||||||
onItemChecked={(itemIndex, checked) => {
|
onItemChecked={(itemIndex, checked) => {
|
||||||
const entry = entries[itemIndex];
|
const entry = entries[itemIndex];
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
@@ -558,7 +565,7 @@ export function VerificationResultHistoryDialog({
|
|||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-[80%]" aria-describedby={undefined}>
|
<DialogContent className="max-w-[80%]" aria-describedby={undefined}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{`${title} - ${queryContext?.statsYr} ${queryContext?.ctpvNm} ${queryContext?.sggNm}`}</DialogTitle>
|
<DialogTitle>{`${title} - ${queryContext?.statsYr} ${queryContext?.ctpvNm} ${queryContext?.sggNm ? queryContext?.sggNm : ''}`}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="max-h-[85vh] space-y-5 overflow-auto">
|
<div className="max-h-[85vh] space-y-5 overflow-auto">
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
DoubleArrowLeftIcon,
|
DoubleArrowLeftIcon,
|
||||||
DoubleArrowRightIcon,
|
DoubleArrowRightIcon,
|
||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
|
|
||||||
import { Button, cn } from "@common/components";
|
import { Button, cn } from "@common/components";
|
||||||
|
|
||||||
export type VerificationResultHistoryPaginationProps = {
|
export type VerificationResultHistoryPaginationProps = {
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { CellTooltipCallbackParams } from "@/components/type/DataGrid.type";
|
||||||
|
|
||||||
|
/** RealGrid 셀 호버 시 title처럼 전체 내용 표시 */
|
||||||
|
export function cellContentTooltip({
|
||||||
|
row,
|
||||||
|
fieldName,
|
||||||
|
}: CellTooltipCallbackParams): string | undefined {
|
||||||
|
const value = row[fieldName];
|
||||||
|
if (value == null || value === "") return undefined;
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
@@ -1,47 +1,48 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { useTabStore as useGridTabStore } from "@rsone/components";
|
||||||
import {
|
import {
|
||||||
type InfiniteData,
|
type InfiniteData,
|
||||||
keepPreviousData,
|
keepPreviousData,
|
||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
} from "@tanstack/react-query";
|
} from "@tanstack/react-query";
|
||||||
import { useTabStore as useGridTabStore } from "@rsone/components";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
import { useAlertStore, useMenuStore } from "@/shared/lib";
|
||||||
|
import {
|
||||||
|
findMenuItemByUrlPath,
|
||||||
|
resolveInputPageNavigationFromStatsCd,
|
||||||
|
} from "@/shared/lib/stats-cd-input-navigation";
|
||||||
|
import { CodeSelectWithAll } from "@/entities/code";
|
||||||
import { verificationInputReportApi } from "@/entities/statistics/api/verification-input-report-service";
|
import { verificationInputReportApi } from "@/entities/statistics/api/verification-input-report-service";
|
||||||
import type {
|
import type {
|
||||||
VerificationInputReportItem,
|
VerificationInputReportItem,
|
||||||
VerificationInputReportResponse,
|
VerificationInputReportResponse,
|
||||||
VerificationInputReportTypeCountingInfo,
|
VerificationInputReportTypeCountingInfo,
|
||||||
} from "@/entities/statistics/model/verification-input-report-interface";
|
} from "@/entities/statistics/model/verification-input-report-interface";
|
||||||
import { useAlertStore, useMenuStore } from "@/shared/lib";
|
|
||||||
import {
|
|
||||||
findMenuItemByUrlPath,
|
|
||||||
resolveInputPageNavigationFromStatsCd,
|
|
||||||
} from "@/shared/lib/stats-cd-input-navigation";
|
|
||||||
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
|
|
||||||
import { useTabStore } from "@/store/tab-store";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
type BadgeProps,
|
type BadgeProps,
|
||||||
|
Label,
|
||||||
LoadingWithAnimation,
|
LoadingWithAnimation,
|
||||||
Tabs,
|
Tabs,
|
||||||
TabsContent,
|
TabsContent,
|
||||||
TabsList,
|
TabsList,
|
||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
Label,
|
|
||||||
} from "@common/components";
|
} from "@common/components";
|
||||||
import { CodeSelectWithAll } from "@/entities/code";
|
|
||||||
|
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
|
||||||
|
import { useTabStore } from "@/store/tab-store";
|
||||||
|
|
||||||
type VerificationReportTabKey = "NV" | "IMR" | "HB";
|
type VerificationReportTabKey = "NV" | "IMR" | "HB";
|
||||||
|
|
||||||
const VERIFICATION_REPORT_TABS: { value: VerificationReportTabKey; label: string }[] = [
|
const VERIFICATION_REPORT_TABS: { value: VerificationReportTabKey; label: string }[] = [
|
||||||
{ value: "NV", label: "일반" },
|
{ value: "NV", label: "일반" },
|
||||||
{ value: "IMR", label: "I-MR" },
|
{ value: "IMR", label: "시계열" },
|
||||||
{ value: "HB", label: "HB" },
|
{ value: "HB", label: "모집단" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const TYPE_CODE_COLOR_MAP: Record<string, BadgeProps["color"]> = {
|
const TYPE_CODE_COLOR_MAP: Record<string, BadgeProps["color"]> = {
|
||||||
@@ -52,8 +53,8 @@ const TYPE_CODE_COLOR_MAP: Record<string, BadgeProps["color"]> = {
|
|||||||
|
|
||||||
const TYPE_CODE_NAME_MAP: Record<string, string> = {
|
const TYPE_CODE_NAME_MAP: Record<string, string> = {
|
||||||
NV: "일반",
|
NV: "일반",
|
||||||
IMR: "I-MR",
|
IMR: "시계열",
|
||||||
HB: "HB",
|
HB: "모집단",
|
||||||
};
|
};
|
||||||
|
|
||||||
const PAGE_SIZE = 100;
|
const PAGE_SIZE = 100;
|
||||||
@@ -357,16 +358,16 @@ const VerificationSelectionReport = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-2">
|
||||||
<span className="font-medium">
|
<div className="font-medium">
|
||||||
{formatReportItemTitle(item)}
|
{formatReportItemTitle(item)}
|
||||||
</span>
|
</div>
|
||||||
<Badge
|
<Badge
|
||||||
color={
|
color={
|
||||||
TYPE_CODE_COLOR_MAP[item.vrfcMthdCd] ??
|
TYPE_CODE_COLOR_MAP[item.vrfcMthdCd] ??
|
||||||
"gray"
|
"gray"
|
||||||
}
|
}
|
||||||
className="text-sm"
|
className="text-sm float-end"
|
||||||
>
|
>
|
||||||
{TYPE_CODE_NAME_MAP[item.vrfcMthdCd] ??
|
{TYPE_CODE_NAME_MAP[item.vrfcMthdCd] ??
|
||||||
item.vrfcMthdNm}
|
item.vrfcMthdNm}
|
||||||
@@ -397,8 +398,8 @@ const VerificationSelectionReport = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-1/5 bg-container p-4 !pb-2 flex border-r border-border">
|
<div className="w-1/5 min-w-0 bg-container p-4 !pb-2 flex border-r border-border">
|
||||||
<div className="card card-border flex-1 bg-background !px-0 max-h-[1000px] flex flex-col">
|
<div className="card card-border min-w-0 flex-1 bg-background !px-0 max-h-[1000px] flex flex-col">
|
||||||
<div className="border-b border-border flex justify-between">
|
<div className="border-b border-border flex justify-between">
|
||||||
<div className="flex justify-center items-center text-[2.4rem] ">
|
<div className="flex justify-center items-center text-[2.4rem] ">
|
||||||
<h2 className="font-semibold text-title p-4 pr-1">이상치 선별 보고서</h2>
|
<h2 className="font-semibold text-title p-4 pr-1">이상치 선별 보고서</h2>
|
||||||
@@ -434,14 +435,14 @@ const VerificationSelectionReport = ({
|
|||||||
<i className="ri-close-line" />
|
<i className="ri-close-line" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 pt-2.5 px-4">
|
<div className="flex min-w-0 items-center gap-2 pt-2.5 px-4">
|
||||||
<Label className="text-label shrink-0">통계항목</Label>
|
<Label className="text-label shrink-0">통계항목</Label>
|
||||||
<CodeSelectWithAll
|
<CodeSelectWithAll
|
||||||
upCd="STITEM"
|
upCd="STITEM"
|
||||||
value={statsCd}
|
value={statsCd}
|
||||||
onValueChange={handleStatsCdChange}
|
onValueChange={handleStatsCdChange}
|
||||||
placeholder="전체"
|
placeholder="전체"
|
||||||
className="w-full"
|
className="min-w-0 flex-1 overflow-hidden"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-body p-2.5 flex-1 flex flex-col overflow-hidden relative min-h-0">
|
<div className="card-body p-2.5 flex-1 flex flex-col overflow-hidden relative min-h-0">
|
||||||
|
|||||||
@@ -1,20 +1,56 @@
|
|||||||
import {
|
import {
|
||||||
|
type RefObject,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
type RefObject,
|
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import { flushSync } from "react-dom";
|
import { flushSync } from "react-dom";
|
||||||
|
|
||||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||||
|
|
||||||
import { ValueType } from "realgrid";
|
import { ValueType } from "realgrid";
|
||||||
import type { RealGridReact } from "realgrid-react";
|
import type { RealGridReact } from "realgrid-react";
|
||||||
|
|
||||||
|
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate, useSearchConditionSidoList } from "@/shared/lib";
|
||||||
|
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
|
||||||
|
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
|
||||||
|
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||||
|
import { nullableTrimmedString } from "@/components/util/input/nullableString";
|
||||||
|
import { statisticsInputExcretaTreatmentFacilityApi } from "@/entities/statistics/api/input/statistics-input-excreta-treatment-facility-service";
|
||||||
|
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||||
|
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||||
|
import {
|
||||||
|
EXCRETA_TREATMENT_FACILITY_STATS_CD,
|
||||||
|
type ExcretaTreatmentFacilityItem,
|
||||||
|
type ExcretaTreatmentFacilityRequestParams,
|
||||||
|
type ExcretaTreatmentFacilitySaveItem,
|
||||||
|
type ExcretaTreatmentFacilityVerificationResult,
|
||||||
|
} from "@/entities/statistics/model/input/statistics-input-excreta-treatment-facility-interface";
|
||||||
|
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
|
||||||
|
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
|
||||||
|
import type {
|
||||||
|
ApiResponse,
|
||||||
|
cancelSubmitRequest,
|
||||||
|
ImrStatusResponse,
|
||||||
|
SewerageStatusInfo,
|
||||||
|
Sgg,
|
||||||
|
Sido,
|
||||||
|
} from "@/entities/statistics/model/statistics-interface";
|
||||||
|
import {
|
||||||
|
filterBySelectedAdmdstCd,
|
||||||
|
isGugunOkForToolbar,
|
||||||
|
isSubmitOkForToolbar,
|
||||||
|
resolveNoApplicableForRequest,
|
||||||
|
toInputSearchRegion,
|
||||||
|
} from "@/entities/statistics";
|
||||||
|
|
||||||
import { LoadingWithAnimation } from "@common/components";
|
import { LoadingWithAnimation } from "@common/components";
|
||||||
|
|
||||||
|
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||||
|
|
||||||
|
import SubmitDailog from "./SubmitDailog";
|
||||||
|
|
||||||
import AddArticleButton from "@/components/button/AddArticleButton";
|
import AddArticleButton from "@/components/button/AddArticleButton";
|
||||||
import CancelSubmitButton from "@/components/button/CancelSubmitButton";
|
import CancelSubmitButton from "@/components/button/CancelSubmitButton";
|
||||||
import DeleteArticleButton from "@/components/button/DeleteArticleButton";
|
import DeleteArticleButton from "@/components/button/DeleteArticleButton";
|
||||||
@@ -24,49 +60,24 @@ import SaveButton from "@/components/button/SaveButton";
|
|||||||
import SubmitButton from "@/components/button/SumitButton";
|
import SubmitButton from "@/components/button/SumitButton";
|
||||||
import VerificationResultButton from "@/components/button/VerificationResultButton";
|
import VerificationResultButton from "@/components/button/VerificationResultButton";
|
||||||
import DataGrid from "@/components/module/DataGrid";
|
import DataGrid from "@/components/module/DataGrid";
|
||||||
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
|
|
||||||
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
|
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
|
||||||
import SearchForm from "@/components/module/SearchForm";
|
import SearchForm from "@/components/module/SearchForm";
|
||||||
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
|
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
|
||||||
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
|
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
|
||||||
import SubmitDailog from "./SubmitDailog";
|
|
||||||
import { ExcretaTreatFactInpColumn } from "@/components/util/column/input/ExcretaTreatFactInpColumn";
|
|
||||||
import type {
|
import type {
|
||||||
CellStyleCallbackParams,
|
CellStyleCallbackParams,
|
||||||
CellTooltipCallbackParams,
|
CellTooltipCallbackParams,
|
||||||
ColumnConfig,
|
ColumnConfig,
|
||||||
} from "@/components/type/DataGrid.type";
|
} from "@/components/type/DataGrid.type";
|
||||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||||
import { doList } from "@/components/util/column/input/DoList";
|
import { ExcretaTreatFactInpColumn } from "@/components/util/column/input/ExcretaTreatFactInpColumn";
|
||||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||||
import { isVerificationFailedFromCommonInfo } from "@/components/util/input/verification-failed-flag";
|
|
||||||
import { formatCompactYmdForGrid, toCompactYmdOrNull } from "@/components/util/grid/ymdFormat";
|
import { formatCompactYmdForGrid, toCompactYmdOrNull } from "@/components/util/grid/ymdFormat";
|
||||||
import { statisticsInputExcretaTreatmentFacilityApi } from "@/entities/statistics/api/input/statistics-input-excreta-treatment-facility-service";
|
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
|
||||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
import { isVerificationFailedFromCommonInfo } from "@/components/util/input/verification-failed-flag";
|
||||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
|
||||||
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
|
|
||||||
import {
|
|
||||||
EXCRETA_TREATMENT_FACILITY_STATS_CD,
|
|
||||||
type ExcretaTreatmentFacilityItem,
|
|
||||||
type ExcretaTreatmentFacilityRequestParams,
|
|
||||||
type ExcretaTreatmentFacilitySaveItem,
|
|
||||||
type ExcretaTreatmentFacilityVerificationResult,
|
|
||||||
} from "@/entities/statistics/model/input/statistics-input-excreta-treatment-facility-interface";
|
|
||||||
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
|
|
||||||
import type {
|
|
||||||
ApiResponse,
|
|
||||||
cancelSubmitRequest,
|
|
||||||
ImrStatusResponse,
|
|
||||||
Sgg,
|
|
||||||
SewerageStatusInfo,
|
|
||||||
} from "@/entities/statistics/model/statistics-interface";
|
|
||||||
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
|
|
||||||
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
|
|
||||||
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
|
|
||||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
|
||||||
import { useAdminStore } from "@/store/admin-store";
|
import { useAdminStore } from "@/store/admin-store";
|
||||||
|
import { useTabStore } from "@/store/tab-store";
|
||||||
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
|
||||||
|
|
||||||
type GridRowWithVerification = Record<string, string | number> & {
|
type GridRowWithVerification = Record<string, string | number> & {
|
||||||
verificationResult?: ExcretaTreatmentFacilityVerificationResult;
|
verificationResult?: ExcretaTreatmentFacilityVerificationResult;
|
||||||
@@ -91,10 +102,22 @@ const computeTabEnterable = (
|
|||||||
return periodEnterable;
|
return periodEnterable;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sidoListRef: { current: Sido[] } = { current: [] };
|
||||||
|
|
||||||
function buildGugunOkForToolbar(values: SearchFormValues | null): boolean {
|
function buildGugunOkForToolbar(values: SearchFormValues | null): boolean {
|
||||||
return doList.includes(values?.bsnmCd || "")
|
return isGugunOkForToolbar(sidoListRef.current, values);
|
||||||
? Boolean(values?.gugun && values.gugun !== "ALL")
|
}
|
||||||
: true;
|
|
||||||
|
function buildSubmitOkForToolbar(values: SearchFormValues | null): boolean {
|
||||||
|
return isSubmitOkForToolbar(sidoListRef.current, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildNoApplicableForRequest(
|
||||||
|
values: SearchFormValues | null,
|
||||||
|
checked: boolean,
|
||||||
|
isSaveListEmpty: boolean,
|
||||||
|
): boolean {
|
||||||
|
return resolveNoApplicableForRequest(sidoListRef.current, values, checked, isSaveListEmpty);
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeTabToolbarSnapshot(
|
function computeTabToolbarSnapshot(
|
||||||
@@ -108,7 +131,7 @@ function computeTabToolbarSnapshot(
|
|||||||
const gugunOk = buildGugunOkForToolbar(searchContext);
|
const gugunOk = buildGugunOkForToolbar(searchContext);
|
||||||
const enterable = computeTabEnterable(statusInfo, periodEnterable);
|
const enterable = computeTabEnterable(statusInfo, periodEnterable);
|
||||||
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
|
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
|
||||||
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk;
|
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
|
||||||
const showEnterableToolbarButtons = enterable && gugunOk;
|
const showEnterableToolbarButtons = enterable && gugunOk;
|
||||||
const showSubmittedInInputPeriodToolbar =
|
const showSubmittedInInputPeriodToolbar =
|
||||||
isSubmitted && periodEnterable && gugunOk;
|
isSubmitted && periodEnterable && gugunOk;
|
||||||
@@ -128,9 +151,8 @@ function buildExcretaTreatmentFacilitySearchParam(
|
|||||||
const fcltNm = values.fctName?.trim();
|
const fcltNm = values.fctName?.trim();
|
||||||
return {
|
return {
|
||||||
statsYr: values.year,
|
statsYr: values.year,
|
||||||
sewerBzmnCd: values.bsnmCd,
|
|
||||||
ctpvNm: values.sido,
|
ctpvNm: values.sido,
|
||||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
...toInputSearchRegion(sidoListRef.current, values),
|
||||||
...(fcltNm ? { fcltNm } : {}),
|
...(fcltNm ? { fcltNm } : {}),
|
||||||
...(overrides?.mode ? { mode: overrides.mode } : {}),
|
...(overrides?.mode ? { mode: overrides.mode } : {}),
|
||||||
};
|
};
|
||||||
@@ -191,13 +213,6 @@ function mapExcretaTreatmentFacilityListToGridRows(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function nullableTrimmedString(
|
|
||||||
value: string | number | null | undefined,
|
|
||||||
): string | null {
|
|
||||||
const s = String(value ?? "").trim();
|
|
||||||
return s === "" ? null : s;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toGridNumber(value: string | number | null | undefined): number {
|
function toGridNumber(value: string | number | null | undefined): number {
|
||||||
const n = Number(value);
|
const n = Number(value);
|
||||||
return Number.isFinite(n) ? n : 0;
|
return Number.isFinite(n) ? n : 0;
|
||||||
@@ -210,9 +225,9 @@ function toApiYmd(value: string | number | Date | null | undefined): string | nu
|
|||||||
function resolveExcretaTreatmentFacilityAdmdst(
|
function resolveExcretaTreatmentFacilityAdmdst(
|
||||||
row: Record<string, string | number>,
|
row: Record<string, string | number>,
|
||||||
sggOptions: Sgg[],
|
sggOptions: Sgg[],
|
||||||
): { admdstCd: string; admdstNm: string } {
|
): { admdstCd: string | null; admdstNm: string | null } {
|
||||||
const existingCd = String(row.admdstCd ?? "").trim();
|
const existingCd = nullableTrimmedString(row.admdstCd);
|
||||||
const existingNm = String(row.admdstNm ?? "").trim();
|
const existingNm = nullableTrimmedString(row.admdstNm);
|
||||||
if (existingCd && existingNm) {
|
if (existingCd && existingNm) {
|
||||||
return { admdstCd: existingCd, admdstNm: existingNm };
|
return { admdstCd: existingCd, admdstNm: existingNm };
|
||||||
}
|
}
|
||||||
@@ -221,7 +236,10 @@ function resolveExcretaTreatmentFacilityAdmdst(
|
|||||||
if (matched) {
|
if (matched) {
|
||||||
return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun };
|
return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun };
|
||||||
}
|
}
|
||||||
return { admdstCd: existingCd, admdstNm: existingNm || sggNm };
|
return {
|
||||||
|
admdstCd: existingCd,
|
||||||
|
admdstNm: existingNm ?? nullableTrimmedString(sggNm),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 그리드 행 → ST00070001 저장/삭제 API 항목 */
|
/** 그리드 행 → ST00070001 저장/삭제 API 항목 */
|
||||||
@@ -236,18 +254,18 @@ function buildExcretaTreatmentFacilitySaveItem(
|
|||||||
return {
|
return {
|
||||||
admdstCd,
|
admdstCd,
|
||||||
admdstNm,
|
admdstNm,
|
||||||
fcltSn: String(row.fcltSn ?? "").trim(),
|
fcltSn: nullableTrimmedString(row.fcltSn),
|
||||||
fcltNm: String(row.fcltNm ?? ""),
|
fcltNm: nullableTrimmedString(row.fcltNm),
|
||||||
fcltPstnCn: String(row.fcltPstnCn ?? ""),
|
fcltPstnCn: nullableTrimmedString(row.fcltPstnCn),
|
||||||
telNo: String(row.telNo ?? ""),
|
telNo: nullableTrimmedString(row.telNo),
|
||||||
cmptnStrtYmd: toApiYmd(row.cmptnStrtYmd),
|
cmptnStrtYmd: toApiYmd(row.cmptnStrtYmd),
|
||||||
oprtngStrtYmd: toApiYmd(row.oprtngStrtYmd),
|
oprtngStrtYmd: toApiYmd(row.oprtngStrtYmd),
|
||||||
wholPjtcoAmt: toGridNumber(row.wholPjtcoAmt),
|
wholPjtcoAmt: toGridNumber(row.wholPjtcoAmt),
|
||||||
prcsFcltCpct: toGridNumber(row.prcsFcltCpct),
|
prcsFcltCpct: toGridNumber(row.prcsFcltCpct),
|
||||||
cpct: toGridNumber(row.cpct),
|
cpct: toGridNumber(row.cpct),
|
||||||
oprtngDayCnt: toGridNumber(row.oprtngDayCnt),
|
oprtngDayCnt: toGridNumber(row.oprtngDayCnt),
|
||||||
prcsMthdExpln: String(row.prcsMthdExpln ?? ""),
|
prcsMthdExpln: nullableTrimmedString(row.prcsMthdExpln),
|
||||||
linkTmplntCn: String(row.linkTmplntCn ?? ""),
|
linkTmplntCn: nullableTrimmedString(row.linkTmplntCn),
|
||||||
linkPrcsQnt: toGridNumber(row.linkPrcsQnt),
|
linkPrcsQnt: toGridNumber(row.linkPrcsQnt),
|
||||||
inflwBodWqVl: toGridNumber(row.inflwBodWqVl),
|
inflwBodWqVl: toGridNumber(row.inflwBodWqVl),
|
||||||
inflwTocWqVl: toGridNumber(row.inflwTocWqVl),
|
inflwTocWqVl: toGridNumber(row.inflwTocWqVl),
|
||||||
@@ -261,11 +279,11 @@ function buildExcretaTreatmentFacilitySaveItem(
|
|||||||
dschgTnWqVl: toGridNumber(row.dschgTnWqVl),
|
dschgTnWqVl: toGridNumber(row.dschgTnWqVl),
|
||||||
dschgTpWqVl: toGridNumber(row.dschgTpWqVl),
|
dschgTpWqVl: toGridNumber(row.dschgTpWqVl),
|
||||||
dschgClfgCnt: toGridNumber(row.dschgClfgCnt),
|
dschgClfgCnt: toGridNumber(row.dschgClfgCnt),
|
||||||
wtsysNm: String(row.wtsysNm ?? ""),
|
wtsysNm: nullableTrimmedString(row.wtsysNm),
|
||||||
trbtrNm: String(row.trbtrNm ?? ""),
|
trbtrNm: nullableTrimmedString(row.trbtrNm),
|
||||||
msirbCn: String(row.msirbCn ?? ""),
|
msirbCn: nullableTrimmedString(row.msirbCn),
|
||||||
ssirbCn: String(row.ssirbCn ?? ""),
|
ssirbCn: nullableTrimmedString(row.ssirbCn),
|
||||||
operMthdCdCn: String(row.operMthdCdCn ?? ""),
|
operMthdCdCn: nullableTrimmedString(row.operMthdCdCn),
|
||||||
wholNope: toGridNumber(row.wholNope),
|
wholNope: toGridNumber(row.wholNope),
|
||||||
sftyAcdntEvntcsCnt: toGridNumber(row.sftyAcdntEvntcsCnt),
|
sftyAcdntEvntcsCnt: toGridNumber(row.sftyAcdntEvntcsCnt),
|
||||||
rmrk: nullableTrimmedString(row.rmrk),
|
rmrk: nullableTrimmedString(row.rmrk),
|
||||||
@@ -290,11 +308,11 @@ async function waitForImrJobCompleted(
|
|||||||
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
|
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
|
||||||
if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return;
|
if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return;
|
||||||
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
|
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
|
||||||
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`);
|
throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
|
||||||
}
|
}
|
||||||
await sleep(pollIntervalMs);
|
await sleep(pollIntervalMs);
|
||||||
}
|
}
|
||||||
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다.");
|
throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEmptyExcretaTreatmentFacilityRow(
|
function createEmptyExcretaTreatmentFacilityRow(
|
||||||
@@ -316,6 +334,8 @@ const EXCRETA_TREATMENT_FACILITY_NAME_GUIDE_MESSAGE =
|
|||||||
|
|
||||||
// 하수도 통계 관리 > 07. 분뇨처리시설
|
// 하수도 통계 관리 > 07. 분뇨처리시설
|
||||||
const ExcretaTreatmentFacilityInp = () => {
|
const ExcretaTreatmentFacilityInp = () => {
|
||||||
|
const { sidoList } = useSearchConditionSidoList({ isInput: true });
|
||||||
|
sidoListRef.current = sidoList;
|
||||||
const { setMessage: alert } = useAlertStore((state) => state);
|
const { setMessage: alert } = useAlertStore((state) => state);
|
||||||
const { confirm } = useConfirm();
|
const { confirm } = useConfirm();
|
||||||
const { isAdminInfo } = useAdminStore();
|
const { isAdminInfo } = useAdminStore();
|
||||||
@@ -338,6 +358,8 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
const [statusInfo, setStatusInfo] = useState<SewerageStatusInfo | null>(null);
|
const [statusInfo, setStatusInfo] = useState<SewerageStatusInfo | null>(null);
|
||||||
const [statsCd, setStatsCd] = useState("");
|
const [statsCd, setStatsCd] = useState("");
|
||||||
const [isVerificationFailed, setIsVerificationFailed] = useState(false);
|
const [isVerificationFailed, setIsVerificationFailed] = useState(false);
|
||||||
|
const [footerVerificationResult, setFooterVerificationResult] =
|
||||||
|
useState<VerificationResultLikeMap | null>(null);
|
||||||
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
|
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [sggOptions, setSggOptions] = useState<Sgg[]>([]);
|
const [sggOptions, setSggOptions] = useState<Sgg[]>([]);
|
||||||
@@ -356,7 +378,10 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
const sggOptionsSidoRef = useRef<string | null>(null);
|
const sggOptionsSidoRef = useRef<string | null>(null);
|
||||||
const [searchLoaded, setSearchLoaded] = useState(false);
|
const [searchLoaded, setSearchLoaded] = useState(false);
|
||||||
|
|
||||||
|
const activeTabId = useTabStore((s) => s.activeTabId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (activeTabId !== STATISTICS_INPUT_URL_PATH.EXCRETA_TREATMENT_FACILITY) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
@@ -382,7 +407,7 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, []);
|
}, [activeTabId]);
|
||||||
|
|
||||||
const loadSggOptions = useCallback(async (sido: string): Promise<Sgg[]> => {
|
const loadSggOptions = useCallback(async (sido: string): Promise<Sgg[]> => {
|
||||||
if (!sido || sido === "ALL") {
|
if (!sido || sido === "ALL") {
|
||||||
@@ -402,7 +427,7 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
|
|
||||||
const columnInfo = useMemo(() => {
|
const columnInfo = useMemo(() => {
|
||||||
const enterable =
|
const enterable =
|
||||||
computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo;
|
(computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo) && inputPeriodEnterable;
|
||||||
const rawColumns = ExcretaTreatFactInpColumn.columns ?? [];
|
const rawColumns = ExcretaTreatFactInpColumn.columns ?? [];
|
||||||
const base = enterable
|
const base = enterable
|
||||||
? rawColumns
|
? rawColumns
|
||||||
@@ -430,7 +455,7 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
}, [statusInfo, inputPeriodEnterable, isAdminInfo, sggOptions, columnGuidesByEngCol]);
|
}, [statusInfo, inputPeriodEnterable, isAdminInfo, sggOptions, columnGuidesByEngCol]);
|
||||||
|
|
||||||
const canEditGrid =
|
const canEditGrid =
|
||||||
computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo;
|
(computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo) && inputPeriodEnterable;
|
||||||
|
|
||||||
const getCurrentRows = (): Record<string, string | number>[] =>
|
const getCurrentRows = (): Record<string, string | number>[] =>
|
||||||
gridGetRowsRef.current?.() ?? rowData;
|
gridGetRowsRef.current?.() ?? rowData;
|
||||||
@@ -466,13 +491,18 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
statusInfo: nextStatusInfo,
|
statusInfo: nextStatusInfo,
|
||||||
excretaTreatmentFacilityList,
|
excretaTreatmentFacilityList,
|
||||||
} = response.data;
|
} = response.data;
|
||||||
const list = excretaTreatmentFacilityList ?? [];
|
const list = filterBySelectedAdmdstCd(
|
||||||
|
excretaTreatmentFacilityList ?? [],
|
||||||
|
sidoListRef.current,
|
||||||
|
values,
|
||||||
|
);
|
||||||
const gridRows = mapExcretaTreatmentFacilityListToGridRows(list, commonInfo.ctpvNm);
|
const gridRows = mapExcretaTreatmentFacilityListToGridRows(list, commonInfo.ctpvNm);
|
||||||
|
|
||||||
setRowData(gridRows);
|
setRowData(gridRows);
|
||||||
setStatusInfo(nextStatusInfo);
|
setStatusInfo(nextStatusInfo);
|
||||||
setStatsCd(commonInfo.statsCd);
|
setStatsCd(commonInfo.statsCd);
|
||||||
setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo));
|
setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo));
|
||||||
|
setFooterVerificationResult(commonInfo.verificationResult ?? null);
|
||||||
if (overrides?.mode !== "PRE") {
|
if (overrides?.mode !== "PRE") {
|
||||||
setIsNotApplicable(gridRows.length === 0);
|
setIsNotApplicable(gridRows.length === 0);
|
||||||
} else if (gridRows.length > 0) {
|
} else if (gridRows.length > 0) {
|
||||||
@@ -532,6 +562,7 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
setStatusInfo(null);
|
setStatusInfo(null);
|
||||||
setStatsCd("");
|
setStatsCd("");
|
||||||
setIsVerificationFailed(false);
|
setIsVerificationFailed(false);
|
||||||
|
setFooterVerificationResult(null);
|
||||||
setIsNotApplicable(false);
|
setIsNotApplicable(false);
|
||||||
setInputPeriodEnterable(true);
|
setInputPeriodEnterable(true);
|
||||||
setSggOptions([]);
|
setSggOptions([]);
|
||||||
@@ -581,12 +612,8 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
|
|
||||||
const userInfo = await getUserInfo();
|
const userInfo = await getUserInfo();
|
||||||
const requestData: cancelSubmitRequest = {
|
const requestData: cancelSubmitRequest = {
|
||||||
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
|
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
|
||||||
ctpvNm: lastSearchedValues.sido ?? "",
|
ctpvNm: lastSearchedValues.sido ?? "",
|
||||||
sggNm:
|
|
||||||
lastSearchedValues.gugun === "ALL"
|
|
||||||
? ""
|
|
||||||
: (lastSearchedValues.gugun ?? ""),
|
|
||||||
statsYr: lastSearchedValues.year ?? "",
|
statsYr: lastSearchedValues.year ?? "",
|
||||||
mdfrId: userInfo.userUnqId,
|
mdfrId: userInfo.userUnqId,
|
||||||
statsCd: statsCd || EXCRETA_TREATMENT_FACILITY_STATS_CD,
|
statsCd: statsCd || EXCRETA_TREATMENT_FACILITY_STATS_CD,
|
||||||
@@ -622,7 +649,7 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
if (!(await confirm("저장 하시겠습니까?"))) return;
|
if (!(await confirm("저장 하시겠습니까?"))) return;
|
||||||
|
|
||||||
commitGrid();
|
commitGrid();
|
||||||
const noApplicable = isNotApplicable;
|
const noApplicable = buildNoApplicableForRequest(lastSearchedValues, isNotApplicable, getCurrentRows().length === 0);
|
||||||
const currentData = getCurrentRows();
|
const currentData = getCurrentRows();
|
||||||
const saveList = noApplicable
|
const saveList = noApplicable
|
||||||
? []
|
? []
|
||||||
@@ -633,7 +660,7 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
.filter((row) => String(row.fcltSn ?? "").trim() !== "")
|
.filter((row) => String(row.fcltSn ?? "").trim() !== "")
|
||||||
.map((row) => buildExcretaTreatmentFacilitySaveItem(row, sggOptions));
|
.map((row) => buildExcretaTreatmentFacilitySaveItem(row, sggOptions));
|
||||||
|
|
||||||
if (!noApplicable && saveList.length === 0) {
|
if (!noApplicable && saveList.length === 0 && buildSubmitOkForToolbar(lastSearchedValues)) {
|
||||||
alert("해당사항 없음을 체크해주세요.");
|
alert("해당사항 없음을 체크해주세요.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -646,11 +673,7 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
{
|
{
|
||||||
statsYr: lastSearchedValues.year ?? "",
|
statsYr: lastSearchedValues.year ?? "",
|
||||||
ctpvNm: lastSearchedValues.sido ?? "",
|
ctpvNm: lastSearchedValues.sido ?? "",
|
||||||
sggNm:
|
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
|
||||||
lastSearchedValues.gugun === "ALL"
|
|
||||||
? ""
|
|
||||||
: (lastSearchedValues.gugun ?? ""),
|
|
||||||
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
|
|
||||||
userId: userInfo.userUnqId,
|
userId: userInfo.userUnqId,
|
||||||
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
|
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
|
||||||
noApplicable,
|
noApplicable,
|
||||||
@@ -686,9 +709,9 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const noApplicable = isNotApplicable;
|
const noApplicable = buildNoApplicableForRequest(lastSearchedValues, isNotApplicable, getCurrentRows().length === 0);
|
||||||
const currentData = getCurrentRows();
|
const currentData = getCurrentRows();
|
||||||
if (!noApplicable && currentData.length === 0) {
|
if (!noApplicable && currentData.length === 0 && buildSubmitOkForToolbar(lastSearchedValues)) {
|
||||||
alert("해당사항 없음을 체크해주세요.");
|
alert("해당사항 없음을 체크해주세요.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -713,7 +736,7 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
commitGrid();
|
commitGrid();
|
||||||
const noApplicable = isNotApplicable;
|
const noApplicable = buildNoApplicableForRequest(lastSearchedValues, isNotApplicable, getCurrentRows().length === 0);
|
||||||
const currentData = getCurrentRows();
|
const currentData = getCurrentRows();
|
||||||
const submitList = noApplicable
|
const submitList = noApplicable
|
||||||
? []
|
? []
|
||||||
@@ -724,19 +747,15 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
.filter((row) => String(row.fcltSn ?? "").trim() !== "")
|
.filter((row) => String(row.fcltSn ?? "").trim() !== "")
|
||||||
.map((row) => buildExcretaTreatmentFacilitySaveItem(row, sggOptions));
|
.map((row) => buildExcretaTreatmentFacilitySaveItem(row, sggOptions));
|
||||||
|
|
||||||
if (!noApplicable && submitList.length === 0) {
|
if (!noApplicable && submitList.length === 0 && buildSubmitOkForToolbar(lastSearchedValues)) {
|
||||||
alert("해당사항 없음을 체크해주세요.");
|
alert("해당사항 없음을 체크해주세요.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const statsCdForSubmit = statsCd || EXCRETA_TREATMENT_FACILITY_STATS_CD;
|
const statsCdForSubmit = statsCd || EXCRETA_TREATMENT_FACILITY_STATS_CD;
|
||||||
const verificationQuery = {
|
const verificationQuery = {
|
||||||
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
|
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
|
||||||
ctpvNm: lastSearchedValues.sido ?? "",
|
ctpvNm: lastSearchedValues.sido ?? "",
|
||||||
sggNm:
|
|
||||||
lastSearchedValues.gugun === "ALL"
|
|
||||||
? ""
|
|
||||||
: (lastSearchedValues.gugun ?? ""),
|
|
||||||
statsYr: lastSearchedValues.year ?? "",
|
statsYr: lastSearchedValues.year ?? "",
|
||||||
statsCd: statsCdForSubmit,
|
statsCd: statsCdForSubmit,
|
||||||
};
|
};
|
||||||
@@ -755,11 +774,7 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
{
|
{
|
||||||
statsYr: lastSearchedValues.year ?? "",
|
statsYr: lastSearchedValues.year ?? "",
|
||||||
ctpvNm: lastSearchedValues.sido ?? "",
|
ctpvNm: lastSearchedValues.sido ?? "",
|
||||||
sggNm:
|
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
|
||||||
lastSearchedValues.gugun === "ALL"
|
|
||||||
? ""
|
|
||||||
: (lastSearchedValues.gugun ?? ""),
|
|
||||||
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
|
|
||||||
userId: userInfo.userUnqId,
|
userId: userInfo.userUnqId,
|
||||||
laePicNm: submitInfo.laePicNm,
|
laePicNm: submitInfo.laePicNm,
|
||||||
laePicTelNo: submitInfo.laePicTelNo,
|
laePicTelNo: submitInfo.laePicTelNo,
|
||||||
@@ -786,15 +801,16 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
|
|
||||||
if (finalStatus === "5") {
|
if (finalStatus === "5") {
|
||||||
setGlobalProgress(50);
|
setGlobalProgress(50);
|
||||||
setGlobalProgressLabel("IMR 검증 진행 중...");
|
setGlobalProgressLabel("시계열 검증 진행 중...");
|
||||||
const imrParam = {
|
const imrParam = {
|
||||||
user_id: userInfo.userUnqId,
|
user_id: userInfo.userUnqId,
|
||||||
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "",
|
sewer_bzmn_cd: lastSearchedValues?.bsnmCd ?? "",
|
||||||
stats_cd: statsCdForSubmit,
|
stats_cd: statsCdForSubmit,
|
||||||
stats_yr: lastSearchedValues.year ?? "",
|
stats_yr: lastSearchedValues.year ?? "",
|
||||||
lae_sbmsn_stts_cd:
|
lae_sbmsn_stts_cd:
|
||||||
statusInfo?.laeSbmsnSttsCd === "4" ? "4" : "2",
|
statusInfo?.laeSbmsnSttsCd === "4" ? "4" : "2",
|
||||||
gen_verify_stts_cd: nvpassed ? "1" : "2",
|
gen_verify_stts_cd: nvpassed ? "1" : "2",
|
||||||
|
hb_pass_yn: response.data?.isHbPassed,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -872,6 +888,7 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
flushSync(() => {
|
flushSync(() => {
|
||||||
setSggOptions(list);
|
setSggOptions(list);
|
||||||
setRowData([...currentRows, newRow]);
|
setRowData([...currentRows, newRow]);
|
||||||
|
setIsNotApplicable(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -940,10 +957,10 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
.map((e) => {
|
.map((e) => {
|
||||||
const prefix =
|
const prefix =
|
||||||
e.vrfcCd === "IMR"
|
e.vrfcCd === "IMR"
|
||||||
? "IMR검증 - "
|
? "시계열 검증 - "
|
||||||
: e.vrfcCd === "NV"
|
: e.vrfcCd === "NV"
|
||||||
? "일반검증 - "
|
? "일반검증 - "
|
||||||
: "HB검증 -";
|
: "모집단 검증 -";
|
||||||
return `${prefix}${e.rsnExpln}
|
return `${prefix}${e.rsnExpln}
|
||||||
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
|
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
|
||||||
})
|
})
|
||||||
@@ -952,7 +969,7 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchContextForToolbar = lastSearchedValues ?? searchValues;
|
const searchContextForToolbar = lastSearchedValues;
|
||||||
const toolbarSnapshot = computeTabToolbarSnapshot(
|
const toolbarSnapshot = computeTabToolbarSnapshot(
|
||||||
statusInfo,
|
statusInfo,
|
||||||
rowData.length > 0,
|
rowData.length > 0,
|
||||||
@@ -972,23 +989,23 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
hasSearched &&
|
hasSearched &&
|
||||||
toolbarGroupAllowed &&
|
toolbarGroupAllowed &&
|
||||||
(showSaveInToolbar || showEnterableToolbarButtons);
|
(showSaveInToolbar || showEnterableToolbarButtons);
|
||||||
|
const tabEnterableForGrid =
|
||||||
|
(computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo) && inputPeriodEnterable;
|
||||||
|
const gugunOk = buildGugunOkForToolbar(searchContextForToolbar);
|
||||||
|
const submitOk = buildSubmitOkForToolbar(searchContextForToolbar);
|
||||||
const showNotApplicable =
|
const showNotApplicable =
|
||||||
searchLoaded &&
|
searchLoaded &&
|
||||||
hasSearched &&
|
hasSearched &&
|
||||||
rowData.length === 0 &&
|
rowData.length === 0 &&
|
||||||
computeTabEnterable(statusInfo, inputPeriodEnterable);
|
computeTabEnterable(statusInfo, inputPeriodEnterable) &&
|
||||||
|
submitOk;
|
||||||
const tabEnterableForGrid =
|
|
||||||
computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo;
|
|
||||||
const gugunOk = buildGugunOkForToolbar(lastSearchedValues);
|
|
||||||
|
|
||||||
const verificationQueryContext = (() => {
|
const verificationQueryContext = (() => {
|
||||||
const ctx = lastSearchedValues ?? searchValues;
|
const ctx = lastSearchedValues ?? searchValues;
|
||||||
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null;
|
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null;
|
||||||
return {
|
return {
|
||||||
sewerBzmnCd: ctx.bsnmCd,
|
...toInputSearchRegion(sidoListRef.current, ctx),
|
||||||
ctpvNm: ctx.sido,
|
ctpvNm: ctx.sido,
|
||||||
sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""),
|
|
||||||
statsYr: ctx.year,
|
statsYr: ctx.year,
|
||||||
statsCd,
|
statsCd,
|
||||||
};
|
};
|
||||||
@@ -1004,6 +1021,7 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
<>
|
<>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<SearchForm
|
<SearchForm
|
||||||
|
isInput
|
||||||
key={searchFormRemountKey}
|
key={searchFormRemountKey}
|
||||||
defaultValues={verificationSearchDefaults ?? undefined}
|
defaultValues={verificationSearchDefaults ?? undefined}
|
||||||
fctName={true}
|
fctName={true}
|
||||||
@@ -1012,7 +1030,7 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
onValuesChange={setSearchValues}
|
onValuesChange={setSearchValues}
|
||||||
/>
|
/>
|
||||||
{showActionToolbarCard && (
|
{showActionToolbarCard && (
|
||||||
<div className="card !py-0">
|
<div className="card !py-0">
|
||||||
<div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]">
|
<div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]">
|
||||||
<VerificationResultButton
|
<VerificationResultButton
|
||||||
VerificationResultButtonFunc={openVerificationHistoryDialog}
|
VerificationResultButtonFunc={openVerificationHistoryDialog}
|
||||||
@@ -1023,18 +1041,18 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
<LastYearDataButton
|
<LastYearDataButton
|
||||||
getLastYearDataFunc={() => void getLastYearDataFunc()}
|
getLastYearDataFunc={() => void getLastYearDataFunc()}
|
||||||
/>
|
/>
|
||||||
<SubmitButton submitFunc={submitFunc} />
|
{submitOk && <SubmitButton submitFunc={submitFunc} />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{statusInfo?.laeSbmsnSttsCd === "2" && (
|
{statusInfo?.laeSbmsnSttsCd === "2" && submitOk && (
|
||||||
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
|
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
|
||||||
)}
|
)}
|
||||||
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
|
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showArticleToolbarCard && (
|
{showArticleToolbarCard && (
|
||||||
<div className="card !py-0">
|
<div className="card !py-0">
|
||||||
<div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]">
|
<div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]">
|
||||||
{showNotApplicable && (
|
{showNotApplicable && (
|
||||||
<NotApplicableCheckButton
|
<NotApplicableCheckButton
|
||||||
@@ -1042,10 +1060,10 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
setIsCheck={setIsNotApplicable}
|
setIsCheck={setIsNotApplicable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AddArticleButton addArticleFunc={addArticleFunc} />
|
<AddArticleButton addArticleFunc={addArticleFunc} />
|
||||||
<DeleteArticleButton deleteArticleFunc={deleteArticleFunc} />
|
<DeleteArticleButton deleteArticleFunc={deleteArticleFunc} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10 [data-theme='black']:bg-transparent">
|
<div className="absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10 [data-theme='black']:bg-transparent">
|
||||||
@@ -1054,14 +1072,14 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<DataGrid
|
<DataGrid
|
||||||
key={canEditGrid ? "enterable" : "readonly"}
|
key={canEditGrid ? "enterable" : "readonly"}
|
||||||
getGridRef={gridRef as RefObject<RealGridReact | null>}
|
getGridRef={gridRef as RefObject<RealGridReact | null>}
|
||||||
columnInfo={columnInfo}
|
columnInfo={columnInfo}
|
||||||
rowStartNum={ExcretaTreatFactInpColumn.rowStartNum}
|
rowStartNum={ExcretaTreatFactInpColumn.rowStartNum}
|
||||||
fixedColumns={ExcretaTreatFactInpColumn.fixedColumns}
|
fixedColumns={ExcretaTreatFactInpColumn.fixedColumns}
|
||||||
rowData={rowData}
|
rowData={rowData}
|
||||||
setRowData={setRowData}
|
setRowData={setRowData}
|
||||||
getGridDataRef={gridGetRowsRef}
|
getGridDataRef={gridGetRowsRef}
|
||||||
showHeaderCheckbox
|
showHeaderCheckbox
|
||||||
statusInfo={statusInfo}
|
statusInfo={statusInfo}
|
||||||
@@ -1071,6 +1089,7 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd}
|
excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd}
|
||||||
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
|
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
|
||||||
cellStyleCallback={verificationCellStyle}
|
cellStyleCallback={verificationCellStyle}
|
||||||
|
footerVerificationResult={footerVerificationResult}
|
||||||
cellTooltipCallback={verificationCellTooltip}
|
cellTooltipCallback={verificationCellTooltip}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,9 +2,13 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { CalendarIcon } from "lucide-react";
|
||||||
|
|
||||||
import { useAlertStore } from "@/shared/lib";
|
import { useAlertStore } from "@/shared/lib";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Calendar,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -12,8 +16,10 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
Input,
|
Input,
|
||||||
InputDatePicker,
|
|
||||||
Label,
|
Label,
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
} from "@common/components";
|
} from "@common/components";
|
||||||
|
|
||||||
export interface MainFacilityMappingSaveDialogValues {
|
export interface MainFacilityMappingSaveDialogValues {
|
||||||
@@ -79,6 +85,95 @@ function validateContractRange(start: Date | undefined, end: Date | undefined):
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CONTRACT_DATE_END_MONTH = new Date(new Date().getFullYear() + 10, 11);
|
||||||
|
|
||||||
|
function isValidDate(date: Date | undefined) {
|
||||||
|
if (!date) return false;
|
||||||
|
return !Number.isNaN(date.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 이 다이얼로그 전용 — 연도 드롭다운을 현재+10년까지 허용 */
|
||||||
|
function ContractDatePicker({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
value?: Date;
|
||||||
|
onChange: (date: Date | undefined) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [month, setMonth] = useState<Date | undefined>(value);
|
||||||
|
const [displayValue, setDisplayValue] = useState<Date | undefined>(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDisplayValue(value);
|
||||||
|
setMonth(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex gap-2">
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={displayValue ? dayjs(displayValue).format("YYYY-MM-DD") : ""}
|
||||||
|
disabled={disabled}
|
||||||
|
max={dayjs(CONTRACT_DATE_END_MONTH).format("YYYY-MM-DD")}
|
||||||
|
className={`bg-background pr-10 ${className ?? ""}`}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newDate = new Date(e.target.value);
|
||||||
|
setDisplayValue(newDate);
|
||||||
|
if (isValidDate(newDate)) {
|
||||||
|
onChange(newDate);
|
||||||
|
setMonth(newDate);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "ArrowDown") {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
color="white"
|
||||||
|
disabled={disabled}
|
||||||
|
className="absolute right-2 top-1/2 size-7 -translate-y-1/2 rounded-[0.4rem] border-none p-0"
|
||||||
|
>
|
||||||
|
<CalendarIcon className="!size-full" />
|
||||||
|
<span className="sr-only">Select date</span>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="w-auto overflow-hidden p-0"
|
||||||
|
align="end"
|
||||||
|
alignOffset={-8}
|
||||||
|
sideOffset={10}
|
||||||
|
>
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={value}
|
||||||
|
captionLayout="dropdown"
|
||||||
|
month={month}
|
||||||
|
onMonthChange={setMonth}
|
||||||
|
endMonth={CONTRACT_DATE_END_MONTH}
|
||||||
|
onSelect={(newDate) => {
|
||||||
|
onChange(newDate);
|
||||||
|
setDisplayValue(newDate);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function MainFacilityMappingSaveDialog({
|
export function MainFacilityMappingSaveDialog({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
@@ -220,7 +315,7 @@ export function MainFacilityMappingSaveDialog({
|
|||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:gap-4">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:gap-4">
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||||||
<Label className="text-label text-[1.2rem]">시작일</Label>
|
<Label className="text-label text-[1.2rem]">시작일</Label>
|
||||||
<InputDatePicker
|
<ContractDatePicker
|
||||||
value={prxyCtrtStart}
|
value={prxyCtrtStart}
|
||||||
onChange={setPrxyCtrtStart}
|
onChange={setPrxyCtrtStart}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@@ -229,7 +324,7 @@ export function MainFacilityMappingSaveDialog({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||||||
<Label className="text-label text-[1.2rem]">종료일</Label>
|
<Label className="text-label text-[1.2rem]">종료일</Label>
|
||||||
<InputDatePicker
|
<ContractDatePicker
|
||||||
value={prxyCtrtEnd}
|
value={prxyCtrtEnd}
|
||||||
onChange={setPrxyCtrtEnd}
|
onChange={setPrxyCtrtEnd}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user