행정지원 검토의견 수정
This commit is contained in:
@@ -51,6 +51,7 @@ export {
|
||||
} from "./ui/pbadms-task-apply-dialog";
|
||||
export { PrivacyConsentSection } from "./ui/privacy-consent-section";
|
||||
export { ProgressStatusSection } from "./ui/progress-status-section";
|
||||
export { RvwOpnnListTable, type RvwOpnnListTableProps } from "./ui/rvw-opnn-list-table";
|
||||
export {
|
||||
SplmntDmndDialog,
|
||||
type SplmntDmndDialogConfirmItem,
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
|
||||
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
||||
import type { AtflMngClientResponseDto, RvwOpnnUpdateRequestDto } from "@/shared/model";
|
||||
import { AttachmentFileField } from "@/shared/ui";
|
||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||
import {
|
||||
AttachmentDownloadLink,
|
||||
attachService,
|
||||
useDownloadAttachmentByAtflId,
|
||||
} from "@/entities/attach";
|
||||
import {
|
||||
Button,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
const RVW_OPNN_EDIT_FILE_KEY_PREFIX = "rvw-opnn-edit::";
|
||||
|
||||
/** 업무별 검토의견 응답 DTO 공통 필드 (업무마다 null 허용 여부가 달라 느슨하게 정의) */
|
||||
interface RvwOpnnListItem {
|
||||
opnrSeComCd?: string | null;
|
||||
opnrNm?: string | null;
|
||||
rvwRsltCn?: string | null;
|
||||
rvwRsltAnmlyYn?: string | null;
|
||||
atchFileGroupId?: string | null;
|
||||
atchFileInfo?: AtflMngClientResponseDto | null;
|
||||
regDt?: string | null;
|
||||
rgtr?: string | null;
|
||||
}
|
||||
|
||||
interface RvwOpnnListTableProps<T extends RvwOpnnListItem> {
|
||||
items: T[];
|
||||
getRowKey: (item: T) => string;
|
||||
/** 로그인 사용자 UUID — 검토의견 등록자(rgtr)와 같을 때만 수정·삭제 버튼 노출 */
|
||||
currentUserUuid?: string;
|
||||
/** 현재 검토 단계가 로그인 사용자 차례인지 여부 — false면 등록자라도 수정·삭제 불가 */
|
||||
isReviewTurn?: boolean;
|
||||
onUpdate: (item: T, body: RvwOpnnUpdateRequestDto) => Promise<void>;
|
||||
onDelete: (item: T) => Promise<void>;
|
||||
}
|
||||
|
||||
interface EditDraft {
|
||||
rvwRsltCn: string;
|
||||
rvwRsltAnmlyYn: string;
|
||||
atchFileGroupId: string;
|
||||
}
|
||||
|
||||
interface EditFile {
|
||||
fileName: string;
|
||||
fileKey: string;
|
||||
atflId?: string;
|
||||
atflCd?: string;
|
||||
/** 이번 수정 중 새로 업로드한 파일 여부 (취소 시 정리 대상) */
|
||||
isNew: boolean;
|
||||
}
|
||||
|
||||
function formatAnmlyYn(value?: string | null) {
|
||||
if (value === "Y") return "이상";
|
||||
if (value === "N") return "정상";
|
||||
return "-";
|
||||
}
|
||||
|
||||
function RvwOpnnListTable<T extends RvwOpnnListItem>({
|
||||
items,
|
||||
getRowKey,
|
||||
currentUserUuid,
|
||||
isReviewTurn = false,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
}: RvwOpnnListTableProps<T>) {
|
||||
const { confirm } = useConfirm();
|
||||
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
|
||||
const [editingKey, setEditingKey] = useState<string | null>(null);
|
||||
const [draft, setDraft] = useState<EditDraft | null>(null);
|
||||
const [editFile, setEditFile] = useState<EditFile | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||
if (!atflCd || !atflId) return;
|
||||
|
||||
setDraft((prev) => (prev ? { ...prev, atchFileGroupId: atflCd } : prev));
|
||||
setEditFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||
},
|
||||
onError: (_error, _file, fileKey) => {
|
||||
setEditFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||
},
|
||||
});
|
||||
|
||||
const editFileUploading = editFile ? (isUploading[editFile.fileKey] ?? false) : false;
|
||||
|
||||
function canModify(item: T) {
|
||||
return isReviewTurn && Boolean(currentUserUuid) && item.rgtr === currentUserUuid;
|
||||
}
|
||||
|
||||
async function discardNewFile(file: EditFile | null) {
|
||||
if (!file?.isNew) return;
|
||||
|
||||
uploads[file.fileKey]?.abort();
|
||||
if (file.atflId) {
|
||||
try {
|
||||
await attachService.deleteAttachmentByAtflId(file.atflId);
|
||||
} catch (error) {
|
||||
console.error("검토의견 첨부파일 삭제 실패:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetEdit() {
|
||||
setEditingKey(null);
|
||||
setDraft(null);
|
||||
setEditFile(null);
|
||||
}
|
||||
|
||||
function startEdit(item: T) {
|
||||
void discardNewFile(editFile);
|
||||
|
||||
setEditingKey(getRowKey(item));
|
||||
setDraft({
|
||||
rvwRsltCn: item.rvwRsltCn ?? "",
|
||||
rvwRsltAnmlyYn: item.rvwRsltAnmlyYn ?? "",
|
||||
atchFileGroupId: item.atchFileGroupId ?? "",
|
||||
});
|
||||
setEditFile(
|
||||
item.atchFileInfo?.atflCd
|
||||
? {
|
||||
fileName: item.atchFileInfo.orgnlFileNm || "첨부파일",
|
||||
fileKey: `${RVW_OPNN_EDIT_FILE_KEY_PREFIX}${item.atchFileInfo.atflCd}`,
|
||||
atflId: item.atchFileInfo.atflId,
|
||||
atflCd: item.atchFileInfo.atflCd,
|
||||
isNew: false,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
void discardNewFile(editFile);
|
||||
resetEdit();
|
||||
}
|
||||
|
||||
async function handleFileAdd(files: FileList | null) {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
const fileKey = `${RVW_OPNN_EDIT_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||
|
||||
setEditFile({ fileName: file.name, fileKey, isNew: true });
|
||||
|
||||
try {
|
||||
await uploadFileAsync(file, { fileKey });
|
||||
} catch {
|
||||
// onError callback handles cleanup
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileRemove() {
|
||||
// 기존 파일은 저장 시점에 연결만 해제하고, 새로 올린 파일은 즉시 정리
|
||||
void discardNewFile(editFile);
|
||||
setEditFile(null);
|
||||
setDraft((prev) => (prev ? { ...prev, atchFileGroupId: "" } : prev));
|
||||
}
|
||||
|
||||
async function handleSave(item: T) {
|
||||
if (!draft) return;
|
||||
if (!draft.rvwRsltCn.trim()) {
|
||||
alert("의견내용을 입력하세요.");
|
||||
return;
|
||||
}
|
||||
if (!draft.rvwRsltAnmlyYn) {
|
||||
alert("검토결과를 선택하세요.");
|
||||
return;
|
||||
}
|
||||
if (!(await confirm("검토의견을 수정하시겠습니까?"))) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await onUpdate(item, {
|
||||
opnrSeComCd: item.opnrSeComCd ?? undefined,
|
||||
opnrNm: item.opnrNm ?? undefined,
|
||||
rvwRsltCn: draft.rvwRsltCn,
|
||||
rvwRsltAnmlyYn: draft.rvwRsltAnmlyYn,
|
||||
atchFileGroupId: draft.atchFileGroupId,
|
||||
});
|
||||
alert("수정되었습니다.");
|
||||
resetEdit();
|
||||
} catch (error) {
|
||||
console.error("검토의견 수정 실패:", error);
|
||||
alert("검토의견 수정 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(item: T) {
|
||||
if (!(await confirm("검토의견을 삭제하시겠습니까?"))) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await onDelete(item);
|
||||
alert("삭제되었습니다.");
|
||||
if (editingKey === getRowKey(item)) resetEdit();
|
||||
} catch (error) {
|
||||
console.error("검토의견 삭제 실패:", error);
|
||||
alert("검토의견 삭제 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
function renderViewRow(item: T) {
|
||||
return (
|
||||
<>
|
||||
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
|
||||
<TableCell align="center">{formatAnmlyYn(item.rvwRsltAnmlyYn)}</TableCell>
|
||||
<TableCell align="center">
|
||||
{item.atchFileInfo?.atflCd ? (
|
||||
<AttachmentDownloadLink
|
||||
atflCd={item.atchFileInfo.atflCd}
|
||||
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
|
||||
/>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderEditRow(rowKey: string, editDraft: EditDraft) {
|
||||
return (
|
||||
<>
|
||||
<TableCell align="left">
|
||||
<Textarea
|
||||
value={editDraft.rvwRsltCn}
|
||||
onChange={(event) =>
|
||||
setDraft((prev) => (prev ? { ...prev, rvwRsltCn: event.target.value } : prev))
|
||||
}
|
||||
placeholder="의견내용을 입력하세요"
|
||||
rows={4}
|
||||
className="min-h-[8rem] !text-xl"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<FieldSet>
|
||||
<RadioGroup
|
||||
value={editDraft.rvwRsltAnmlyYn}
|
||||
onValueChange={(value) =>
|
||||
setDraft((prev) => (prev ? { ...prev, rvwRsltAnmlyYn: value } : prev))
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-edit-normal-${rowKey}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="N" id={`rvw-opnn-edit-normal-${rowKey}`} />
|
||||
<span className="font-normal">정상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-edit-abnormal-${rowKey}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="Y" id={`rvw-opnn-edit-abnormal-${rowKey}`} />
|
||||
<span className="font-normal">이상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<AttachmentFileField
|
||||
files={
|
||||
editFile
|
||||
? [
|
||||
{
|
||||
fileKey: editFile.fileKey,
|
||||
fileName: editFile.fileName,
|
||||
onDownload: editFile.atflId
|
||||
? () => downloadByAtflId(editFile.atflId!)
|
||||
: undefined,
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
inputId={`rvw-opnn-edit-${rowKey}-file-input`}
|
||||
isUploadingByKey={isUploading}
|
||||
multiple={false}
|
||||
showAddButton={!editFile}
|
||||
addDisabled={editFileUploading}
|
||||
onAdd={(fileList) => void handleFileAdd(fileList)}
|
||||
onRemove={handleFileRemove}
|
||||
/>
|
||||
</TableCell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table
|
||||
caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시, 관리 정보 제공"
|
||||
variant="secondary"
|
||||
>
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="47%" />
|
||||
<col width="6%" />
|
||||
<col width="17%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>의견자명</TableHead>
|
||||
<TableHead>의견내용</TableHead>
|
||||
<TableHead>검토결과</TableHead>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableHead>등록일시</TableHead>
|
||||
<TableHead>관리</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
const rowKey = getRowKey(item);
|
||||
const isEditing = editingKey === rowKey && draft !== null;
|
||||
|
||||
return (
|
||||
<TableRow key={rowKey}>
|
||||
<TableCell align="center">{item.opnrNm}</TableCell>
|
||||
{isEditing ? renderEditRow(rowKey, draft) : renderViewRow(item)}
|
||||
<TableCell align="center">
|
||||
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{!canModify(item) ? (
|
||||
"-"
|
||||
) : isEditing ? (
|
||||
<div className="flex flex-wrap justify-center gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={() => void handleSave(item)}
|
||||
disabled={isSubmitting || editFileUploading}
|
||||
>
|
||||
저장
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="white"
|
||||
onClick={cancelEdit}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-wrap justify-center gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="white"
|
||||
onClick={() => startEdit(item)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
수정
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="gray"
|
||||
onClick={() => void handleDelete(item)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { RvwOpnnListTable, type RvwOpnnListTableProps };
|
||||
Reference in New Issue
Block a user