fix: 프로젝트 페이지네이션 추가 및 수정

main
jschoi 9 months ago
parent c2fb4c6966
commit 6e41bb966b

@ -1,5 +1,5 @@
<script setup> <script setup>
import ListComponent from "@/components/templates/Project/ListComponent.vue"; import ListComponent from "@/components/templates/users/ListComponent.vue";
</script> </script>
<template> <template>

@ -12,12 +12,12 @@ export const menuUtils = {
value: "workflows", value: "workflows",
icon: "mdi-code-braces", icon: "mdi-code-braces",
}, },
{ // {
title: "Workflow Step Config", // title: "Workflow Step Config",
path: "/workflow-step-config", // path: "/workflow-step-config",
value: "workflow-step-config", // value: "workflow-step-config",
icon: "mdi-hammer-wrench", // icon: "mdi-hammer-wrench",
}, // },
{ {
title: "Run", title: "Run",
path: "/run", path: "/run",

@ -5,7 +5,6 @@ import { useAutoflowStore } from "@/stores/autoflowStore";
import type { import type {
UiProject, UiProject,
ApiProject,
Permission, Permission,
} from "@/components/models/project/Project"; } from "@/components/models/project/Project";
import { ProjectService } from "@/components/service/project/projectService"; import { ProjectService } from "@/components/service/project/projectService";
@ -31,6 +30,11 @@ const menuX = ref(0);
const menuY = ref(0); const menuY = ref(0);
const selectedIndex = ref<number | null>(null); const selectedIndex = ref<number | null>(null);
// id (reg) ( , / X)
const projectRegById = ref<Record<number, { regId?: string; regNm?: string }>>(
{},
);
const projects = ref<UiProject[]>([]); const projects = ref<UiProject[]>([]);
type UserOption = { id: number | string; username: string }; type UserOption = { id: number | string; username: string };
const userOptions = ref<UserOption[]>([]); const userOptions = ref<UserOption[]>([]);
@ -43,7 +47,8 @@ const form = ref({
prjDesc: "", prjDesc: "",
selectedUsers: [] as string[], selectedUsers: [] as string[],
}); });
/** ===== 서버 응답 타입 ===== */
/** ===== 롤 ===== */
const roles = ref<string[]>([]); const roles = ref<string[]>([]);
const refreshRoles = () => { const refreshRoles = () => {
const auth = storage.getAuth?.() ?? storage.get?.("vpp-Auth") ?? null; const auth = storage.getAuth?.() ?? storage.get?.("vpp-Auth") ?? null;
@ -52,27 +57,93 @@ const refreshRoles = () => {
}; };
const isAdmin = computed(() => roles.value.includes("ROLE_ADMIN")); const isAdmin = computed(() => roles.value.includes("ROLE_ADMIN"));
/** ===== 페이지네이션 상태 ===== */
const pager = ref({ pageNum: 1, pageSize: 8, total: 0, pageLength: 1 });
const pagedProjects = computed(() => {
const total = projects.value.length;
pager.value.total = total;
pager.value.pageLength = Math.max(1, Math.ceil(total / pager.value.pageSize));
if (pager.value.pageNum > pager.value.pageLength)
pager.value.pageNum = pager.value.pageLength;
if (pager.value.pageNum < 1) pager.value.pageNum = 1;
const start = (pager.value.pageNum - 1) * pager.value.pageSize;
return projects.value.slice(start, start + pager.value.pageSize);
});
const changePageNum = (page: number) => {
pager.value.pageNum = page;
};
const changePageSize = (size: number) => {
pager.value.pageSize = size;
pager.value.pageNum = 1;
};
/** ===== 서버 응답 타입 ===== */ /** ===== 서버 응답 타입 ===== */
interface ProjectSearchResponseItem { interface ProjectSearchResponseItem {
id: number; id: number;
prjNm: string; prjNm: string;
prjDesc: string; prjDesc: string;
prjStartDt?: string; prjStartDt?: string;
regUserId?: string; // "" ( username) regUserId?: string;
regUserNm?: string;
modUserId?: string;
modUserNm?: string;
} }
interface UserResponseItem { interface UserResponseItem {
id: number | string; id: number | string;
username: string; username: string;
} }
/** ===== 페이로드 타입(서비스 시그니처를 못 바꾼다는 가정에서 지역 타입 분리) ===== */
// mod*
type NewProjectPayload = {
id: null;
prjCd: string;
prjNm: string;
prjDesc: string;
prjStartDt: string;
prjEndDt: string;
delYn: string;
regDate: string;
regUserId?: string;
regUserNm?: string;
};
// reg* , mod*
type UpdateProjectPayload = {
id: number;
prjCd: string;
prjNm: string;
prjDesc: string;
prjStartDt: string;
prjEndDt: string;
delYn: string;
regDate: string;
regUserId?: string;
regUserNm?: string;
modDate: string;
modUserId?: string;
modUserNm?: string;
};
/** ===== 유틸 ===== */ /** ===== 유틸 ===== */
const buildApiProjectPayload = (): ApiProject => { function buildCreatePayload(): NewProjectPayload {
const today = new Date().toISOString().slice(0, 10); const today = new Date().toISOString().slice(0, 10);
const nowIso = new Date().toISOString(); const nowIso = new Date().toISOString();
const namesCsv = form.value.selectedUsers.join(",");
const names = form.value.selectedUsers;
const namesCsv = names.join(",");
const idsCsv = names
.map((name) => userOptions.value.find((u) => u.username === name)?.id)
.filter(
(v): v is number | string => v !== undefined && v !== null && v !== "",
)
.map(String)
.join(","); // "6,5"
// mod* ()
return { return {
id: modalMode.value === "edit" ? editingProjectId.value! : null, id: null,
prjCd: form.value.prjCd, prjCd: form.value.prjCd,
prjNm: form.value.prjNm, prjNm: form.value.prjNm,
prjDesc: form.value.prjDesc, prjDesc: form.value.prjDesc,
@ -80,13 +151,50 @@ const buildApiProjectPayload = (): ApiProject => {
prjEndDt: today, prjEndDt: today,
delYn: "N", delYn: "N",
regDate: nowIso, regDate: nowIso,
regUserId: namesCsv, regUserId: idsCsv,
regUserNm: namesCsv, regUserNm: namesCsv,
};
}
function buildUpdatePayload(): UpdateProjectPayload {
const today = new Date().toISOString().slice(0, 10);
const nowIso = new Date().toISOString();
const names = form.value.selectedUsers;
const namesCsv = names.join(",");
const idsCsv = names
.map((name) => userOptions.value.find((u) => u.username === name)?.id)
.filter(
(v): v is number | string => v !== undefined && v !== null && v !== "",
)
.map(String)
.join(",");
const id = editingProjectId.value!;
const kept = projectRegById.value[id] || {};
// reg* DB , mod*
return {
id,
prjCd: form.value.prjCd,
prjNm: form.value.prjNm,
prjDesc: form.value.prjDesc,
prjStartDt: today,
prjEndDt: today,
delYn: "N",
regDate: nowIso,
regUserId: kept.regId,
regUserNm: kept.regNm,
modDate: nowIso, modDate: nowIso,
modUserId: namesCsv, modUserId: idsCsv,
modUserNm: namesCsv, modUserNm: namesCsv,
}; };
}; }
const fillerCount = computed(() =>
Math.max(0, pager.value.pageSize - pagedProjects.value.length),
);
const fillers = computed(() => Array.from({ length: fillerCount.value }));
const resetForm = () => { const resetForm = () => {
form.value.prjCd = `PRJ${Date.now()}`; form.value.prjCd = `PRJ${Date.now()}`;
@ -99,13 +207,33 @@ const loadProjects = async () => {
try { try {
const { data } = await ProjectService.search(); const { data } = await ProjectService.search();
const rawList = data as ProjectSearchResponseItem[]; const rawList = data as ProjectSearchResponseItem[];
projects.value = rawList.map((p) => ({
const sorted = [...rawList].sort((a, b) => (b.id ?? 0) - (a.id ?? 0));
projectRegById.value = {};
projects.value = sorted.map((p) => {
// reg ( reg* )
projectRegById.value[p.id] = { regId: p.regUserId, regNm: p.regUserNm };
// / mod_user_nm , reg_user_nm
const displayNm =
p.modUserNm && p.modUserNm.length > 0 ? p.modUserNm : p.regUserNm || "";
return {
id: p.id, id: p.id,
title: p.prjNm, title: p.prjNm,
creator: p.regUserId ?? "", creator: displayNm, // v-select v-model
date: p.prjStartDt ?? "", date: p.prjStartDt, // fallback
description: p.prjDesc, description: p.prjDesc,
})); };
});
if (
pager.value.pageNum >
Math.max(1, Math.ceil(projects.value.length / pager.value.pageSize))
) {
pager.value.pageNum = 1;
}
} catch (e) { } catch (e) {
console.error("프로젝트 조회 실패:", e); console.error("프로젝트 조회 실패:", e);
} }
@ -136,30 +264,27 @@ const closeDialog = () => {
}; };
const selectProject = (index: number) => { const selectProject = (index: number) => {
const selected = projects.value[index]; const selected = pagedProjects.value[index];
autoflowStore.setProjectId(selected.id); autoflowStore.setProjectId(selected.id);
autoflowStore.setProjectName(selected.title); autoflowStore.setProjectName(selected.title);
router.push("/home"); router.push("/home");
}; };
/** ===== 프로젝트 저장 & 권한 부여 ===== */
const grantDefaultPermissions = async ( const grantDefaultPermissions = async (
projectId: number, projectId: number,
usernames: string[], usernames: string[],
) => { ) => {
if (!usernames?.length) return; if (!usernames?.length) return;
const nameSet = new Set(usernames); const nameSet = new Set(usernames);
const numericIds = userOptions.value const numericIds = userOptions.value
.filter((u) => nameSet.has(u.username)) .filter((u) => nameSet.has(u.username))
.map((u) => Number(u.id)) .map((u) => Number(u.id))
.filter((n) => Number.isFinite(n)); .filter((n) => Number.isFinite(n));
await Promise.all( await Promise.all(
numericIds.map((uid) => numericIds.map((uid) =>
ProjectService.projectAuthority(projectId, { ProjectService.projectAuthority(projectId, {
projectId, projectId,
userId: uid, // number userId: uid,
permissions: DEFAULT_PERMISSIONS, permissions: DEFAULT_PERMISSIONS,
}), }),
), ),
@ -171,20 +296,17 @@ const saveProject = async () => {
alert("권한이 없습니다. (ROLE_ADMIN 전용)"); alert("권한이 없습니다. (ROLE_ADMIN 전용)");
return; return;
} }
const payload = buildApiProjectPayload();
try { try {
let projectId: number; let projectId: number;
if (modalMode.value === "create") { if (modalMode.value === "create") {
const createRes = await ProjectService.add(payload); const createPayload = buildCreatePayload(); // mod*
const createRes = await ProjectService.add(createPayload); //
projectId = createRes.data.id; projectId = createRes.data.id;
} else { } else {
await ProjectService.update(editingProjectId.value!, payload); const updatePayload = buildUpdatePayload(); // reg* , mod*
await ProjectService.update(editingProjectId.value!, updatePayload); // non-null
projectId = editingProjectId.value!; projectId = editingProjectId.value!;
} }
await grantDefaultPermissions(projectId, form.value.selectedUsers); await grantDefaultPermissions(projectId, form.value.selectedUsers);
await loadProjects(); await loadProjects();
closeDialog(); closeDialog();
@ -197,8 +319,7 @@ const saveProject = async () => {
const deleteProject = async () => { const deleteProject = async () => {
try { try {
if (selectedIndex.value === null) return; if (selectedIndex.value === null) return;
const target = projects.value[selectedIndex.value]; const target = pagedProjects.value[selectedIndex.value];
await ProjectService.delete(target.id); await ProjectService.delete(target.id);
await loadProjects(); await loadProjects();
} catch (e: any) { } catch (e: any) {
@ -209,43 +330,6 @@ const deleteProject = async () => {
} }
}; };
//
// const deleteProject = async (): Promise<void> => {
// try {
// if (selectedIndex.value === null) return;
// const target = projects.value[selectedIndex.value];
// const projectId = target.id;
// // 1) username
// const usernames = (target.creator || "")
// .split(",")
// .map((s) => s.trim())
// .filter(Boolean);
// // 2) username -> userId
// if (usernames.length) {
// const ids = userOptions.value
// .filter((u) => usernames.includes(u.username))
// .map((u) => u.id);
// // 3) /
// await Promise.all(
// ids.map((uid) => ProjectService.deleteProjectAuthority(projectId, uid)),
// );
// }
// // 4)
// await ProjectService.delete(projectId);
// await loadProjects();
// } catch (e: any) {
// console.error(" :", e?.response?.status, e?.response?.data || e);
// alert(" : " + (e?.response?.data?.message || e.message || ""));
// } finally {
// contextMenu.value = false;
// }
// };
const onAddProject = () => { const onAddProject = () => {
if (!isAdmin.value) { if (!isAdmin.value) {
alert("권한이 없습니다. (ROLE_ADMIN 전용)"); alert("권한이 없습니다. (ROLE_ADMIN 전용)");
@ -260,19 +344,19 @@ const onAddProject = () => {
const modifyProject = () => { const modifyProject = () => {
contextMenu.value = false; contextMenu.value = false;
if (selectedIndex.value === null) return; if (selectedIndex.value === null) return;
const selected = pagedProjects.value[selectedIndex.value];
const selected = projects.value[selectedIndex.value];
modalMode.value = "edit"; modalMode.value = "edit";
editingProjectId.value = selected.id; editingProjectId.value = selected.id;
form.value.prjCd = selected.title; form.value.prjCd = selected.title;
form.value.prjNm = selected.title; form.value.prjNm = selected.title;
form.value.prjDesc = selected.description; form.value.prjDesc = selected.description;
// creator reg_user_nm . []
form.value.selectedUsers = form.value.selectedUsers =
selected.creator typeof selected.creator === "string" && selected.creator.length
?.split(",") ? selected.creator.split(",").map((s) => s.trim())
.map((s) => s.trim()) : [];
.filter(Boolean) ?? [];
dialog.value = true; dialog.value = true;
}; };
@ -301,7 +385,6 @@ onBeforeUnmount(() => {
</v-col> </v-col>
<v-col cols="auto"> <v-col cols="auto">
<!-- ADMIN만 노출 -->
<v-btn <v-btn
v-show="isAdmin" v-show="isAdmin"
color="secondary" color="secondary"
@ -315,10 +398,13 @@ onBeforeUnmount(() => {
</v-col> </v-col>
</v-row> </v-row>
<!-- 카드 그리드: 현재 페이지 데이터만 렌더 -->
<v-row dense> <v-row dense>
<!-- 실제 카드 -->
<v-col <v-col
v-for="(project, index) in projects" v-for="(project, index) in pagedProjects"
:key="index" :key="project.id"
cols="12" cols="12"
sm="6" sm="6"
md="6" md="6"
@ -326,7 +412,7 @@ onBeforeUnmount(() => {
class="d-flex" class="d-flex"
> >
<v-card <v-card
class="pa-4 flex-grow-1 d-flex flex-column" class="project-card pa-4 flex-grow-1 d-flex flex-column"
color="primary" color="primary"
variant="elevated" variant="elevated"
elevation="6" elevation="6"
@ -354,8 +440,46 @@ onBeforeUnmount(() => {
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-col> </v-col>
<!-- 플레이스홀더(보이지 않지만 자리 차지) -->
<v-col
v-for="(_, i) in fillers"
:key="'filler-' + i"
cols="12"
sm="6"
md="6"
lg="6"
class="d-flex"
>
<v-card
class="project-card pa-4 flex-grow-1"
style="visibility: hidden; pointer-events: none"
rounded="lg"
>
&nbsp;
</v-card>
</v-col>
</v-row> </v-row>
<!-- 하단 페이지네이션 영역 -->
<v-row class="mt-6" align="center" justify="center">
<v-col
cols="12"
class="d-flex justify-center align-center"
style="gap: 16px"
>
<v-pagination
v-model="pager.pageNum"
:length="pager.pageLength"
:total-visible="7"
color="primary"
rounded="circle"
@update:model-value="changePageNum"
/>
</v-col>
</v-row>
<!-- 컨텍스트 메뉴/다이얼로그 기존 그대로 -->
<v-menu <v-menu
v-model="contextMenu" v-model="contextMenu"
absolute absolute
@ -407,14 +531,18 @@ onBeforeUnmount(() => {
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer />
<v-btn text @click="closeDialog">Cancel</v-btn>
<v-btn color="primary" @click="saveProject"> <v-btn color="primary" @click="saveProject">
{{ modalMode === "create" ? "Create" : "Save" }} {{ modalMode === "create" ? "Create" : "Save" }}
</v-btn> </v-btn>
<v-btn text @click="closeDialog">Cancel</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
</v-container> </v-container>
</template> </template>
<style scoped></style> <style scoped>
.project-card {
min-height: 140px;
}
</style>

Loading…
Cancel
Save